mirror of
https://github.com/LukeHagar/better-auth.git
synced 2025-12-09 04:19:26 +00:00
feat: rate limit moved to core
This commit is contained in:
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@@ -16,5 +16,8 @@
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"typescript.tsdk": "node_modules/typescript/lib"
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"[astro]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ const PasswordInput = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}
|
||||
);
|
||||
PasswordInput.displayName = "PasswordInput";
|
||||
|
||||
|
||||
@@ -52,7 +52,10 @@ export const Tabs = ({
|
||||
}}
|
||||
onMouseEnter={() => setHovering(true)}
|
||||
onMouseLeave={() => setHovering(false)}
|
||||
className={cn("relative px-4 py-2 rounded-full opacity-80 hover:opacity-100", tabClassName)}
|
||||
className={cn(
|
||||
"relative px-4 py-2 rounded-full opacity-80 hover:opacity-100",
|
||||
tabClassName
|
||||
)}
|
||||
style={{
|
||||
transformStyle: "preserve-3d",
|
||||
}}
|
||||
@@ -63,21 +66,26 @@ export const Tabs = ({
|
||||
duration: 0.2,
|
||||
delay: 0.1,
|
||||
|
||||
type: "keyframes"
|
||||
type: "keyframes",
|
||||
}}
|
||||
animate={{
|
||||
x: tabs.indexOf(tab) === 0 ? [0, 0, 0] : [0, 0, 0],
|
||||
}}
|
||||
className={cn(
|
||||
"absolute inset-0 bg-gray-200 dark:bg-zinc-900/90 opacity-100",
|
||||
activeTabClassName,
|
||||
activeTabClassName
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<span className={
|
||||
cn("relative block text-black dark:text-white", active.value === tab.value ? "text-opacity-100 font-medium" : "opacity-40 ")
|
||||
}>
|
||||
<span
|
||||
className={cn(
|
||||
"relative block text-black dark:text-white",
|
||||
active.value === tab.value
|
||||
? "text-opacity-100 font-medium"
|
||||
: "opacity-40 "
|
||||
)}
|
||||
>
|
||||
{tab.title}
|
||||
</span>
|
||||
</button>
|
||||
@@ -121,10 +129,14 @@ export const FadeInDiv = ({
|
||||
transition: {
|
||||
duration: 0.2,
|
||||
delay: 0.1,
|
||||
type: "keyframes"
|
||||
}
|
||||
type: "keyframes",
|
||||
},
|
||||
}}
|
||||
className={cn("w-50 h-full", isActive(tab) ? "" : "hidden", className)}
|
||||
className={cn(
|
||||
"w-50 h-full",
|
||||
isActive(tab) ? "" : "hidden",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{tab.content}
|
||||
</motion.div>
|
||||
|
||||
@@ -157,7 +157,6 @@ export const contents: Content[] = [
|
||||
),
|
||||
href: "/docs/concepts/database",
|
||||
},
|
||||
|
||||
{
|
||||
href: "/docs/concepts/plugins",
|
||||
title: "Plugins",
|
||||
@@ -179,6 +178,11 @@ export const contents: Content[] = [
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Rate Limit",
|
||||
icon: Clock,
|
||||
href: "/docs/concepts/rate-limit",
|
||||
},
|
||||
{
|
||||
title: "Session Management",
|
||||
href: "/docs/concepts/session-management",
|
||||
@@ -556,11 +560,7 @@ export const contents: Content[] = [
|
||||
href: "/docs/plugins/1st-party-plugins",
|
||||
icon: LucideAArrowDown,
|
||||
},
|
||||
{
|
||||
title: "Rate Limit",
|
||||
icon: Clock,
|
||||
href: "/docs/plugins/rate-limit",
|
||||
},
|
||||
|
||||
{
|
||||
title: "Bearer",
|
||||
icon: Key,
|
||||
|
||||
132
docs/content/docs/concepts/rate-limit.mdx
Normal file
132
docs/content/docs/concepts/rate-limit.mdx
Normal file
@@ -0,0 +1,132 @@
|
||||
---
|
||||
title: Rate Limit
|
||||
description: How to limit the number of requests a user can make to the server in a given time period.
|
||||
---
|
||||
|
||||
Better Auth includes a built-in rate limiter to help manage traffic and prevent abuse. By default, in production mode, the rate limiter is set to:
|
||||
|
||||
- Window: 60 seconds
|
||||
- Max Requests: 100 requests
|
||||
|
||||
You can easily customize these settings by passing the rateLimit plugin to the betterAuth function.
|
||||
|
||||
In addition to the default settings, Better Auth provides custom rules for specific paths. For example:
|
||||
|
||||
"/sign-in/email": Limited to 7 requests within 10 seconds.
|
||||
|
||||
In addition to that, plugins also define custom rules for specific paths. For example, `twoFactor` plugin has the following custom rules:
|
||||
- "/two-factor/verify": Limited to 3 requests within 10 seconds.
|
||||
|
||||
These custom rules ensure that sensitive operations are protected with stricter limits.
|
||||
|
||||
## Configuring Rate Limit
|
||||
|
||||
### Rate Limit Window
|
||||
|
||||
```ts title="auth.ts"
|
||||
import { betterAuth } from "better-auth";
|
||||
|
||||
export const auth = betterAuth({
|
||||
//...other options
|
||||
rateLimit: {
|
||||
window: 60, // time window in seconds
|
||||
max: 100, // max requests in the window
|
||||
customRules: { // custom rules for specific paths
|
||||
"/sign-in/email": {
|
||||
window: 10,
|
||||
max: 7,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
### Storage
|
||||
|
||||
By default, rate limit data is stored in memory. However, you can either use your database or custom storage to store rate limit data.
|
||||
|
||||
**Using Database**
|
||||
|
||||
```ts title="auth.ts"
|
||||
import { betterAuth } from "better-auth";
|
||||
|
||||
export const auth = betterAuth({
|
||||
//...other options
|
||||
rateLimit: {
|
||||
storage: "database",
|
||||
tableName: "rateLimit", //optional by default "rateLimit" is used
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
make sure to run `migrate` to create the rate limit table in your database.
|
||||
|
||||
```bash
|
||||
npx better-auth migrate
|
||||
```
|
||||
|
||||
**Custom Storage**
|
||||
|
||||
```ts title="auth.ts"
|
||||
import { betterAuth } from "better-auth";
|
||||
|
||||
export const auth = betterAuth({
|
||||
//...other options
|
||||
rateLimit: {
|
||||
customStorage: {
|
||||
get: async (key) => {
|
||||
// get rate limit data
|
||||
},
|
||||
set: async (key, value) => {
|
||||
// set rate limit data
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Handling Rate Limit Errors
|
||||
|
||||
When a request exceeds the rate limit, Better Auth returns the following header:
|
||||
|
||||
- `X-Retry-After`: The number of seconds until the user can make another request.
|
||||
|
||||
To handle rate limit errors on the client side, you can manage them either globally or on a per-request basis. Since Better Auth clients wrap over Better Fetch, you can pass fetchOptions to handle rate limit errors
|
||||
|
||||
**Global Handling**
|
||||
|
||||
```ts title="client.ts"
|
||||
import { createAuthClient } from "better-auth/client";
|
||||
|
||||
export const client = createAuthClient({
|
||||
fetchOptions: {
|
||||
onError: async (context) => {
|
||||
const { response } = context;
|
||||
if (response.status === 429) {
|
||||
const retryAfter = response.headers.get("X-Retry-After");
|
||||
console.log(`Rate limit exceeded. Retry after ${retryAfter} seconds`);
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
**Per Request Handling**
|
||||
|
||||
```ts title="client.ts"
|
||||
import { client } from "./client";
|
||||
|
||||
await client.signIn.email({
|
||||
fetchOptions: {
|
||||
onError: async (context) => {
|
||||
const { response } = context;
|
||||
if (response.status === 429) {
|
||||
const retryAfter = response.headers.get("X-Retry-After");
|
||||
console.log(`Rate limit exceeded. Retry after ${retryAfter} seconds`);
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
@@ -3,8 +3,12 @@ import { defineConfig } from 'astro/config';
|
||||
|
||||
import tailwind from "@astrojs/tailwind";
|
||||
|
||||
import solidJs from "@astrojs/solid-js";
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
output: "server",
|
||||
integrations: [tailwind()]
|
||||
integrations: [tailwind({
|
||||
applyBaseStyles: false
|
||||
}), solidJs()]
|
||||
});
|
||||
16
examples/astro-example/components.json
Normal file
16
examples/astro-example/components.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"$schema": "https://shadcn-solid.com/schema.json",
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.mjs",
|
||||
"css": {
|
||||
"path": "src/app.css",
|
||||
"variable": true
|
||||
},
|
||||
"color": "stone",
|
||||
"prefix": ""
|
||||
},
|
||||
"alias": {
|
||||
"component": "@/components",
|
||||
"cn": "@/libs/cn"
|
||||
}
|
||||
}
|
||||
@@ -10,12 +10,31 @@
|
||||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ark-ui/solid": "^3.12.1",
|
||||
"@astrojs/check": "^0.9.3",
|
||||
"@astrojs/solid-js": "^4.4.2",
|
||||
"@astrojs/tailwind": "^5.1.1",
|
||||
"@corvu/drawer": "^0.2.2",
|
||||
"@corvu/otp-field": "^0.1.2",
|
||||
"@corvu/resizable": "^0.2.3",
|
||||
"@kobalte/core": "^0.13.6",
|
||||
"@oslojs/encoding": "^1.0.0",
|
||||
"astro": "^4.15.9",
|
||||
"better-auth": "workspace:*",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk-solid": "^1.1.0",
|
||||
"embla-carousel-solid": "^8.3.0",
|
||||
"lucide-solid": "^0.445.0",
|
||||
"solid-js": "^1.8.23",
|
||||
"solid-sonner": "^0.2.8",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"tailwindcss": "^3.4.13",
|
||||
"typescript": "^5.6.2"
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"typescript": "^5.6.2",
|
||||
"ua-parser-js": "^0.7.39"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/ua-parser-js": "^0.7.39"
|
||||
}
|
||||
}
|
||||
|
||||
76
examples/astro-example/src/app.css
Normal file
76
examples/astro-example/src/app.css
Normal file
@@ -0,0 +1,76 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 20 14.3% 4.1%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 20 14.3% 4.1%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 20 14.3% 4.1%;
|
||||
|
||||
--primary: 24 9.8% 10%;
|
||||
--primary-foreground: 60 9.1% 97.8%;
|
||||
|
||||
--secondary: 60 4.8% 95.9%;
|
||||
--secondary-foreground: 24 9.8% 10%;
|
||||
|
||||
--muted: 60 4.8% 95.9%;
|
||||
--muted-foreground: 25 5.3% 44.7%;
|
||||
|
||||
--accent: 60 4.8% 95.9%;
|
||||
--accent-foreground: 24 9.8% 10%;
|
||||
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 60 9.1% 97.8%;
|
||||
|
||||
--border: 20 5.9% 90%;
|
||||
--input: 20 5.9% 90%;
|
||||
--ring: 20 14.3% 4.1%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
[data-kb-theme="dark"] {
|
||||
--background: 20 14.3% 4.1%;
|
||||
--foreground: 60 9.1% 97.8%;
|
||||
|
||||
--card: 20 14.3% 4.1%;
|
||||
--card-foreground: 60 9.1% 97.8%;
|
||||
|
||||
--popover: 20 14.3% 4.1%;
|
||||
--popover-foreground: 60 9.1% 97.8%;
|
||||
|
||||
--primary: 60 9.1% 97.8%;
|
||||
--primary-foreground: 24 9.8% 10%;
|
||||
|
||||
--secondary: 12 6.5% 15.1%;
|
||||
--secondary-foreground: 60 9.1% 97.8%;
|
||||
|
||||
--muted: 12 6.5% 15.1%;
|
||||
--muted-foreground: 24 5.4% 63.9%;
|
||||
|
||||
--accent: 12 6.5% 15.1%;
|
||||
--accent-foreground: 60 9.1% 97.8%;
|
||||
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 60 9.1% 97.8%;
|
||||
|
||||
--border: 12 6.5% 15.1%;
|
||||
--input: 12 6.5% 15.1%;
|
||||
--ring: 24 5.7% 82.9%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { betterAuth } from "better-auth";
|
||||
import { passkey, twoFactor, rateLimiter } from "better-auth/plugins";
|
||||
|
||||
export const auth = betterAuth({
|
||||
database: {
|
||||
@@ -11,10 +12,23 @@ export const auth = betterAuth({
|
||||
trustedProviders: ["google"],
|
||||
},
|
||||
},
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
},
|
||||
socialProviders: {
|
||||
google: {
|
||||
clientId: import.meta.env.GOOGLE_CLIENT_ID || "",
|
||||
clientSecret: import.meta.env.GOOGLE_CLIENT_SECRET || "",
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
passkey(),
|
||||
twoFactor(),
|
||||
rateLimiter({
|
||||
enabled: true,
|
||||
storage: {
|
||||
provider: "memory",
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
16
examples/astro-example/src/components/loader.tsx
Normal file
16
examples/astro-example/src/components/loader.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
export function Loader() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
class="animate-spin"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 22c5.421 0 10-4.579 10-10h-2c0 4.337-3.663 8-8 8s-8-3.663-8-8s3.663-8 8-8V2C6.579 2 2 6.58 2 12c0 5.421 4.579 10 10 10"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
174
examples/astro-example/src/components/sign-in.tsx
Normal file
174
examples/astro-example/src/components/sign-in.tsx
Normal file
@@ -0,0 +1,174 @@
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { TextField, TextFieldLabel, TextFieldRoot } from "./ui/textfield";
|
||||
import { Button } from "./ui/button";
|
||||
import { Checkbox, CheckboxControl, CheckboxLabel } from "./ui/checkbox";
|
||||
import { passkeyActions, signIn } from "@/libs/auth-client";
|
||||
import { createSignal } from "solid-js";
|
||||
|
||||
export function SignInCard() {
|
||||
const [email, setEmail] = createSignal("");
|
||||
const [password, setPassword] = createSignal("");
|
||||
const [rememberMe, setRememberMe] = createSignal(false);
|
||||
return (
|
||||
<Card class="max-w-max">
|
||||
<CardHeader>
|
||||
<CardTitle class="text-lg md:text-xl">Sign In</CardTitle>
|
||||
<CardDescription class="text-xs md:text-sm">
|
||||
Enter your email below to login to your account
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid gap-4">
|
||||
<div class="grid gap-2">
|
||||
<TextFieldRoot class="w-full">
|
||||
<TextFieldLabel for="email">Email</TextFieldLabel>
|
||||
<TextField
|
||||
type="email"
|
||||
placeholder="Email"
|
||||
value={email()}
|
||||
onInput={(e) => {
|
||||
if ("value" in e.target) setEmail(e.target.value as string);
|
||||
}}
|
||||
/>
|
||||
</TextFieldRoot>
|
||||
<TextFieldRoot class="w-full">
|
||||
<div class="flex items-center justify-between">
|
||||
<TextFieldLabel for="password">Password</TextFieldLabel>
|
||||
<a
|
||||
href="/forget-password"
|
||||
class="ml-auto inline-block text-sm underline"
|
||||
>
|
||||
Forgot your password?
|
||||
</a>
|
||||
</div>
|
||||
<TextField
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
value={password()}
|
||||
onInput={(e) => {
|
||||
if ("value" in e.target)
|
||||
setPassword(e.target.value as string);
|
||||
}}
|
||||
/>
|
||||
</TextFieldRoot>
|
||||
<Checkbox
|
||||
class="flex items-center gap-2 z-50"
|
||||
onChange={(e) => {
|
||||
setRememberMe(e);
|
||||
}}
|
||||
checked={rememberMe()}
|
||||
>
|
||||
<CheckboxControl />
|
||||
<CheckboxLabel class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
|
||||
Remember Me
|
||||
</CheckboxLabel>
|
||||
</Checkbox>
|
||||
<Button
|
||||
onclick={() => {
|
||||
signIn.email({
|
||||
email: email(),
|
||||
password: password(),
|
||||
dontRememberMe: !rememberMe(),
|
||||
fetchOptions: {
|
||||
onError(context) {
|
||||
alert(context.error.message);
|
||||
},
|
||||
},
|
||||
callbackURL: "/",
|
||||
});
|
||||
}}
|
||||
>
|
||||
Sign In
|
||||
</Button>
|
||||
<Button class="gap-2" variant="outline">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1.2em"
|
||||
height="1.2em"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 2A10 10 0 0 0 2 12c0 4.42 2.87 8.17 6.84 9.5c.5.08.66-.23.66-.5v-1.69c-2.77.6-3.36-1.34-3.36-1.34c-.46-1.16-1.11-1.47-1.11-1.47c-.91-.62.07-.6.07-.6c1 .07 1.53 1.03 1.53 1.03c.87 1.52 2.34 1.07 2.91.83c.09-.65.35-1.09.63-1.34c-2.22-.25-4.55-1.11-4.55-4.92c0-1.11.38-2 1.03-2.71c-.1-.25-.45-1.29.1-2.64c0 0 .84-.27 2.75 1.02c.79-.22 1.65-.33 2.5-.33s1.71.11 2.5.33c1.91-1.29 2.75-1.02 2.75-1.02c.55 1.35.2 2.39.1 2.64c.65.71 1.03 1.6 1.03 2.71c0 3.82-2.34 4.66-4.57 4.91c.36.31.69.92.69 1.85V21c0 .27.16.59.67.5C19.14 20.16 22 16.42 22 12A10 10 0 0 0 12 2"
|
||||
></path>
|
||||
</svg>
|
||||
Continue with Github
|
||||
</Button>
|
||||
<Button class="gap-2" variant="outline">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1.2em"
|
||||
height="1.2em"
|
||||
viewBox="0 0 512 512"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="m473.16 221.48l-2.26-9.59H262.46v88.22H387c-12.93 61.4-72.93 93.72-121.94 93.72c-35.66 0-73.25-15-98.13-39.11a140.08 140.08 0 0 1-41.8-98.88c0-37.16 16.7-74.33 41-98.78s61-38.13 97.49-38.13c41.79 0 71.74 22.19 82.94 32.31l62.69-62.36C390.86 72.72 340.34 32 261.6 32c-60.75 0-119 23.27-161.58 65.71C58 139.5 36.25 199.93 36.25 256s20.58 113.48 61.3 155.6c43.51 44.92 105.13 68.4 168.58 68.4c57.73 0 112.45-22.62 151.45-63.66c38.34-40.4 58.17-96.3 58.17-154.9c0-24.67-2.48-39.32-2.59-39.96"
|
||||
></path>
|
||||
</svg>
|
||||
Continue with Google
|
||||
</Button>
|
||||
<Button
|
||||
class="gap-2"
|
||||
variant="outline"
|
||||
onClick={async () => {
|
||||
await signIn.passkey({
|
||||
callbackURL: "/dashboard",
|
||||
fetchOptions: {
|
||||
onError(context) {
|
||||
alert(context.error.message);
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="lucide lucide-key"
|
||||
>
|
||||
<path d="m15.5 7.5 2.3 2.3a1 1 0 0 0 1.4 0l2.1-2.1a1 1 0 0 0 0-1.4L19 4" />
|
||||
<path d="m21 2-9.6 9.6" />
|
||||
<circle cx="7.5" cy="15.5" r="5.5" />
|
||||
</svg>
|
||||
Sign-In with Passkey
|
||||
</Button>
|
||||
</div>
|
||||
<p class="text-sm text-center">
|
||||
Don't have an account yet?{" "}
|
||||
<a
|
||||
href="/sign-up"
|
||||
class="text-blue-900 dark:text-orange-200 underline"
|
||||
>
|
||||
Sign Up
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter class="flex-col">
|
||||
<div class="flex justify-center w-full border-t py-4">
|
||||
<p class="text-center text-xs text-neutral-500">
|
||||
Secured by{" "}
|
||||
<span class="text-orange-900 dark:text-orange-200">
|
||||
better-auth.
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
164
examples/astro-example/src/components/sign-up.tsx
Normal file
164
examples/astro-example/src/components/sign-up.tsx
Normal file
@@ -0,0 +1,164 @@
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { TextField, TextFieldLabel, TextFieldRoot } from "./ui/textfield";
|
||||
import { Button } from "./ui/button";
|
||||
import { Checkbox, CheckboxControl, CheckboxLabel } from "./ui/checkbox";
|
||||
import { signIn, signUp } from "@/libs/auth-client";
|
||||
import { createSignal } from "solid-js";
|
||||
import { convertImageToBase64 } from "@/libs/utils";
|
||||
|
||||
export function SignUpCard() {
|
||||
const [firstName, setFirstName] = createSignal("");
|
||||
const [lastName, setLastName] = createSignal("");
|
||||
const [email, setEmail] = createSignal("");
|
||||
const [password, setPassword] = createSignal("");
|
||||
const [image, setImage] = createSignal<File>();
|
||||
const [rememberMe, setRememberMe] = createSignal(false);
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle class="text-lg md:text-xl">Sign Up</CardTitle>
|
||||
<CardDescription class="text-xs md:text-sm">
|
||||
Enter your information to create an account
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="grid gap-4">
|
||||
<div class="grid gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<TextFieldRoot class="w-full">
|
||||
<TextFieldLabel for="name">First Name</TextFieldLabel>
|
||||
<TextField
|
||||
type="first-name"
|
||||
placeholder="First Name"
|
||||
value={firstName()}
|
||||
onInput={(e) => {
|
||||
if ("value" in e.target)
|
||||
setFirstName(e.target.value as string);
|
||||
}}
|
||||
/>
|
||||
</TextFieldRoot>
|
||||
<TextFieldRoot class="w-full">
|
||||
<TextFieldLabel for="name">Last Name</TextFieldLabel>
|
||||
<TextField
|
||||
type="last-name"
|
||||
placeholder="Last Name"
|
||||
value={lastName()}
|
||||
onInput={(e) => {
|
||||
if ("value" in e.target)
|
||||
setLastName(e.target.value as string);
|
||||
}}
|
||||
/>
|
||||
</TextFieldRoot>
|
||||
</div>
|
||||
<TextFieldRoot class="w-full">
|
||||
<TextFieldLabel for="email">Email</TextFieldLabel>
|
||||
<TextField
|
||||
type="email"
|
||||
placeholder="Email"
|
||||
value={email()}
|
||||
onInput={(e) => {
|
||||
if ("value" in e.target) setEmail(e.target.value as string);
|
||||
}}
|
||||
/>
|
||||
</TextFieldRoot>
|
||||
<TextFieldRoot class="w-full">
|
||||
<TextFieldLabel for="password">Password</TextFieldLabel>
|
||||
<TextField
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
value={password()}
|
||||
onInput={(e) => {
|
||||
if ("value" in e.target)
|
||||
setPassword(e.target.value as string);
|
||||
}}
|
||||
/>
|
||||
</TextFieldRoot>
|
||||
<TextFieldRoot>
|
||||
<TextFieldLabel>Image</TextFieldLabel>
|
||||
<TextField
|
||||
type="file"
|
||||
accept="image/*"
|
||||
placeholder="Image"
|
||||
onChange={(e: any) => {
|
||||
const file = e.target.files?.[0];
|
||||
if ("value" in e.target) setImage(file);
|
||||
}}
|
||||
/>
|
||||
</TextFieldRoot>
|
||||
<Button
|
||||
onclick={async () => {
|
||||
signUp.email({
|
||||
name: `${firstName()} ${lastName()}`,
|
||||
image: image()
|
||||
? await convertImageToBase64(image()!)
|
||||
: undefined,
|
||||
email: email(),
|
||||
password: password(),
|
||||
callbackURL: "/",
|
||||
fetchOptions: {
|
||||
onError(context) {
|
||||
alert(context.error.message);
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
Sign Up
|
||||
</Button>
|
||||
<Button class="gap-2" variant="outline">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1.2em"
|
||||
height="1.2em"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 2A10 10 0 0 0 2 12c0 4.42 2.87 8.17 6.84 9.5c.5.08.66-.23.66-.5v-1.69c-2.77.6-3.36-1.34-3.36-1.34c-.46-1.16-1.11-1.47-1.11-1.47c-.91-.62.07-.6.07-.6c1 .07 1.53 1.03 1.53 1.03c.87 1.52 2.34 1.07 2.91.83c.09-.65.35-1.09.63-1.34c-2.22-.25-4.55-1.11-4.55-4.92c0-1.11.38-2 1.03-2.71c-.1-.25-.45-1.29.1-2.64c0 0 .84-.27 2.75 1.02c.79-.22 1.65-.33 2.5-.33s1.71.11 2.5.33c1.91-1.29 2.75-1.02 2.75-1.02c.55 1.35.2 2.39.1 2.64c.65.71 1.03 1.6 1.03 2.71c0 3.82-2.34 4.66-4.57 4.91c.36.31.69.92.69 1.85V21c0 .27.16.59.67.5C19.14 20.16 22 16.42 22 12A10 10 0 0 0 12 2"
|
||||
></path>
|
||||
</svg>
|
||||
Continue with Github
|
||||
</Button>
|
||||
<Button class="gap-2" variant="outline">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1.2em"
|
||||
height="1.2em"
|
||||
viewBox="0 0 512 512"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="m473.16 221.48l-2.26-9.59H262.46v88.22H387c-12.93 61.4-72.93 93.72-121.94 93.72c-35.66 0-73.25-15-98.13-39.11a140.08 140.08 0 0 1-41.8-98.88c0-37.16 16.7-74.33 41-98.78s61-38.13 97.49-38.13c41.79 0 71.74 22.19 82.94 32.31l62.69-62.36C390.86 72.72 340.34 32 261.6 32c-60.75 0-119 23.27-161.58 65.71C58 139.5 36.25 199.93 36.25 256s20.58 113.48 61.3 155.6c43.51 44.92 105.13 68.4 168.58 68.4c57.73 0 112.45-22.62 151.45-63.66c38.34-40.4 58.17-96.3 58.17-154.9c0-24.67-2.48-39.32-2.59-39.96"
|
||||
></path>
|
||||
</svg>
|
||||
Continue with Google
|
||||
</Button>
|
||||
</div>
|
||||
<p class="text-sm text-center">
|
||||
Already have an account?{" "}
|
||||
<a href="/sign-in" class="text-blue-500">
|
||||
Sign In
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter class="flex-col">
|
||||
<div class="flex justify-center w-full border-t py-4">
|
||||
<p class="text-center text-xs text-neutral-500">
|
||||
Secured by{" "}
|
||||
<span class="text-orange-900 dark:text-orange-200">
|
||||
better-auth.
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
188
examples/astro-example/src/components/two-factor.tsx
Normal file
188
examples/astro-example/src/components/two-factor.tsx
Normal file
@@ -0,0 +1,188 @@
|
||||
import { createEffect, createSignal, Show } from "solid-js";
|
||||
import { Button } from "./ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "./ui/card";
|
||||
import {
|
||||
OTPField,
|
||||
OTPFieldGroup,
|
||||
OTPFieldInput,
|
||||
OTPFieldSeparator,
|
||||
OTPFieldSlot,
|
||||
} from "./ui/otp-field";
|
||||
import { twoFactorActions } from "@/libs/auth-client";
|
||||
|
||||
export function TwoFactorComponent() {
|
||||
const [otp, setOTP] = createSignal("");
|
||||
|
||||
createEffect(() => {
|
||||
if (otp().length === 6) {
|
||||
twoFactorActions.verifyTotp({
|
||||
code: otp(),
|
||||
callbackURL: "/dashboard",
|
||||
fetchOptions: {
|
||||
onError(context) {
|
||||
if (context.error.status === 429) {
|
||||
const retryAfter = context.response.headers.get("X-Retry-After");
|
||||
alert(
|
||||
`Too many requests. Please try again after ${retryAfter} seconds`
|
||||
);
|
||||
} else {
|
||||
alert(
|
||||
context.error.message ||
|
||||
context.error.statusText ||
|
||||
context.error.status
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
return (
|
||||
<main class="flex flex-col items-center justify-center min-h-[calc(100vh-10rem)]">
|
||||
<Card class="w-[350px]">
|
||||
<CardHeader>
|
||||
<CardTitle>TOTP Verification</CardTitle>
|
||||
<CardDescription>
|
||||
Enter your 6-digit TOTP code to authenticate
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div class="flex flex-col gap-2 items-center">
|
||||
<OTPField
|
||||
maxLength={6}
|
||||
value={otp()}
|
||||
onValueChange={(value) => {
|
||||
setOTP(value);
|
||||
}}
|
||||
>
|
||||
<OTPFieldInput />
|
||||
<OTPFieldGroup>
|
||||
<OTPFieldSlot index={0} />
|
||||
<OTPFieldSlot index={1} />
|
||||
<OTPFieldSlot index={2} />
|
||||
<OTPFieldSlot index={3} />
|
||||
<OTPFieldSlot index={4} />
|
||||
<OTPFieldSlot index={5} />
|
||||
</OTPFieldGroup>
|
||||
</OTPField>
|
||||
<span class="text-center text-xs">
|
||||
Enter your one-time password.
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex justify-center">
|
||||
<a
|
||||
href="/two-factor/email"
|
||||
class="text-xs border-b pb-1 mt-2 w-max hover:border-black transition-all"
|
||||
>
|
||||
Switch to Email Verification
|
||||
</a>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
export function TwoFactorEmail() {
|
||||
const [otp, setOTP] = createSignal("");
|
||||
|
||||
createEffect(() => {
|
||||
if (otp().length === 6) {
|
||||
twoFactorActions.verifyTotp({
|
||||
code: otp(),
|
||||
callbackURL: "/dashboard",
|
||||
fetchOptions: {
|
||||
onError(context) {
|
||||
alert(context.error.message);
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
const [sentEmail, setSentEmail] = createSignal(false);
|
||||
return (
|
||||
<main class="flex flex-col items-center justify-center min-h-[calc(100vh-10rem)]">
|
||||
<Card class="w-[350px]">
|
||||
<CardHeader>
|
||||
<CardTitle>Email Verification</CardTitle>
|
||||
<CardDescription>
|
||||
Enter your 6-digit TOTP code to authenticate
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Show
|
||||
when={sentEmail()}
|
||||
fallback={
|
||||
<Button
|
||||
onClick={async () => {
|
||||
await twoFactorActions.sendOtp({
|
||||
fetchOptions: {
|
||||
onSuccess(context) {
|
||||
setSentEmail(true);
|
||||
},
|
||||
onError(context) {
|
||||
alert(context.error.message);
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
class="w-full gap-2"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1.2em"
|
||||
height="1.2em"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M4 20q-.825 0-1.412-.587T2 18V6q0-.825.588-1.412T4 4h16q.825 0 1.413.588T22 6v12q0 .825-.587 1.413T20 20zm8-7.175q.125 0 .263-.038t.262-.112L19.6 8.25q.2-.125.3-.312t.1-.413q0-.5-.425-.75T18.7 6.8L12 11L5.3 6.8q-.45-.275-.875-.012T4 7.525q0 .25.1.438t.3.287l7.075 4.425q.125.075.263.113t.262.037"
|
||||
></path>
|
||||
</svg>{" "}
|
||||
Send OTP to Email
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<div class="flex flex-col gap-2 items-center">
|
||||
<OTPField
|
||||
maxLength={6}
|
||||
value={otp()}
|
||||
onValueChange={(value) => {
|
||||
setOTP(value);
|
||||
}}
|
||||
>
|
||||
<OTPFieldInput />
|
||||
<OTPFieldGroup>
|
||||
<OTPFieldSlot index={0} />
|
||||
<OTPFieldSlot index={1} />
|
||||
<OTPFieldSlot index={2} />
|
||||
<OTPFieldSlot index={3} />
|
||||
<OTPFieldSlot index={4} />
|
||||
<OTPFieldSlot index={5} />
|
||||
</OTPFieldGroup>
|
||||
</OTPField>
|
||||
<span class="text-center text-xs">
|
||||
Enter your one-time password.
|
||||
</span>
|
||||
</div>
|
||||
</Show>
|
||||
<div class="flex justify-center">
|
||||
<a
|
||||
href="/two-factor"
|
||||
class="text-xs border-b pb-1 mt-2 w-max hover:border-black transition-all"
|
||||
>
|
||||
Switch to TOTP Verification
|
||||
</a>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
97
examples/astro-example/src/components/ui/accordion.tsx
Normal file
97
examples/astro-example/src/components/ui/accordion.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import { cn } from "@/libs/cn";
|
||||
import type {
|
||||
AccordionContentProps,
|
||||
AccordionItemProps,
|
||||
AccordionTriggerProps,
|
||||
} from "@kobalte/core/accordion";
|
||||
import { Accordion as AccordionPrimitive } from "@kobalte/core/accordion";
|
||||
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
|
||||
import { type ParentProps, type ValidComponent, splitProps } from "solid-js";
|
||||
|
||||
export const Accordion = AccordionPrimitive;
|
||||
|
||||
type accordionItemProps<T extends ValidComponent = "div"> =
|
||||
AccordionItemProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export const AccordionItem = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, accordionItemProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as accordionItemProps, ["class"]);
|
||||
|
||||
return (
|
||||
<AccordionPrimitive.Item class={cn("border-b", local.class)} {...rest} />
|
||||
);
|
||||
};
|
||||
|
||||
type accordionTriggerProps<T extends ValidComponent = "button"> = ParentProps<
|
||||
AccordionTriggerProps<T> & {
|
||||
class?: string;
|
||||
}
|
||||
>;
|
||||
|
||||
export const AccordionTrigger = <T extends ValidComponent = "button">(
|
||||
props: PolymorphicProps<T, accordionTriggerProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as accordionTriggerProps, [
|
||||
"class",
|
||||
"children",
|
||||
]);
|
||||
|
||||
return (
|
||||
<AccordionPrimitive.Header class="flex" as="div">
|
||||
<AccordionPrimitive.Trigger
|
||||
class={cn(
|
||||
"flex flex-1 items-center justify-between py-4 text-sm font-medium transition-shadow hover:underline focus-visible:outline-none focus-visible:ring-[1.5px] focus-visible:ring-ring [&[data-expanded]>svg]:rotate-180",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{local.children}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
class="h-4 w-4 text-muted-foreground transition-transform duration-200"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="m6 9l6 6l6-6"
|
||||
/>
|
||||
<title>Arrow</title>
|
||||
</svg>
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
);
|
||||
};
|
||||
|
||||
type accordionContentProps<T extends ValidComponent = "div"> = ParentProps<
|
||||
AccordionContentProps<T> & {
|
||||
class?: string;
|
||||
}
|
||||
>;
|
||||
|
||||
export const AccordionContent = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, accordionContentProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as accordionContentProps, [
|
||||
"class",
|
||||
"children",
|
||||
]);
|
||||
|
||||
return (
|
||||
<AccordionPrimitive.Content
|
||||
class={cn(
|
||||
"animate-accordion-up overflow-hidden text-sm data-[expanded]:animate-accordion-down",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
<div class="pb-4 pt-0">{local.children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
);
|
||||
};
|
||||
152
examples/astro-example/src/components/ui/alert-dialog.tsx
Normal file
152
examples/astro-example/src/components/ui/alert-dialog.tsx
Normal file
@@ -0,0 +1,152 @@
|
||||
import { cn } from "@/libs/cn";
|
||||
import type {
|
||||
AlertDialogCloseButtonProps,
|
||||
AlertDialogContentProps,
|
||||
AlertDialogDescriptionProps,
|
||||
AlertDialogTitleProps,
|
||||
} from "@kobalte/core/alert-dialog";
|
||||
import { AlertDialog as AlertDialogPrimitive } from "@kobalte/core/alert-dialog";
|
||||
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
|
||||
import type { ComponentProps, ParentProps, ValidComponent } from "solid-js";
|
||||
import { splitProps } from "solid-js";
|
||||
import { buttonVariants } from "./button";
|
||||
|
||||
export const AlertDialog = AlertDialogPrimitive;
|
||||
export const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
|
||||
|
||||
type alertDialogContentProps<T extends ValidComponent = "div"> = ParentProps<
|
||||
AlertDialogContentProps<T> & {
|
||||
class?: string;
|
||||
}
|
||||
>;
|
||||
|
||||
export const AlertDialogContent = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, alertDialogContentProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as alertDialogContentProps, [
|
||||
"class",
|
||||
"children",
|
||||
]);
|
||||
|
||||
return (
|
||||
<AlertDialogPrimitive.Portal>
|
||||
<AlertDialogPrimitive.Overlay
|
||||
class={cn(
|
||||
"fixed inset-0 z-50 bg-background/80 data-[expanded]:animate-in data-[closed]:animate-out data-[closed]:fade-out-0 data-[expanded]:fade-in-0",
|
||||
)}
|
||||
/>
|
||||
<AlertDialogPrimitive.Content
|
||||
class={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg data-[closed]:duration-200 data-[expanded]:duration-200 data-[expanded]:animate-in data-[closed]:animate-out data-[closed]:fade-out-0 data-[expanded]:fade-in-0 data-[closed]:zoom-out-95 data-[expanded]:zoom-in-95 data-[closed]:slide-out-to-left-1/2 data-[closed]:slide-out-to-top-[48%] data-[expanded]:slide-in-from-left-1/2 data-[expanded]:slide-in-from-top-[48%] sm:rounded-lg md:w-full",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{local.children}
|
||||
</AlertDialogPrimitive.Content>
|
||||
</AlertDialogPrimitive.Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export const AlertDialogHeader = (props: ComponentProps<"div">) => {
|
||||
const [local, rest] = splitProps(props, ["class"]);
|
||||
|
||||
return (
|
||||
<div
|
||||
class={cn(
|
||||
"flex flex-col space-y-2 text-center sm:text-left",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const AlertDialogFooter = (props: ComponentProps<"div">) => {
|
||||
const [local, rest] = splitProps(props, ["class"]);
|
||||
|
||||
return (
|
||||
<div
|
||||
class={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type alertDialogTitleProps<T extends ValidComponent = "h2"> =
|
||||
AlertDialogTitleProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export const AlertDialogTitle = <T extends ValidComponent = "h2">(
|
||||
props: PolymorphicProps<T, alertDialogTitleProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as alertDialogTitleProps, ["class"]);
|
||||
|
||||
return (
|
||||
<AlertDialogPrimitive.Title
|
||||
class={cn("text-lg font-semibold", local.class)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type alertDialogDescriptionProps<T extends ValidComponent = "p"> =
|
||||
AlertDialogDescriptionProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export const AlertDialogDescription = <T extends ValidComponent = "p">(
|
||||
props: PolymorphicProps<T, alertDialogDescriptionProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as alertDialogDescriptionProps, [
|
||||
"class",
|
||||
]);
|
||||
|
||||
return (
|
||||
<AlertDialogPrimitive.Description
|
||||
class={cn("text-sm text-muted-foreground", local.class)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type alertDialogCloseProps<T extends ValidComponent = "button"> =
|
||||
AlertDialogCloseButtonProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export const AlertDialogClose = <T extends ValidComponent = "button">(
|
||||
props: PolymorphicProps<T, alertDialogCloseProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as alertDialogCloseProps, ["class"]);
|
||||
|
||||
return (
|
||||
<AlertDialogPrimitive.CloseButton
|
||||
class={cn(
|
||||
buttonVariants({
|
||||
variant: "outline",
|
||||
}),
|
||||
"mt-2 md:mt-0",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const AlertDialogAction = <T extends ValidComponent = "button">(
|
||||
props: PolymorphicProps<T, alertDialogCloseProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as alertDialogCloseProps, ["class"]);
|
||||
|
||||
return (
|
||||
<AlertDialogPrimitive.CloseButton
|
||||
class={cn(buttonVariants(), local.class)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
66
examples/astro-example/src/components/ui/alert.tsx
Normal file
66
examples/astro-example/src/components/ui/alert.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import { cn } from "@/libs/cn";
|
||||
import type { AlertRootProps } from "@kobalte/core/alert";
|
||||
import { Alert as AlertPrimitive } from "@kobalte/core/alert";
|
||||
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
|
||||
import type { VariantProps } from "class-variance-authority";
|
||||
import { cva } from "class-variance-authority";
|
||||
import type { ComponentProps, ValidComponent } from "solid-js";
|
||||
import { splitProps } from "solid-js";
|
||||
|
||||
export const alertVariants = cva(
|
||||
"relative w-full rounded-lg border px-4 py-3 text-sm [&:has(svg)]:pl-11 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-background text-foreground",
|
||||
destructive:
|
||||
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
type alertProps<T extends ValidComponent = "div"> = AlertRootProps<T> &
|
||||
VariantProps<typeof alertVariants> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export const Alert = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, alertProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as alertProps, ["class", "variant"]);
|
||||
|
||||
return (
|
||||
<AlertPrimitive
|
||||
class={cn(
|
||||
alertVariants({
|
||||
variant: props.variant,
|
||||
}),
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const AlertTitle = (props: ComponentProps<"div">) => {
|
||||
const [local, rest] = splitProps(props, ["class"]);
|
||||
|
||||
return (
|
||||
<div
|
||||
class={cn("font-medium leading-5 tracking-tight", local.class)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const AlertDescription = (props: ComponentProps<"div">) => {
|
||||
const [local, rest] = splitProps(props, ["class"]);
|
||||
|
||||
return (
|
||||
<div class={cn("text-sm [&_p]:leading-relaxed", local.class)} {...rest} />
|
||||
);
|
||||
};
|
||||
42
examples/astro-example/src/components/ui/badge.tsx
Normal file
42
examples/astro-example/src/components/ui/badge.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { cn } from "@/libs/cn";
|
||||
import type { VariantProps } from "class-variance-authority";
|
||||
import { cva } from "class-variance-authority";
|
||||
import { type ComponentProps, splitProps } from "solid-js";
|
||||
|
||||
export const badgeVariants = cva(
|
||||
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-shadow focus-visible:outline-none focus-visible:ring-[1.5px] focus-visible:ring-ring",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
|
||||
outline: "text-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export const Badge = (
|
||||
props: ComponentProps<"div"> & VariantProps<typeof badgeVariants>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props, ["class", "variant"]);
|
||||
|
||||
return (
|
||||
<div
|
||||
class={cn(
|
||||
badgeVariants({
|
||||
variant: local.variant,
|
||||
}),
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
66
examples/astro-example/src/components/ui/button.tsx
Normal file
66
examples/astro-example/src/components/ui/button.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import { cn } from "@/libs/cn";
|
||||
import type { ButtonRootProps } from "@kobalte/core/button";
|
||||
import { Button as ButtonPrimitive } from "@kobalte/core/button";
|
||||
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
|
||||
import type { VariantProps } from "class-variance-authority";
|
||||
import { cva } from "class-variance-authority";
|
||||
import type { ValidComponent } from "solid-js";
|
||||
import { splitProps } from "solid-js";
|
||||
|
||||
export const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-[color,background-color,box-shadow] focus-visible:outline-none focus-visible:ring-[1.5px] focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2",
|
||||
sm: "h-8 rounded-md px-3 text-xs",
|
||||
lg: "h-10 rounded-md px-8",
|
||||
icon: "h-9 w-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
type buttonProps<T extends ValidComponent = "button"> = ButtonRootProps<T> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export const Button = <T extends ValidComponent = "button">(
|
||||
props: PolymorphicProps<T, buttonProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as buttonProps, [
|
||||
"class",
|
||||
"variant",
|
||||
"size",
|
||||
]);
|
||||
|
||||
return (
|
||||
<ButtonPrimitive
|
||||
class={cn(
|
||||
buttonVariants({
|
||||
size: local.size,
|
||||
variant: local.variant,
|
||||
}),
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
60
examples/astro-example/src/components/ui/card.tsx
Normal file
60
examples/astro-example/src/components/ui/card.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import { cn } from "@/libs/cn";
|
||||
import type { ComponentProps, ParentComponent } from "solid-js";
|
||||
import { splitProps } from "solid-js";
|
||||
|
||||
export const Card = (props: ComponentProps<"div">) => {
|
||||
const [local, rest] = splitProps(props, ["class"]);
|
||||
|
||||
return (
|
||||
<div
|
||||
class={cn(
|
||||
"rounded-xl border bg-card text-card-foreground shadow",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const CardHeader = (props: ComponentProps<"div">) => {
|
||||
const [local, rest] = splitProps(props, ["class"]);
|
||||
|
||||
return (
|
||||
<div class={cn("flex flex-col space-y-1.5 p-6", local.class)} {...rest} />
|
||||
);
|
||||
};
|
||||
|
||||
export const CardTitle: ParentComponent<ComponentProps<"h1">> = (props) => {
|
||||
const [local, rest] = splitProps(props, ["class"]);
|
||||
|
||||
return (
|
||||
<h1
|
||||
class={cn("font-semibold leading-none tracking-tight", local.class)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const CardDescription: ParentComponent<ComponentProps<"h3">> = (
|
||||
props,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props, ["class"]);
|
||||
|
||||
return (
|
||||
<h3 class={cn("text-sm text-muted-foreground", local.class)} {...rest} />
|
||||
);
|
||||
};
|
||||
|
||||
export const CardContent = (props: ComponentProps<"div">) => {
|
||||
const [local, rest] = splitProps(props, ["class"]);
|
||||
|
||||
return <div class={cn("p-6 pt-0", local.class)} {...rest} />;
|
||||
};
|
||||
|
||||
export const CardFooter = (props: ComponentProps<"div">) => {
|
||||
const [local, rest] = splitProps(props, ["class"]);
|
||||
|
||||
return (
|
||||
<div class={cn("flex items-center p-6 pt-0", local.class)} {...rest} />
|
||||
);
|
||||
};
|
||||
272
examples/astro-example/src/components/ui/carousel.tsx
Normal file
272
examples/astro-example/src/components/ui/carousel.tsx
Normal file
@@ -0,0 +1,272 @@
|
||||
import { cn } from "@/libs/cn";
|
||||
import type { CreateEmblaCarouselType } from "embla-carousel-solid";
|
||||
import createEmblaCarousel from "embla-carousel-solid";
|
||||
import type {
|
||||
Accessor,
|
||||
ComponentProps,
|
||||
ParentProps,
|
||||
VoidProps,
|
||||
} from "solid-js";
|
||||
import {
|
||||
createContext,
|
||||
createEffect,
|
||||
createMemo,
|
||||
createSignal,
|
||||
mergeProps,
|
||||
onCleanup,
|
||||
splitProps,
|
||||
useContext,
|
||||
} from "solid-js";
|
||||
import { Button } from "./button";
|
||||
|
||||
export type CarouselApi = CreateEmblaCarouselType[1];
|
||||
type UseCarouselParameters = Parameters<typeof createEmblaCarousel>;
|
||||
type CarouselOptions = NonNullable<UseCarouselParameters[0]>;
|
||||
type CarouselPlugin = NonNullable<UseCarouselParameters[1]>;
|
||||
|
||||
type CarouselProps = {
|
||||
opts?: ReturnType<CarouselOptions>;
|
||||
plugins?: ReturnType<CarouselPlugin>;
|
||||
orientation?: "horizontal" | "vertical";
|
||||
setApi?: (api: CarouselApi) => void;
|
||||
};
|
||||
|
||||
type CarouselContextProps = {
|
||||
carouselRef: ReturnType<typeof createEmblaCarousel>[0];
|
||||
api: ReturnType<typeof createEmblaCarousel>[1];
|
||||
scrollPrev: () => void;
|
||||
scrollNext: () => void;
|
||||
canScrollPrev: Accessor<boolean>;
|
||||
canScrollNext: Accessor<boolean>;
|
||||
} & CarouselProps;
|
||||
|
||||
const CarouselContext = createContext<Accessor<CarouselContextProps> | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const useCarousel = () => {
|
||||
const context = useContext(CarouselContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useCarousel must be used within a <Carousel />");
|
||||
}
|
||||
|
||||
return context();
|
||||
};
|
||||
|
||||
export const Carousel = (props: ComponentProps<"div"> & CarouselProps) => {
|
||||
const merge = mergeProps<
|
||||
ParentProps<ComponentProps<"div"> & CarouselProps>[]
|
||||
>({ orientation: "horizontal" }, props);
|
||||
|
||||
const [local, rest] = splitProps(merge, [
|
||||
"orientation",
|
||||
"opts",
|
||||
"setApi",
|
||||
"plugins",
|
||||
"class",
|
||||
"children",
|
||||
]);
|
||||
|
||||
const [carouselRef, api] = createEmblaCarousel(
|
||||
() => ({
|
||||
...local.opts,
|
||||
axis: local.orientation === "horizontal" ? "x" : "y",
|
||||
}),
|
||||
() => (local.plugins === undefined ? [] : local.plugins),
|
||||
);
|
||||
const [canScrollPrev, setCanScrollPrev] = createSignal(false);
|
||||
const [canScrollNext, setCanScrollNext] = createSignal(false);
|
||||
|
||||
const onSelect = (api: NonNullable<ReturnType<CarouselApi>>) => {
|
||||
setCanScrollPrev(api.canScrollPrev());
|
||||
setCanScrollNext(api.canScrollNext());
|
||||
};
|
||||
|
||||
const scrollPrev = () => api()?.scrollPrev();
|
||||
|
||||
const scrollNext = () => api()?.scrollNext();
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === "ArrowLeft") {
|
||||
event.preventDefault();
|
||||
scrollPrev();
|
||||
} else if (event.key === "ArrowRight") {
|
||||
event.preventDefault();
|
||||
scrollNext();
|
||||
}
|
||||
};
|
||||
|
||||
createEffect(() => {
|
||||
if (!api() || !local.setApi) return;
|
||||
|
||||
local.setApi(api);
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
const _api = api();
|
||||
if (_api === undefined) return;
|
||||
|
||||
onSelect(_api);
|
||||
_api.on("reInit", onSelect);
|
||||
_api.on("select", onSelect);
|
||||
|
||||
onCleanup(() => {
|
||||
_api.off("select", onSelect);
|
||||
});
|
||||
});
|
||||
|
||||
const value = createMemo(
|
||||
() =>
|
||||
({
|
||||
carouselRef,
|
||||
api,
|
||||
opts: local.opts,
|
||||
orientation:
|
||||
local.orientation ||
|
||||
(local.opts?.axis === "y" ? "vertical" : "horizontal"),
|
||||
scrollPrev,
|
||||
scrollNext,
|
||||
canScrollPrev,
|
||||
canScrollNext,
|
||||
}) satisfies CarouselContextProps,
|
||||
);
|
||||
|
||||
return (
|
||||
<CarouselContext.Provider value={value}>
|
||||
<div
|
||||
onKeyDown={handleKeyDown}
|
||||
class={cn("relative", local.class)}
|
||||
role="region"
|
||||
aria-roledescription="carousel"
|
||||
{...rest}
|
||||
>
|
||||
{local.children}
|
||||
</div>
|
||||
</CarouselContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const CarouselContent = (props: ComponentProps<"div">) => {
|
||||
const [local, rest] = splitProps(props, ["class"]);
|
||||
const { carouselRef, orientation } = useCarousel();
|
||||
|
||||
return (
|
||||
<div ref={carouselRef} class="overflow-hidden">
|
||||
<div
|
||||
class={cn(
|
||||
"flex",
|
||||
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const CarouselItem = (props: ComponentProps<"div">) => {
|
||||
const [local, rest] = splitProps(props, ["class"]);
|
||||
const { orientation } = useCarousel();
|
||||
|
||||
return (
|
||||
<div
|
||||
role="group"
|
||||
aria-roledescription="slide"
|
||||
class={cn(
|
||||
"min-w-0 shrink-0 grow-0 basis-full",
|
||||
orientation === "horizontal" ? "pl-4" : "pt-4",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const CarouselPrevious = (
|
||||
props: VoidProps<ComponentProps<typeof Button>>,
|
||||
) => {
|
||||
const merge = mergeProps<VoidProps<ComponentProps<typeof Button>[]>>(
|
||||
{ variant: "outline", size: "icon" },
|
||||
props,
|
||||
);
|
||||
const [local, rest] = splitProps(merge, ["class", "variant", "size"]);
|
||||
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant={local.variant}
|
||||
size={local.size}
|
||||
class={cn(
|
||||
"absolute h-8 w-8 touch-manipulation rounded-full",
|
||||
orientation === "horizontal"
|
||||
? "-left-12 top-1/2 -translate-y-1/2"
|
||||
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||
local.class,
|
||||
)}
|
||||
disabled={!canScrollPrev()}
|
||||
onClick={scrollPrev}
|
||||
{...rest}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
class="size-4"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M5 12h14M5 12l6 6m-6-6l6-6"
|
||||
/>
|
||||
<title>Previous slide</title>
|
||||
</svg>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export const CarouselNext = (
|
||||
props: VoidProps<ComponentProps<typeof Button>>,
|
||||
) => {
|
||||
const merge = mergeProps<VoidProps<ComponentProps<typeof Button>[]>>(
|
||||
{ variant: "outline", size: "icon" },
|
||||
props,
|
||||
);
|
||||
const [local, rest] = splitProps(merge, ["class", "variant", "size"]);
|
||||
const { orientation, scrollNext, canScrollNext } = useCarousel();
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant={local.variant}
|
||||
size={local.size}
|
||||
class={cn(
|
||||
"absolute h-8 w-8 touch-manipulation rounded-full",
|
||||
orientation === "horizontal"
|
||||
? "-right-12 top-1/2 -translate-y-1/2"
|
||||
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
|
||||
local.class,
|
||||
)}
|
||||
disabled={!canScrollNext()}
|
||||
onClick={scrollNext}
|
||||
{...rest}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
class="size-4"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M5 12h14m-4 4l4-4m-4-4l4 4"
|
||||
/>
|
||||
<title>Next slide</title>
|
||||
</svg>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
55
examples/astro-example/src/components/ui/checkbox.tsx
Normal file
55
examples/astro-example/src/components/ui/checkbox.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import { cn } from "@/libs/cn";
|
||||
import type { CheckboxControlProps } from "@kobalte/core/checkbox";
|
||||
import { Checkbox as CheckboxPrimitive } from "@kobalte/core/checkbox";
|
||||
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
|
||||
import type { ValidComponent, VoidProps } from "solid-js";
|
||||
import { splitProps } from "solid-js";
|
||||
|
||||
export const CheckboxLabel = CheckboxPrimitive.Label;
|
||||
export const Checkbox = CheckboxPrimitive;
|
||||
export const CheckboxErrorMessage = CheckboxPrimitive.ErrorMessage;
|
||||
export const CheckboxDescription = CheckboxPrimitive.Description;
|
||||
|
||||
type checkboxControlProps<T extends ValidComponent = "div"> = VoidProps<
|
||||
CheckboxControlProps<T> & { class?: string }
|
||||
>;
|
||||
|
||||
export const CheckboxControl = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, checkboxControlProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as checkboxControlProps, [
|
||||
"class",
|
||||
"children",
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CheckboxPrimitive.Input class="[&:focus-visible+div]:outline-none [&:focus-visible+div]:ring-[1.5px] [&:focus-visible+div]:ring-ring [&:focus-visible+div]:ring-offset-2 [&:focus-visible+div]:ring-offset-background" />
|
||||
<CheckboxPrimitive.Control
|
||||
class={cn(
|
||||
"h-4 w-4 shrink-0 rounded-sm border border-primary shadow transition-shadow focus-visible:outline-none focus-visible:ring-[1.5px] focus-visible:ring-ring data-[disabled]:cursor-not-allowed data-[checked]:bg-primary data-[checked]:text-primary-foreground data-[disabled]:opacity-50",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator class="flex items-center justify-center text-current">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
class="h-4 w-4"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="m5 12l5 5L20 7"
|
||||
/>
|
||||
<title>Checkbox</title>
|
||||
</svg>
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Control>
|
||||
</>
|
||||
);
|
||||
};
|
||||
31
examples/astro-example/src/components/ui/collapsible.tsx
Normal file
31
examples/astro-example/src/components/ui/collapsible.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { cn } from "@/libs/cn";
|
||||
import type { CollapsibleContentProps } from "@kobalte/core/collapsible";
|
||||
import { Collapsible as CollapsiblePrimitive } from "@kobalte/core/collapsible";
|
||||
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
|
||||
import type { ValidComponent } from "solid-js";
|
||||
import { splitProps } from "solid-js";
|
||||
|
||||
export const Collapsible = CollapsiblePrimitive;
|
||||
|
||||
export const CollapsibleTrigger = CollapsiblePrimitive.Trigger;
|
||||
|
||||
type collapsibleContentProps<T extends ValidComponent = "div"> =
|
||||
CollapsibleContentProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export const CollapsibleContent = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, collapsibleContentProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as collapsibleContentProps, ["class"]);
|
||||
|
||||
return (
|
||||
<CollapsiblePrimitive.Content
|
||||
class={cn(
|
||||
"animate-collapsible-up data-[expanded]:animate-collapsible-down",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
156
examples/astro-example/src/components/ui/combobox.tsx
Normal file
156
examples/astro-example/src/components/ui/combobox.tsx
Normal file
@@ -0,0 +1,156 @@
|
||||
import { cn } from "@/libs/cn";
|
||||
import type {
|
||||
ComboboxContentProps,
|
||||
ComboboxInputProps,
|
||||
ComboboxItemProps,
|
||||
ComboboxTriggerProps,
|
||||
} from "@kobalte/core/combobox";
|
||||
import { Combobox as ComboboxPrimitive } from "@kobalte/core/combobox";
|
||||
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
|
||||
import type { ParentProps, ValidComponent, VoidProps } from "solid-js";
|
||||
import { splitProps } from "solid-js";
|
||||
|
||||
export const Combobox = ComboboxPrimitive;
|
||||
export const ComboboxDescription = ComboboxPrimitive.Description;
|
||||
export const ComboboxErrorMessage = ComboboxPrimitive.ErrorMessage;
|
||||
export const ComboboxItemDescription = ComboboxPrimitive.ItemDescription;
|
||||
export const ComboboxHiddenSelect = ComboboxPrimitive.HiddenSelect;
|
||||
|
||||
type comboboxInputProps<T extends ValidComponent = "input"> = VoidProps<
|
||||
ComboboxInputProps<T> & {
|
||||
class?: string;
|
||||
}
|
||||
>;
|
||||
|
||||
export const ComboboxInput = <T extends ValidComponent = "input">(
|
||||
props: PolymorphicProps<T, comboboxInputProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as comboboxInputProps, ["class"]);
|
||||
|
||||
return (
|
||||
<ComboboxPrimitive.Input
|
||||
class={cn(
|
||||
"h-full bg-transparent text-sm placeholder:text-muted-foreground focus:outline-none disabled:cursor-not-allowed disabled:opacity-50",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type comboboxTriggerProps<T extends ValidComponent = "button"> = ParentProps<
|
||||
ComboboxTriggerProps<T> & {
|
||||
class?: string;
|
||||
}
|
||||
>;
|
||||
|
||||
export const ComboboxTrigger = <T extends ValidComponent = "button">(
|
||||
props: PolymorphicProps<T, comboboxTriggerProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as comboboxTriggerProps, [
|
||||
"class",
|
||||
"children",
|
||||
]);
|
||||
|
||||
return (
|
||||
<ComboboxPrimitive.Control>
|
||||
<ComboboxPrimitive.Trigger
|
||||
class={cn(
|
||||
"flex h-9 w-full items-center justify-between rounded-md border border-input px-3 shadow-sm",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{local.children}
|
||||
<ComboboxPrimitive.Icon class="flex h-3.5 w-3.5 items-center justify-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
class="h-4 w-4 opacity-50"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="m8 9l4-4l4 4m0 6l-4 4l-4-4"
|
||||
/>
|
||||
<title>Arrow</title>
|
||||
</svg>
|
||||
</ComboboxPrimitive.Icon>
|
||||
</ComboboxPrimitive.Trigger>
|
||||
</ComboboxPrimitive.Control>
|
||||
);
|
||||
};
|
||||
|
||||
type comboboxContentProps<T extends ValidComponent = "div"> =
|
||||
ComboboxContentProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export const ComboboxContent = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, comboboxContentProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as comboboxContentProps, ["class"]);
|
||||
|
||||
return (
|
||||
<ComboboxPrimitive.Portal>
|
||||
<ComboboxPrimitive.Content
|
||||
class={cn(
|
||||
"relative z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[expanded]:animate-in data-[closed]:animate-out data-[closed]:fade-out-0 data-[expanded]:fade-in-0 data-[closed]:zoom-out-95 data-[expanded]:zoom-in-95 origin-[--kb-combobox-content-transform-origin]",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
<ComboboxPrimitive.Listbox class="p-1" />
|
||||
</ComboboxPrimitive.Content>
|
||||
</ComboboxPrimitive.Portal>
|
||||
);
|
||||
};
|
||||
|
||||
type comboboxItemProps<T extends ValidComponent = "li"> = ParentProps<
|
||||
ComboboxItemProps<T> & {
|
||||
class?: string;
|
||||
}
|
||||
>;
|
||||
|
||||
export const ComboboxItem = <T extends ValidComponent = "li">(
|
||||
props: PolymorphicProps<T, comboboxItemProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as comboboxItemProps, [
|
||||
"class",
|
||||
"children",
|
||||
]);
|
||||
|
||||
return (
|
||||
<ComboboxPrimitive.Item
|
||||
class={cn(
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none data-[disabled]:pointer-events-none data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground data-[disabled]:opacity-50",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
<ComboboxPrimitive.ItemIndicator class="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
class="h-4 w-4"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="m5 12l5 5L20 7"
|
||||
/>
|
||||
<title>Checked</title>
|
||||
</svg>
|
||||
</ComboboxPrimitive.ItemIndicator>
|
||||
<ComboboxPrimitive.ItemLabel>
|
||||
{local.children}
|
||||
</ComboboxPrimitive.ItemLabel>
|
||||
</ComboboxPrimitive.Item>
|
||||
);
|
||||
};
|
||||
151
examples/astro-example/src/components/ui/command.tsx
Normal file
151
examples/astro-example/src/components/ui/command.tsx
Normal file
@@ -0,0 +1,151 @@
|
||||
import { cn } from "@/libs/cn";
|
||||
import type {
|
||||
CommandDialogProps,
|
||||
CommandEmptyProps,
|
||||
CommandGroupProps,
|
||||
CommandInputProps,
|
||||
CommandItemProps,
|
||||
CommandListProps,
|
||||
CommandRootProps,
|
||||
} from "cmdk-solid";
|
||||
import { Command as CommandPrimitive } from "cmdk-solid";
|
||||
import type { ComponentProps, VoidProps } from "solid-js";
|
||||
import { splitProps } from "solid-js";
|
||||
import { Dialog, DialogContent } from "./dialog";
|
||||
|
||||
export const Command = (props: CommandRootProps) => {
|
||||
const [local, rest] = splitProps(props, ["class"]);
|
||||
|
||||
return (
|
||||
<CommandPrimitive
|
||||
class={cn(
|
||||
"flex size-full flex-col overflow-hidden bg-popover text-popover-foreground",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const CommandList = (props: CommandListProps) => {
|
||||
const [local, rest] = splitProps(props, ["class"]);
|
||||
|
||||
return (
|
||||
<CommandPrimitive.List
|
||||
class={cn(
|
||||
"max-h-[300px] overflow-y-auto overflow-x-hidden p-1",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const CommandInput = (props: VoidProps<CommandInputProps>) => {
|
||||
const [local, rest] = splitProps(props, ["class"]);
|
||||
|
||||
return (
|
||||
<div class="flex items-center border-b px-3" cmdk-input-wrapper="">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
class="mr-2 h-4 w-4 shrink-0 opacity-50"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M3 10a7 7 0 1 0 14 0a7 7 0 1 0-14 0m18 11l-6-6"
|
||||
/>
|
||||
<title>Search</title>
|
||||
</svg>
|
||||
<CommandPrimitive.Input
|
||||
class={cn(
|
||||
"flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const CommandItem = (props: CommandItemProps) => {
|
||||
const [local, rest] = splitProps(props, ["class"]);
|
||||
|
||||
return (
|
||||
<CommandPrimitive.Item
|
||||
class={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-disabled:pointer-events-none aria-disabled:opacity-50 aria-selected:bg-accent aria-selected:text-accent-foreground",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const CommandShortcut = (props: ComponentProps<"span">) => {
|
||||
const [local, rest] = splitProps(props, ["class"]);
|
||||
|
||||
return (
|
||||
<span
|
||||
class={cn(
|
||||
"ml-auto text-xs tracking-widest text-muted-foreground",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const CommandDialog = (props: CommandDialogProps) => {
|
||||
const [local, rest] = splitProps(props, ["children"]);
|
||||
|
||||
return (
|
||||
<Dialog {...rest}>
|
||||
<DialogContent class="overflow-hidden p-0">
|
||||
<Command class="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:size-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:size-5">
|
||||
{local.children}
|
||||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export const CommandEmpty = (props: CommandEmptyProps) => {
|
||||
const [local, rest] = splitProps(props, ["class"]);
|
||||
|
||||
return (
|
||||
<CommandPrimitive.Empty
|
||||
class={cn("py-6 text-center text-sm", local.class)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const CommandGroup = (props: CommandGroupProps) => {
|
||||
const [local, rest] = splitProps(props, ["class"]);
|
||||
|
||||
return (
|
||||
<CommandPrimitive.Group
|
||||
class={cn(
|
||||
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const CommandSeparator = (props: CommandEmptyProps) => {
|
||||
const [local, rest] = splitProps(props, ["class"]);
|
||||
|
||||
return (
|
||||
<CommandPrimitive.Separator
|
||||
class={cn("-mx-1 h-px bg-border", local.class)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
327
examples/astro-example/src/components/ui/context-menu.tsx
Normal file
327
examples/astro-example/src/components/ui/context-menu.tsx
Normal file
@@ -0,0 +1,327 @@
|
||||
import { cn } from "@/libs/cn";
|
||||
import type {
|
||||
ContextMenuCheckboxItemProps,
|
||||
ContextMenuContentProps,
|
||||
ContextMenuGroupLabelProps,
|
||||
ContextMenuItemLabelProps,
|
||||
ContextMenuItemProps,
|
||||
ContextMenuRadioItemProps,
|
||||
ContextMenuSeparatorProps,
|
||||
ContextMenuSubContentProps,
|
||||
ContextMenuSubTriggerProps,
|
||||
} from "@kobalte/core/context-menu";
|
||||
import { ContextMenu as ContextMenuPrimitive } from "@kobalte/core/context-menu";
|
||||
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
|
||||
import type {
|
||||
ComponentProps,
|
||||
ParentProps,
|
||||
ValidComponent,
|
||||
VoidProps,
|
||||
} from "solid-js";
|
||||
import { splitProps } from "solid-js";
|
||||
|
||||
export const ContextMenu = ContextMenuPrimitive;
|
||||
export const ContextMenuTrigger = ContextMenuPrimitive.Trigger;
|
||||
export const ContextMenuGroup = ContextMenuPrimitive.Group;
|
||||
export const ContextMenuSub = ContextMenuPrimitive.Sub;
|
||||
export const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;
|
||||
|
||||
type contextMenuSubTriggerProps<T extends ValidComponent = "div"> = ParentProps<
|
||||
ContextMenuSubTriggerProps<T> & {
|
||||
class?: string;
|
||||
inset?: boolean;
|
||||
}
|
||||
>;
|
||||
|
||||
export const ContextMenuSubTrigger = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, contextMenuSubTriggerProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as contextMenuSubTriggerProps, [
|
||||
"class",
|
||||
"children",
|
||||
"inset",
|
||||
]);
|
||||
|
||||
return (
|
||||
<ContextMenuPrimitive.SubTrigger
|
||||
class={cn(
|
||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[expanded]:bg-accent data-[expanded]:text-accent-foreground",
|
||||
local.inset && "pl-8",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{local.children}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="ml-auto h-4 w-4"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="m9 6l6 6l-6 6"
|
||||
/>
|
||||
<title>Arrow</title>
|
||||
</svg>
|
||||
</ContextMenuPrimitive.SubTrigger>
|
||||
);
|
||||
};
|
||||
|
||||
type contextMenuSubContentProps<T extends ValidComponent = "div"> =
|
||||
ContextMenuSubContentProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export const ContextMenuSubContent = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, contextMenuSubContentProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as contextMenuSubContentProps, [
|
||||
"class",
|
||||
]);
|
||||
|
||||
return (
|
||||
<ContextMenuPrimitive.Portal>
|
||||
<ContextMenuPrimitive.SubContent
|
||||
class={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[expanded]:animate-in data-[closed]:animate-out data-[closed]:fade-out-0 data-[expanded]:fade-in-0 data-[closed]:zoom-out-95 data-[expanded]:zoom-in-95",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
</ContextMenuPrimitive.Portal>
|
||||
);
|
||||
};
|
||||
|
||||
type contextMenuContentProps<T extends ValidComponent = "div"> =
|
||||
ContextMenuContentProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export const ContextMenuContent = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, contextMenuContentProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as contextMenuContentProps, ["class"]);
|
||||
|
||||
return (
|
||||
<ContextMenuPrimitive.Portal>
|
||||
<ContextMenuPrimitive.Content
|
||||
class={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md transition-shadow focus-visible:outline-none focus-visible:ring-[1.5px] focus-visible:ring-ring data-[expanded]:animate-in data-[closed]:animate-out data-[closed]:fade-out-0 data-[expanded]:fade-in-0 data-[closed]:zoom-out-95 data-[expanded]:zoom-in-95",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
</ContextMenuPrimitive.Portal>
|
||||
);
|
||||
};
|
||||
|
||||
type contextMenuItemProps<T extends ValidComponent = "div"> =
|
||||
ContextMenuItemProps<T> & {
|
||||
class?: string;
|
||||
inset?: boolean;
|
||||
};
|
||||
|
||||
export const ContextMenuItem = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, contextMenuItemProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as contextMenuItemProps, [
|
||||
"class",
|
||||
"inset",
|
||||
]);
|
||||
|
||||
return (
|
||||
<ContextMenuPrimitive.Item
|
||||
class={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
local.inset && "pl-8",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type contextMenuCheckboxItemProps<T extends ValidComponent = "div"> =
|
||||
ParentProps<
|
||||
ContextMenuCheckboxItemProps<T> & {
|
||||
class?: string;
|
||||
}
|
||||
>;
|
||||
|
||||
export const ContextMenuCheckboxItem = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, contextMenuCheckboxItemProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as contextMenuCheckboxItemProps, [
|
||||
"class",
|
||||
"children",
|
||||
]);
|
||||
|
||||
return (
|
||||
<ContextMenuPrimitive.CheckboxItem
|
||||
class={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
<ContextMenuPrimitive.ItemIndicator class="absolute left-2 inline-flex h-3.5 w-3.5 items-center justify-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
class="h-4 w-4"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="m5 12l5 5L20 7"
|
||||
/>
|
||||
<title>Checkbox</title>
|
||||
</svg>
|
||||
</ContextMenuPrimitive.ItemIndicator>
|
||||
{local.children}
|
||||
</ContextMenuPrimitive.CheckboxItem>
|
||||
);
|
||||
};
|
||||
|
||||
type contextMenuRadioItemProps<T extends ValidComponent = "div"> = ParentProps<
|
||||
ContextMenuRadioItemProps<T> & {
|
||||
class?: string;
|
||||
}
|
||||
>;
|
||||
|
||||
export const ContextMenuRadioItem = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, contextMenuRadioItemProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as contextMenuRadioItemProps, [
|
||||
"class",
|
||||
"children",
|
||||
]);
|
||||
|
||||
return (
|
||||
<ContextMenuPrimitive.RadioItem
|
||||
class={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
<ContextMenuPrimitive.ItemIndicator class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
class="h-2 w-2"
|
||||
>
|
||||
<g
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M0 0h24v24H0z" />
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M7 3.34a10 10 0 1 1-4.995 8.984L2 12l.005-.324A10 10 0 0 1 7 3.34"
|
||||
/>
|
||||
</g>
|
||||
<title>Radio</title>
|
||||
</svg>
|
||||
</ContextMenuPrimitive.ItemIndicator>
|
||||
{local.children}
|
||||
</ContextMenuPrimitive.RadioItem>
|
||||
);
|
||||
};
|
||||
|
||||
type contextMenuItemLabelProps<T extends ValidComponent = "div"> =
|
||||
ContextMenuItemLabelProps<T> & {
|
||||
class?: string;
|
||||
inset?: boolean;
|
||||
};
|
||||
|
||||
export const ContextMenuItemLabel = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, contextMenuItemLabelProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as contextMenuItemLabelProps, [
|
||||
"class",
|
||||
"inset",
|
||||
]);
|
||||
|
||||
return (
|
||||
<ContextMenuPrimitive.ItemLabel
|
||||
class={cn(
|
||||
"px-2 py-1.5 text-sm font-semibold text-foreground",
|
||||
local.inset && "pl-8",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type contextMenuGroupLabelProps<T extends ValidComponent = "span"> =
|
||||
ContextMenuGroupLabelProps<T> & {
|
||||
class?: string;
|
||||
inset?: boolean;
|
||||
};
|
||||
|
||||
export const ContextMenuGroupLabel = <T extends ValidComponent = "span">(
|
||||
props: PolymorphicProps<T, contextMenuGroupLabelProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as contextMenuGroupLabelProps, [
|
||||
"class",
|
||||
"inset",
|
||||
]);
|
||||
|
||||
return (
|
||||
<ContextMenuPrimitive.GroupLabel
|
||||
as="div"
|
||||
class={cn(
|
||||
"px-2 py-1.5 text-sm font-semibold text-foreground",
|
||||
local.inset && "pl-8",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type contextMenuSeparatorProps<T extends ValidComponent = "hr"> = VoidProps<
|
||||
ContextMenuSeparatorProps<T> & {
|
||||
class?: string;
|
||||
}
|
||||
>;
|
||||
|
||||
export const ContextMenuSeparator = <T extends ValidComponent = "hr">(
|
||||
props: PolymorphicProps<T, contextMenuSeparatorProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as contextMenuSeparatorProps, [
|
||||
"class",
|
||||
]);
|
||||
|
||||
return (
|
||||
<ContextMenuPrimitive.Separator
|
||||
class={cn("-mx-1 my-1 h-px bg-border", local.class)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const ContextMenuShortcut = (props: ComponentProps<"span">) => {
|
||||
const [local, rest] = splitProps(props, ["class"]);
|
||||
|
||||
return (
|
||||
<span
|
||||
class={cn(
|
||||
"ml-auto text-xs tracking-widest text-muted-foreground",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
269
examples/astro-example/src/components/ui/date-picker.tsx
Normal file
269
examples/astro-example/src/components/ui/date-picker.tsx
Normal file
@@ -0,0 +1,269 @@
|
||||
import { cn } from "@/libs/cn";
|
||||
import type {
|
||||
DatePickerContentProps,
|
||||
DatePickerInputProps,
|
||||
DatePickerRangeTextProps,
|
||||
DatePickerRootProps,
|
||||
DatePickerTableCellProps,
|
||||
DatePickerTableCellTriggerProps,
|
||||
DatePickerTableHeaderProps,
|
||||
DatePickerTableProps,
|
||||
DatePickerTableRowProps,
|
||||
DatePickerViewControlProps,
|
||||
DatePickerViewProps,
|
||||
DatePickerViewTriggerProps,
|
||||
} from "@ark-ui/solid";
|
||||
import { DatePicker as DatePickerPrimitive } from "@ark-ui/solid";
|
||||
import type { VoidProps } from "solid-js";
|
||||
import { splitProps } from "solid-js";
|
||||
import { buttonVariants } from "./button";
|
||||
|
||||
export const DatePickerLabel = DatePickerPrimitive.Label;
|
||||
export const DatePickerTableHead = DatePickerPrimitive.TableHead;
|
||||
export const DatePickerTableBody = DatePickerPrimitive.TableBody;
|
||||
export const DatePickerClearTrigger = DatePickerPrimitive.ClearTrigger;
|
||||
export const DatePickerYearSelect = DatePickerPrimitive.YearSelect;
|
||||
export const DatePickerMonthSelect = DatePickerPrimitive.MonthSelect;
|
||||
export const DatePickerContext = DatePickerPrimitive.Context;
|
||||
export const DatePickerRootProvider = DatePickerPrimitive.RootProvider;
|
||||
|
||||
export const DatePicker = (props: DatePickerRootProps) => {
|
||||
return (
|
||||
<DatePickerPrimitive.Root
|
||||
format={(e) => {
|
||||
const parsedDate = new Date(Date.parse(e.toString()));
|
||||
|
||||
const normalizedDate = new Date(
|
||||
parsedDate.getUTCFullYear(),
|
||||
parsedDate.getUTCMonth(),
|
||||
parsedDate.getUTCDate(),
|
||||
);
|
||||
|
||||
return new Intl.DateTimeFormat("en-US", {
|
||||
dateStyle: "long",
|
||||
}).format(normalizedDate);
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const DatePickerView = (props: DatePickerViewProps) => {
|
||||
const [local, rest] = splitProps(props, ["class"]);
|
||||
|
||||
return (
|
||||
<DatePickerPrimitive.View class={cn("space-y-4", local.class)} {...rest} />
|
||||
);
|
||||
};
|
||||
|
||||
export const DatePickerViewControl = (props: DatePickerViewControlProps) => {
|
||||
const [local, rest] = splitProps(props, ["class", "children"]);
|
||||
|
||||
return (
|
||||
<DatePickerPrimitive.ViewControl
|
||||
class={cn("flex items-center justify-between", local.class)}
|
||||
{...rest}
|
||||
>
|
||||
<DatePickerPrimitive.PrevTrigger
|
||||
class={cn(
|
||||
buttonVariants({
|
||||
variant: "outline",
|
||||
}),
|
||||
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100",
|
||||
)}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="m15 6l-6 6l6 6"
|
||||
/>
|
||||
<title>Previous</title>
|
||||
</svg>
|
||||
</DatePickerPrimitive.PrevTrigger>
|
||||
{local.children}
|
||||
<DatePickerPrimitive.NextTrigger
|
||||
class={cn(
|
||||
buttonVariants({
|
||||
variant: "outline",
|
||||
}),
|
||||
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100",
|
||||
)}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="m9 6l6 6l-6 6"
|
||||
/>
|
||||
<title>Next</title>
|
||||
</svg>
|
||||
</DatePickerPrimitive.NextTrigger>
|
||||
</DatePickerPrimitive.ViewControl>
|
||||
);
|
||||
};
|
||||
|
||||
export const DatePickerRangeText = (
|
||||
props: VoidProps<DatePickerRangeTextProps>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props, ["class"]);
|
||||
|
||||
return (
|
||||
<DatePickerPrimitive.RangeText
|
||||
class={cn("text-sm font-medium", local.class)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const DatePickerTable = (props: DatePickerTableProps) => {
|
||||
const [local, rest] = splitProps(props, ["class"]);
|
||||
|
||||
return (
|
||||
<DatePickerPrimitive.Table
|
||||
class={cn("w-full border-collapse space-y-1", local.class)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const DatePickerTableRow = (props: DatePickerTableRowProps) => {
|
||||
const [local, rest] = splitProps(props, ["class"]);
|
||||
|
||||
return (
|
||||
<DatePickerPrimitive.TableRow
|
||||
class={cn("mt-2 flex w-full", local.class)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const DatePickerTableHeader = (props: DatePickerTableHeaderProps) => {
|
||||
const [local, rest] = splitProps(props, ["class"]);
|
||||
|
||||
return (
|
||||
<DatePickerPrimitive.TableHeader
|
||||
class={cn(
|
||||
"w-8 flex-1 text-[0.8rem] font-normal text-muted-foreground",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const DatePickerTableCell = (props: DatePickerTableCellProps) => {
|
||||
const [local, rest] = splitProps(props, ["class"]);
|
||||
|
||||
return (
|
||||
<DatePickerPrimitive.TableCell
|
||||
class={cn(
|
||||
"flex-1 p-0 text-center text-sm",
|
||||
"has-[[data-in-range]]:bg-accent has-[[data-in-range]]:first-of-type:rounded-l-md has-[[data-in-range]]:last-of-type:rounded-r-md",
|
||||
"has-[[data-range-end]]:rounded-r-md has-[[data-range-start]]:rounded-l-md",
|
||||
"has-[[data-outside-range][data-in-range]]:bg-accent/50",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const DatePickerTableCellTrigger = (
|
||||
props: DatePickerTableCellTriggerProps,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props, ["class"]);
|
||||
|
||||
return (
|
||||
<DatePickerPrimitive.TableCellTrigger
|
||||
class={cn(
|
||||
buttonVariants({ variant: "ghost" }),
|
||||
"size-8 w-full p-0 font-normal data-[selected]:opacity-100",
|
||||
"data-[today]:bg-accent data-[today]:text-accent-foreground",
|
||||
"[&:is([data-today][data-selected])]:bg-primary [&:is([data-today][data-selected])]:text-primary-foreground",
|
||||
"data-[selected]:bg-primary data-[selected]:text-primary-foreground data-[selected]:hover:bg-primary data-[selected]:hover:text-primary-foreground",
|
||||
"data-[disabled]:text-muted-foreground data-[disabled]:opacity-50",
|
||||
"data-[outside-range]:text-muted-foreground data-[outside-range]:opacity-50",
|
||||
"[&:is([data-outside-range][data-in-range])]:bg-accent/50 [&:is([data-outside-range][data-in-range])]:text-muted-foreground [&:is([data-outside-range][data-in-range])]:opacity-30",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const DatePickerViewTrigger = (props: DatePickerViewTriggerProps) => {
|
||||
const [local, rest] = splitProps(props, ["class"]);
|
||||
|
||||
return (
|
||||
<DatePickerPrimitive.ViewTrigger
|
||||
class={cn(buttonVariants({ variant: "ghost" }), "h-7", local.class)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const DatePickerContent = (props: DatePickerContentProps) => {
|
||||
const [local, rest] = splitProps(props, ["class", "children"]);
|
||||
|
||||
return (
|
||||
<DatePickerPrimitive.Positioner>
|
||||
<DatePickerPrimitive.Content
|
||||
class={cn(
|
||||
"rounded-md border bg-popover p-3 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 z-50",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{local.children}
|
||||
</DatePickerPrimitive.Content>
|
||||
</DatePickerPrimitive.Positioner>
|
||||
);
|
||||
};
|
||||
|
||||
export const DatePickerInput = (props: DatePickerInputProps) => {
|
||||
const [local, rest] = splitProps(props, ["class", "children"]);
|
||||
|
||||
return (
|
||||
<DatePickerPrimitive.Control class="flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-[1.5px] focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50">
|
||||
<DatePickerPrimitive.Input
|
||||
class={cn(
|
||||
"w-full appearance-none bg-transparent outline-none",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
<DatePickerPrimitive.Trigger class="transition-shadow focus-visible:outline-none focus-visible:ring-[1.5px] focus-visible:ring-ring">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="mx-1 h-4 w-4"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 7a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2zm12-4v4M8 3v4m-4 4h16m-9 4h1m0 0v3"
|
||||
/>
|
||||
<title>Calendar</title>
|
||||
</svg>
|
||||
</DatePickerPrimitive.Trigger>
|
||||
</DatePickerPrimitive.Control>
|
||||
);
|
||||
};
|
||||
128
examples/astro-example/src/components/ui/dialog.tsx
Normal file
128
examples/astro-example/src/components/ui/dialog.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
import { cn } from "@/libs/cn";
|
||||
import type {
|
||||
DialogContentProps,
|
||||
DialogDescriptionProps,
|
||||
DialogTitleProps,
|
||||
} from "@kobalte/core/dialog";
|
||||
import { Dialog as DialogPrimitive } from "@kobalte/core/dialog";
|
||||
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
|
||||
import type { ComponentProps, ParentProps, ValidComponent } from "solid-js";
|
||||
import { splitProps } from "solid-js";
|
||||
|
||||
export const Dialog = DialogPrimitive;
|
||||
export const DialogTrigger = DialogPrimitive.Trigger;
|
||||
|
||||
type dialogContentProps<T extends ValidComponent = "div"> = ParentProps<
|
||||
DialogContentProps<T> & {
|
||||
class?: string;
|
||||
}
|
||||
>;
|
||||
|
||||
export const DialogContent = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, dialogContentProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as dialogContentProps, [
|
||||
"class",
|
||||
"children",
|
||||
]);
|
||||
|
||||
return (
|
||||
<DialogPrimitive.Portal>
|
||||
<DialogPrimitive.Overlay
|
||||
class={cn(
|
||||
"fixed inset-0 z-50 bg-background/80 data-[expanded]:animate-in data-[closed]:animate-out data-[closed]:fade-out-0 data-[expanded]:fade-in-0",
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
<DialogPrimitive.Content
|
||||
class={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg data-[closed]:duration-200 data-[expanded]:duration-200 data-[expanded]:animate-in data-[closed]:animate-out data-[closed]:fade-out-0 data-[expanded]:fade-in-0 data-[closed]:zoom-out-95 data-[expanded]:zoom-in-95 data-[closed]:slide-out-to-left-1/2 data-[closed]:slide-out-to-top-[48%] data-[expanded]:slide-in-from-left-1/2 data-[expanded]:slide-in-from-top-[48%] sm:rounded-lg md:w-full",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{local.children}
|
||||
<DialogPrimitive.CloseButton class="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-[opacity,box-shadow] hover:opacity-100 focus:outline-none focus:ring-[1.5px] focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
class="h-4 w-4"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M18 6L6 18M6 6l12 12"
|
||||
/>
|
||||
<title>Close</title>
|
||||
</svg>
|
||||
</DialogPrimitive.CloseButton>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPrimitive.Portal>
|
||||
);
|
||||
};
|
||||
|
||||
type dialogTitleProps<T extends ValidComponent = "h2"> = DialogTitleProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export const DialogTitle = <T extends ValidComponent = "h2">(
|
||||
props: PolymorphicProps<T, dialogTitleProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as dialogTitleProps, ["class"]);
|
||||
|
||||
return (
|
||||
<DialogPrimitive.Title
|
||||
class={cn("text-lg font-semibold text-foreground", local.class)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type dialogDescriptionProps<T extends ValidComponent = "p"> =
|
||||
DialogDescriptionProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export const DialogDescription = <T extends ValidComponent = "p">(
|
||||
props: PolymorphicProps<T, dialogDescriptionProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as dialogDescriptionProps, ["class"]);
|
||||
|
||||
return (
|
||||
<DialogPrimitive.Description
|
||||
class={cn("text-sm text-muted-foreground", local.class)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const DialogHeader = (props: ComponentProps<"div">) => {
|
||||
const [local, rest] = splitProps(props, ["class"]);
|
||||
|
||||
return (
|
||||
<div
|
||||
class={cn(
|
||||
"flex flex-col space-y-2 text-center sm:text-left",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const DialogFooter = (props: ComponentProps<"div">) => {
|
||||
const [local, rest] = splitProps(props, ["class"]);
|
||||
|
||||
return (
|
||||
<div
|
||||
class={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
107
examples/astro-example/src/components/ui/drawer.tsx
Normal file
107
examples/astro-example/src/components/ui/drawer.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import { cn } from "@/libs/cn";
|
||||
import type {
|
||||
ContentProps,
|
||||
DescriptionProps,
|
||||
DynamicProps,
|
||||
LabelProps,
|
||||
} from "@corvu/drawer";
|
||||
import DrawerPrimitive from "@corvu/drawer";
|
||||
import type { ComponentProps, ParentProps, ValidComponent } from "solid-js";
|
||||
import { splitProps } from "solid-js";
|
||||
|
||||
export const Drawer = DrawerPrimitive;
|
||||
export const DrawerTrigger = DrawerPrimitive.Trigger;
|
||||
export const DrawerClose = DrawerPrimitive.Close;
|
||||
|
||||
type drawerContentProps<T extends ValidComponent = "div"> = ParentProps<
|
||||
ContentProps<T> & {
|
||||
class?: string;
|
||||
}
|
||||
>;
|
||||
|
||||
export const DrawerContent = <T extends ValidComponent = "div">(
|
||||
props: DynamicProps<T, drawerContentProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as drawerContentProps, [
|
||||
"class",
|
||||
"children",
|
||||
]);
|
||||
const ctx = DrawerPrimitive.useContext();
|
||||
|
||||
return (
|
||||
<DrawerPrimitive.Portal>
|
||||
<DrawerPrimitive.Overlay
|
||||
class="fixed inset-0 z-50 data-[transitioning]:transition-colors data-[transitioning]:duration-200"
|
||||
style={{
|
||||
"background-color": `hsl(var(--background) / ${0.8 * ctx.openPercentage()})`,
|
||||
}}
|
||||
/>
|
||||
<DrawerPrimitive.Content
|
||||
class={cn(
|
||||
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-xl border bg-background after:absolute after:inset-x-0 after:top-full after:h-[50%] after:bg-inherit data-[transitioning]:transition-transform data-[transitioning]:duration-200 md:select-none",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
<div class="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
|
||||
{local.children}
|
||||
</DrawerPrimitive.Content>
|
||||
</DrawerPrimitive.Portal>
|
||||
);
|
||||
};
|
||||
|
||||
export const DrawerHeader = (props: ComponentProps<"div">) => {
|
||||
const [local, rest] = splitProps(props, ["class"]);
|
||||
|
||||
return (
|
||||
<div
|
||||
class={cn("grid gap-1.5 p-4 text-center sm:text-left", local.class)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const DrawerFooter = (props: ComponentProps<"div">) => {
|
||||
const [local, rest] = splitProps(props, ["class"]);
|
||||
|
||||
return (
|
||||
<div class={cn("mt-auto flex flex-col gap-2 p-4", local.class)} {...rest} />
|
||||
);
|
||||
};
|
||||
|
||||
type DrawerLabelProps = LabelProps & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export const DrawerLabel = <T extends ValidComponent = "h2">(
|
||||
props: DynamicProps<T, DrawerLabelProps>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as DrawerLabelProps, ["class"]);
|
||||
|
||||
return (
|
||||
<DrawerPrimitive.Label
|
||||
class={cn(
|
||||
"text-lg font-semibold leading-none tracking-tight",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type DrawerDescriptionProps = DescriptionProps & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export const DrawerDescription = <T extends ValidComponent = "p">(
|
||||
props: DynamicProps<T, DrawerDescriptionProps>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as DrawerDescriptionProps, ["class"]);
|
||||
|
||||
return (
|
||||
<DrawerPrimitive.Description
|
||||
class={cn("text-sm text-muted-foreground", local.class)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
314
examples/astro-example/src/components/ui/dropdown-menu.tsx
Normal file
314
examples/astro-example/src/components/ui/dropdown-menu.tsx
Normal file
@@ -0,0 +1,314 @@
|
||||
import { cn } from "@/libs/cn";
|
||||
import type {
|
||||
DropdownMenuCheckboxItemProps,
|
||||
DropdownMenuContentProps,
|
||||
DropdownMenuGroupLabelProps,
|
||||
DropdownMenuItemLabelProps,
|
||||
DropdownMenuItemProps,
|
||||
DropdownMenuRadioItemProps,
|
||||
DropdownMenuRootProps,
|
||||
DropdownMenuSeparatorProps,
|
||||
DropdownMenuSubTriggerProps,
|
||||
} from "@kobalte/core/dropdown-menu";
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "@kobalte/core/dropdown-menu";
|
||||
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
|
||||
import type { ComponentProps, ParentProps, ValidComponent } from "solid-js";
|
||||
import { mergeProps, splitProps } from "solid-js";
|
||||
|
||||
export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
||||
export const DropdownMenuGroup = DropdownMenuPrimitive.Group;
|
||||
export const DropdownMenuSub = DropdownMenuPrimitive.Sub;
|
||||
export const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
|
||||
|
||||
export const DropdownMenu = (props: DropdownMenuRootProps) => {
|
||||
const merge = mergeProps<DropdownMenuRootProps[]>({ gutter: 4 }, props);
|
||||
|
||||
return <DropdownMenuPrimitive {...merge} />;
|
||||
};
|
||||
|
||||
type dropdownMenuContentProps<T extends ValidComponent = "div"> =
|
||||
DropdownMenuContentProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export const DropdownMenuContent = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, dropdownMenuContentProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as dropdownMenuContentProps, [
|
||||
"class",
|
||||
]);
|
||||
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
class={cn(
|
||||
"min-w-8rem z-50 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md transition-shadow focus-visible:outline-none focus-visible:ring-[1.5px] focus-visible:ring-ring data-[expanded]:animate-in data-[closed]:animate-out data-[closed]:fade-out-0 data-[expanded]:fade-in-0 data-[closed]:zoom-out-95 data-[expanded]:zoom-in-95",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
);
|
||||
};
|
||||
|
||||
type dropdownMenuItemProps<T extends ValidComponent = "div"> =
|
||||
DropdownMenuItemProps<T> & {
|
||||
class?: string;
|
||||
inset?: boolean;
|
||||
};
|
||||
|
||||
export const DropdownMenuItem = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, dropdownMenuItemProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as dropdownMenuItemProps, [
|
||||
"class",
|
||||
"inset",
|
||||
]);
|
||||
|
||||
return (
|
||||
<DropdownMenuPrimitive.Item
|
||||
class={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
local.inset && "pl-8",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type dropdownMenuGroupLabelProps<T extends ValidComponent = "span"> =
|
||||
DropdownMenuGroupLabelProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export const DropdownMenuGroupLabel = <T extends ValidComponent = "span">(
|
||||
props: PolymorphicProps<T, dropdownMenuGroupLabelProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as dropdownMenuGroupLabelProps, [
|
||||
"class",
|
||||
]);
|
||||
|
||||
return (
|
||||
<DropdownMenuPrimitive.GroupLabel
|
||||
as="div"
|
||||
class={cn("px-2 py-1.5 text-sm font-semibold", local.class)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type dropdownMenuItemLabelProps<T extends ValidComponent = "div"> =
|
||||
DropdownMenuItemLabelProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export const DropdownMenuItemLabel = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, dropdownMenuItemLabelProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as dropdownMenuItemLabelProps, [
|
||||
"class",
|
||||
]);
|
||||
|
||||
return (
|
||||
<DropdownMenuPrimitive.ItemLabel
|
||||
as="div"
|
||||
class={cn("px-2 py-1.5 text-sm font-semibold", local.class)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type dropdownMenuSeparatorProps<T extends ValidComponent = "hr"> =
|
||||
DropdownMenuSeparatorProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export const DropdownMenuSeparator = <T extends ValidComponent = "hr">(
|
||||
props: PolymorphicProps<T, dropdownMenuSeparatorProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as dropdownMenuSeparatorProps, [
|
||||
"class",
|
||||
]);
|
||||
|
||||
return (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
class={cn("-mx-1 my-1 h-px bg-muted", local.class)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const DropdownMenuShortcut = (props: ComponentProps<"span">) => {
|
||||
const [local, rest] = splitProps(props, ["class"]);
|
||||
|
||||
return (
|
||||
<span
|
||||
class={cn("ml-auto text-xs tracking-widest opacity-60", local.class)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type dropdownMenuSubTriggerProps<T extends ValidComponent = "div"> =
|
||||
ParentProps<
|
||||
DropdownMenuSubTriggerProps<T> & {
|
||||
class?: string;
|
||||
}
|
||||
>;
|
||||
|
||||
export const DropdownMenuSubTrigger = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, dropdownMenuSubTriggerProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as dropdownMenuSubTriggerProps, [
|
||||
"class",
|
||||
"children",
|
||||
]);
|
||||
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
class={cn(
|
||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[expanded]:bg-accent",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{local.children}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
class="ml-auto h-4 w-4"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="m9 6l6 6l-6 6"
|
||||
/>
|
||||
<title>Arrow</title>
|
||||
</svg>
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
);
|
||||
};
|
||||
|
||||
type dropdownMenuSubContentProps<T extends ValidComponent = "div"> =
|
||||
DropdownMenuSubTriggerProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export const DropdownMenuSubContent = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, dropdownMenuSubContentProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as dropdownMenuSubContentProps, [
|
||||
"class",
|
||||
]);
|
||||
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
class={cn(
|
||||
"min-w-8rem z-50 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[expanded]:animate-in data-[closed]:animate-out data-[closed]:fade-out-0 data-[expanded]:fade-in-0 data-[closed]:zoom-out-95 data-[expanded]:zoom-in-95",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
);
|
||||
};
|
||||
|
||||
type dropdownMenuCheckboxItemProps<T extends ValidComponent = "div"> =
|
||||
ParentProps<
|
||||
DropdownMenuCheckboxItemProps<T> & {
|
||||
class?: string;
|
||||
}
|
||||
>;
|
||||
|
||||
export const DropdownMenuCheckboxItem = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, dropdownMenuCheckboxItemProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as dropdownMenuCheckboxItemProps, [
|
||||
"class",
|
||||
"children",
|
||||
]);
|
||||
|
||||
return (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
class={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
<DropdownMenuPrimitive.ItemIndicator class="absolute left-2 inline-flex h-4 w-4 items-center justify-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
class="h-4 w-4"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="m5 12l5 5L20 7"
|
||||
/>
|
||||
<title>Checkbox</title>
|
||||
</svg>
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
{props.children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
);
|
||||
};
|
||||
|
||||
type dropdownMenuRadioItemProps<T extends ValidComponent = "div"> = ParentProps<
|
||||
DropdownMenuRadioItemProps<T> & {
|
||||
class?: string;
|
||||
}
|
||||
>;
|
||||
|
||||
export const DropdownMenuRadioItem = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, dropdownMenuRadioItemProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as dropdownMenuRadioItemProps, [
|
||||
"class",
|
||||
"children",
|
||||
]);
|
||||
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
class={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
<DropdownMenuPrimitive.ItemIndicator class="absolute left-2 inline-flex h-4 w-4 items-center justify-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
class="h-2 w-2"
|
||||
>
|
||||
<g
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M0 0h24v24H0z" />
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M7 3.34a10 10 0 1 1-4.995 8.984L2 12l.005-.324A10 10 0 0 1 7 3.34"
|
||||
/>
|
||||
</g>
|
||||
<title>Radio</title>
|
||||
</svg>
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
{props.children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
);
|
||||
};
|
||||
32
examples/astro-example/src/components/ui/hover-card.tsx
Normal file
32
examples/astro-example/src/components/ui/hover-card.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { cn } from "@/libs/cn";
|
||||
import type { HoverCardContentProps } from "@kobalte/core/hover-card";
|
||||
import { HoverCard as HoverCardPrimitive } from "@kobalte/core/hover-card";
|
||||
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
|
||||
import type { ValidComponent } from "solid-js";
|
||||
import { splitProps } from "solid-js";
|
||||
|
||||
export const HoverCard = HoverCardPrimitive;
|
||||
export const HoverCardTrigger = HoverCardPrimitive.Trigger;
|
||||
|
||||
type hoverCardContentProps<T extends ValidComponent = "div"> =
|
||||
HoverCardContentProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export const HoverCardContent = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, hoverCardContentProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as hoverCardContentProps, ["class"]);
|
||||
|
||||
return (
|
||||
<HoverCardPrimitive.Portal>
|
||||
<HoverCardPrimitive.Content
|
||||
class={cn(
|
||||
"z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[expanded]:animate-in data-[closed]:animate-out data-[closed]:fade-out-0 data-[expanded]:fade-in-0 data-[closed]:zoom-out-95 data-[expanded]:zoom-in-95",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
</HoverCardPrimitive.Portal>
|
||||
);
|
||||
};
|
||||
68
examples/astro-example/src/components/ui/image.tsx
Normal file
68
examples/astro-example/src/components/ui/image.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import { cn } from "@/libs/cn";
|
||||
import type {
|
||||
ImageFallbackProps,
|
||||
ImageImgProps,
|
||||
ImageRootProps,
|
||||
} from "@kobalte/core/image";
|
||||
import { Image as ImagePrimitive } from "@kobalte/core/image";
|
||||
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
|
||||
import type { ValidComponent } from "solid-js";
|
||||
import { splitProps } from "solid-js";
|
||||
|
||||
type imageRootProps<T extends ValidComponent = "span"> = ImageRootProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export const ImageRoot = <T extends ValidComponent = "span">(
|
||||
props: PolymorphicProps<T, imageRootProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as imageRootProps, ["class"]);
|
||||
|
||||
return (
|
||||
<ImagePrimitive
|
||||
class={cn(
|
||||
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type imageProps<T extends ValidComponent = "img"> = ImageImgProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export const Image = <T extends ValidComponent = "img">(
|
||||
props: PolymorphicProps<T, imageProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as imageProps, ["class"]);
|
||||
|
||||
return (
|
||||
<ImagePrimitive.Img
|
||||
class={cn("aspect-square h-full w-full", local.class)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type imageFallbackProps<T extends ValidComponent = "span"> =
|
||||
ImageFallbackProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export const ImageFallback = <T extends ValidComponent = "span">(
|
||||
props: PolymorphicProps<T, imageFallbackProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as imageFallbackProps, ["class"]);
|
||||
|
||||
return (
|
||||
<ImagePrimitive.Fallback
|
||||
class={cn(
|
||||
"flex h-full w-full items-center justify-center rounded-full bg-muted",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
349
examples/astro-example/src/components/ui/menubar.tsx
Normal file
349
examples/astro-example/src/components/ui/menubar.tsx
Normal file
@@ -0,0 +1,349 @@
|
||||
import { cn } from "@/libs/cn";
|
||||
import type {
|
||||
MenubarCheckboxItemProps,
|
||||
MenubarContentProps,
|
||||
MenubarItemLabelProps,
|
||||
MenubarItemProps,
|
||||
MenubarMenuProps,
|
||||
MenubarRadioItemProps,
|
||||
MenubarRootProps,
|
||||
MenubarSeparatorProps,
|
||||
MenubarSubContentProps,
|
||||
MenubarSubTriggerProps,
|
||||
MenubarTriggerProps,
|
||||
} from "@kobalte/core/menubar";
|
||||
import { Menubar as MenubarPrimitive } from "@kobalte/core/menubar";
|
||||
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
|
||||
import type { ComponentProps, ParentProps, ValidComponent } from "solid-js";
|
||||
import { mergeProps, splitProps } from "solid-js";
|
||||
|
||||
export const MenubarSub = MenubarPrimitive.Sub;
|
||||
export const MenubarRadioGroup = MenubarPrimitive.RadioGroup;
|
||||
|
||||
type menubarProps<T extends ValidComponent = "div"> = MenubarRootProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export const Menubar = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, menubarProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as menubarProps, ["class"]);
|
||||
|
||||
return (
|
||||
<MenubarPrimitive
|
||||
class={cn(
|
||||
"flex h-9 items-center space-x-1 rounded-md border bg-background p-1 shadow-sm",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const MenubarMenu = (props: MenubarMenuProps) => {
|
||||
const merge = mergeProps<MenubarMenuProps[]>({ gutter: 8, shift: -4 }, props);
|
||||
|
||||
return <MenubarPrimitive.Menu {...merge} />;
|
||||
};
|
||||
|
||||
type menubarTriggerProps<T extends ValidComponent = "button"> =
|
||||
MenubarTriggerProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export const MenubarTrigger = <T extends ValidComponent = "button">(
|
||||
props: PolymorphicProps<T, menubarTriggerProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as menubarTriggerProps, ["class"]);
|
||||
|
||||
return (
|
||||
<MenubarPrimitive.Trigger
|
||||
class={cn(
|
||||
"flex cursor-default select-none items-center rounded-sm px-3 py-1 text-sm font-medium outline-none focus:bg-accent focus:text-accent-foreground data-[expanded]:bg-accent data-[expanded]:text-accent-foreground",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type menubarSubTriggerProps<T extends ValidComponent = "button"> = ParentProps<
|
||||
MenubarSubTriggerProps<T> & {
|
||||
class?: string;
|
||||
inset?: boolean;
|
||||
}
|
||||
>;
|
||||
|
||||
export const MenubarSubTrigger = <T extends ValidComponent = "button">(
|
||||
props: PolymorphicProps<T, menubarSubTriggerProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as menubarSubTriggerProps, [
|
||||
"class",
|
||||
"children",
|
||||
"inset",
|
||||
]);
|
||||
|
||||
return (
|
||||
<MenubarPrimitive.SubTrigger
|
||||
class={cn(
|
||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[expanded]:bg-accent data-[expanded]:text-accent-foreground",
|
||||
local.inset && "pl-8",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{local.children}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
class="ml-auto h-4 w-4"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="m9 6l6 6l-6 6"
|
||||
/>
|
||||
<title>Arrow</title>
|
||||
</svg>
|
||||
</MenubarPrimitive.SubTrigger>
|
||||
);
|
||||
};
|
||||
|
||||
type menubarSubContentProps<T extends ValidComponent = "div"> = ParentProps<
|
||||
MenubarSubContentProps<T> & {
|
||||
class?: string;
|
||||
}
|
||||
>;
|
||||
|
||||
export const MenubarSubContent = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, menubarSubContentProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as menubarSubContentProps, [
|
||||
"class",
|
||||
"children",
|
||||
]);
|
||||
|
||||
return (
|
||||
<MenubarPrimitive.Portal>
|
||||
<MenubarPrimitive.SubContent
|
||||
class={cn(
|
||||
"z-50 min-w-[8rem] origin-[--kb-menu-content-transform-origin] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg outline-none data-[expanded]:animate-in data-[closed]:animate-out data-[closed]:fade-out-0 data-[expanded]:fade-in-0 data-[closed]:zoom-out-95 data-[expanded]:zoom-in-95",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{local.children}
|
||||
</MenubarPrimitive.SubContent>
|
||||
</MenubarPrimitive.Portal>
|
||||
);
|
||||
};
|
||||
|
||||
type menubarContentProps<T extends ValidComponent = "div"> = ParentProps<
|
||||
MenubarContentProps<T> & {
|
||||
class?: string;
|
||||
}
|
||||
>;
|
||||
|
||||
export const MenubarContent = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, menubarContentProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as menubarContentProps, [
|
||||
"class",
|
||||
"children",
|
||||
]);
|
||||
|
||||
return (
|
||||
<MenubarPrimitive.Portal>
|
||||
<MenubarPrimitive.Content
|
||||
class={cn(
|
||||
"z-50 min-w-[12rem] origin-[--kb-menu-content-transform-origin] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md outline-none data-[expanded]:animate-in data-[closed]:fade-out-0 data-[expanded]:fade-in-0 data-[closed]:zoom-out-95 data-[expanded]:zoom-in-95",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{local.children}
|
||||
</MenubarPrimitive.Content>
|
||||
</MenubarPrimitive.Portal>
|
||||
);
|
||||
};
|
||||
|
||||
type menubarItemProps<T extends ValidComponent = "div"> =
|
||||
MenubarItemProps<T> & {
|
||||
class?: string;
|
||||
inset?: boolean;
|
||||
};
|
||||
|
||||
export const MenubarItem = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, menubarItemProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as menubarItemProps, [
|
||||
"class",
|
||||
"inset",
|
||||
]);
|
||||
|
||||
return (
|
||||
<MenubarPrimitive.Item
|
||||
class={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
local.inset && "pl-8",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type menubarItemLabelProps<T extends ValidComponent = "div"> =
|
||||
MenubarItemLabelProps<T> & {
|
||||
class?: string;
|
||||
inset?: boolean;
|
||||
};
|
||||
|
||||
export const MenubarItemLabel = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, menubarItemLabelProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as menubarItemLabelProps, [
|
||||
"class",
|
||||
"inset",
|
||||
]);
|
||||
|
||||
return (
|
||||
<MenubarPrimitive.ItemLabel
|
||||
class={cn(
|
||||
"px-2 py-1.5 text-sm font-semibold",
|
||||
local.inset && "pl-8",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type menubarSeparatorProps<T extends ValidComponent = "hr"> =
|
||||
MenubarSeparatorProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export const MenubarSeparator = <T extends ValidComponent = "hr">(
|
||||
props: PolymorphicProps<T, menubarSeparatorProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as menubarSeparatorProps, ["class"]);
|
||||
|
||||
return (
|
||||
<MenubarPrimitive.Separator
|
||||
class={cn("-mx-1 my-1 h-px bg-muted", local.class)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type menubarCheckboxItemProps<T extends ValidComponent = "div"> = ParentProps<
|
||||
MenubarCheckboxItemProps<T> & {
|
||||
class?: string;
|
||||
}
|
||||
>;
|
||||
|
||||
export const MenubarCheckboxItem = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, menubarCheckboxItemProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as menubarCheckboxItemProps, [
|
||||
"class",
|
||||
"children",
|
||||
]);
|
||||
|
||||
return (
|
||||
<MenubarPrimitive.CheckboxItem
|
||||
class={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
<MenubarPrimitive.ItemIndicator class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
class="h-4 w-4"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="m5 12l5 5L20 7"
|
||||
/>
|
||||
<title>Checkbox</title>
|
||||
</svg>
|
||||
</MenubarPrimitive.ItemIndicator>
|
||||
{local.children}
|
||||
</MenubarPrimitive.CheckboxItem>
|
||||
);
|
||||
};
|
||||
|
||||
type menubarRadioItemProps<T extends ValidComponent = "div"> = ParentProps<
|
||||
MenubarRadioItemProps<T> & {
|
||||
class?: string;
|
||||
}
|
||||
>;
|
||||
|
||||
export const MenubarRadioItem = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, menubarRadioItemProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as menubarRadioItemProps, [
|
||||
"class",
|
||||
"children",
|
||||
]);
|
||||
|
||||
return (
|
||||
<MenubarPrimitive.RadioItem
|
||||
class={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
<MenubarPrimitive.ItemIndicator class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
class="h-2 w-2"
|
||||
>
|
||||
<g
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M0 0h24v24H0z" />
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M7 3.34a10 10 0 1 1-4.995 8.984L2 12l.005-.324A10 10 0 0 1 7 3.34"
|
||||
/>
|
||||
</g>
|
||||
<title>Radio</title>
|
||||
</svg>
|
||||
</MenubarPrimitive.ItemIndicator>
|
||||
{local.children}
|
||||
</MenubarPrimitive.RadioItem>
|
||||
);
|
||||
};
|
||||
|
||||
export const MenubarShortcut = (props: ComponentProps<"span">) => {
|
||||
const [local, rest] = splitProps(props, ["class"]);
|
||||
|
||||
return (
|
||||
<span
|
||||
class={cn(
|
||||
"ml-auto text-xs tracking-widest text-muted-foreground",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
168
examples/astro-example/src/components/ui/navigation-menu.tsx
Normal file
168
examples/astro-example/src/components/ui/navigation-menu.tsx
Normal file
@@ -0,0 +1,168 @@
|
||||
import { cn } from "@/libs/cn";
|
||||
import type {
|
||||
NavigationMenuContentProps,
|
||||
NavigationMenuRootProps,
|
||||
NavigationMenuTriggerProps,
|
||||
} from "@kobalte/core/navigation-menu";
|
||||
import { NavigationMenu as NavigationMenuPrimitive } from "@kobalte/core/navigation-menu";
|
||||
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
|
||||
import {
|
||||
type ParentProps,
|
||||
Show,
|
||||
type ValidComponent,
|
||||
mergeProps,
|
||||
splitProps,
|
||||
} from "solid-js";
|
||||
|
||||
export const NavigationMenuItem = NavigationMenuPrimitive.Menu;
|
||||
export const NavigationMenuLink = NavigationMenuPrimitive.Item;
|
||||
export const NavigationMenuItemLabel = NavigationMenuPrimitive.ItemLabel;
|
||||
export const NavigationMenuDescription =
|
||||
NavigationMenuPrimitive.ItemDescription;
|
||||
export const NavigationMenuItemIndicator =
|
||||
NavigationMenuPrimitive.ItemIndicator;
|
||||
export const NavigationMenuSub = NavigationMenuPrimitive.Sub;
|
||||
export const NavigationMenuSubTrigger = NavigationMenuPrimitive.SubTrigger;
|
||||
export const NavigationMenuSubContent = NavigationMenuPrimitive.SubContent;
|
||||
export const NavigationMenuRadioGroup = NavigationMenuPrimitive.RadioGroup;
|
||||
export const NavigationMenuRadioItem = NavigationMenuPrimitive.RadioItem;
|
||||
export const NavigationMenuCheckboxItem = NavigationMenuPrimitive.CheckboxItem;
|
||||
export const NavigationMenuSeparator = NavigationMenuPrimitive.Separator;
|
||||
|
||||
type withArrow = {
|
||||
withArrow?: boolean;
|
||||
};
|
||||
|
||||
type navigationMenuProps<T extends ValidComponent = "ul"> = ParentProps<
|
||||
NavigationMenuRootProps<T> &
|
||||
withArrow & {
|
||||
class?: string;
|
||||
}
|
||||
>;
|
||||
|
||||
export const NavigationMenu = <T extends ValidComponent = "ul">(
|
||||
props: PolymorphicProps<T, navigationMenuProps<T>>,
|
||||
) => {
|
||||
const merge = mergeProps<navigationMenuProps<T>[]>(
|
||||
{
|
||||
get gutter() {
|
||||
return props.withArrow ? props.gutter : 6;
|
||||
},
|
||||
withArrow: false,
|
||||
},
|
||||
props,
|
||||
);
|
||||
const [local, rest] = splitProps(merge as navigationMenuProps, [
|
||||
"class",
|
||||
"children",
|
||||
"withArrow",
|
||||
]);
|
||||
|
||||
return (
|
||||
<NavigationMenuPrimitive
|
||||
class={cn("flex w-max items-center justify-center gap-x-1", local.class)}
|
||||
{...rest}
|
||||
>
|
||||
{local.children}
|
||||
<NavigationMenuPrimitive.Viewport
|
||||
class={cn(
|
||||
"pointer-events-none z-50 overflow-x-clip overflow-y-visible rounded-md border bg-popover text-popover-foreground shadow",
|
||||
"h-[--kb-navigation-menu-viewport-height] w-[--kb-navigation-menu-viewport-width] transition-[width,height] duration-300",
|
||||
"origin-[--kb-menu-content-transform-origin]",
|
||||
"data-[expanded]:duration-300 data-[expanded]:animate-in data-[expanded]:fade-in data-[expanded]:zoom-in-95",
|
||||
"data-[closed]:duration-300 data-[closed]:animate-out data-[closed]:fade-out data-[closed]:zoom-out-95",
|
||||
)}
|
||||
>
|
||||
<Show when={local.withArrow}>
|
||||
<NavigationMenuPrimitive.Arrow class="transition-transform duration-300" />
|
||||
</Show>
|
||||
</NavigationMenuPrimitive.Viewport>
|
||||
</NavigationMenuPrimitive>
|
||||
);
|
||||
};
|
||||
|
||||
type navigationMenuTriggerProps<T extends ValidComponent = "button"> =
|
||||
ParentProps<
|
||||
NavigationMenuTriggerProps<T> &
|
||||
withArrow & {
|
||||
class?: string;
|
||||
}
|
||||
>;
|
||||
|
||||
export const NavigationMenuTrigger = <T extends ValidComponent = "button">(
|
||||
props: PolymorphicProps<T, navigationMenuTriggerProps<T>>,
|
||||
) => {
|
||||
const merge = mergeProps<navigationMenuTriggerProps<T>[]>(
|
||||
{
|
||||
get withArrow() {
|
||||
return props.as === undefined ? true : props.withArrow;
|
||||
},
|
||||
},
|
||||
props,
|
||||
);
|
||||
const [local, rest] = splitProps(merge as navigationMenuTriggerProps, [
|
||||
"class",
|
||||
"children",
|
||||
"withArrow",
|
||||
]);
|
||||
|
||||
return (
|
||||
<NavigationMenuPrimitive.Trigger
|
||||
class={cn(
|
||||
"inline-flex w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium outline-none transition-colors duration-300 hover:bg-accent hover:text-accent-foreground disabled:pointer-events-none disabled:opacity-50",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{local.children}
|
||||
<Show when={local.withArrow}>
|
||||
<NavigationMenuPrimitive.Icon
|
||||
class="ml-1 size-3 transition-transform duration-300 data-[expanded]:rotate-180"
|
||||
as="svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="m6 9l6 6l6-6"
|
||||
/>
|
||||
</NavigationMenuPrimitive.Icon>
|
||||
</Show>
|
||||
</NavigationMenuPrimitive.Trigger>
|
||||
);
|
||||
};
|
||||
|
||||
type navigationMenuContentProps<T extends ValidComponent = "ul"> = ParentProps<
|
||||
NavigationMenuContentProps<T> & {
|
||||
class?: string;
|
||||
}
|
||||
>;
|
||||
|
||||
export const NavigationMenuContent = <T extends ValidComponent = "ul">(
|
||||
props: PolymorphicProps<T, navigationMenuContentProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as navigationMenuContentProps, [
|
||||
"class",
|
||||
"children",
|
||||
]);
|
||||
|
||||
return (
|
||||
<NavigationMenuPrimitive.Portal>
|
||||
<NavigationMenuPrimitive.Content
|
||||
class={cn(
|
||||
"absolute left-0 top-0 p-4 outline-none",
|
||||
"data-[motion^=from-]:duration-300 data-[motion^=from-]:animate-in data-[motion^=from-]:fade-in data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52",
|
||||
"data-[motion^=to-]:duration-300 data-[motion^=to-]:animate-out data-[motion^=to-]:fade-out data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{local.children}
|
||||
</NavigationMenuPrimitive.Content>
|
||||
</NavigationMenuPrimitive.Portal>
|
||||
);
|
||||
};
|
||||
214
examples/astro-example/src/components/ui/number-field.tsx
Normal file
214
examples/astro-example/src/components/ui/number-field.tsx
Normal file
@@ -0,0 +1,214 @@
|
||||
import { cn } from "@/libs/cn";
|
||||
import type {
|
||||
NumberFieldDecrementTriggerProps,
|
||||
NumberFieldDescriptionProps,
|
||||
NumberFieldErrorMessageProps,
|
||||
NumberFieldIncrementTriggerProps,
|
||||
NumberFieldInputProps,
|
||||
NumberFieldLabelProps,
|
||||
NumberFieldRootProps,
|
||||
} from "@kobalte/core/number-field";
|
||||
import { NumberField as NumberFieldPrimitive } from "@kobalte/core/number-field";
|
||||
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
|
||||
import type { ComponentProps, ValidComponent, VoidProps } from "solid-js";
|
||||
import { splitProps } from "solid-js";
|
||||
import { textfieldLabel } from "./textfield";
|
||||
|
||||
export const NumberFieldHiddenInput = NumberFieldPrimitive.HiddenInput;
|
||||
|
||||
type numberFieldLabelProps<T extends ValidComponent = "div"> =
|
||||
NumberFieldLabelProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export const NumberFieldLabel = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, numberFieldLabelProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as numberFieldLabelProps, ["class"]);
|
||||
|
||||
return (
|
||||
<NumberFieldPrimitive.Label
|
||||
class={cn(textfieldLabel({ label: true }), local.class)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type numberFieldDescriptionProps<T extends ValidComponent = "div"> =
|
||||
NumberFieldDescriptionProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export const NumberFieldDescription = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, numberFieldDescriptionProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as numberFieldDescriptionProps, [
|
||||
"class",
|
||||
]);
|
||||
|
||||
return (
|
||||
<NumberFieldPrimitive.Description
|
||||
class={cn(
|
||||
textfieldLabel({ description: true, label: false }),
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type numberFieldErrorMessageProps<T extends ValidComponent = "div"> =
|
||||
NumberFieldErrorMessageProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export const NumberFieldErrorMessage = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, numberFieldErrorMessageProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as numberFieldErrorMessageProps, [
|
||||
"class",
|
||||
]);
|
||||
|
||||
return (
|
||||
<NumberFieldPrimitive.ErrorMessage
|
||||
class={cn(textfieldLabel({ error: true }), local.class)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type numberFieldProps<T extends ValidComponent = "div"> =
|
||||
NumberFieldRootProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export const NumberField = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, numberFieldProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as numberFieldProps, ["class"]);
|
||||
|
||||
return (
|
||||
<NumberFieldPrimitive class={cn("grid gap-1.5", local.class)} {...rest} />
|
||||
);
|
||||
};
|
||||
|
||||
export const NumberFieldGroup = (props: ComponentProps<"div">) => {
|
||||
const [local, rest] = splitProps(props, ["class"]);
|
||||
|
||||
return (
|
||||
<div
|
||||
class={cn(
|
||||
"relative rounded-md transition-shadow focus-within:outline-none focus-within:ring-[1.5px] focus-within:ring-ring",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type numberFieldInputProps<T extends ValidComponent = "input"> =
|
||||
NumberFieldInputProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export const NumberFieldInput = <T extends ValidComponent = "input">(
|
||||
props: PolymorphicProps<T, VoidProps<numberFieldInputProps<T>>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as numberFieldInputProps, ["class"]);
|
||||
|
||||
return (
|
||||
<NumberFieldPrimitive.Input
|
||||
class={cn(
|
||||
"flex h-9 w-full rounded-md border border-input bg-transparent px-10 py-1 text-center text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type numberFieldDecrementTriggerProps<T extends ValidComponent = "button"> =
|
||||
VoidProps<
|
||||
NumberFieldDecrementTriggerProps<T> & {
|
||||
class?: string;
|
||||
}
|
||||
>;
|
||||
|
||||
export const NumberFieldDecrementTrigger = <
|
||||
T extends ValidComponent = "button",
|
||||
>(
|
||||
props: PolymorphicProps<T, VoidProps<numberFieldDecrementTriggerProps<T>>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as numberFieldDecrementTriggerProps, [
|
||||
"class",
|
||||
]);
|
||||
|
||||
return (
|
||||
<NumberFieldPrimitive.DecrementTrigger
|
||||
class={cn(
|
||||
"absolute left-0 top-1/2 -translate-y-1/2 p-3 disabled:cursor-not-allowed disabled:opacity-20",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="size-4"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M5 12h14"
|
||||
/>
|
||||
<title>Decreasing number</title>
|
||||
</svg>
|
||||
</NumberFieldPrimitive.DecrementTrigger>
|
||||
);
|
||||
};
|
||||
|
||||
type numberFieldIncrementTriggerProps<T extends ValidComponent = "button"> =
|
||||
VoidProps<
|
||||
NumberFieldIncrementTriggerProps<T> & {
|
||||
class?: string;
|
||||
}
|
||||
>;
|
||||
|
||||
export const NumberFieldIncrementTrigger = <
|
||||
T extends ValidComponent = "button",
|
||||
>(
|
||||
props: PolymorphicProps<T, numberFieldIncrementTriggerProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as numberFieldIncrementTriggerProps, [
|
||||
"class",
|
||||
]);
|
||||
|
||||
return (
|
||||
<NumberFieldPrimitive.IncrementTrigger
|
||||
class={cn(
|
||||
"absolute right-0 top-1/2 -translate-y-1/2 p-3 disabled:cursor-not-allowed disabled:opacity-20",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="size-4"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 5v14m-7-7h14"
|
||||
/>
|
||||
<title>Increase number</title>
|
||||
</svg>
|
||||
</NumberFieldPrimitive.IncrementTrigger>
|
||||
);
|
||||
};
|
||||
83
examples/astro-example/src/components/ui/otp-field.tsx
Normal file
83
examples/astro-example/src/components/ui/otp-field.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import { cn } from "@/libs/cn";
|
||||
import type { DynamicProps, RootProps } from "@corvu/otp-field";
|
||||
import OTPFieldPrimitive from "@corvu/otp-field";
|
||||
import type { ComponentProps, ValidComponent } from "solid-js";
|
||||
import { Show, splitProps } from "solid-js";
|
||||
|
||||
export const OTPFieldInput = OTPFieldPrimitive.Input;
|
||||
|
||||
type OTPFieldProps<T extends ValidComponent = "div"> = RootProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export const OTPField = <T extends ValidComponent = "div">(
|
||||
props: DynamicProps<T, OTPFieldProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props, ["class"]);
|
||||
|
||||
return (
|
||||
<OTPFieldPrimitive
|
||||
class={cn(
|
||||
"flex items-center gap-2 has-[:disabled]:opacity-50",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const OTPFieldGroup = (props: ComponentProps<"div">) => {
|
||||
const [local, rest] = splitProps(props, ["class"]);
|
||||
|
||||
return <div class={cn("flex items-center", local.class)} {...rest} />;
|
||||
};
|
||||
|
||||
export const OTPFieldSeparator = (props: ComponentProps<"div">) => {
|
||||
return (
|
||||
// biome-ignore lint/a11y/useAriaPropsForRole: []
|
||||
<div role="separator" {...props}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="size-4"
|
||||
viewBox="0 0 15 15"
|
||||
>
|
||||
<title>Separator</title>
|
||||
<path
|
||||
fill="currentColor"
|
||||
fill-rule="evenodd"
|
||||
d="M5 7.5a.5.5 0 0 1 .5-.5h4a.5.5 0 0 1 0 1h-4a.5.5 0 0 1-.5-.5"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const OTPFieldSlot = (
|
||||
props: ComponentProps<"div"> & { index: number },
|
||||
) => {
|
||||
const [local, rest] = splitProps(props, ["class", "index"]);
|
||||
const context = OTPFieldPrimitive.useContext();
|
||||
const char = () => context.value()[local.index];
|
||||
const hasFakeCaret = () =>
|
||||
context.value().length === local.index && context.isInserting();
|
||||
const isActive = () => context.activeSlots().includes(local.index);
|
||||
|
||||
return (
|
||||
<div
|
||||
class={cn(
|
||||
"relative flex size-9 items-center justify-center border-y border-r border-input text-sm shadow-sm transition-shadow first:rounded-l-md first:border-l last:rounded-r-md",
|
||||
isActive() && "z-10 ring-[1.5px] ring-ring",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{char()}
|
||||
<Show when={hasFakeCaret()}>
|
||||
<div class="pointer-events-none absolute inset-0 flex items-center justify-center">
|
||||
<div class="h-4 w-px animate-caret-blink bg-foreground" />
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
194
examples/astro-example/src/components/ui/pagination.tsx
Normal file
194
examples/astro-example/src/components/ui/pagination.tsx
Normal file
@@ -0,0 +1,194 @@
|
||||
import { cn } from "@/libs/cn";
|
||||
import type {
|
||||
PaginationEllipsisProps,
|
||||
PaginationItemProps,
|
||||
PaginationPreviousProps,
|
||||
PaginationRootProps,
|
||||
} from "@kobalte/core/pagination";
|
||||
import { Pagination as PaginationPrimitive } from "@kobalte/core/pagination";
|
||||
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
|
||||
import type { VariantProps } from "class-variance-authority";
|
||||
import type { ValidComponent, VoidProps } from "solid-js";
|
||||
import { mergeProps, splitProps } from "solid-js";
|
||||
import { buttonVariants } from "./button";
|
||||
|
||||
export const PaginationItems = PaginationPrimitive.Items;
|
||||
|
||||
type paginationProps<T extends ValidComponent = "nav"> =
|
||||
PaginationRootProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export const Pagination = <T extends ValidComponent = "nav">(
|
||||
props: PolymorphicProps<T, paginationProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as paginationProps, ["class"]);
|
||||
|
||||
return (
|
||||
<PaginationPrimitive
|
||||
class={cn(
|
||||
"mx-auto flex w-full justify-center [&>ul]:flex [&>ul]:flex-row [&>ul]:items-center [&>ul]:gap-1",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type paginationItemProps<T extends ValidComponent = "button"> =
|
||||
PaginationItemProps<T> &
|
||||
Pick<VariantProps<typeof buttonVariants>, "size"> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export const PaginationItem = <T extends ValidComponent = "button">(
|
||||
props: PolymorphicProps<T, paginationItemProps<T>>,
|
||||
) => {
|
||||
// @ts-expect-error - required `page`
|
||||
const merge = mergeProps<paginationItemProps[]>({ size: "icon" }, props);
|
||||
const [local, rest] = splitProps(merge as paginationItemProps, [
|
||||
"class",
|
||||
"size",
|
||||
]);
|
||||
|
||||
return (
|
||||
<PaginationPrimitive.Item
|
||||
class={cn(
|
||||
buttonVariants({
|
||||
variant: "ghost",
|
||||
size: local.size,
|
||||
}),
|
||||
"aria-[current=page]:border aria-[current=page]:border-input aria-[current=page]:bg-background aria-[current=page]:shadow-sm aria-[current=page]:hover:bg-accent aria-[current=page]:hover:text-accent-foreground",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type paginationEllipsisProps<T extends ValidComponent = "div"> = VoidProps<
|
||||
PaginationEllipsisProps<T> & {
|
||||
class?: string;
|
||||
}
|
||||
>;
|
||||
|
||||
export const PaginationEllipsis = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, paginationEllipsisProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as paginationEllipsisProps, ["class"]);
|
||||
|
||||
return (
|
||||
<PaginationPrimitive.Ellipsis
|
||||
class={cn("flex h-9 w-9 items-center justify-center", local.class)}
|
||||
{...rest}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
class="h-4 w-4"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 12a1 1 0 1 0 2 0a1 1 0 1 0-2 0m7 0a1 1 0 1 0 2 0a1 1 0 1 0-2 0m7 0a1 1 0 1 0 2 0a1 1 0 1 0-2 0"
|
||||
/>
|
||||
<title>More pages</title>
|
||||
</svg>
|
||||
</PaginationPrimitive.Ellipsis>
|
||||
);
|
||||
};
|
||||
|
||||
type paginationPreviousProps<T extends ValidComponent = "button"> =
|
||||
PaginationPreviousProps<T> &
|
||||
Pick<VariantProps<typeof buttonVariants>, "size"> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export const PaginationPrevious = <T extends ValidComponent = "button">(
|
||||
props: PolymorphicProps<T, paginationPreviousProps<T>>,
|
||||
) => {
|
||||
const merge = mergeProps<paginationPreviousProps<T>[]>(
|
||||
{ size: "icon" },
|
||||
props,
|
||||
);
|
||||
const [local, rest] = splitProps(merge as paginationPreviousProps, [
|
||||
"class",
|
||||
"size",
|
||||
]);
|
||||
|
||||
return (
|
||||
<PaginationPrimitive.Previous
|
||||
class={cn(
|
||||
buttonVariants({
|
||||
variant: "ghost",
|
||||
size: local.size,
|
||||
}),
|
||||
"aria-[current=page]:border aria-[current=page]:border-input aria-[current=page]:bg-background aria-[current=page]:shadow-sm aria-[current=page]:hover:bg-accent aria-[current=page]:hover:text-accent-foreground",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
class="h-4 w-4"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="m15 6l-6 6l6 6"
|
||||
/>
|
||||
<title>Previous page</title>
|
||||
</svg>
|
||||
</PaginationPrimitive.Previous>
|
||||
);
|
||||
};
|
||||
|
||||
type paginationNextProps<T extends ValidComponent = "button"> =
|
||||
paginationPreviousProps<T>;
|
||||
|
||||
export const PaginationNext = <T extends ValidComponent = "button">(
|
||||
props: PolymorphicProps<T, paginationNextProps<T>>,
|
||||
) => {
|
||||
const merge = mergeProps<paginationNextProps<T>[]>({ size: "icon" }, props);
|
||||
const [local, rest] = splitProps(merge as paginationNextProps, [
|
||||
"class",
|
||||
"size",
|
||||
]);
|
||||
|
||||
return (
|
||||
<PaginationPrimitive.Next
|
||||
class={cn(
|
||||
buttonVariants({
|
||||
variant: "ghost",
|
||||
size: local.size,
|
||||
}),
|
||||
"aria-[current=page]:border aria-[current=page]:border-input aria-[current=page]:bg-background aria-[current=page]:shadow-sm aria-[current=page]:hover:bg-accent aria-[current=page]:hover:text-accent-foreground",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="m9 6l6 6l-6 6"
|
||||
/>
|
||||
<title>Next page</title>
|
||||
</svg>
|
||||
</PaginationPrimitive.Next>
|
||||
);
|
||||
};
|
||||
65
examples/astro-example/src/components/ui/popover.tsx
Normal file
65
examples/astro-example/src/components/ui/popover.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import { cn } from "@/libs/cn";
|
||||
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
|
||||
import type {
|
||||
PopoverContentProps,
|
||||
PopoverRootProps,
|
||||
} from "@kobalte/core/popover";
|
||||
import { Popover as PopoverPrimitive } from "@kobalte/core/popover";
|
||||
import type { ParentProps, ValidComponent } from "solid-js";
|
||||
import { mergeProps, splitProps } from "solid-js";
|
||||
|
||||
export const PopoverTrigger = PopoverPrimitive.Trigger;
|
||||
export const PopoverTitle = PopoverPrimitive.Title;
|
||||
export const PopoverDescription = PopoverPrimitive.Description;
|
||||
|
||||
export const Popover = (props: PopoverRootProps) => {
|
||||
const merge = mergeProps<PopoverRootProps[]>({ gutter: 4 }, props);
|
||||
|
||||
return <PopoverPrimitive {...merge} />;
|
||||
};
|
||||
|
||||
type popoverContentProps<T extends ValidComponent = "div"> = ParentProps<
|
||||
PopoverContentProps<T> & {
|
||||
class?: string;
|
||||
}
|
||||
>;
|
||||
|
||||
export const PopoverContent = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, popoverContentProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as popoverContentProps, [
|
||||
"class",
|
||||
"children",
|
||||
]);
|
||||
|
||||
return (
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Content
|
||||
class={cn(
|
||||
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[expanded]:animate-in data-[closed]:animate-out data-[closed]:fade-out-0 data-[expanded]:fade-in-0 data-[closed]:zoom-out-95 data-[expanded]:zoom-in-95",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{local.children}
|
||||
<PopoverPrimitive.CloseButton class="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-[opacity,box-shadow] hover:opacity-100 focus:outline-none focus:ring-[1.5px] focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
class="h-4 w-4"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M18 6L6 18M6 6l12 12"
|
||||
/>
|
||||
<title>Close</title>
|
||||
</svg>
|
||||
</PopoverPrimitive.CloseButton>
|
||||
</PopoverPrimitive.Content>
|
||||
</PopoverPrimitive.Portal>
|
||||
);
|
||||
};
|
||||
36
examples/astro-example/src/components/ui/progress.tsx
Normal file
36
examples/astro-example/src/components/ui/progress.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { cn } from "@/libs/cn";
|
||||
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
|
||||
import type { ProgressRootProps } from "@kobalte/core/progress";
|
||||
import { Progress as ProgressPrimitive } from "@kobalte/core/progress";
|
||||
import type { ParentProps, ValidComponent } from "solid-js";
|
||||
import { splitProps } from "solid-js";
|
||||
|
||||
export const ProgressLabel = ProgressPrimitive.Label;
|
||||
export const ProgressValueLabel = ProgressPrimitive.ValueLabel;
|
||||
|
||||
type progressProps<T extends ValidComponent = "div"> = ParentProps<
|
||||
ProgressRootProps<T> & {
|
||||
class?: string;
|
||||
}
|
||||
>;
|
||||
|
||||
export const Progress = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, progressProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as progressProps, [
|
||||
"class",
|
||||
"children",
|
||||
]);
|
||||
|
||||
return (
|
||||
<ProgressPrimitive
|
||||
class={cn("flex w-full flex-col gap-2", local.class)}
|
||||
{...rest}
|
||||
>
|
||||
{local.children}
|
||||
<ProgressPrimitive.Track class="h-2 overflow-hidden rounded-full bg-primary/20">
|
||||
<ProgressPrimitive.Fill class="h-full w-[--kb-progress-fill-width] bg-primary transition-all duration-500 ease-linear data-[progress=complete]:bg-primary" />
|
||||
</ProgressPrimitive.Track>
|
||||
</ProgressPrimitive>
|
||||
);
|
||||
};
|
||||
39
examples/astro-example/src/components/ui/radio-group.tsx
Normal file
39
examples/astro-example/src/components/ui/radio-group.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { cn } from "@/libs/cn";
|
||||
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
|
||||
import type { RadioGroupItemControlProps } from "@kobalte/core/radio-group";
|
||||
import { RadioGroup as RadioGroupPrimitive } from "@kobalte/core/radio-group";
|
||||
import type { ValidComponent, VoidProps } from "solid-js";
|
||||
import { splitProps } from "solid-js";
|
||||
|
||||
export const RadioGroupDescription = RadioGroupPrimitive.Description;
|
||||
export const RadioGroupErrorMessage = RadioGroupPrimitive.ErrorMessage;
|
||||
export const RadioGroupItemDescription = RadioGroupPrimitive.ItemDescription;
|
||||
export const RadioGroupItemInput = RadioGroupPrimitive.ItemInput;
|
||||
export const RadioGroupItemLabel = RadioGroupPrimitive.ItemLabel;
|
||||
export const RadioGroupLabel = RadioGroupPrimitive.Label;
|
||||
export const RadioGroup = RadioGroupPrimitive;
|
||||
export const RadioGroupItem = RadioGroupPrimitive.Item;
|
||||
|
||||
type radioGroupItemControlProps<T extends ValidComponent = "div"> = VoidProps<
|
||||
RadioGroupItemControlProps<T> & { class?: string }
|
||||
>;
|
||||
|
||||
export const RadioGroupItemControl = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, radioGroupItemControlProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as radioGroupItemControlProps, [
|
||||
"class",
|
||||
]);
|
||||
|
||||
return (
|
||||
<RadioGroupPrimitive.ItemControl
|
||||
class={cn(
|
||||
"flex aspect-square h-4 w-4 items-center justify-center rounded-full border border-primary text-primary shadow transition-shadow focus:outline-none focus-visible:ring-[1.5px] focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[checked]:bg-foreground",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
<RadioGroupPrimitive.ItemIndicator class="h-2 w-2 rounded-full data-[checked]:bg-background" />
|
||||
</RadioGroupPrimitive.ItemControl>
|
||||
);
|
||||
};
|
||||
63
examples/astro-example/src/components/ui/resizable.tsx
Normal file
63
examples/astro-example/src/components/ui/resizable.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import { cn } from "@/libs/cn";
|
||||
import type { DynamicProps, HandleProps, RootProps } from "@corvu/resizable";
|
||||
import ResizablePrimitive from "@corvu/resizable";
|
||||
import type { ValidComponent, VoidProps } from "solid-js";
|
||||
import { Show, splitProps } from "solid-js";
|
||||
|
||||
export const ResizablePanel = ResizablePrimitive.Panel;
|
||||
|
||||
type resizableProps<T extends ValidComponent = "div"> = RootProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export const Resizable = <T extends ValidComponent = "div">(
|
||||
props: DynamicProps<T, resizableProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as resizableProps, ["class"]);
|
||||
|
||||
return <ResizablePrimitive class={cn("size-full", local.class)} {...rest} />;
|
||||
};
|
||||
|
||||
type resizableHandleProps<T extends ValidComponent = "button"> = VoidProps<
|
||||
HandleProps<T> & {
|
||||
class?: string;
|
||||
withHandle?: boolean;
|
||||
}
|
||||
>;
|
||||
|
||||
export const ResizableHandle = <T extends ValidComponent = "button">(
|
||||
props: DynamicProps<T, resizableHandleProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as resizableHandleProps, [
|
||||
"class",
|
||||
"withHandle",
|
||||
]);
|
||||
|
||||
return (
|
||||
<ResizablePrimitive.Handle
|
||||
class={cn(
|
||||
"flex w-px items-center justify-center bg-border transition-shadow focus-visible:outline-none focus-visible:ring-[1.5px] focus-visible:ring-ring focus-visible:ring-offset-1 data-[orientation=vertical]:h-px data-[orientation=vertical]:w-full",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
<Show when={local.withHandle}>
|
||||
<div class="z-10 flex h-4 w-3 items-center justify-center rounded-sm border bg-border">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-2.5 w-2.5"
|
||||
viewBox="0 0 15 15"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
fill-rule="evenodd"
|
||||
d="M5.5 4.625a1.125 1.125 0 1 0 0-2.25a1.125 1.125 0 0 0 0 2.25m4 0a1.125 1.125 0 1 0 0-2.25a1.125 1.125 0 0 0 0 2.25M10.625 7.5a1.125 1.125 0 1 1-2.25 0a1.125 1.125 0 0 1 2.25 0M5.5 8.625a1.125 1.125 0 1 0 0-2.25a1.125 1.125 0 0 0 0 2.25m5.125 2.875a1.125 1.125 0 1 1-2.25 0a1.125 1.125 0 0 1 2.25 0M5.5 12.625a1.125 1.125 0 1 0 0-2.25a1.125 1.125 0 0 0 0 2.25"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
<title>Resizable handle</title>
|
||||
</svg>
|
||||
</div>
|
||||
</Show>
|
||||
</ResizablePrimitive.Handle>
|
||||
);
|
||||
};
|
||||
127
examples/astro-example/src/components/ui/select.tsx
Normal file
127
examples/astro-example/src/components/ui/select.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import { cn } from "@/libs/cn";
|
||||
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
|
||||
import type {
|
||||
SelectContentProps,
|
||||
SelectItemProps,
|
||||
SelectTriggerProps,
|
||||
} from "@kobalte/core/select";
|
||||
import { Select as SelectPrimitive } from "@kobalte/core/select";
|
||||
import type { ParentProps, ValidComponent } from "solid-js";
|
||||
import { splitProps } from "solid-js";
|
||||
|
||||
export const Select = SelectPrimitive;
|
||||
export const SelectValue = SelectPrimitive.Value;
|
||||
export const SelectDescription = SelectPrimitive.Description;
|
||||
export const SelectErrorMessage = SelectPrimitive.ErrorMessage;
|
||||
export const SelectItemDescription = SelectPrimitive.ItemDescription;
|
||||
export const SelectHiddenSelect = SelectPrimitive.HiddenSelect;
|
||||
export const SelectSection = SelectPrimitive.Section;
|
||||
|
||||
type selectTriggerProps<T extends ValidComponent = "button"> = ParentProps<
|
||||
SelectTriggerProps<T> & { class?: string }
|
||||
>;
|
||||
|
||||
export const SelectTrigger = <T extends ValidComponent = "button">(
|
||||
props: PolymorphicProps<T, selectTriggerProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as selectTriggerProps, [
|
||||
"class",
|
||||
"children",
|
||||
]);
|
||||
|
||||
return (
|
||||
<SelectPrimitive.Trigger
|
||||
class={cn(
|
||||
"flex h-9 w-full items-center justify-between rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background transition-shadow placeholder:text-muted-foreground focus:outline-none focus-visible:ring-[1.5px] focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{local.children}
|
||||
<SelectPrimitive.Icon
|
||||
as="svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
class="flex size-4 items-center justify-center opacity-50"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="m8 9l4-4l4 4m0 6l-4 4l-4-4"
|
||||
/>
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
);
|
||||
};
|
||||
|
||||
type selectContentProps<T extends ValidComponent = "div"> =
|
||||
SelectContentProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export const SelectContent = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, selectContentProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as selectContentProps, ["class"]);
|
||||
|
||||
return (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
class={cn(
|
||||
"relative z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[expanded]:animate-in data-[closed]:animate-out data-[closed]:fade-out-0 data-[expanded]:fade-in-0 data-[closed]:zoom-out-95 data-[expanded]:zoom-in-95",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
<SelectPrimitive.Listbox class="p-1 focus-visible:outline-none" />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
);
|
||||
};
|
||||
|
||||
type selectItemProps<T extends ValidComponent = "li"> = ParentProps<
|
||||
SelectItemProps<T> & { class?: string }
|
||||
>;
|
||||
|
||||
export const SelectItem = <T extends ValidComponent = "li">(
|
||||
props: PolymorphicProps<T, selectItemProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as selectItemProps, [
|
||||
"class",
|
||||
"children",
|
||||
]);
|
||||
|
||||
return (
|
||||
<SelectPrimitive.Item
|
||||
class={cn(
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
<SelectPrimitive.ItemIndicator class="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="m5 12l5 5L20 7"
|
||||
/>
|
||||
<title>Checked</title>
|
||||
</svg>
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
<SelectPrimitive.ItemLabel>{local.children}</SelectPrimitive.ItemLabel>
|
||||
</SelectPrimitive.Item>
|
||||
);
|
||||
};
|
||||
26
examples/astro-example/src/components/ui/separator.tsx
Normal file
26
examples/astro-example/src/components/ui/separator.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { cn } from "@/libs/cn";
|
||||
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
|
||||
import type { SeparatorRootProps } from "@kobalte/core/separator";
|
||||
import { Separator as SeparatorPrimitive } from "@kobalte/core/separator";
|
||||
import type { ValidComponent } from "solid-js";
|
||||
import { splitProps } from "solid-js";
|
||||
|
||||
type separatorProps<T extends ValidComponent = "hr"> = SeparatorRootProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export const Separator = <T extends ValidComponent = "hr">(
|
||||
props: PolymorphicProps<T, separatorProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as separatorProps, ["class"]);
|
||||
|
||||
return (
|
||||
<SeparatorPrimitive
|
||||
class={cn(
|
||||
"shrink-0 bg-border data-[orientation=horizontal]:h-[1px] data-[orientation=vertical]:h-full data-[orientation=horizontal]:w-full data-[orientation=vertical]:w-[1px]",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
148
examples/astro-example/src/components/ui/sheet.tsx
Normal file
148
examples/astro-example/src/components/ui/sheet.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
import { cn } from "@/libs/cn";
|
||||
import type {
|
||||
DialogContentProps,
|
||||
DialogDescriptionProps,
|
||||
DialogTitleProps,
|
||||
} from "@kobalte/core/dialog";
|
||||
import { Dialog as DialogPrimitive } from "@kobalte/core/dialog";
|
||||
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
|
||||
import type { VariantProps } from "class-variance-authority";
|
||||
import { cva } from "class-variance-authority";
|
||||
import type { ComponentProps, ParentProps, ValidComponent } from "solid-js";
|
||||
import { mergeProps, splitProps } from "solid-js";
|
||||
|
||||
export const Sheet = DialogPrimitive;
|
||||
export const SheetTrigger = DialogPrimitive.Trigger;
|
||||
|
||||
export const sheetVariants = cva(
|
||||
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[expanded]:animate-in data-[closed]:animate-out data-[expanded]:duration-200 data-[closed]:duration-200",
|
||||
{
|
||||
variants: {
|
||||
side: {
|
||||
top: "inset-x-0 top-0 border-b data-[closed]:slide-out-to-top data-[expanded]:slide-in-from-top",
|
||||
bottom:
|
||||
"inset-x-0 bottom-0 border-t data-[closed]:slide-out-to-bottom data-[expanded]:slide-in-from-bottom",
|
||||
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[closed]:slide-out-to-left data-[expanded]:slide-in-from-left sm:max-w-sm",
|
||||
right:
|
||||
"inset-y-0 right-0 h-full w-3/4 border-l data-[closed]:slide-out-to-right data-[expanded]:slide-in-from-right sm:max-w-sm",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
side: "right",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
type sheetContentProps<T extends ValidComponent = "div"> = ParentProps<
|
||||
DialogContentProps<T> &
|
||||
VariantProps<typeof sheetVariants> & {
|
||||
class?: string;
|
||||
}
|
||||
>;
|
||||
|
||||
export const SheetContent = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, sheetContentProps<T>>,
|
||||
) => {
|
||||
const merge = mergeProps<sheetContentProps<T>[]>({ side: "right" }, props);
|
||||
const [local, rest] = splitProps(merge as sheetContentProps, [
|
||||
"class",
|
||||
"children",
|
||||
"side",
|
||||
]);
|
||||
|
||||
return (
|
||||
<DialogPrimitive.Portal>
|
||||
<DialogPrimitive.Overlay
|
||||
class={cn(
|
||||
"fixed inset-0 z-50 bg-background/80 data-[expanded]:animate-in data-[closed]:animate-out data-[closed]:fade-out-0 data-[expanded]:fade-in-0",
|
||||
)}
|
||||
/>
|
||||
<DialogPrimitive.Content
|
||||
class={sheetVariants({ side: local.side, class: local.class })}
|
||||
{...rest}
|
||||
>
|
||||
{local.children}
|
||||
<DialogPrimitive.CloseButton class="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-[opacity,box-shadow] hover:opacity-100 focus:outline-none focus:ring-[1.5px] focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
class="h-4 w-4"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M18 6L6 18M6 6l12 12"
|
||||
/>
|
||||
<title>Close</title>
|
||||
</svg>
|
||||
</DialogPrimitive.CloseButton>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPrimitive.Portal>
|
||||
);
|
||||
};
|
||||
|
||||
type sheetTitleProps<T extends ValidComponent = "h2"> = DialogTitleProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export const SheetTitle = <T extends ValidComponent = "h2">(
|
||||
props: PolymorphicProps<T, sheetTitleProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as sheetTitleProps, ["class"]);
|
||||
|
||||
return (
|
||||
<DialogPrimitive.Title
|
||||
class={cn("text-lg font-semibold text-foreground", local.class)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type sheetDescriptionProps<T extends ValidComponent = "p"> =
|
||||
DialogDescriptionProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export const SheetDescription = <T extends ValidComponent = "p">(
|
||||
props: PolymorphicProps<T, sheetDescriptionProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as sheetDescriptionProps, ["class"]);
|
||||
|
||||
return (
|
||||
<DialogPrimitive.Description
|
||||
class={cn("text-sm text-muted-foreground", local.class)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const SheetHeader = (props: ComponentProps<"div">) => {
|
||||
const [local, rest] = splitProps(props, ["class"]);
|
||||
|
||||
return (
|
||||
<div
|
||||
class={cn(
|
||||
"flex flex-col space-y-2 text-center sm:text-left",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const SheetFooter = (props: ComponentProps<"div">) => {
|
||||
const [local, rest] = splitProps(props, ["class"]);
|
||||
|
||||
return (
|
||||
<div
|
||||
class={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
13
examples/astro-example/src/components/ui/skeleton.tsx
Normal file
13
examples/astro-example/src/components/ui/skeleton.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { cn } from "@/libs/cn";
|
||||
import { type ComponentProps, splitProps } from "solid-js";
|
||||
|
||||
export const Skeleton = (props: ComponentProps<"div">) => {
|
||||
const [local, rest] = splitProps(props, ["class"]);
|
||||
|
||||
return (
|
||||
<div
|
||||
class={cn("animate-pulse rounded-md bg-primary/10", local.class)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
21
examples/astro-example/src/components/ui/sonner.tsx
Normal file
21
examples/astro-example/src/components/ui/sonner.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Toaster as Sonner } from "solid-sonner";
|
||||
|
||||
export const Toaster = (props: Parameters<typeof Sonner>[0]) => {
|
||||
return (
|
||||
<Sonner
|
||||
class="toaster group"
|
||||
toastOptions={{
|
||||
classes: {
|
||||
toast:
|
||||
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
|
||||
description: "group-[.toast]:text-muted-foreground",
|
||||
actionButton:
|
||||
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
|
||||
cancelButton:
|
||||
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
62
examples/astro-example/src/components/ui/switch.tsx
Normal file
62
examples/astro-example/src/components/ui/switch.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import { cn } from "@/libs/cn";
|
||||
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
|
||||
import type {
|
||||
SwitchControlProps,
|
||||
SwitchThumbProps,
|
||||
} from "@kobalte/core/switch";
|
||||
import { Switch as SwitchPrimitive } from "@kobalte/core/switch";
|
||||
import type { ParentProps, ValidComponent, VoidProps } from "solid-js";
|
||||
import { splitProps } from "solid-js";
|
||||
|
||||
export const SwitchLabel = SwitchPrimitive.Label;
|
||||
export const Switch = SwitchPrimitive;
|
||||
export const SwitchErrorMessage = SwitchPrimitive.ErrorMessage;
|
||||
export const SwitchDescription = SwitchPrimitive.Description;
|
||||
|
||||
type switchControlProps<T extends ValidComponent = "input"> = ParentProps<
|
||||
SwitchControlProps<T> & { class?: string }
|
||||
>;
|
||||
|
||||
export const SwitchControl = <T extends ValidComponent = "input">(
|
||||
props: PolymorphicProps<T, switchControlProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as switchControlProps, [
|
||||
"class",
|
||||
"children",
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SwitchPrimitive.Input class="[&:focus-visible+div]:outline-none [&:focus-visible+div]:ring-[1.5px] [&:focus-visible+div]:ring-ring [&:focus-visible+div]:ring-offset-2 [&:focus-visible+div]:ring-offset-background" />
|
||||
<SwitchPrimitive.Control
|
||||
class={cn(
|
||||
"inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent bg-input shadow-sm transition-[color,background-color,box-shadow] data-[disabled]:cursor-not-allowed data-[checked]:bg-primary data-[disabled]:opacity-50",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{local.children}
|
||||
</SwitchPrimitive.Control>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
type switchThumbProps<T extends ValidComponent = "div"> = VoidProps<
|
||||
SwitchThumbProps<T> & { class?: string }
|
||||
>;
|
||||
|
||||
export const SwitchThumb = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, switchThumbProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as switchThumbProps, ["class"]);
|
||||
|
||||
return (
|
||||
<SwitchPrimitive.Thumb
|
||||
class={cn(
|
||||
"pointer-events-none block h-4 w-4 translate-x-0 rounded-full bg-background shadow-lg ring-0 transition-transform data-[checked]:translate-x-4",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
93
examples/astro-example/src/components/ui/table.tsx
Normal file
93
examples/astro-example/src/components/ui/table.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import { cn } from "@/libs/cn";
|
||||
import { type ComponentProps, splitProps } from "solid-js";
|
||||
|
||||
export const Table = (props: ComponentProps<"table">) => {
|
||||
const [local, rest] = splitProps(props, ["class"]);
|
||||
|
||||
return (
|
||||
<div class="w-full overflow-auto">
|
||||
<table
|
||||
class={cn("w-full caption-bottom text-sm", local.class)}
|
||||
{...rest}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const TableHeader = (props: ComponentProps<"thead">) => {
|
||||
const [local, rest] = splitProps(props, ["class"]);
|
||||
|
||||
return <thead class={cn("[&_tr]:border-b", local.class)} {...rest} />;
|
||||
};
|
||||
|
||||
export const TableBody = (props: ComponentProps<"tbody">) => {
|
||||
const [local, rest] = splitProps(props, ["class"]);
|
||||
|
||||
return (
|
||||
<tbody class={cn("[&_tr:last-child]:border-0", local.class)} {...rest} />
|
||||
);
|
||||
};
|
||||
|
||||
export const TableFooter = (props: ComponentProps<"tfoot">) => {
|
||||
const [local, rest] = splitProps(props, ["class"]);
|
||||
|
||||
return (
|
||||
<tbody
|
||||
class={cn("bg-primary font-medium text-primary-foreground", local.class)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const TableRow = (props: ComponentProps<"tr">) => {
|
||||
const [local, rest] = splitProps(props, ["class"]);
|
||||
|
||||
return (
|
||||
<tr
|
||||
class={cn(
|
||||
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const TableHead = (props: ComponentProps<"th">) => {
|
||||
const [local, rest] = splitProps(props, ["class"]);
|
||||
|
||||
return (
|
||||
<th
|
||||
class={cn(
|
||||
"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const TableCell = (props: ComponentProps<"td">) => {
|
||||
const [local, rest] = splitProps(props, ["class"]);
|
||||
|
||||
return (
|
||||
<td
|
||||
class={cn(
|
||||
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const TableCaption = (props: ComponentProps<"caption">) => {
|
||||
const [local, rest] = splitProps(props, ["class"]);
|
||||
|
||||
return (
|
||||
<caption
|
||||
class={cn("mt-4 text-sm text-muted-foreground", local.class)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
133
examples/astro-example/src/components/ui/tabs.tsx
Normal file
133
examples/astro-example/src/components/ui/tabs.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
import { cn } from "@/libs/cn";
|
||||
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
|
||||
import type {
|
||||
TabsContentProps,
|
||||
TabsIndicatorProps,
|
||||
TabsListProps,
|
||||
TabsRootProps,
|
||||
TabsTriggerProps,
|
||||
} from "@kobalte/core/tabs";
|
||||
import { Tabs as TabsPrimitive } from "@kobalte/core/tabs";
|
||||
import type { VariantProps } from "class-variance-authority";
|
||||
import { cva } from "class-variance-authority";
|
||||
import type { ValidComponent, VoidProps } from "solid-js";
|
||||
import { splitProps } from "solid-js";
|
||||
|
||||
type tabsProps<T extends ValidComponent = "div"> = TabsRootProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export const Tabs = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, tabsProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as tabsProps, ["class"]);
|
||||
|
||||
return (
|
||||
<TabsPrimitive
|
||||
class={cn("w-full data-[orientation=vertical]:flex", local.class)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type tabsListProps<T extends ValidComponent = "div"> = TabsListProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export const TabsList = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, tabsListProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as tabsListProps, ["class"]);
|
||||
|
||||
return (
|
||||
<TabsPrimitive.List
|
||||
class={cn(
|
||||
"relative flex w-full rounded-lg bg-muted p-1 text-muted-foreground data-[orientation=vertical]:flex-col data-[orientation=horizontal]:items-center data-[orientation=vertical]:items-stretch",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type tabsContentProps<T extends ValidComponent = "div"> =
|
||||
TabsContentProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export const TabsContent = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, tabsContentProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as tabsContentProps, ["class"]);
|
||||
|
||||
return (
|
||||
<TabsPrimitive.Content
|
||||
class={cn(
|
||||
"transition-shadow duration-200 focus-visible:outline-none focus-visible:ring-[1.5px] focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background data-[orientation=horizontal]:mt-2 data-[orientation=vertical]:ml-2",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type tabsTriggerProps<T extends ValidComponent = "button"> =
|
||||
TabsTriggerProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export const TabsTrigger = <T extends ValidComponent = "button">(
|
||||
props: PolymorphicProps<T, tabsTriggerProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as tabsTriggerProps, ["class"]);
|
||||
|
||||
return (
|
||||
<TabsPrimitive.Trigger
|
||||
class={cn(
|
||||
"peer relative z-10 inline-flex h-7 w-full items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium outline-none transition-colors disabled:pointer-events-none disabled:opacity-50 data-[selected]:text-foreground",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const tabsIndicatorVariants = cva(
|
||||
"absolute transition-all duration-200 outline-none",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
block:
|
||||
"data-[orientation=horizontal]:bottom-1 data-[orientation=horizontal]:left-0 data-[orientation=vertical]:right-1 data-[orientation=vertical]:top-0 data-[orientation=horizontal]:h-[calc(100%-0.5rem)] data-[orientation=vertical]:w-[calc(100%-0.5rem)] bg-background shadow rounded-md peer-focus-visible:ring-[1.5px] peer-focus-visible:ring-ring peer-focus-visible:ring-offset-2 peer-focus-visible:ring-offset-background peer-focus-visible:outline-none",
|
||||
underline:
|
||||
"data-[orientation=horizontal]:-bottom-[1px] data-[orientation=horizontal]:left-0 data-[orientation=vertical]:-right-[1px] data-[orientation=vertical]:top-0 data-[orientation=horizontal]:h-[2px] data-[orientation=vertical]:w-[2px] bg-primary",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "block",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
type tabsIndicatorProps<T extends ValidComponent = "div"> = VoidProps<
|
||||
TabsIndicatorProps<T> &
|
||||
VariantProps<typeof tabsIndicatorVariants> & {
|
||||
class?: string;
|
||||
}
|
||||
>;
|
||||
|
||||
export const TabsIndicator = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, tabsIndicatorProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as tabsIndicatorProps, [
|
||||
"class",
|
||||
"variant",
|
||||
]);
|
||||
|
||||
return (
|
||||
<TabsPrimitive.Indicator
|
||||
class={cn(tabsIndicatorVariants({ variant: local.variant }), local.class)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
28
examples/astro-example/src/components/ui/textarea.tsx
Normal file
28
examples/astro-example/src/components/ui/textarea.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { cn } from "@/libs/cn";
|
||||
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
|
||||
import type { TextFieldTextAreaProps } from "@kobalte/core/text-field";
|
||||
import { TextArea as TextFieldPrimitive } from "@kobalte/core/text-field";
|
||||
import type { ValidComponent, VoidProps } from "solid-js";
|
||||
import { splitProps } from "solid-js";
|
||||
|
||||
type textAreaProps<T extends ValidComponent = "textarea"> = VoidProps<
|
||||
TextFieldTextAreaProps<T> & {
|
||||
class?: string;
|
||||
}
|
||||
>;
|
||||
|
||||
export const TextArea = <T extends ValidComponent = "textarea">(
|
||||
props: PolymorphicProps<T, textAreaProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as textAreaProps, ["class"]);
|
||||
|
||||
return (
|
||||
<TextFieldPrimitive
|
||||
class={cn(
|
||||
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm transition-shadow placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-[1.5px] focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
129
examples/astro-example/src/components/ui/textfield.tsx
Normal file
129
examples/astro-example/src/components/ui/textfield.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
import { cn } from "@/libs/cn";
|
||||
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
|
||||
import type {
|
||||
TextFieldDescriptionProps,
|
||||
TextFieldErrorMessageProps,
|
||||
TextFieldInputProps,
|
||||
TextFieldLabelProps,
|
||||
TextFieldRootProps,
|
||||
} from "@kobalte/core/text-field";
|
||||
import { TextField as TextFieldPrimitive } from "@kobalte/core/text-field";
|
||||
import { cva } from "class-variance-authority";
|
||||
import type { ValidComponent, VoidProps } from "solid-js";
|
||||
import { splitProps } from "solid-js";
|
||||
|
||||
type textFieldProps<T extends ValidComponent = "div"> =
|
||||
TextFieldRootProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export const TextFieldRoot = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, textFieldProps<T>>
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as textFieldProps, ["class"]);
|
||||
|
||||
return <TextFieldPrimitive class={cn("space-y-1", local.class)} {...rest} />;
|
||||
};
|
||||
|
||||
export const textfieldLabel = cva(
|
||||
"text-sm data-[disabled]:cursor-not-allowed data-[disabled]:opacity-70 font-medium",
|
||||
{
|
||||
variants: {
|
||||
label: {
|
||||
true: "data-[invalid]:text-destructive",
|
||||
},
|
||||
error: {
|
||||
true: "text-destructive text-xs",
|
||||
},
|
||||
description: {
|
||||
true: "font-normal text-muted-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
label: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
type textFieldLabelProps<T extends ValidComponent = "label"> =
|
||||
TextFieldLabelProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export const TextFieldLabel = <T extends ValidComponent = "label">(
|
||||
props: PolymorphicProps<T, textFieldLabelProps<T>>
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as textFieldLabelProps, ["class"]);
|
||||
|
||||
return (
|
||||
<TextFieldPrimitive.Label
|
||||
class={cn(textfieldLabel(), local.class)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type textFieldErrorMessageProps<T extends ValidComponent = "div"> =
|
||||
TextFieldErrorMessageProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export const TextFieldErrorMessage = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, textFieldErrorMessageProps<T>>
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as textFieldErrorMessageProps, [
|
||||
"class",
|
||||
]);
|
||||
|
||||
return (
|
||||
<TextFieldPrimitive.ErrorMessage
|
||||
class={cn(textfieldLabel({ error: true }), local.class)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type textFieldDescriptionProps<T extends ValidComponent = "div"> =
|
||||
TextFieldDescriptionProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export const TextFieldDescription = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, textFieldDescriptionProps<T>>
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as textFieldDescriptionProps, [
|
||||
"class",
|
||||
]);
|
||||
|
||||
return (
|
||||
<TextFieldPrimitive.Description
|
||||
class={cn(
|
||||
textfieldLabel({ description: true, label: false }),
|
||||
local.class
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type textFieldInputProps<T extends ValidComponent = "input"> = VoidProps<
|
||||
TextFieldInputProps<T> & {
|
||||
class?: string;
|
||||
}
|
||||
>;
|
||||
|
||||
export const TextField = <T extends ValidComponent = "input">(
|
||||
props: PolymorphicProps<T, textFieldInputProps<T>>
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as textFieldInputProps, ["class"]);
|
||||
|
||||
return (
|
||||
<TextFieldPrimitive.Input
|
||||
class={cn(
|
||||
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-shadow file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-[1.5px] focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
||||
local.class
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
168
examples/astro-example/src/components/ui/toast.tsx
Normal file
168
examples/astro-example/src/components/ui/toast.tsx
Normal file
@@ -0,0 +1,168 @@
|
||||
import { cn } from "@/libs/cn";
|
||||
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
|
||||
import type {
|
||||
ToastDescriptionProps,
|
||||
ToastListProps,
|
||||
ToastRegionProps,
|
||||
ToastRootProps,
|
||||
ToastTitleProps,
|
||||
} from "@kobalte/core/toast";
|
||||
import { Toast as ToastPrimitive } from "@kobalte/core/toast";
|
||||
import type { VariantProps } from "class-variance-authority";
|
||||
import { cva } from "class-variance-authority";
|
||||
import type {
|
||||
ComponentProps,
|
||||
ValidComponent,
|
||||
VoidComponent,
|
||||
VoidProps,
|
||||
} from "solid-js";
|
||||
import { mergeProps, splitProps } from "solid-js";
|
||||
import { Portal } from "solid-js/web";
|
||||
|
||||
export const toastVariants = cva(
|
||||
"group pointer-events-auto relative flex flex-col gap-3 w-full items-center justify-between overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-y-0 data-[swipe=end]:translate-y-[var(--kb-toast-swipe-end-y)] data-[swipe=move]:translate-y-[--kb-toast-swipe-move-y] data-[swipe=move]:transition-none data-[opened]:animate-in data-[closed]:animate-out data-[swipe=end]:animate-out data-[closed]:fade-out-80 data-[closed]:slide-out-to-top-full data-[closed]:sm:slide-out-to-bottom-full data-[opened]:slide-in-from-top-full data-[opened]:sm:slide-in-from-bottom-full",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "border bg-background",
|
||||
destructive:
|
||||
"destructive group border-destructive bg-destructive text-destructive-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
type toastProps<T extends ValidComponent = "li"> = ToastRootProps<T> &
|
||||
VariantProps<typeof toastVariants> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export const Toast = <T extends ValidComponent = "li">(
|
||||
props: PolymorphicProps<T, toastProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as toastProps, ["class", "variant"]);
|
||||
|
||||
return (
|
||||
<ToastPrimitive
|
||||
class={cn(toastVariants({ variant: local.variant }), local.class)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type toastTitleProps<T extends ValidComponent = "div"> = ToastTitleProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export const ToastTitle = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, toastTitleProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as toastTitleProps, ["class"]);
|
||||
|
||||
return (
|
||||
<ToastPrimitive.Title
|
||||
class={cn("text-sm font-semibold [&+div]:text-xs", local.class)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type toastDescriptionProps<T extends ValidComponent = "div"> =
|
||||
ToastDescriptionProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export const ToastDescription = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, toastDescriptionProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as toastDescriptionProps, ["class"]);
|
||||
|
||||
return (
|
||||
<ToastPrimitive.Description
|
||||
class={cn("text-sm opacity-90", local.class)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type toastRegionProps<T extends ValidComponent = "div"> =
|
||||
ToastRegionProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export const ToastRegion = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, toastRegionProps<T>>,
|
||||
) => {
|
||||
const merge = mergeProps<toastRegionProps[]>(
|
||||
{
|
||||
swipeDirection: "down",
|
||||
},
|
||||
props,
|
||||
);
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<ToastPrimitive.Region {...merge} />
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
type toastListProps<T extends ValidComponent = "ol"> = VoidProps<
|
||||
ToastListProps<T> & {
|
||||
class?: string;
|
||||
}
|
||||
>;
|
||||
|
||||
export const ToastList = <T extends ValidComponent = "ol">(
|
||||
props: PolymorphicProps<T, toastListProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as toastListProps, ["class"]);
|
||||
|
||||
return (
|
||||
<ToastPrimitive.List
|
||||
class={cn(
|
||||
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse gap-2 p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const ToastContent = (props: ComponentProps<"div">) => {
|
||||
const [local, rest] = splitProps(props, ["class", "children"]);
|
||||
|
||||
return (
|
||||
<div class={cn("flex w-full flex-col", local.class)} {...rest}>
|
||||
<div>{local.children}</div>
|
||||
<ToastPrimitive.CloseButton class="absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M18 6L6 18M6 6l12 12"
|
||||
/>
|
||||
<title>Close</title>
|
||||
</svg>
|
||||
</ToastPrimitive.CloseButton>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ToastProgress: VoidComponent = () => {
|
||||
return (
|
||||
<ToastPrimitive.ProgressTrack class="h-1 w-full overflow-hidden rounded-xl bg-primary/20 group-[.destructive]:bg-background/20">
|
||||
<ToastPrimitive.ProgressFill class="h-full w-[--kb-toast-progress-fill-width] bg-primary transition-all duration-150 ease-linear group-[.destructive]:bg-destructive-foreground" />
|
||||
</ToastPrimitive.ProgressTrack>
|
||||
);
|
||||
};
|
||||
85
examples/astro-example/src/components/ui/toggle-group.tsx
Normal file
85
examples/astro-example/src/components/ui/toggle-group.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import { cn } from "@/libs/cn";
|
||||
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
|
||||
import type {
|
||||
ToggleGroupItemProps,
|
||||
ToggleGroupRootProps,
|
||||
} from "@kobalte/core/toggle-group";
|
||||
import { ToggleGroup as ToggleGroupPrimitive } from "@kobalte/core/toggle-group";
|
||||
import type { VariantProps } from "class-variance-authority";
|
||||
import type { Accessor, ParentProps, ValidComponent } from "solid-js";
|
||||
import { createContext, createMemo, splitProps, useContext } from "solid-js";
|
||||
import { toggleVariants } from "./toggle";
|
||||
|
||||
const ToggleGroupContext =
|
||||
createContext<Accessor<VariantProps<typeof toggleVariants>>>();
|
||||
|
||||
const useToggleGroup = () => {
|
||||
const context = useContext(ToggleGroupContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
"`useToggleGroup`: must be used within a `ToggleGroup` component",
|
||||
);
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
type toggleGroupProps<T extends ValidComponent = "div"> = ParentProps<
|
||||
ToggleGroupRootProps<T> &
|
||||
VariantProps<typeof toggleVariants> & {
|
||||
class?: string;
|
||||
}
|
||||
>;
|
||||
|
||||
export const ToggleGroup = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, toggleGroupProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as toggleGroupProps, [
|
||||
"class",
|
||||
"children",
|
||||
"size",
|
||||
"variant",
|
||||
]);
|
||||
|
||||
const value = createMemo<VariantProps<typeof toggleVariants>>(() => ({
|
||||
size: local.size,
|
||||
variant: local.variant,
|
||||
}));
|
||||
|
||||
return (
|
||||
<ToggleGroupPrimitive
|
||||
class={cn("flex items-center justify-center gap-1", local.class)}
|
||||
{...rest}
|
||||
>
|
||||
<ToggleGroupContext.Provider value={value}>
|
||||
{local.children}
|
||||
</ToggleGroupContext.Provider>
|
||||
</ToggleGroupPrimitive>
|
||||
);
|
||||
};
|
||||
|
||||
type toggleGroupItemProps<T extends ValidComponent = "button"> =
|
||||
ToggleGroupItemProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export const ToggleGroupItem = <T extends ValidComponent = "button">(
|
||||
props: PolymorphicProps<T, toggleGroupItemProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as toggleGroupItemProps, ["class"]);
|
||||
const context = useToggleGroup();
|
||||
|
||||
return (
|
||||
<ToggleGroupPrimitive.Item
|
||||
class={cn(
|
||||
toggleVariants({
|
||||
variant: context().variant,
|
||||
size: context().size,
|
||||
}),
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
56
examples/astro-example/src/components/ui/toggle.tsx
Normal file
56
examples/astro-example/src/components/ui/toggle.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import { cn } from "@/libs/cn";
|
||||
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
|
||||
import type { ToggleButtonRootProps } from "@kobalte/core/toggle-button";
|
||||
import { ToggleButton as ToggleButtonPrimitive } from "@kobalte/core/toggle-button";
|
||||
import type { VariantProps } from "class-variance-authority";
|
||||
import { cva } from "class-variance-authority";
|
||||
import type { ValidComponent } from "solid-js";
|
||||
import { splitProps } from "solid-js";
|
||||
|
||||
export const toggleVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-[box-shadow,color,background-color] hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-[1.5px] focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[pressed]:bg-accent data-[pressed]:text-accent-foreground",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-transparent",
|
||||
outline:
|
||||
"border border-input bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-3",
|
||||
sm: "h-8 px-2",
|
||||
lg: "h-10 px-3",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
type toggleButtonProps<T extends ValidComponent = "button"> =
|
||||
ToggleButtonRootProps<T> &
|
||||
VariantProps<typeof toggleVariants> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export const ToggleButton = <T extends ValidComponent = "button">(
|
||||
props: PolymorphicProps<T, toggleButtonProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as toggleButtonProps, [
|
||||
"class",
|
||||
"variant",
|
||||
"size",
|
||||
]);
|
||||
|
||||
return (
|
||||
<ToggleButtonPrimitive
|
||||
class={cn(
|
||||
toggleVariants({ variant: local.variant, size: local.size }),
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
39
examples/astro-example/src/components/ui/tooltip.tsx
Normal file
39
examples/astro-example/src/components/ui/tooltip.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { cn } from "@/libs/cn";
|
||||
import type { PolymorphicProps } from "@kobalte/core/polymorphic";
|
||||
import type {
|
||||
TooltipContentProps,
|
||||
TooltipRootProps,
|
||||
} from "@kobalte/core/tooltip";
|
||||
import { Tooltip as TooltipPrimitive } from "@kobalte/core/tooltip";
|
||||
import { type ValidComponent, mergeProps, splitProps } from "solid-js";
|
||||
|
||||
export const TooltipTrigger = TooltipPrimitive.Trigger;
|
||||
|
||||
export const Tooltip = (props: TooltipRootProps) => {
|
||||
const merge = mergeProps<TooltipRootProps[]>({ gutter: 4 }, props);
|
||||
|
||||
return <TooltipPrimitive {...merge} />;
|
||||
};
|
||||
|
||||
type tooltipContentProps<T extends ValidComponent = "div"> =
|
||||
TooltipContentProps<T> & {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
export const TooltipContent = <T extends ValidComponent = "div">(
|
||||
props: PolymorphicProps<T, tooltipContentProps<T>>,
|
||||
) => {
|
||||
const [local, rest] = splitProps(props as tooltipContentProps, ["class"]);
|
||||
|
||||
return (
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
class={cn(
|
||||
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground data-[expanded]:animate-in data-[closed]:animate-out data-[closed]:fade-out-0 data-[expanded]:fade-in-0 data-[closed]:zoom-out-95 data-[expanded]:zoom-in-95",
|
||||
local.class,
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
</TooltipPrimitive.Portal>
|
||||
);
|
||||
};
|
||||
488
examples/astro-example/src/components/user-card.tsx
Normal file
488
examples/astro-example/src/components/user-card.tsx
Normal file
@@ -0,0 +1,488 @@
|
||||
import {
|
||||
passkeyActions,
|
||||
signOut,
|
||||
twoFactorActions,
|
||||
useListPasskeys,
|
||||
userActions,
|
||||
useSession,
|
||||
} from "@/libs/auth-client";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "./ui/card";
|
||||
import { UAParser } from "ua-parser-js";
|
||||
import { Image, ImageFallback, ImageRoot } from "./ui/image";
|
||||
import type { Session, User } from "better-auth/types";
|
||||
import { createEffect, createSignal, Show } from "solid-js";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "./ui/dialog";
|
||||
import { Button } from "./ui/button";
|
||||
import { TextField, TextFieldLabel, TextFieldRoot } from "./ui/textfield";
|
||||
import { convertImageToBase64 } from "@/libs/utils";
|
||||
import { Loader } from "./loader";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "./ui/table";
|
||||
import type { ActiveSession } from "@/libs/types";
|
||||
|
||||
export function UserCard({
|
||||
activeSessions,
|
||||
initialSession,
|
||||
}: {
|
||||
activeSessions: Session[];
|
||||
initialSession: ActiveSession | null;
|
||||
}) {
|
||||
const [session, setSession] = createSignal(initialSession);
|
||||
const res = useSession();
|
||||
createEffect(() => {
|
||||
setSession(res().data);
|
||||
});
|
||||
return (
|
||||
<Card class="w-full">
|
||||
<CardHeader>
|
||||
<CardTitle>User</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent class="grid gap-8 grid-cols-1">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex items-center gap-4">
|
||||
<ImageRoot>
|
||||
<Image src={session()?.user.image} alt="picture" />
|
||||
<ImageFallback>{session()?.user.name.charAt(0)}</ImageFallback>
|
||||
</ImageRoot>
|
||||
<div class="grid gap-1">
|
||||
<p class="text-sm font-medium leading-none">
|
||||
{session()?.user.name}
|
||||
</p>
|
||||
<p class="text-sm">{session()?.user.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
<EditUserDialog user={session()?.user} />
|
||||
</div>
|
||||
<div class="border-l-2 px-2 w-max gap-1 flex flex-col">
|
||||
<p class="text-xs font-medium ">Active Sessions</p>
|
||||
{activeSessions.map((activeSession) => {
|
||||
return (
|
||||
<div>
|
||||
<div class="flex items-center gap-2 text-sm text-black font-medium dark:text-white">
|
||||
{new UAParser(activeSession.userAgent).getDevice().type ===
|
||||
"mobile" ? (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M15.5 1h-8A2.5 2.5 0 0 0 5 3.5v17A2.5 2.5 0 0 0 7.5 23h8a2.5 2.5 0 0 0 2.5-2.5v-17A2.5 2.5 0 0 0 15.5 1m-4 21c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5s1.5.67 1.5 1.5s-.67 1.5-1.5 1.5m4.5-4H7V4h9z"
|
||||
></path>
|
||||
</svg>
|
||||
) : (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M0 20v-2h4v-1q-.825 0-1.412-.587T2 15V5q0-.825.588-1.412T4 3h16q.825 0 1.413.588T22 5v10q0 .825-.587 1.413T20 17v1h4v2zm4-5h16V5H4zm0 0V5z"
|
||||
></path>
|
||||
</svg>
|
||||
)}
|
||||
{new UAParser(activeSession.userAgent).getOS().name},{" "}
|
||||
{new UAParser(activeSession.userAgent).getBrowser().name}
|
||||
<button
|
||||
class="text-red-500 opacity-80 cursor-pointer text-xs border-muted-foreground border-red-600 underline "
|
||||
onClick={async () => {
|
||||
const res = await userActions.revokeSession({
|
||||
id: activeSession.id,
|
||||
});
|
||||
|
||||
if (res.error) {
|
||||
alert(res.error.message);
|
||||
} else {
|
||||
alert("Session terminated");
|
||||
}
|
||||
}}
|
||||
>
|
||||
{activeSession.id === session()?.session.id
|
||||
? "Sign Out"
|
||||
: "Terminate"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<Button
|
||||
variant="outline"
|
||||
class="gap-2"
|
||||
onClick={async () => {
|
||||
await signOut();
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1.2em"
|
||||
height="1.2em"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M9 20.75H6a2.64 2.64 0 0 1-2.75-2.53V5.78A2.64 2.64 0 0 1 6 3.25h3a.75.75 0 0 1 0 1.5H6a1.16 1.16 0 0 0-1.25 1v12.47a1.16 1.16 0 0 0 1.25 1h3a.75.75 0 0 1 0 1.5Zm7-4a.74.74 0 0 1-.53-.22a.75.75 0 0 1 0-1.06L18.94 12l-3.47-3.47a.75.75 0 1 1 1.06-1.06l4 4a.75.75 0 0 1 0 1.06l-4 4a.74.74 0 0 1-.53.22"
|
||||
></path>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M20 12.75H9a.75.75 0 0 1 0-1.5h11a.75.75 0 0 1 0 1.5"
|
||||
></path>
|
||||
</svg>
|
||||
Sign Out
|
||||
</Button>
|
||||
<div>
|
||||
<TwoFactorDialog enabled={session()?.user.twoFactorEnabled} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="border-y py-4 flex items-center flex-wrap justify-between gap-2">
|
||||
<div class="flex flex-col gap-2">
|
||||
<p class="text-sm">Passkeys</p>
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<AddPasskeyDialog />
|
||||
<ListPasskeys />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function EditUserDialog(props: { user?: User }) {
|
||||
const user = props.user;
|
||||
const [isLoading, setIsLoading] = createSignal(false);
|
||||
const [image, setImage] = createSignal<File>();
|
||||
const [name, setName] = createSignal<string>();
|
||||
const [isOpen, setIsOpen] = createSignal(false);
|
||||
return (
|
||||
<Dialog onOpenChange={setIsOpen} open={isOpen()}>
|
||||
<DialogTrigger>
|
||||
<Button variant="secondary">Edit User</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Edit User</DialogTitle>
|
||||
<DialogDescription>Edit User Information</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div class="grid gap-2">
|
||||
<TextFieldRoot>
|
||||
<TextFieldLabel for="full-name">Full Name</TextFieldLabel>
|
||||
<TextField
|
||||
placeholder={user?.name}
|
||||
type="text"
|
||||
value={name()}
|
||||
onInput={(e) => {
|
||||
if ("value" in e.target) setName(e.target.value as string);
|
||||
}}
|
||||
/>
|
||||
</TextFieldRoot>
|
||||
<TextFieldRoot>
|
||||
<TextFieldLabel>Profile Image</TextFieldLabel>
|
||||
<TextField
|
||||
type="file"
|
||||
onChange={(e: any) => {
|
||||
const file = e.target.files?.[0];
|
||||
if ("value" in e.target) setImage(file);
|
||||
}}
|
||||
/>
|
||||
</TextFieldRoot>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
setIsLoading(true);
|
||||
await userActions.update({
|
||||
image: image()
|
||||
? await convertImageToBase64(image()!)
|
||||
: undefined,
|
||||
name: name(),
|
||||
fetchOptions: {
|
||||
onResponse(context) {
|
||||
setIsLoading(false);
|
||||
},
|
||||
onError(context) {
|
||||
alert(context.error.message);
|
||||
},
|
||||
onSuccess() {
|
||||
alert("User Updated Successfully");
|
||||
setIsOpen(false);
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Show fallback={<p>Update</p>} when={isLoading()}>
|
||||
<Loader />
|
||||
</Show>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function AddPasskeyDialog() {
|
||||
const [name, setName] = createSignal("");
|
||||
const [isLoading, setIsLoading] = createSignal(false);
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger>
|
||||
<Button variant="outline">Add Passkey</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Register New Passkey</DialogTitle>
|
||||
<DialogDescription>
|
||||
Add a new passkey to your account
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div class="grid gap-2">
|
||||
<TextFieldRoot>
|
||||
<TextFieldLabel for="passkey-name">
|
||||
Passkey Name (optional)
|
||||
</TextFieldLabel>
|
||||
<TextField
|
||||
type="text"
|
||||
placeholder="My Passkey"
|
||||
value={name()}
|
||||
onInput={(e) => {
|
||||
if ("value" in e.target) setName(e.target.value as string);
|
||||
}}
|
||||
/>
|
||||
</TextFieldRoot>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
const res = await passkeyActions.addPasskey({
|
||||
name: name(),
|
||||
fetchOptions: {
|
||||
onSuccess() {
|
||||
alert("Successfully added");
|
||||
setName("");
|
||||
},
|
||||
},
|
||||
});
|
||||
if (res?.error) {
|
||||
alert(res.error.message);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Show
|
||||
when={isLoading()}
|
||||
fallback={
|
||||
<div class="flex items-center gap-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1.2em"
|
||||
height="1.2em"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M3.25 9.65q-.175-.125-.213-.312t.113-.388q1.55-2.125 3.888-3.3t4.987-1.175q2.65 0 5 1.138T20.95 8.9q.175.225.113.4t-.213.3q-.15.125-.35.113t-.35-.213q-1.375-1.95-3.537-2.987t-4.588-1.038q-2.425 0-4.55 1.038T3.95 9.5q-.15.225-.35.25t-.35-.1m11.6 12.325q-2.6-.65-4.25-2.588T8.95 14.65q0-1.25.9-2.1t2.175-.85q1.275 0 2.175.85t.9 2.1q0 .825.625 1.388t1.475.562q.85 0 1.45-.562t.6-1.388q0-2.9-2.125-4.875T12.05 7.8q-2.95 0-5.075 1.975t-2.125 4.85q0 .6.113 1.5t.537 2.1q.075.225-.012.4t-.288.25q-.2.075-.387-.012t-.263-.288q-.375-.975-.537-1.937T3.85 14.65q0-3.325 2.413-5.575t5.762-2.25q3.375 0 5.8 2.25t2.425 5.575q0 1.25-.887 2.087t-2.163.838q-1.275 0-2.187-.837T14.1 14.65q0-.825-.612-1.388t-1.463-.562q-.85 0-1.463.563T9.95 14.65q0 2.425 1.438 4.05t3.712 2.275q.225.075.3.25t.025.375q-.05.175-.2.3t-.375.075M6.5 4.425q-.2.125-.4.063t-.3-.263q-.1-.2-.05-.362T6 3.575q1.4-.75 2.925-1.15t3.1-.4q1.6 0 3.125.388t2.95 1.112q.225.125.263.3t-.038.35q-.075.175-.25.275t-.425-.025q-1.325-.675-2.738-1.037t-2.887-.363q-1.45 0-2.85.338T6.5 4.425m2.95 17.2q-1.475-1.55-2.262-3.162T6.4 14.65q0-2.275 1.65-3.838t3.975-1.562q2.325 0 4 1.563T17.7 14.65q0 .225-.137.363t-.363.137q-.2 0-.35-.137t-.15-.363q0-1.875-1.388-3.137t-3.287-1.263q-1.9 0-3.262 1.263T7.4 14.65q0 2.025.7 3.438t2.05 2.837q.15.15.15.35t-.15.35q-.15.15-.35.15t-.35-.15m7.55-1.7q-2.225 0-3.863-1.5T11.5 14.65q0-.2.138-.35t.362-.15q.225 0 .363.15t.137.35q0 1.875 1.35 3.075t3.15 1.2q.15 0 .425-.025t.575-.075q.225-.05.388.063t.212.337q.05.2-.075.35t-.325.2q-.45.125-.787.138t-.413.012"
|
||||
></path>
|
||||
</svg>
|
||||
Add Passkey
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Loader />
|
||||
</Show>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function ListPasskeys() {
|
||||
const passkeys = useListPasskeys();
|
||||
const [isDeletePasskey, setIsDeletePasskey] = createSignal(false);
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger>
|
||||
<Button variant="outline">
|
||||
Passkeys{" "}
|
||||
{passkeys().data?.length ? `[${passkeys().data?.length}]` : ""}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Passkeys</DialogTitle>
|
||||
<DialogDescription>List of passkeys</DialogDescription>
|
||||
</DialogHeader>
|
||||
{passkeys().data?.length ? (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Name</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{passkeys().data?.map((passkey) => (
|
||||
<TableRow class="flex justify-between items-center">
|
||||
<TableCell>{passkey.name || "My Passkey"}</TableCell>
|
||||
<TableCell class="text-right">
|
||||
<button
|
||||
onClick={async () => {
|
||||
const res = await passkeyActions.deletePasskey({
|
||||
id: passkey.id,
|
||||
fetchOptions: {
|
||||
onRequest: () => {
|
||||
setIsDeletePasskey(true);
|
||||
},
|
||||
onSuccess: () => {
|
||||
alert("Passkey deleted successfully");
|
||||
setIsDeletePasskey(false);
|
||||
},
|
||||
onError: (error) => {
|
||||
alert(error.error.message);
|
||||
setIsDeletePasskey(false);
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Show
|
||||
when={isDeletePasskey()}
|
||||
fallback={
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M5 21V6H4V4h5V3h6v1h5v2h-1v15zm2-2h10V6H7zm2-2h2V8H9zm4 0h2V8h-2zM7 6v13z"
|
||||
></path>
|
||||
</svg>
|
||||
}
|
||||
>
|
||||
<Loader />
|
||||
</Show>
|
||||
</button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
) : (
|
||||
<p class="text-sm text-muted-foreground">No passkeys found</p>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function TwoFactorDialog(props: { enabled?: boolean }) {
|
||||
const [isOpen, setIsOpen] = createSignal(false);
|
||||
const [password, setPassword] = createSignal<string>();
|
||||
const [isLoading, setIsLoading] = createSignal(false);
|
||||
return (
|
||||
<Dialog onOpenChange={setIsOpen} open={isOpen()}>
|
||||
<DialogTrigger>
|
||||
<Button variant="secondary">
|
||||
{props.enabled ? "Disable 2FA" : "Enable 2FA"}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Enable Two Factor</DialogTitle>
|
||||
<DialogDescription>
|
||||
Enable two factor authentication
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div class="grid gap-2">
|
||||
<TextFieldRoot>
|
||||
<TextFieldLabel for="password">Password</TextFieldLabel>
|
||||
<TextField
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
value={password()}
|
||||
onInput={(e) => {
|
||||
if ("value" in e.target) setPassword(e.target.value as string);
|
||||
}}
|
||||
/>
|
||||
</TextFieldRoot>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
if (!password()) {
|
||||
alert("Password is required!");
|
||||
}
|
||||
setIsLoading(true);
|
||||
if (props.enabled) {
|
||||
await twoFactorActions.disable({
|
||||
password: password()!,
|
||||
fetchOptions: {
|
||||
onResponse(context) {
|
||||
setIsLoading(false);
|
||||
},
|
||||
onError(context) {
|
||||
alert(context.error.message);
|
||||
},
|
||||
onSuccess() {
|
||||
alert("Two factor is disabled!");
|
||||
setIsOpen(false);
|
||||
},
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
await twoFactorActions.enable({
|
||||
password: password()!,
|
||||
fetchOptions: {
|
||||
onResponse(context) {
|
||||
setIsLoading(false);
|
||||
},
|
||||
onError(context) {
|
||||
alert(context.error.message);
|
||||
},
|
||||
onSuccess() {
|
||||
alert("Two factor successfully enabled!");
|
||||
setIsOpen(false);
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Show
|
||||
fallback={<p>{props.enabled ? "Disable" : "Enable"}</p>}
|
||||
when={isLoading()}
|
||||
>
|
||||
<Loader />
|
||||
</Show>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
52
examples/astro-example/src/layouts/root-layout.astro
Normal file
52
examples/astro-example/src/layouts/root-layout.astro
Normal file
@@ -0,0 +1,52 @@
|
||||
---
|
||||
import "../app.css";
|
||||
---
|
||||
|
||||
<script is:inline>
|
||||
const getThemePreference = () => {
|
||||
if (
|
||||
typeof localStorage !== "undefined" &&
|
||||
localStorage.getItem("theme")
|
||||
) {
|
||||
return localStorage.getItem("theme");
|
||||
}
|
||||
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light";
|
||||
};
|
||||
|
||||
const setColorMode = () => {
|
||||
const isDark = getThemePreference() === "dark";
|
||||
document.documentElement.classList[isDark ? "add" : "remove"]("dark");
|
||||
};
|
||||
|
||||
if (typeof localStorage !== "undefined") {
|
||||
const observer = new MutationObserver(() => {
|
||||
const isDark = document.documentElement.classList.contains("dark");
|
||||
localStorage.setItem("theme", isDark ? "dark" : "light");
|
||||
});
|
||||
observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ["class"],
|
||||
});
|
||||
}
|
||||
|
||||
setColorMode();
|
||||
|
||||
document.addEventListener("astro:after-swap", setColorMode);
|
||||
</script>
|
||||
|
||||
|
||||
<html>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<title>Astro</title>
|
||||
</head>
|
||||
<body class="w-full h-screen flex-col items-center flex justify-center">
|
||||
<div class="w-1/2 flex justify-center">
|
||||
<slot />
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,3 +0,0 @@
|
||||
import { createAuthClient } from "better-auth/client";
|
||||
|
||||
export const { signIn, signOut, useSession } = createAuthClient();
|
||||
32
examples/astro-example/src/libs/auth-client.ts
Normal file
32
examples/astro-example/src/libs/auth-client.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { passkeyClient, twoFactorClient } from "better-auth/client/plugins";
|
||||
import { createAuthClient } from "better-auth/solid";
|
||||
import { createAuthClient as createVanillaClient } from "better-auth/client";
|
||||
export const {
|
||||
signIn,
|
||||
signOut,
|
||||
useSession,
|
||||
signUp,
|
||||
passkey: passkeyActions,
|
||||
useListPasskeys,
|
||||
user: userActions,
|
||||
twoFactor: twoFactorActions,
|
||||
$Infer,
|
||||
} = createAuthClient({
|
||||
baseURL:
|
||||
process.env.NODE_ENV === "development"
|
||||
? "http://localhost:3000"
|
||||
: undefined,
|
||||
plugins: [
|
||||
passkeyClient(),
|
||||
twoFactorClient({
|
||||
twoFactorPage: "/two-factor",
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
export const { useSession: useVanillaSession } = createVanillaClient({
|
||||
baseURL:
|
||||
process.env.NODE_ENV === "development"
|
||||
? "http://localhost:3000"
|
||||
: undefined,
|
||||
});
|
||||
5
examples/astro-example/src/libs/cn.ts
Normal file
5
examples/astro-example/src/libs/cn.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import type { ClassValue } from "clsx";
|
||||
import clsx from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export const cn = (...classLists: ClassValue[]) => twMerge(clsx(classLists));
|
||||
3
examples/astro-example/src/libs/types.ts
Normal file
3
examples/astro-example/src/libs/types.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import type { $Infer } from "./auth-client";
|
||||
|
||||
export type ActiveSession = typeof $Infer.Session;
|
||||
8
examples/astro-example/src/libs/utils.ts
Normal file
8
examples/astro-example/src/libs/utils.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export async function convertImageToBase64(file: File): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => resolve(reader.result as string);
|
||||
reader.onerror = reject;
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
17
examples/astro-example/src/middleware.ts
Normal file
17
examples/astro-example/src/middleware.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { auth } from "@/auth";
|
||||
import { defineMiddleware } from "astro:middleware";
|
||||
|
||||
// `context` and `next` are automatically typed
|
||||
export const onRequest = defineMiddleware(async (context, next) => {
|
||||
const isAuthed = await auth.api
|
||||
.getSession({
|
||||
headers: context.request.headers,
|
||||
})
|
||||
.catch((e) => {
|
||||
return null;
|
||||
});
|
||||
if (context.url.pathname === "/dashboard" && !isAuthed) {
|
||||
return context.redirect("/");
|
||||
}
|
||||
return next();
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { APIRoute } from "astro";
|
||||
import { auth } from "../../../lib/auth";
|
||||
import { auth } from "../../../auth";
|
||||
|
||||
export const GET: APIRoute = async (ctx) => {
|
||||
return auth.handler(ctx.request);
|
||||
|
||||
38
examples/astro-example/src/pages/dashboard.astro
Normal file
38
examples/astro-example/src/pages/dashboard.astro
Normal file
@@ -0,0 +1,38 @@
|
||||
---
|
||||
import { UserCard } from "@/components/user-card";
|
||||
import RootLayout from "@/layouts/root-layout.astro";
|
||||
import { auth } from "@/auth";
|
||||
|
||||
const activeSessions = await auth.api
|
||||
.listSessions({
|
||||
headers: Astro.request.headers,
|
||||
})
|
||||
.catch((e) => {
|
||||
return [];
|
||||
});
|
||||
const session = await auth.api.getSession({
|
||||
headers: Astro.request.headers,
|
||||
});
|
||||
---
|
||||
|
||||
<RootLayout>
|
||||
<UserCard activeSessions={activeSessions} initialSession={session} client:only/>
|
||||
<button
|
||||
id="login"
|
||||
class="bg-black mt-3 flex items-center gap-2 rounded-sm py-2 px-3 text-white text-sm"
|
||||
>
|
||||
Sign In
|
||||
</button>
|
||||
<script>
|
||||
import { useVanillaSession} from "../libs/auth-client";
|
||||
useVanillaSession.subscribe((val) => {
|
||||
if (val.data) {
|
||||
document.getElementById("login")!.style.display = "none";
|
||||
} else {
|
||||
if(val.error){
|
||||
document.getElementById("login")!.style.display = "flex";
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</RootLayout>
|
||||
@@ -1,22 +1,10 @@
|
||||
---
|
||||
import RootLayout from "@/layouts/root-layout.astro";
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<title>Astro</title>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
class="min-h-[80vh] flex flex-col items-center justify-center overflow-hidden no-visible-scrollbar px-6 md:px-0"
|
||||
<div
|
||||
class="flex flex-col gap-2"
|
||||
>
|
||||
<h3 class="font-bold text-4xl text-black dark:text-white text-center">
|
||||
<RootLayout>
|
||||
<div class="min-h-[80vh] flex flex-col items-center justify-center overflow-hidden no-visible-scrollbar px-6 md:px-0">
|
||||
<h3 class="font-bold text-xl text-black dark:text-white text-center">
|
||||
Better Auth.
|
||||
</h3>
|
||||
<p class="text-center break-words text-sm md:text-base">
|
||||
@@ -30,62 +18,36 @@
|
||||
</a>{" "}
|
||||
example. <br />
|
||||
</p>
|
||||
<div id="user" class="w-1/2 flex gap-2 justify-center items-center mt-2">
|
||||
<img src="" alt="" id="img" class="w-12 h-12">
|
||||
<div>
|
||||
<p id="name">
|
||||
</p>
|
||||
<p id="email">
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/sign-in">
|
||||
<button
|
||||
id="login"
|
||||
class="bg-black mt-3 flex items-center gap-2 rounded-sm py-2 px-3 text-white text-sm"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="22"
|
||||
height="22"
|
||||
viewBox="0 0 24 24"
|
||||
><path
|
||||
fill="currentColor"
|
||||
d="M3.064 7.51A10 10 0 0 1 12 2c2.695 0 4.959.991 6.69 2.605l-2.867 2.868C14.786 6.482 13.468 5.977 12 5.977c-2.605 0-4.81 1.76-5.595 4.123c-.2.6-.314 1.24-.314 1.9s.114 1.3.314 1.9c.786 2.364 2.99 4.123 5.595 4.123c1.345 0 2.49-.355 3.386-.955a4.6 4.6 0 0 0 1.996-3.018H12v-3.868h9.418c.118.654.182 1.336.182 2.045c0 3.046-1.09 5.61-2.982 7.35C16.964 21.105 14.7 22 12 22A9.996 9.996 0 0 1 2 12c0-1.614.386-3.14 1.064-4.49"
|
||||
></path></svg
|
||||
Sign In
|
||||
</button>
|
||||
</a>
|
||||
|
||||
<a href="/dashboard">
|
||||
<button
|
||||
id="dashboard"
|
||||
class="bg-black mt-3 flex items-center gap-2 rounded-sm py-2 px-3 text-white text-sm"
|
||||
>
|
||||
Continue with Google
|
||||
</button>
|
||||
<button id="sign-out" class="bg-black mt-3 flex items-center gap-2 rounded-sm py-2 px-3 text-white text-sm">
|
||||
Sign Out
|
||||
Dashboard
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
<script>
|
||||
import { signIn, useSession, signOut } from "../lib/auth-client";
|
||||
document.getElementById("login")!.addEventListener("click", () => {
|
||||
signIn.social({
|
||||
provider: "google",
|
||||
});
|
||||
});
|
||||
useSession.get();
|
||||
useSession.subscribe((val) => {
|
||||
import { useVanillaSession } from "../libs/auth-client";
|
||||
useVanillaSession.subscribe((val) => {
|
||||
if (val.data) {
|
||||
document.getElementById("name")!.textContent = val.data?.user.name || "";
|
||||
document.getElementById("email")!.textContent = val.data?.user.email || "";
|
||||
document.getElementById("img")!.src = val.data?.user.image || "";
|
||||
document.getElementById("login")!.style.display = "none";
|
||||
document.getElementById("dashboard")!.style.display = "flex";
|
||||
} else {
|
||||
if(val.error){
|
||||
document.getElementById("login")!.style.display = "flex";
|
||||
document.getElementById("sign-out")!.style.display = "none";
|
||||
document.getElementById("img")!.style.display = "none";
|
||||
document.getElementById("name")!.textContent = "";
|
||||
document.getElementById("email")!.textContent = "";
|
||||
document.getElementById("dashboard")!.style.display = "none";
|
||||
}
|
||||
}
|
||||
});
|
||||
document.getElementById("sign-out")!.addEventListener("click", () => {
|
||||
signOut();
|
||||
});
|
||||
</script>
|
||||
</RootLayout>
|
||||
|
||||
8
examples/astro-example/src/pages/sign-in.astro
Normal file
8
examples/astro-example/src/pages/sign-in.astro
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
import RootLayout from "@/layouts/root-layout.astro";
|
||||
import { SignInCard } from "../components/sign-in";
|
||||
---
|
||||
|
||||
<RootLayout>
|
||||
<SignInCard client:load />
|
||||
</RootLayout>
|
||||
9
examples/astro-example/src/pages/sign-up.astro
Normal file
9
examples/astro-example/src/pages/sign-up.astro
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
import RootLayout from "@/layouts/root-layout.astro";
|
||||
import { SignUpCard } from "../components/sign-up";
|
||||
---
|
||||
|
||||
|
||||
<RootLayout>
|
||||
<SignUpCard client:load />
|
||||
</RootLayout>
|
||||
9
examples/astro-example/src/pages/two-factor.astro
Normal file
9
examples/astro-example/src/pages/two-factor.astro
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
import RootLayout from "@/layouts/root-layout.astro";
|
||||
import { TwoFactorComponent } from "@/components/two-factor";
|
||||
---
|
||||
|
||||
<RootLayout>
|
||||
<TwoFactorComponent client:load />
|
||||
</RootLayout>
|
||||
|
||||
10
examples/astro-example/src/pages/two-factor/email.astro
Normal file
10
examples/astro-example/src/pages/two-factor/email.astro
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
import RootLayout from "@/layouts/root-layout.astro";
|
||||
import { TwoFactorEmail } from "@/components/two-factor";
|
||||
---
|
||||
|
||||
|
||||
|
||||
<RootLayout>
|
||||
<TwoFactorEmail client:load />
|
||||
</RootLayout>
|
||||
@@ -1,8 +1,87 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
|
||||
module.exports = {
|
||||
darkMode: ["class", '[data-kb-theme="dark"]'],
|
||||
content: ['./src/**/*.{tsx,astro}',],
|
||||
prefix: "",
|
||||
theme: {
|
||||
extend: {},
|
||||
container: {
|
||||
center: true,
|
||||
padding: "2rem",
|
||||
screens: {
|
||||
"2xl": "1400px",
|
||||
},
|
||||
plugins: [],
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
border: "hsl(var(--border))",
|
||||
input: "hsl(var(--input))",
|
||||
ring: "hsl(var(--ring))",
|
||||
background: "hsl(var(--background))",
|
||||
foreground: "hsl(var(--foreground))",
|
||||
primary: {
|
||||
DEFAULT: "hsl(var(--primary))",
|
||||
foreground: "hsl(var(--primary-foreground))",
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: "hsl(var(--secondary))",
|
||||
foreground: "hsl(var(--secondary-foreground))",
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: "hsl(var(--destructive))",
|
||||
foreground: "hsl(var(--destructive-foreground))",
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: "hsl(var(--muted))",
|
||||
foreground: "hsl(var(--muted-foreground))",
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: "hsl(var(--accent))",
|
||||
foreground: "hsl(var(--accent-foreground))",
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: "hsl(var(--popover))",
|
||||
foreground: "hsl(var(--popover-foreground))",
|
||||
},
|
||||
card: {
|
||||
DEFAULT: "hsl(var(--card))",
|
||||
foreground: "hsl(var(--card-foreground))",
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
lg: "var(--radius)",
|
||||
md: "calc(var(--radius) - 2px)",
|
||||
sm: "calc(var(--radius) - 4px)",
|
||||
},
|
||||
keyframes: {
|
||||
"accordion-down": {
|
||||
from: { height: 0 },
|
||||
to: { height: "var(--kb-accordion-content-height)" },
|
||||
},
|
||||
"accordion-up": {
|
||||
from: { height: "var(--kb-accordion-content-height)" },
|
||||
to: { height: 0 },
|
||||
},
|
||||
"collapsible-down": {
|
||||
from: { height: 0 },
|
||||
to: { height: "var(--kb-collapsible-content-height)" },
|
||||
},
|
||||
"collapsible-up": {
|
||||
from: { height: "var(--kb-collapsible-content-height)" },
|
||||
to: { height: 0 },
|
||||
},
|
||||
"caret-blink": {
|
||||
"0%,70%,100%": { opacity: "1" },
|
||||
"20%,50%": { opacity: "0" }
|
||||
}
|
||||
},
|
||||
animation: {
|
||||
"accordion-down": "accordion-down 0.2s ease-out",
|
||||
"accordion-up": "accordion-up 0.2s ease-out",
|
||||
"collapsible-down": "collapsible-down 0.2s ease-out",
|
||||
"collapsible-up": "collapsible-up 0.2s ease-out",
|
||||
"caret-blink": "caret-blink 1.25s ease-out infinite"
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require("tailwindcss-animate")],
|
||||
};
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/strict"
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"compilerOptions": {
|
||||
"jsx": "preserve",
|
||||
"jsxImportSource": "solid-js",
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -81,7 +81,7 @@
|
||||
"@simplewebauthn/browser": "^10.0.0",
|
||||
"@simplewebauthn/server": "^10.0.1",
|
||||
"arctic": "2.0.0-next.9",
|
||||
"better-call": "0.2.3-beta.8",
|
||||
"better-call": "0.2.3-beta.10",
|
||||
"better-sqlite3": "^11.1.2",
|
||||
"c12": "^1.11.2",
|
||||
"chalk": "^5.3.0",
|
||||
@@ -89,6 +89,7 @@
|
||||
"consola": "^3.2.3",
|
||||
"defu": "^6.1.4",
|
||||
"dotenv": "^16.4.5",
|
||||
"i": "^0.3.7",
|
||||
"jose": "^5.7.0",
|
||||
"kysely": "^0.27.4",
|
||||
"kysely-postgres-js": "^2.0.0",
|
||||
|
||||
@@ -33,6 +33,24 @@ export const getAuthTables = (options: BetterAuthOptions) => {
|
||||
>,
|
||||
);
|
||||
|
||||
const shouldAddRateLimitTable = options.rateLimit?.storage === "database";
|
||||
const rateLimitTable = {
|
||||
rateLimit: {
|
||||
tableName: options.rateLimit?.tableName || "rateLimit",
|
||||
fields: {
|
||||
key: {
|
||||
type: "string",
|
||||
},
|
||||
count: {
|
||||
type: "number",
|
||||
},
|
||||
lastRequest: {
|
||||
type: "number",
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies BetterAuthDbSchema;
|
||||
|
||||
const { user, session, account, ...pluginTables } = pluginSchema || {};
|
||||
|
||||
return {
|
||||
@@ -133,5 +151,6 @@ export const getAuthTables = (options: BetterAuthOptions) => {
|
||||
order: 2,
|
||||
},
|
||||
...pluginTables,
|
||||
...(shouldAddRateLimitTable ? rateLimitTable : {}),
|
||||
} satisfies BetterAuthDbSchema;
|
||||
};
|
||||
|
||||
@@ -30,6 +30,14 @@ import { logger } from "../utils/logger";
|
||||
import { changePassword, updateUser } from "./routes/update-user";
|
||||
import type { BetterAuthPlugin } from "../plugins";
|
||||
import chalk from "chalk";
|
||||
import { getIp } from "../utils";
|
||||
import {
|
||||
getRateLimitStorage,
|
||||
getRetryAfter,
|
||||
onRequestRateLimit,
|
||||
rateLimitResponse,
|
||||
shouldRateLimit,
|
||||
} from "./rate-limiter";
|
||||
|
||||
export function getEndpoints<
|
||||
C extends AuthContext,
|
||||
@@ -167,6 +175,9 @@ export const router = <C extends AuthContext, Option extends BetterAuthOptions>(
|
||||
},
|
||||
...middlewares,
|
||||
],
|
||||
async onRequest(req) {
|
||||
return onRequestRateLimit(req, ctx);
|
||||
},
|
||||
onError(e) {
|
||||
const log = options.verboseLog ? logger : undefined;
|
||||
if (options.disableLog !== true) {
|
||||
@@ -180,7 +191,7 @@ export const router = <C extends AuthContext, Option extends BetterAuthOptions>(
|
||||
return;
|
||||
}
|
||||
if (errorMessage.includes("no such table")) {
|
||||
log?.error(
|
||||
logger?.error(
|
||||
`Please run ${chalk.green(
|
||||
"npx better-auth migrate",
|
||||
)} to create the tables. There are missing tables in your SQLite database.`,
|
||||
@@ -198,7 +209,7 @@ export const router = <C extends AuthContext, Option extends BetterAuthOptions>(
|
||||
errorMessage.includes("Table") &&
|
||||
errorMessage.includes("doesn't exist")
|
||||
) {
|
||||
log?.error(
|
||||
logger?.error(
|
||||
`Please run ${chalk.green(
|
||||
"npx better-auth migrate",
|
||||
)} to create the tables. There are missing tables in your MySQL database.`,
|
||||
|
||||
79
packages/better-auth/src/api/rate-limiter.test.ts
Normal file
79
packages/better-auth/src/api/rate-limiter.test.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { getTestInstance } from "../test-utils/test-instance";
|
||||
|
||||
describe("rate-limiter", async () => {
|
||||
const { client, testUser } = await getTestInstance({
|
||||
rateLimit: {
|
||||
window: 10,
|
||||
max: 20,
|
||||
},
|
||||
});
|
||||
|
||||
it("should return 429 after 7 request for sign-in", async () => {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const response = await client.signIn.email({
|
||||
email: testUser.email,
|
||||
password: testUser.password,
|
||||
});
|
||||
if (i >= 7) {
|
||||
expect(response.error?.status).toBe(429);
|
||||
} else {
|
||||
expect(response.error).toBeNull();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("should reset the limit after the window period", async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.advanceTimersByTime(11000);
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const res = await client.signIn.email({
|
||||
email: testUser.email,
|
||||
password: testUser.password,
|
||||
});
|
||||
if (i >= 7) {
|
||||
expect(res.error?.status).toBe(429);
|
||||
} else {
|
||||
expect(res.error).toBeNull();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("should respond the correct retry-after header", async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.advanceTimersByTime(3000);
|
||||
let retryAfter = "";
|
||||
await client.signIn.email({
|
||||
email: testUser.email,
|
||||
password: testUser.password,
|
||||
fetchOptions: {
|
||||
onError(context) {
|
||||
retryAfter = context.response.headers.get("X-Retry-After") ?? "";
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(retryAfter).toBe("7");
|
||||
});
|
||||
|
||||
it("should rate limit based on the path", async () => {
|
||||
const signInRes = await client.signIn.email({
|
||||
email: testUser.email,
|
||||
password: testUser.password,
|
||||
});
|
||||
expect(signInRes.error?.status).toBe(429);
|
||||
|
||||
const signUpRes = await client.signUp.email({
|
||||
email: "new-test@email.com",
|
||||
password: testUser.password,
|
||||
name: "test",
|
||||
});
|
||||
expect(signUpRes.error).toBeNull();
|
||||
});
|
||||
|
||||
it("non-special-rules limits", async () => {
|
||||
for (let i = 0; i < 25; i++) {
|
||||
const response = await client.session();
|
||||
expect(response.error?.status).toBe(i >= 20 ? 429 : 401);
|
||||
}
|
||||
});
|
||||
});
|
||||
177
packages/better-auth/src/api/rate-limiter.ts
Normal file
177
packages/better-auth/src/api/rate-limiter.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
import type { AuthContext, RateLimit } from "../types";
|
||||
import { getIp, logger } from "../utils";
|
||||
|
||||
export function shouldRateLimit(
|
||||
max: number,
|
||||
window: number,
|
||||
rateLimitData: RateLimit,
|
||||
) {
|
||||
const now = Date.now();
|
||||
const windowInMs = window * 1000;
|
||||
const timeSinceLastRequest = now - rateLimitData.lastRequest;
|
||||
return timeSinceLastRequest < windowInMs && rateLimitData.count >= max;
|
||||
}
|
||||
|
||||
export function rateLimitResponse(retryAfter: number) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
message: "Too many requests. Please try again later.",
|
||||
}),
|
||||
{
|
||||
status: 429,
|
||||
statusText: "Too Many Requests",
|
||||
headers: {
|
||||
"X-Retry-After": retryAfter.toString(),
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function getRetryAfter(lastRequest: number, window: number) {
|
||||
const now = Date.now();
|
||||
const windowInMs = window * 1000;
|
||||
return Math.ceil((lastRequest + windowInMs - now) / 1000);
|
||||
}
|
||||
|
||||
function createDBStorage(ctx: AuthContext, tableName?: string) {
|
||||
const db = ctx.db;
|
||||
return {
|
||||
get: async (key: string) => {
|
||||
const result = await db
|
||||
.selectFrom(tableName ?? "rateLimit")
|
||||
.where("key", "=", key)
|
||||
.selectAll()
|
||||
.executeTakeFirst();
|
||||
return result as RateLimit | undefined;
|
||||
},
|
||||
set: async (key: string, value: RateLimit, _update?: boolean) => {
|
||||
try {
|
||||
if (_update) {
|
||||
await db
|
||||
.updateTable(tableName ?? "rateLimit")
|
||||
.set({
|
||||
count: value.count,
|
||||
lastRequest: value.lastRequest,
|
||||
})
|
||||
.where("key", "=", key)
|
||||
.execute();
|
||||
} else {
|
||||
await db
|
||||
.insertInto(tableName ?? "rateLimit")
|
||||
.values({
|
||||
key,
|
||||
count: value.count,
|
||||
lastRequest: value.lastRequest,
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error("Error setting rate limit", e);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const memory = new Map<string, RateLimit>();
|
||||
export function getRateLimitStorage(ctx: AuthContext) {
|
||||
if (ctx.rateLimit.customStorage) {
|
||||
return ctx.rateLimit.customStorage;
|
||||
}
|
||||
const storage = ctx.rateLimit.storage;
|
||||
if (storage === "memory") {
|
||||
return {
|
||||
async get(key: string) {
|
||||
return memory.get(key);
|
||||
},
|
||||
async set(key: string, value: RateLimit, _update?: boolean) {
|
||||
memory.set(key, value);
|
||||
},
|
||||
};
|
||||
}
|
||||
return createDBStorage(ctx, ctx.rateLimit.tableName);
|
||||
}
|
||||
|
||||
export async function onRequestRateLimit(req: Request, ctx: AuthContext) {
|
||||
if (!ctx.rateLimit.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const baseURL = ctx.baseURL;
|
||||
const path = req.url.replace(baseURL, "");
|
||||
let window = ctx.rateLimit.window;
|
||||
let max = ctx.rateLimit.max;
|
||||
const key = getIp(req) + path;
|
||||
const specialRules = getDefaultSpecialRules();
|
||||
const specialRule = specialRules.find((rule) => rule.pathMatcher(path));
|
||||
|
||||
if (specialRule) {
|
||||
window = specialRule.window;
|
||||
max = specialRule.max;
|
||||
}
|
||||
|
||||
for (const plugin of ctx.options.plugins || []) {
|
||||
if (plugin.rateLimit) {
|
||||
const matchedRule = plugin.rateLimit.find((rule) =>
|
||||
rule.pathMatcher(path),
|
||||
);
|
||||
if (matchedRule) {
|
||||
window = matchedRule.window;
|
||||
max = matchedRule.max;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ctx.rateLimit.customRules) {
|
||||
const customRule = ctx.rateLimit.customRules[path];
|
||||
if (customRule) {
|
||||
window = customRule.window;
|
||||
max = customRule.max;
|
||||
}
|
||||
}
|
||||
|
||||
const storage = getRateLimitStorage(ctx);
|
||||
const data = await storage.get(key);
|
||||
const now = Date.now();
|
||||
|
||||
if (!data) {
|
||||
await storage.set(key, {
|
||||
key,
|
||||
count: 1,
|
||||
lastRequest: now,
|
||||
});
|
||||
} else {
|
||||
const timeSinceLastRequest = now - data.lastRequest;
|
||||
|
||||
if (shouldRateLimit(max, window, data)) {
|
||||
const retryAfter = getRetryAfter(data.lastRequest, window);
|
||||
return rateLimitResponse(retryAfter);
|
||||
} else if (timeSinceLastRequest > window * 1000) {
|
||||
// Reset the count if the window has passed since the last request
|
||||
await storage.set(key, {
|
||||
...data,
|
||||
count: 1,
|
||||
lastRequest: now,
|
||||
});
|
||||
} else {
|
||||
await storage.set(key, {
|
||||
...data,
|
||||
count: data.count + 1,
|
||||
lastRequest: now,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getDefaultSpecialRules() {
|
||||
const specialRules = [
|
||||
{
|
||||
pathMatcher(path: string) {
|
||||
return path.startsWith("/sign-in") || path.startsWith("/sign-up");
|
||||
},
|
||||
window: 10,
|
||||
max: 7,
|
||||
},
|
||||
];
|
||||
return specialRules;
|
||||
}
|
||||
@@ -113,7 +113,9 @@ export const signInEmail = createAuthEndpoint(
|
||||
},
|
||||
async (ctx) => {
|
||||
if (!ctx.context.options?.emailAndPassword?.enabled) {
|
||||
ctx.context.logger.error("Email and password is not enabled");
|
||||
ctx.context.logger.error(
|
||||
"Email and password is not enabled. Make sure to enable it in the options on you `auth.ts` file. Check `https://better-auth.com/docs/authentication/email-password` for more!",
|
||||
);
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "Email and password is not enabled",
|
||||
});
|
||||
|
||||
@@ -6,11 +6,11 @@ export const signOut = createAuthEndpoint(
|
||||
"/sign-out",
|
||||
{
|
||||
method: "POST",
|
||||
body: z
|
||||
.object({
|
||||
body: z.optional(
|
||||
z.object({
|
||||
callbackURL: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
}),
|
||||
),
|
||||
},
|
||||
async (ctx) => {
|
||||
const sessionCookieToken = await ctx.getSignedCookie(
|
||||
|
||||
@@ -3,8 +3,6 @@ import { getEndpoints, router } from "./api";
|
||||
import { init } from "./init";
|
||||
import type { BetterAuthOptions } from "./types/options";
|
||||
import type { InferPluginTypes, InferSession, InferUser } from "./types";
|
||||
import type { BetterAuthPlugin } from "./plugins";
|
||||
import type { UnionToIntersection } from "./types/helper";
|
||||
|
||||
type InferAPI<API> = Omit<
|
||||
API,
|
||||
|
||||
@@ -91,7 +91,7 @@ export function createDynamicPathProxy<T extends Record<string, any>>(
|
||||
setTimeout(() => {
|
||||
//@ts-expect-error
|
||||
signal.set(!val);
|
||||
}, 0);
|
||||
}, 10);
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export class BetterAuthError extends Error {
|
||||
constructor(message: string, cause?: string) {
|
||||
constructor(message: string, cause?: string, docsLink?: string) {
|
||||
super(message);
|
||||
this.name = "BetterAuthError";
|
||||
this.message = message;
|
||||
|
||||
@@ -62,6 +62,14 @@ export const init = (options: BetterAuthOptions) => {
|
||||
expiresIn: options.session?.expiresIn || 60 * 60 * 24 * 7, // 7 days
|
||||
},
|
||||
secret,
|
||||
rateLimit: {
|
||||
...options.rateLimit,
|
||||
enabled:
|
||||
options.rateLimit?.enabled ?? process.env.NODE_ENV !== "development",
|
||||
window: options.rateLimit?.window || 60,
|
||||
max: options.rateLimit?.max || 100,
|
||||
storage: options.rateLimit?.storage || "memory",
|
||||
},
|
||||
authCookies: cookies,
|
||||
logger: createLogger({
|
||||
disabled: options.disableLog,
|
||||
@@ -85,6 +93,12 @@ export type AuthContext = {
|
||||
authCookies: BetterAuthCookies;
|
||||
logger: ReturnType<typeof createLogger>;
|
||||
db: Kysely<any>;
|
||||
rateLimit: {
|
||||
enabled: boolean;
|
||||
window: number;
|
||||
max: number;
|
||||
storage: "memory" | "database";
|
||||
} & BetterAuthOptions["rateLimit"];
|
||||
adapter: ReturnType<typeof getAdapter>;
|
||||
internalAdapter: ReturnType<typeof createInternalAdapter>;
|
||||
createAuthCookie: ReturnType<typeof createCookieGetter>;
|
||||
|
||||
@@ -28,6 +28,7 @@ export const getPasskeyActions = (
|
||||
autoFill?: boolean;
|
||||
email?: string;
|
||||
callbackURL?: string;
|
||||
fetchOptions?: BetterFetchOption;
|
||||
}) => {
|
||||
const response = await $fetch<PublicKeyCredentialRequestOptionsJSON>(
|
||||
"/passkey/generate-authenticate-options",
|
||||
@@ -54,6 +55,7 @@ export const getPasskeyActions = (
|
||||
body: {
|
||||
response: res,
|
||||
},
|
||||
...opts?.fetchOptions,
|
||||
});
|
||||
if (!verified.data) {
|
||||
return verified;
|
||||
@@ -64,7 +66,7 @@ export const getPasskeyActions = (
|
||||
};
|
||||
|
||||
const registerPasskey = async (opts?: {
|
||||
options?: BetterFetchOption;
|
||||
fetchOptions?: BetterFetchOption;
|
||||
/**
|
||||
* The name of the passkey. This is used to
|
||||
* identify the passkey in the UI.
|
||||
@@ -75,7 +77,6 @@ export const getPasskeyActions = (
|
||||
"/passkey/generate-register-options",
|
||||
{
|
||||
method: "GET",
|
||||
...opts?.options,
|
||||
},
|
||||
);
|
||||
if (!options.data) {
|
||||
@@ -86,7 +87,7 @@ export const getPasskeyActions = (
|
||||
const verified = await $fetch<{
|
||||
passkey: Passkey;
|
||||
}>("/passkey/verify-registration", {
|
||||
...opts?.options,
|
||||
...opts?.fetchOptions,
|
||||
body: {
|
||||
response: res,
|
||||
name: opts?.name,
|
||||
@@ -127,7 +128,6 @@ export const getPasskeyActions = (
|
||||
},
|
||||
};
|
||||
}
|
||||
logger.error(e, "passkey registration error");
|
||||
return {
|
||||
data: null,
|
||||
error: {
|
||||
|
||||
@@ -345,7 +345,6 @@ export const passkey = (options?: PasskeyOptions) => {
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
console.log({ challengeString });
|
||||
const { expectedChallenge, callbackURL } = JSON.parse(
|
||||
challengeString,
|
||||
) as WebAuthnCookieType;
|
||||
@@ -409,7 +408,6 @@ export const passkey = (options?: PasskeyOptions) => {
|
||||
ctx.request,
|
||||
);
|
||||
await setSessionCookie(ctx, s.id);
|
||||
|
||||
if (callbackURL) {
|
||||
return ctx.json({
|
||||
url: callbackURL,
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import { getSession } from "../../api/routes";
|
||||
import { BetterAuthError } from "../../error/better-auth-error";
|
||||
import { getIp } from "../../utils/get-request-ip";
|
||||
import { logger } from "../../utils/logger";
|
||||
|
||||
export async function getRateLimitKey(req: Request) {
|
||||
if (req.headers.get("Authorization") || req.headers.get("cookie")) {
|
||||
try {
|
||||
const session = await getSession()({
|
||||
headers: req.headers,
|
||||
// @ts-ignore
|
||||
_flag: undefined,
|
||||
});
|
||||
if (session) {
|
||||
return session.user.id;
|
||||
}
|
||||
} catch (e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
const ip = getIp(req);
|
||||
if (!ip) {
|
||||
throw new BetterAuthError("IP not found");
|
||||
}
|
||||
return ip;
|
||||
}
|
||||
@@ -1,270 +0,0 @@
|
||||
import { APIError } from "better-call";
|
||||
import { createAuthMiddleware } from "../../api/call";
|
||||
import type { GenericEndpointContext } from "../../types/context";
|
||||
import type { BetterAuthPlugin } from "../../types/plugins";
|
||||
import { getRateLimitKey } from "./get-key";
|
||||
import { logger } from "../../utils/logger";
|
||||
|
||||
interface RateLimit {
|
||||
key: string;
|
||||
count: number;
|
||||
lastRequest: number;
|
||||
}
|
||||
|
||||
export interface RateLimitOptions {
|
||||
/**
|
||||
* Enable rate limiting. You can also pass a function
|
||||
* to enable rate limiting for specific endpoints.
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
enabled: boolean | ((request: Request) => boolean | Promise<boolean>);
|
||||
/**
|
||||
* The window to use for rate limiting. The value
|
||||
* should be in seconds.
|
||||
* @default 15 minutes (15 * 60)
|
||||
*/
|
||||
window?: number;
|
||||
/**
|
||||
* The maximum number of requests allowed within the window.
|
||||
* @default 100
|
||||
*/
|
||||
max?: number;
|
||||
/**
|
||||
* Function to get the key to use for rate limiting.
|
||||
* @default "ip" or "userId" if the user is logged in.
|
||||
*/
|
||||
getKey?: (request: Request) => string | Promise<string>;
|
||||
storage?: {
|
||||
custom?: {
|
||||
get: (key: string) => Promise<RateLimit | undefined>;
|
||||
set: (key: string, value: RateLimit) => Promise<void>;
|
||||
};
|
||||
/**
|
||||
* The provider to use for rate limiting.
|
||||
* @default "database"
|
||||
*/
|
||||
provider?: "database" | "memory" | "custom";
|
||||
/**
|
||||
* The name of the table to use for rate limiting. Only used if provider is "database".
|
||||
* @default "rateLimit"
|
||||
*/
|
||||
tableName?: string;
|
||||
};
|
||||
/**
|
||||
* Custom rate limiting function.
|
||||
*/
|
||||
customRateLimit?: (request: Request) => Promise<boolean>;
|
||||
/**
|
||||
* Special rules to apply to specific paths.
|
||||
*
|
||||
* By default, endpoints that starts with "/sign-in" or "/sign-up" are added
|
||||
* to the rate limiting mechanism with a count value of 2.
|
||||
* @example
|
||||
* ```ts
|
||||
* specialRules: [
|
||||
* {
|
||||
* matcher: (request) => request.url.startsWith("/sign-in"),
|
||||
* // This will half the amount of requests allowed for the sign-in endpoint
|
||||
* countValue: 2,
|
||||
* }
|
||||
* ]
|
||||
* ```
|
||||
*/
|
||||
specialRules?: {
|
||||
/**
|
||||
* Custom matcher to determine if the special rule should be applied.
|
||||
*/
|
||||
matcher: (path: string) => boolean;
|
||||
/**
|
||||
* The value to use for the count.
|
||||
*
|
||||
*/
|
||||
countValue: number;
|
||||
}[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Rate limiting plugin for BetterAuth. It implements a simple rate limiting
|
||||
* mechanism to prevent abuse. It can be configured to use a database, memory
|
||||
* storage or a custom storage. It can also be configured to use a custom rate
|
||||
* limiting function.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const plugin = rateLimiter({
|
||||
* enabled: true,
|
||||
* window: 60,
|
||||
* max: 100,
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export const rateLimiter = (options: RateLimitOptions) => {
|
||||
const opts = {
|
||||
storage: {
|
||||
provider: "database",
|
||||
tableName: "rateLimit",
|
||||
},
|
||||
max: 100,
|
||||
window: 15 * 60,
|
||||
specialRules: [
|
||||
{
|
||||
matcher(path) {
|
||||
return path.startsWith("/sign-in") || path.startsWith("/sign-up");
|
||||
},
|
||||
countValue: 2,
|
||||
},
|
||||
],
|
||||
...options,
|
||||
} satisfies RateLimitOptions;
|
||||
|
||||
function createDBStorage(ctx: GenericEndpointContext) {
|
||||
const db = ctx.context.db;
|
||||
return {
|
||||
get: async (key: string) => {
|
||||
const result = await db
|
||||
.selectFrom("rateLimit")
|
||||
.where("key", "=", key)
|
||||
.selectAll()
|
||||
.executeTakeFirst();
|
||||
return result as RateLimit | undefined;
|
||||
},
|
||||
set: async (key: string, value: RateLimit, isNew: boolean = true) => {
|
||||
try {
|
||||
if (isNew) {
|
||||
await db
|
||||
.insertInto(opts.storage.tableName ?? "rateLimit")
|
||||
.values({
|
||||
key,
|
||||
count: value.count,
|
||||
lastRequest: value.lastRequest,
|
||||
})
|
||||
.execute();
|
||||
} else {
|
||||
await db
|
||||
.updateTable(opts.storage.tableName ?? "rateLimit")
|
||||
.set({
|
||||
count: value.count,
|
||||
lastRequest: value.lastRequest,
|
||||
})
|
||||
.where("key", "=", key)
|
||||
.execute();
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error("Error setting rate limit", e);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
const storage = new Map<string, RateLimit>();
|
||||
function createMemoryStorage() {
|
||||
return {
|
||||
get: async (key: string) => {
|
||||
return storage.get(key);
|
||||
},
|
||||
set: async (key: string, value: RateLimit) => {
|
||||
storage.set(key, value);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id: "rate-limiter",
|
||||
middlewares: [
|
||||
{
|
||||
path: "/**",
|
||||
middleware: createAuthMiddleware(async (ctx) => {
|
||||
if (!ctx.request) {
|
||||
return;
|
||||
}
|
||||
if (opts.customRateLimit) {
|
||||
const shouldLimit = await opts.customRateLimit(ctx.request);
|
||||
if (!shouldLimit) {
|
||||
throw new APIError("TOO_MANY_REQUESTS", {
|
||||
message: "Too many requests",
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
const key = await getRateLimitKey(ctx.request);
|
||||
const storage = opts.storage.custom
|
||||
? opts.storage.custom
|
||||
: opts.storage.provider === "database"
|
||||
? createDBStorage(ctx)
|
||||
: createMemoryStorage();
|
||||
const rateLimit = await storage.get(key);
|
||||
if (!rateLimit) {
|
||||
await storage.set(key, {
|
||||
key,
|
||||
count: 0,
|
||||
lastRequest: new Date().getTime(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
const now = new Date().getTime();
|
||||
const windowStart = now - opts.window * 1000;
|
||||
if (
|
||||
rateLimit.lastRequest >= windowStart &&
|
||||
rateLimit.count >= opts.max
|
||||
) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
message: "Too many requests. Please try again later.",
|
||||
}),
|
||||
{
|
||||
status: 429,
|
||||
statusText: "Too Many Requests",
|
||||
headers: {
|
||||
"X-RateLimit-Window": opts.window.toString(),
|
||||
"X-RateLimit-Max": opts.max.toString(),
|
||||
"X-RateLimit-Remaining": (
|
||||
opts.max - rateLimit.count
|
||||
).toString(),
|
||||
"X-RateLimit-Reset": (
|
||||
rateLimit.lastRequest +
|
||||
opts.window * 1000 -
|
||||
now
|
||||
).toString(),
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (rateLimit.lastRequest < windowStart) {
|
||||
rateLimit.count = 0;
|
||||
}
|
||||
const count =
|
||||
opts.specialRules.find((rule) => rule.matcher(ctx.path))
|
||||
?.countValue ?? 1;
|
||||
|
||||
await storage.set(
|
||||
key,
|
||||
{
|
||||
key,
|
||||
count: rateLimit.count + count,
|
||||
lastRequest: now,
|
||||
},
|
||||
false,
|
||||
);
|
||||
return;
|
||||
}),
|
||||
},
|
||||
],
|
||||
schema: {
|
||||
rateLimit: {
|
||||
fields: {
|
||||
key: {
|
||||
type: "string",
|
||||
},
|
||||
count: {
|
||||
type: "number",
|
||||
},
|
||||
lastRequest: {
|
||||
type: "number",
|
||||
},
|
||||
},
|
||||
disableMigration: opts.storage.provider !== "database",
|
||||
},
|
||||
},
|
||||
} satisfies BetterAuthPlugin;
|
||||
};
|
||||
@@ -1,57 +0,0 @@
|
||||
import { describe, it, beforeAll, expect, vi } from "vitest";
|
||||
import { getTestInstance } from "../../test-utils/test-instance";
|
||||
import { rateLimiter } from ".";
|
||||
|
||||
describe("rate-limiter", async () => {
|
||||
const { client, testUser } = await getTestInstance({
|
||||
plugins: [
|
||||
rateLimiter({
|
||||
enabled: true,
|
||||
storage: {
|
||||
provider: "memory",
|
||||
},
|
||||
max: 10,
|
||||
window: 10,
|
||||
specialRules: [],
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
it.only("should allow requests within the limit", async () => {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const response = await client.signIn.email({
|
||||
email: testUser.email,
|
||||
password: testUser.password,
|
||||
});
|
||||
|
||||
if (i === 10) {
|
||||
expect(response.error?.status).toBe(429);
|
||||
} else {
|
||||
expect(response.error).toBeNull();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it.only("should reset the limit after the window period", async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
// Make 10 requests to hit the limit
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const res = await client.signIn.email({
|
||||
email: testUser.email,
|
||||
password: testUser.password,
|
||||
});
|
||||
if (res.error?.status === 429) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Advance the timer by 11 seconds (just over the 10-second window)
|
||||
vi.advanceTimersByTime(11000);
|
||||
const response = await client.signIn.email({
|
||||
email: testUser.email,
|
||||
password: testUser.password,
|
||||
});
|
||||
expect(response.error).toBeNull();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
});
|
||||
@@ -244,6 +244,15 @@ export const twoFactor = (options?: TwoFactorOptions) => {
|
||||
},
|
||||
},
|
||||
},
|
||||
rateLimit: [
|
||||
{
|
||||
pathMatcher(path) {
|
||||
return path.startsWith("/two-factor/");
|
||||
},
|
||||
window: 10,
|
||||
max: 3,
|
||||
},
|
||||
],
|
||||
} satisfies BetterAuthPlugin;
|
||||
};
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ export const otp2fa = (options?: OTPOptions) => {
|
||||
async (ctx) => {
|
||||
if (!options || !options.sendOTP) {
|
||||
ctx.context.logger.error(
|
||||
"otp isn't configured. please pass otp option on two factor plugin to enable otp",
|
||||
"send otp isn't configured. Please configure the send otp function on otp options.",
|
||||
);
|
||||
throw new APIError("BAD_REQUEST", {
|
||||
message: "otp isn't configured",
|
||||
|
||||
@@ -83,4 +83,19 @@ export type InferPluginTypes<O extends BetterAuthOptions> =
|
||||
>
|
||||
: {};
|
||||
|
||||
export type { User, Session };
|
||||
interface RateLimit {
|
||||
/**
|
||||
* The key to use for rate limiting
|
||||
*/
|
||||
key: string;
|
||||
/**
|
||||
* The number of requests made
|
||||
*/
|
||||
count: number;
|
||||
/**
|
||||
* The last request time in milliseconds
|
||||
*/
|
||||
lastRequest: number;
|
||||
}
|
||||
|
||||
export type { User, Session, RateLimit };
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { FieldAttribute } from "../db/field";
|
||||
import type { BetterAuthPlugin } from "./plugins";
|
||||
import type { OAuthProviderList } from "./provider";
|
||||
import type { SocialProviders } from "../social-providers";
|
||||
import type { RateLimit } from "./models";
|
||||
|
||||
export interface BetterAuthOptions {
|
||||
/**
|
||||
@@ -215,4 +216,66 @@ export interface BetterAuthOptions {
|
||||
* List of trusted origins.
|
||||
*/
|
||||
trustedOrigins?: string[];
|
||||
/**
|
||||
* Rate limiting configuration
|
||||
*/
|
||||
rateLimit?: {
|
||||
/**
|
||||
* By default, rate limiting is only
|
||||
* enabled on production.
|
||||
*/
|
||||
enabled?: boolean;
|
||||
/**
|
||||
* Default window to use for rate limiting. The value
|
||||
* should be in seconds.
|
||||
*
|
||||
* @default 60 sec
|
||||
*/
|
||||
window: number;
|
||||
/**
|
||||
* Custom rate limit rules to apply to
|
||||
* specific paths.
|
||||
*/
|
||||
customRules?: {
|
||||
[key: string]: {
|
||||
/**
|
||||
* The window to use for the custom rule.
|
||||
*/
|
||||
window: number;
|
||||
/**
|
||||
* The maximum number of requests allowed within the window.
|
||||
*/
|
||||
max: number;
|
||||
};
|
||||
};
|
||||
/**
|
||||
* The default maximum number of requests allowed within the window.
|
||||
*
|
||||
* @default 100
|
||||
*/
|
||||
max: number;
|
||||
/**
|
||||
* Storage configuration
|
||||
*
|
||||
* @default "memory"
|
||||
*/
|
||||
storage?: "memory" | "database";
|
||||
/**
|
||||
* If database is used as storage, the name of the table to
|
||||
* use for rate limiting.
|
||||
*
|
||||
* @default "rateLimit"
|
||||
*/
|
||||
tableName?: string;
|
||||
/**
|
||||
* custom storage configuration.
|
||||
*
|
||||
* NOTE: If custom storage is used storage
|
||||
* is ignored
|
||||
*/
|
||||
customStorage?: {
|
||||
get: (key: string) => Promise<RateLimit | undefined>;
|
||||
set: (key: string, value: RateLimit) => Promise<void>;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -82,4 +82,12 @@ export type BetterAuthPlugin = {
|
||||
*/
|
||||
options?: Record<string, any>;
|
||||
$Infer?: Record<string, any>;
|
||||
/**
|
||||
* The rate limit rules to apply to specific paths.
|
||||
*/
|
||||
rateLimit?: {
|
||||
window: number;
|
||||
max: number;
|
||||
pathMatcher: (path: string) => boolean;
|
||||
}[];
|
||||
};
|
||||
|
||||
1196
pnpm-lock.yaml
generated
1196
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
3
todo.md
3
todo.md
@@ -13,7 +13,7 @@
|
||||
[ ] add a way to specify on the client to use the base path as the whole path
|
||||
[x] implement magic links
|
||||
[ ] rate limiter error handling
|
||||
|
||||
[ ] microsoft oauth
|
||||
|
||||
## Docs
|
||||
[x] specify everywhere `auth` should be exported
|
||||
@@ -23,3 +23,4 @@
|
||||
[x] add a doc about account linking
|
||||
[x] remove the section about using useSession in next with initialValue
|
||||
[x] rate limiting docs
|
||||
[ ] add that passkey login won't require 2fa
|
||||
Reference in New Issue
Block a user