Remove obsolete tests and add new login page layout
- Deleted test files for NavLink, Navbar, NavbarBrand, Progress, Row, SearchBar, Select, Spinner, Textarea, and a basic test for Button. - Introduced a new EmptyLayout component for consistent layout structure. - Added a new login page with a layout that includes a login form and an ASCII art header. - Updated CSS variables for improved styling consistency across components. - Removed Vitest configuration file as it is no longer needed.
This commit is contained in:
21
src/actions/index.ts
Normal file
21
src/actions/index.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { defineAction } from "astro:actions";
|
||||
import { z } from "astro:content";
|
||||
|
||||
export const server = {
|
||||
login: defineAction({
|
||||
accept: "form",
|
||||
input: z.object({
|
||||
username: z
|
||||
.string({ message: "Username is required" })
|
||||
.nonempty("Username is required"),
|
||||
password: z
|
||||
.string({ message: "Password is required" })
|
||||
.nonempty("Password is required"),
|
||||
}),
|
||||
handler(input) {
|
||||
const { username, password } = input;
|
||||
|
||||
return { username, password };
|
||||
},
|
||||
}),
|
||||
};
|
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" width="115" height="48"><path fill="#17191E" d="M7.77 36.35C6.4 35.11 6 32.51 6.57 30.62c.99 1.2 2.35 1.57 3.75 1.78 2.18.33 4.31.2 6.33-.78.23-.12.44-.27.7-.42.18.55.23 1.1.17 1.67a4.56 4.56 0 0 1-1.94 3.23c-.43.32-.9.61-1.34.91-1.38.94-1.76 2.03-1.24 3.62l.05.17a3.63 3.63 0 0 1-1.6-1.38 3.87 3.87 0 0 1-.63-2.1c0-.37 0-.74-.05-1.1-.13-.9-.55-1.3-1.33-1.32a1.56 1.56 0 0 0-1.63 1.26c0 .06-.03.12-.05.2Z"/><path fill="url(#a)" d="M7.77 36.35C6.4 35.11 6 32.51 6.57 30.62c.99 1.2 2.35 1.57 3.75 1.78 2.18.33 4.31.2 6.33-.78.23-.12.44-.27.7-.42.18.55.23 1.1.17 1.67a4.56 4.56 0 0 1-1.94 3.23c-.43.32-.9.61-1.34.91-1.38.94-1.76 2.03-1.24 3.62l.05.17a3.63 3.63 0 0 1-1.6-1.38 3.87 3.87 0 0 1-.63-2.1c0-.37 0-.74-.05-1.1-.13-.9-.55-1.3-1.33-1.32a1.56 1.56 0 0 0-1.63 1.26c0 .06-.03.12-.05.2Z"/><path fill="#17191E" d="M.02 30.31s4.02-1.95 8.05-1.95l3.04-9.4c.11-.45.44-.76.82-.76.37 0 .7.31.82.76l3.04 9.4c4.77 0 8.05 1.95 8.05 1.95L17 11.71c-.2-.56-.53-.91-.98-.91H7.83c-.44 0-.76.35-.97.9L.02 30.31Zm42.37-5.97c0 1.64-2.05 2.62-4.88 2.62-1.85 0-2.5-.45-2.5-1.41 0-1 .8-1.49 2.65-1.49 1.67 0 3.09.03 4.73.23v.05Zm.03-2.04a21.37 21.37 0 0 0-4.37-.36c-5.32 0-7.82 1.25-7.82 4.18 0 3.04 1.71 4.2 5.68 4.2 3.35 0 5.63-.84 6.46-2.92h.14c-.03.5-.05 1-.05 1.4 0 1.07.18 1.16 1.06 1.16h4.15a16.9 16.9 0 0 1-.36-4c0-1.67.06-2.93.06-4.62 0-3.45-2.07-5.64-8.56-5.64-2.8 0-5.9.48-8.26 1.19.22.93.54 2.83.7 4.06 2.04-.96 4.95-1.37 7.2-1.37 3.11 0 3.97.71 3.97 2.15v.57Zm11.37 3c-.56.07-1.33.07-2.12.07-.83 0-1.6-.03-2.12-.1l-.02.58c0 2.85 1.87 4.52 8.45 4.52 6.2 0 8.2-1.64 8.2-4.55 0-2.74-1.33-4.09-7.2-4.39-4.58-.2-4.99-.7-4.99-1.28 0-.66.59-1 3.65-1 3.18 0 4.03.43 4.03 1.35v.2a46.13 46.13 0 0 1 4.24.03l.02-.55c0-3.36-2.8-4.46-8.2-4.46-6.08 0-8.13 1.49-8.13 4.39 0 2.6 1.64 4.23 7.48 4.48 4.3.14 4.77.62 4.77 1.28 0 .7-.7 1.03-3.71 1.03-3.47 0-4.35-.48-4.35-1.47v-.13Zm19.82-12.05a17.5 17.5 0 0 1-6.24 3.48c.03.84.03 2.4.03 3.24l1.5.02c-.02 1.63-.04 3.6-.04 4.9 0 3.04 1.6 5.32 6.58 5.32 2.1 0 3.5-.23 5.23-.6a43.77 43.77 0 0 1-.46-4.13c-1.03.34-2.34.53-3.78.53-2 0-2.82-.55-2.82-2.13 0-1.37 0-2.65.03-3.84 2.57.02 5.13.07 6.64.11-.02-1.18.03-2.9.1-4.04-2.2.04-4.65.07-6.68.07l.07-2.93h-.16Zm13.46 6.04a767.33 767.33 0 0 1 .07-3.18H82.6c.07 1.96.07 3.98.07 6.92 0 2.95-.03 4.99-.07 6.93h5.18c-.09-1.37-.11-3.68-.11-5.65 0-3.1 1.26-4 4.12-4 1.33 0 2.28.16 3.1.46.03-1.16.26-3.43.4-4.43-.86-.25-1.81-.41-2.96-.41-2.46-.03-4.26.98-5.1 3.38l-.17-.02Zm22.55 3.65c0 2.5-1.8 3.66-4.64 3.66-2.81 0-4.61-1.1-4.61-3.66s1.82-3.52 4.61-3.52c2.82 0 4.64 1.03 4.64 3.52Zm4.71-.11c0-4.96-3.87-7.18-9.35-7.18-5.5 0-9.23 2.22-9.23 7.18 0 4.94 3.49 7.59 9.21 7.59 5.77 0 9.37-2.65 9.37-7.6Z"/><defs><linearGradient id="a" x1="6.33" x2="19.43" y1="40.8" y2="34.6" gradientUnits="userSpaceOnUse"><stop stop-color="#D83333"/><stop offset="1" stop-color="#F041FF"/></linearGradient></defs></svg>
|
Before Width: | Height: | Size: 2.8 KiB |
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1440" height="1024" fill="none"><path fill="url(#a)" fill-rule="evenodd" d="M-217.58 475.75c91.82-72.02 225.52-29.38 341.2-44.74C240 415.56 372.33 315.14 466.77 384.9c102.9 76.02 44.74 246.76 90.31 366.31 29.83 78.24 90.48 136.14 129.48 210.23 57.92 109.99 169.67 208.23 155.9 331.77-13.52 121.26-103.42 264.33-224.23 281.37-141.96 20.03-232.72-220.96-374.06-196.99-151.7 25.73-172.68 330.24-325.85 315.72-128.6-12.2-110.9-230.73-128.15-358.76-12.16-90.14 65.87-176.25 44.1-264.57-26.42-107.2-167.12-163.46-176.72-273.45-10.15-116.29 33.01-248.75 124.87-320.79Z" clip-rule="evenodd" style="opacity:.154"/><path fill="url(#b)" fill-rule="evenodd" d="M1103.43 115.43c146.42-19.45 275.33-155.84 413.5-103.59 188.09 71.13 409 212.64 407.06 413.88-1.94 201.25-259.28 278.6-414.96 405.96-130 106.35-240.24 294.39-405.6 265.3-163.7-28.8-161.93-274.12-284.34-386.66-134.95-124.06-436-101.46-445.82-284.6-9.68-180.38 247.41-246.3 413.54-316.9 101.01-42.93 207.83 21.06 316.62 6.61Z" clip-rule="evenodd" style="opacity:.154"/><defs><linearGradient id="b" x1="373" x2="1995.44" y1="1100" y2="118.03" gradientUnits="userSpaceOnUse"><stop stop-color="#D83333"/><stop offset="1" stop-color="#F041FF"/></linearGradient><linearGradient id="a" x1="107.37" x2="1130.66" y1="1993.35" y2="1026.31" gradientUnits="userSpaceOnUse"><stop stop-color="#3245FF"/><stop offset="1" stop-color="#BC52EE"/></linearGradient></defs></svg>
|
Before Width: | Height: | Size: 1.4 KiB |
14
src/assets/logo.svg
Normal file
14
src/assets/logo.svg
Normal file
@ -0,0 +1,14 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" aria-labelledby="title" role="img">
|
||||
<title>Nixi – Logo</title>
|
||||
<defs>
|
||||
<linearGradient id="nixi-g" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0%" stop-color="#9f75ff"/>
|
||||
<stop offset="100%" stop-color="#6b04fd"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g fill="none" stroke="url(#nixi-g)" stroke-width="8" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M16 48V16"/>
|
||||
<path d="M48 48V16"/>
|
||||
<path d="M16 16L48 48"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 524 B |
36
src/components/Ascii.astro
Normal file
36
src/components/Ascii.astro
Normal file
@ -0,0 +1,36 @@
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
<pre
|
||||
set:text=" _ _ _ _
|
||||
| \ | | (_) __ __ (_)
|
||||
| \| | | | \ \/ / | |
|
||||
| |\ | | | > < | |
|
||||
|_| \_| |_| /_/\_\ |_|"
|
||||
/>
|
||||
|
||||
<style>
|
||||
pre {
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
line-height: 1.2;
|
||||
animation: fade 4s infinite;
|
||||
}
|
||||
|
||||
@keyframes fade {
|
||||
0% {
|
||||
color: rgb(255, 143, 143);
|
||||
}
|
||||
25% {
|
||||
color: rgb(143, 173, 255);
|
||||
}
|
||||
50% {
|
||||
color: rgb(219, 143, 255);
|
||||
}
|
||||
100% {
|
||||
color: rgb(255, 143, 143);
|
||||
}
|
||||
}
|
||||
</style>
|
43
src/components/forms/LoginForm.astro
Normal file
43
src/components/forms/LoginForm.astro
Normal file
@ -0,0 +1,43 @@
|
||||
---
|
||||
export interface Props {
|
||||
action: string | null | undefined;
|
||||
error: ActionInputError<{ username: string; password: string }> | null;
|
||||
}
|
||||
|
||||
import type { ActionInputError } from "astro:actions";
|
||||
import Button from "../ui/Button.astro";
|
||||
import FormGroup from "../ui/FormGroup.astro";
|
||||
import FormLabel from "../ui/FormLabel.astro";
|
||||
import Input from "../ui/Input.astro";
|
||||
import FormError from "../ui/FormError.astro";
|
||||
|
||||
const { action, error } = Astro.props;
|
||||
---
|
||||
|
||||
<form action={action} method="POST" enctype="multipart/form-data">
|
||||
<FormGroup>
|
||||
<FormLabel for="username">Nom d'utilisateur</FormLabel>
|
||||
<Input
|
||||
type="text"
|
||||
id="username"
|
||||
name="username"
|
||||
placeholder="Votre nom d'utilisateur"
|
||||
/>
|
||||
{error?.fields.username && <FormError>{error.fields.username}</FormError>}
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel for="password">Mot de passe</FormLabel>
|
||||
<Input
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
placeholder="••••••••"
|
||||
/>
|
||||
{error?.fields.password && <FormError>{error.fields.password}</FormError>}
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<Button type="submit" class="w-100"> Se connecter </Button>
|
||||
</FormGroup>
|
||||
</form>
|
@ -2,14 +2,20 @@
|
||||
export interface Props {
|
||||
fluid?: boolean;
|
||||
class?: string;
|
||||
style?: string;
|
||||
}
|
||||
|
||||
const { fluid = false, class: className = "", ...rest } = Astro.props;
|
||||
const {
|
||||
fluid = false,
|
||||
class: className = "",
|
||||
style = "",
|
||||
...rest
|
||||
} = Astro.props;
|
||||
|
||||
const baseClasses = fluid ? "container-fluid" : "container";
|
||||
const classes = [baseClasses, className].filter(Boolean).join(" ");
|
||||
---
|
||||
|
||||
<div class={classes} {...rest}>
|
||||
<div class={classes} style={style} {...rest}>
|
||||
<slot />
|
||||
</div>
|
||||
|
@ -1,59 +0,0 @@
|
||||
import { experimental_AstroContainer as AstroContainer } from "astro/container";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import Alert from "../Alert.astro";
|
||||
|
||||
describe("Alert", () => {
|
||||
it("renders with default props", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Alert, {
|
||||
slots: { default: "Alert message" },
|
||||
});
|
||||
|
||||
expect(result).toContain("Alert message");
|
||||
expect(result).toContain('class="alert alert-info"');
|
||||
expect(result).toContain("<div");
|
||||
});
|
||||
|
||||
it("renders different variants", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const variants = ["success", "warning", "error", "info"] as const;
|
||||
|
||||
for (const variant of variants) {
|
||||
const result = await container.renderToString(Alert, {
|
||||
props: { variant },
|
||||
slots: { default: "Test message" },
|
||||
});
|
||||
|
||||
expect(result).toContain(`alert-${variant}`);
|
||||
}
|
||||
});
|
||||
|
||||
it("applies custom classes", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Alert, {
|
||||
props: { class: "custom-alert" },
|
||||
slots: { default: "Message" },
|
||||
});
|
||||
|
||||
expect(result).toContain("alert alert-info custom-alert");
|
||||
});
|
||||
|
||||
it("renders as div element", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Alert, {
|
||||
slots: { default: "Test" },
|
||||
});
|
||||
|
||||
expect(result).toContain('<div class="alert alert-info"');
|
||||
});
|
||||
|
||||
it("passes through additional props", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Alert, {
|
||||
props: { role: "alert" },
|
||||
slots: { default: "Important message" },
|
||||
});
|
||||
|
||||
expect(result).toContain('role="alert"');
|
||||
});
|
||||
});
|
@ -1,65 +0,0 @@
|
||||
import { experimental_AstroContainer as AstroContainer } from "astro/container";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import Badge from "../Badge.astro";
|
||||
|
||||
describe("Badge", () => {
|
||||
it("renders with default props", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Badge, {
|
||||
slots: { default: "New" },
|
||||
});
|
||||
|
||||
expect(result).toContain("New");
|
||||
expect(result).toContain("badge-primary");
|
||||
expect(result).toContain("<span");
|
||||
});
|
||||
|
||||
it("renders different variants", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const variants = [
|
||||
"primary",
|
||||
"secondary",
|
||||
"success",
|
||||
"warning",
|
||||
"error",
|
||||
] as const;
|
||||
|
||||
for (const variant of variants) {
|
||||
const result = await container.renderToString(Badge, {
|
||||
props: { variant },
|
||||
slots: { default: "Badge" },
|
||||
});
|
||||
|
||||
expect(result).toContain(`badge-${variant}`);
|
||||
}
|
||||
});
|
||||
|
||||
it("applies custom classes", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Badge, {
|
||||
props: { class: "custom-badge" },
|
||||
slots: { default: "Test" },
|
||||
});
|
||||
|
||||
expect(result).toContain("badge badge-primary custom-badge");
|
||||
});
|
||||
|
||||
it("renders as span element", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Badge, {
|
||||
slots: { default: "Test" },
|
||||
});
|
||||
|
||||
expect(result).toContain('<span class="badge');
|
||||
});
|
||||
|
||||
it("passes through additional props", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Badge, {
|
||||
props: { "data-count": "5" },
|
||||
slots: { default: "5" },
|
||||
});
|
||||
|
||||
expect(result).toContain('data-count="5"');
|
||||
});
|
||||
});
|
@ -1,102 +0,0 @@
|
||||
import { experimental_AstroContainer as AstroContainer } from "astro/container";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import Button from "../Button.astro";
|
||||
|
||||
describe("Button", () => {
|
||||
it("renders with default props", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Button, {
|
||||
slots: { default: "Click me" },
|
||||
});
|
||||
|
||||
expect(result).toContain("Click me");
|
||||
expect(result).toContain('class="btn btn-primary"');
|
||||
expect(result).toContain('type="button"');
|
||||
expect(result).toContain("<button");
|
||||
});
|
||||
|
||||
it("renders different variants", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const variants = ["primary", "secondary", "outline", "ghost"] as const;
|
||||
|
||||
for (const variant of variants) {
|
||||
const result = await container.renderToString(Button, {
|
||||
props: { variant },
|
||||
slots: { default: "Test" },
|
||||
});
|
||||
|
||||
expect(result).toContain(`btn-${variant}`);
|
||||
}
|
||||
});
|
||||
|
||||
it("renders different sizes", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const sizes = ["sm", "lg"] as const;
|
||||
|
||||
for (const size of sizes) {
|
||||
const result = await container.renderToString(Button, {
|
||||
props: { size },
|
||||
slots: { default: "Test" },
|
||||
});
|
||||
|
||||
expect(result).toContain(`btn-${size}`);
|
||||
}
|
||||
});
|
||||
|
||||
it("renders as anchor when href is provided", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Button, {
|
||||
props: { href: "/test" },
|
||||
slots: { default: "Link" },
|
||||
});
|
||||
|
||||
expect(result).toContain("<a");
|
||||
expect(result).toContain('href="/test"');
|
||||
expect(result).toContain("Link");
|
||||
expect(result).not.toContain("<button");
|
||||
});
|
||||
|
||||
it("applies disabled state", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Button, {
|
||||
props: { disabled: true },
|
||||
slots: { default: "Disabled" },
|
||||
});
|
||||
|
||||
expect(result).toContain("disabled");
|
||||
});
|
||||
|
||||
it("applies icon styling", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Button, {
|
||||
props: { icon: true },
|
||||
slots: { default: "🔍" },
|
||||
});
|
||||
|
||||
expect(result).toContain("btn-icon");
|
||||
});
|
||||
|
||||
it("applies custom classes", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Button, {
|
||||
props: { class: "custom-class" },
|
||||
slots: { default: "Test" },
|
||||
});
|
||||
|
||||
expect(result).toContain("custom-class");
|
||||
});
|
||||
|
||||
it("handles different button types", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const types = ["button", "submit", "reset"] as const;
|
||||
|
||||
for (const type of types) {
|
||||
const result = await container.renderToString(Button, {
|
||||
props: { type },
|
||||
slots: { default: "Test" },
|
||||
});
|
||||
|
||||
expect(result).toContain(`type="${type}"`);
|
||||
}
|
||||
});
|
||||
});
|
@ -1,45 +0,0 @@
|
||||
import { experimental_AstroContainer as AstroContainer } from "astro/container";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import Card from "../Card.astro";
|
||||
|
||||
describe("Card", () => {
|
||||
it("renders with default props", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Card, {
|
||||
slots: { default: "Card content" },
|
||||
});
|
||||
|
||||
expect(result).toContain("Card content");
|
||||
expect(result).toContain('class="card"');
|
||||
expect(result).toContain("<div");
|
||||
});
|
||||
|
||||
it("applies custom classes", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Card, {
|
||||
props: { class: "custom-card" },
|
||||
slots: { default: "Content" },
|
||||
});
|
||||
|
||||
expect(result).toContain("card custom-card");
|
||||
});
|
||||
|
||||
it("renders as div element", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Card, {
|
||||
slots: { default: "Test" },
|
||||
});
|
||||
|
||||
expect(result).toContain('<div class="card"');
|
||||
});
|
||||
|
||||
it("passes through additional props", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Card, {
|
||||
props: { "data-testid": "test-card" },
|
||||
slots: { default: "Test" },
|
||||
});
|
||||
|
||||
expect(result).toContain('data-testid="test-card"');
|
||||
});
|
||||
});
|
@ -1,106 +0,0 @@
|
||||
import { experimental_AstroContainer as AstroContainer } from "astro/container";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import Col from "../Col.astro";
|
||||
|
||||
describe("Col", () => {
|
||||
it("renders with default props", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Col, {
|
||||
slots: { default: "Column content" },
|
||||
});
|
||||
|
||||
expect(result).toContain("Column content");
|
||||
expect(result).toContain('class="col"');
|
||||
expect(result).toContain("<div");
|
||||
});
|
||||
|
||||
it("renders with responsive breakpoints", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Col, {
|
||||
props: {
|
||||
xs: 12,
|
||||
sm: 6,
|
||||
md: 4,
|
||||
lg: 3,
|
||||
},
|
||||
slots: { default: "Content" },
|
||||
});
|
||||
|
||||
expect(result).toContain("col--12");
|
||||
expect(result).toContain("col-sm-6");
|
||||
expect(result).toContain("col-md-4");
|
||||
expect(result).toContain("col-lg-3");
|
||||
});
|
||||
|
||||
it("renders with auto sizing", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Col, {
|
||||
props: { md: "auto" },
|
||||
slots: { default: "Content" },
|
||||
});
|
||||
|
||||
expect(result).toContain("col-md-auto");
|
||||
});
|
||||
|
||||
it("renders with offset classes", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Col, {
|
||||
props: {
|
||||
md: 6,
|
||||
offset: { md: 3, lg: 2 },
|
||||
},
|
||||
slots: { default: "Content" },
|
||||
});
|
||||
|
||||
expect(result).toContain("col-md-6");
|
||||
expect(result).toContain("offset-md-3");
|
||||
expect(result).toContain("offset-lg-2");
|
||||
});
|
||||
|
||||
it("renders with xl and xxl breakpoints", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Col, {
|
||||
props: {
|
||||
xl: 4,
|
||||
xxl: 2,
|
||||
},
|
||||
slots: { default: "Content" },
|
||||
});
|
||||
|
||||
expect(result).toContain("col-xl-4");
|
||||
expect(result).toContain("col-xxl-2");
|
||||
});
|
||||
|
||||
it("applies custom classes", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Col, {
|
||||
props: {
|
||||
class: "custom-col",
|
||||
md: 6,
|
||||
},
|
||||
slots: { default: "Content" },
|
||||
});
|
||||
|
||||
expect(result).toContain("col-md-6");
|
||||
expect(result).toContain("custom-col");
|
||||
});
|
||||
|
||||
it("renders as div element", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Col, {
|
||||
slots: { default: "Test" },
|
||||
});
|
||||
|
||||
expect(result).toContain('<div class="col"');
|
||||
});
|
||||
|
||||
it("passes through additional props", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Col, {
|
||||
props: { "data-column": "main" },
|
||||
slots: { default: "Test" },
|
||||
});
|
||||
|
||||
expect(result).toContain('data-column="main"');
|
||||
});
|
||||
});
|
@ -1,56 +0,0 @@
|
||||
import { experimental_AstroContainer as AstroContainer } from "astro/container";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import Container from "../Container.astro";
|
||||
|
||||
describe("Container", () => {
|
||||
it("renders with default props", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Container, {
|
||||
slots: { default: "Container content" },
|
||||
});
|
||||
|
||||
expect(result).toContain("Container content");
|
||||
expect(result).toContain('class="container"');
|
||||
expect(result).toContain("<div");
|
||||
});
|
||||
|
||||
it("renders as fluid container", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Container, {
|
||||
props: { fluid: true },
|
||||
slots: { default: "Fluid content" },
|
||||
});
|
||||
|
||||
expect(result).toContain("Fluid content");
|
||||
expect(result).toContain('class="container-fluid"');
|
||||
});
|
||||
|
||||
it("applies custom classes", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Container, {
|
||||
props: { class: "custom-container" },
|
||||
slots: { default: "Content" },
|
||||
});
|
||||
|
||||
expect(result).toContain("container custom-container");
|
||||
});
|
||||
|
||||
it("renders as div element", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Container, {
|
||||
slots: { default: "Test" },
|
||||
});
|
||||
|
||||
expect(result).toContain('<div class="container"');
|
||||
});
|
||||
|
||||
it("passes through additional props", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Container, {
|
||||
props: { "data-section": "main" },
|
||||
slots: { default: "Test" },
|
||||
});
|
||||
|
||||
expect(result).toContain('data-section="main"');
|
||||
});
|
||||
});
|
@ -1,44 +0,0 @@
|
||||
import { experimental_AstroContainer as AstroContainer } from "astro/container";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import FormGroup from "../FormGroup.astro";
|
||||
|
||||
describe("FormGroup", () => {
|
||||
it("renders with default props", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(FormGroup, {
|
||||
slots: { default: "<label>Form content</label>" },
|
||||
});
|
||||
|
||||
expect(result).toContain("Form content");
|
||||
expect(result).toContain('class="form-group"');
|
||||
});
|
||||
|
||||
it("applies custom classes", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(FormGroup, {
|
||||
props: { class: "custom-form-group" },
|
||||
slots: { default: "Content" },
|
||||
});
|
||||
|
||||
expect(result).toContain("form-group custom-form-group");
|
||||
});
|
||||
|
||||
it("renders as div element", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(FormGroup, {
|
||||
slots: { default: "Test" },
|
||||
});
|
||||
|
||||
expect(result).toContain('<div class="form-group"');
|
||||
});
|
||||
|
||||
it("passes through additional props", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(FormGroup, {
|
||||
props: { "data-testid": "form-section" },
|
||||
slots: { default: "Test" },
|
||||
});
|
||||
|
||||
expect(result).toContain('data-testid="form-section"');
|
||||
});
|
||||
});
|
@ -1,103 +0,0 @@
|
||||
import { experimental_AstroContainer as AstroContainer } from "astro/container";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import Input from "../Input.astro";
|
||||
|
||||
describe("Input", () => {
|
||||
it("renders with default props", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Input);
|
||||
|
||||
expect(result).toContain("<input");
|
||||
expect(result).toContain('type="text"');
|
||||
expect(result).toContain("form-control-base input");
|
||||
});
|
||||
|
||||
it("renders different input types", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const types = [
|
||||
"text",
|
||||
"email",
|
||||
"password",
|
||||
"number",
|
||||
"tel",
|
||||
"url",
|
||||
"search",
|
||||
] as const;
|
||||
|
||||
for (const type of types) {
|
||||
const result = await container.renderToString(Input, {
|
||||
props: { type },
|
||||
});
|
||||
|
||||
expect(result).toContain(`type="${type}"`);
|
||||
}
|
||||
});
|
||||
|
||||
it("renders different sizes", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const sizes = ["sm", "lg"] as const;
|
||||
|
||||
for (const size of sizes) {
|
||||
const result = await container.renderToString(Input, {
|
||||
props: { size },
|
||||
});
|
||||
|
||||
expect(result).toContain(`input-${size}`);
|
||||
}
|
||||
});
|
||||
|
||||
it("applies placeholder", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Input, {
|
||||
props: { placeholder: "Enter text" },
|
||||
});
|
||||
|
||||
expect(result).toContain('placeholder="Enter text"');
|
||||
});
|
||||
|
||||
it("applies value", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Input, {
|
||||
props: { value: "test value" },
|
||||
});
|
||||
|
||||
expect(result).toContain('value="test value"');
|
||||
});
|
||||
|
||||
it("applies name and id attributes", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Input, {
|
||||
props: { name: "username", id: "user-input" },
|
||||
});
|
||||
|
||||
expect(result).toContain('name="username"');
|
||||
expect(result).toContain('id="user-input"');
|
||||
});
|
||||
|
||||
it("applies disabled state", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Input, {
|
||||
props: { disabled: true },
|
||||
});
|
||||
|
||||
expect(result).toContain("disabled");
|
||||
});
|
||||
|
||||
it("applies required attribute", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Input, {
|
||||
props: { required: true },
|
||||
});
|
||||
|
||||
expect(result).toContain("required");
|
||||
});
|
||||
|
||||
it("applies custom classes", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Input, {
|
||||
props: { class: "custom-input" },
|
||||
});
|
||||
|
||||
expect(result).toContain("custom-input");
|
||||
});
|
||||
});
|
@ -1,66 +0,0 @@
|
||||
import { experimental_AstroContainer as AstroContainer } from "astro/container";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import Modal from "../Modal.astro";
|
||||
|
||||
describe("Modal", () => {
|
||||
it("renders with required id prop", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Modal, {
|
||||
props: { id: "test-modal" },
|
||||
slots: { default: "Modal content" },
|
||||
});
|
||||
|
||||
expect(result).toContain('class="modal-backdrop"');
|
||||
expect(result).toContain('class="modal"');
|
||||
expect(result).toContain('id="test-modal"');
|
||||
expect(result).toContain("Modal content");
|
||||
});
|
||||
|
||||
it("starts hidden by default", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Modal, {
|
||||
props: { id: "hidden-modal" },
|
||||
slots: { default: "Content" },
|
||||
});
|
||||
|
||||
expect(result).toContain('style="display: none;"');
|
||||
});
|
||||
|
||||
it("applies custom classes", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Modal, {
|
||||
props: {
|
||||
id: "custom-modal",
|
||||
class: "custom-modal-class",
|
||||
},
|
||||
slots: { default: "Content" },
|
||||
});
|
||||
|
||||
expect(result).toContain("modal-backdrop custom-modal-class");
|
||||
});
|
||||
|
||||
it("renders nested modal structure", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Modal, {
|
||||
props: { id: "nested-modal" },
|
||||
slots: { default: "Nested content" },
|
||||
});
|
||||
|
||||
expect(result).toContain('class="modal-backdrop"');
|
||||
expect(result).toContain('class="modal"');
|
||||
expect(result).toContain("Nested content");
|
||||
});
|
||||
|
||||
it("passes through additional props", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Modal, {
|
||||
props: {
|
||||
id: "props-modal",
|
||||
"data-testid": "modal-test",
|
||||
},
|
||||
slots: { default: "Test" },
|
||||
});
|
||||
|
||||
expect(result).toContain('data-testid="modal-test"');
|
||||
});
|
||||
});
|
@ -1,44 +0,0 @@
|
||||
import { experimental_AstroContainer as AstroContainer } from "astro/container";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import ModalBody from "../ModalBody.astro";
|
||||
|
||||
describe("ModalBody", () => {
|
||||
it("renders with default props", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(ModalBody, {
|
||||
slots: { default: "Modal body content" },
|
||||
});
|
||||
|
||||
expect(result).toContain("Modal body content");
|
||||
expect(result).toContain('class="modal-body"');
|
||||
});
|
||||
|
||||
it("applies custom classes", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(ModalBody, {
|
||||
props: { class: "custom-body" },
|
||||
slots: { default: "Content" },
|
||||
});
|
||||
|
||||
expect(result).toContain("modal-body custom-body");
|
||||
});
|
||||
|
||||
it("renders as div element", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(ModalBody, {
|
||||
slots: { default: "Test" },
|
||||
});
|
||||
|
||||
expect(result).toContain('<div class="modal-body"');
|
||||
});
|
||||
|
||||
it("passes through additional props", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(ModalBody, {
|
||||
props: { "data-content": "main" },
|
||||
slots: { default: "Test" },
|
||||
});
|
||||
|
||||
expect(result).toContain('data-content="main"');
|
||||
});
|
||||
});
|
@ -1,44 +0,0 @@
|
||||
import { experimental_AstroContainer as AstroContainer } from "astro/container";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import ModalFooter from "../ModalFooter.astro";
|
||||
|
||||
describe("ModalFooter", () => {
|
||||
it("renders with default props", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(ModalFooter, {
|
||||
slots: { default: "Footer buttons" },
|
||||
});
|
||||
|
||||
expect(result).toContain("Footer buttons");
|
||||
expect(result).toContain('class="modal-footer"');
|
||||
});
|
||||
|
||||
it("applies custom classes", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(ModalFooter, {
|
||||
props: { class: "custom-footer" },
|
||||
slots: { default: "Content" },
|
||||
});
|
||||
|
||||
expect(result).toContain("modal-footer custom-footer");
|
||||
});
|
||||
|
||||
it("renders as div element", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(ModalFooter, {
|
||||
slots: { default: "Test" },
|
||||
});
|
||||
|
||||
expect(result).toContain('<div class="modal-footer"');
|
||||
});
|
||||
|
||||
it("passes through additional props", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(ModalFooter, {
|
||||
props: { "data-actions": "confirm" },
|
||||
slots: { default: "Test" },
|
||||
});
|
||||
|
||||
expect(result).toContain('data-actions="confirm"');
|
||||
});
|
||||
});
|
@ -1,54 +0,0 @@
|
||||
import { experimental_AstroContainer as AstroContainer } from "astro/container";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import ModalHeader from "../ModalHeader.astro";
|
||||
|
||||
describe("ModalHeader", () => {
|
||||
it("renders with default props", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(ModalHeader, {
|
||||
slots: { default: "Modal Title" },
|
||||
});
|
||||
|
||||
expect(result).toContain("Modal Title");
|
||||
expect(result).toContain('class="modal-header"');
|
||||
});
|
||||
|
||||
it("includes close button", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(ModalHeader, {
|
||||
slots: { default: "Title" },
|
||||
});
|
||||
|
||||
expect(result).toContain("data-modal-close");
|
||||
expect(result).toContain("×");
|
||||
});
|
||||
|
||||
it("applies custom classes", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(ModalHeader, {
|
||||
props: { class: "custom-header" },
|
||||
slots: { default: "Title" },
|
||||
});
|
||||
|
||||
expect(result).toContain("modal-header custom-header");
|
||||
});
|
||||
|
||||
it("renders as div element", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(ModalHeader, {
|
||||
slots: { default: "Test" },
|
||||
});
|
||||
|
||||
expect(result).toContain('<div class="modal-header"');
|
||||
});
|
||||
|
||||
it("passes through additional props", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(ModalHeader, {
|
||||
props: { "data-header": "modal" },
|
||||
slots: { default: "Test" },
|
||||
});
|
||||
|
||||
expect(result).toContain('data-header="modal"');
|
||||
});
|
||||
});
|
@ -1,113 +0,0 @@
|
||||
import { experimental_AstroContainer as AstroContainer } from "astro/container";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import MovieCard from "../MovieCard.astro";
|
||||
|
||||
describe("MovieCard", () => {
|
||||
const mockMovie = {
|
||||
title: "Test Movie",
|
||||
poster: "/test-poster.jpg",
|
||||
};
|
||||
|
||||
it("renders with required props", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(MovieCard, {
|
||||
props: mockMovie,
|
||||
});
|
||||
|
||||
expect(result).toContain('class="movie-card"');
|
||||
expect(result).toContain("Test Movie");
|
||||
expect(result).toContain('src="/test-poster.jpg"');
|
||||
expect(result).toContain('alt="Test Movie"');
|
||||
expect(result).toContain('class="movie-title"');
|
||||
expect(result).toContain('class="movie-poster"');
|
||||
});
|
||||
|
||||
it("renders as div by default", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(MovieCard, {
|
||||
props: mockMovie,
|
||||
});
|
||||
|
||||
expect(result).toContain('<div class="movie-card"');
|
||||
});
|
||||
|
||||
it("renders as anchor when href provided", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(MovieCard, {
|
||||
props: { ...mockMovie, href: "/movie/123" },
|
||||
});
|
||||
|
||||
expect(result).toContain('<a class="movie-card"');
|
||||
expect(result).toContain('href="/movie/123"');
|
||||
});
|
||||
|
||||
it("displays movie year and genre", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(MovieCard, {
|
||||
props: { ...mockMovie, year: 2023, genre: "Action" },
|
||||
});
|
||||
|
||||
expect(result).toContain('class="movie-meta"');
|
||||
expect(result).toContain("2023");
|
||||
expect(result).toContain("Action");
|
||||
expect(result).toContain("•");
|
||||
});
|
||||
|
||||
it("displays only year when genre not provided", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(MovieCard, {
|
||||
props: { ...mockMovie, year: 2023 },
|
||||
});
|
||||
|
||||
expect(result).toContain("2023");
|
||||
expect(result).not.toContain("•");
|
||||
});
|
||||
|
||||
it("displays only genre when year not provided", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(MovieCard, {
|
||||
props: { ...mockMovie, genre: "Drama" },
|
||||
});
|
||||
|
||||
expect(result).toContain("Drama");
|
||||
expect(result).not.toContain("•");
|
||||
});
|
||||
|
||||
it("displays rating when provided", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(MovieCard, {
|
||||
props: { ...mockMovie, rating: 8.5 },
|
||||
});
|
||||
|
||||
expect(result).toContain('class="movie-rating"');
|
||||
expect(result).toContain("⭐");
|
||||
expect(result).toContain("8.5/10");
|
||||
});
|
||||
|
||||
it("does not display rating when not provided", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(MovieCard, {
|
||||
props: mockMovie,
|
||||
});
|
||||
|
||||
expect(result).not.toContain('class="movie-rating"');
|
||||
});
|
||||
|
||||
it("applies custom classes", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(MovieCard, {
|
||||
props: { ...mockMovie, class: "featured-movie" },
|
||||
});
|
||||
|
||||
expect(result).toContain("movie-card featured-movie");
|
||||
});
|
||||
|
||||
it("applies lazy loading to poster", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(MovieCard, {
|
||||
props: mockMovie,
|
||||
});
|
||||
|
||||
expect(result).toContain('loading="lazy"');
|
||||
});
|
||||
});
|
@ -1,85 +0,0 @@
|
||||
import { experimental_AstroContainer as AstroContainer } from "astro/container";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import NavLink from "../NavLink.astro";
|
||||
|
||||
describe("NavLink", () => {
|
||||
it("renders with required href prop", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(NavLink, {
|
||||
props: { href: "/home" },
|
||||
slots: { default: "Home" },
|
||||
});
|
||||
|
||||
expect(result).toContain("<li data-astro-source-file");
|
||||
expect(result).toContain("<a");
|
||||
expect(result).toContain('href="/home"');
|
||||
expect(result).toContain("Home");
|
||||
expect(result).toContain('class="nav-link"');
|
||||
});
|
||||
|
||||
it("applies active state", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(NavLink, {
|
||||
props: {
|
||||
href: "/current",
|
||||
active: true,
|
||||
},
|
||||
slots: { default: "Current Page" },
|
||||
});
|
||||
|
||||
expect(result).toContain("nav-link");
|
||||
expect(result).toContain("active");
|
||||
});
|
||||
|
||||
it("does not apply active class by default", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(NavLink, {
|
||||
props: { href: "/page" },
|
||||
slots: { default: "Page" },
|
||||
});
|
||||
|
||||
expect(result).toContain("nav-link");
|
||||
expect(result).not.toContain("active");
|
||||
});
|
||||
|
||||
it("applies custom classes", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(NavLink, {
|
||||
props: {
|
||||
href: "/test",
|
||||
class: "custom-nav-link",
|
||||
},
|
||||
slots: { default: "Test" },
|
||||
});
|
||||
|
||||
expect(result).toContain("nav-link");
|
||||
expect(result).toContain("custom-nav-link");
|
||||
});
|
||||
|
||||
it("wraps link in list item", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(NavLink, {
|
||||
props: { href: "/wrapped" },
|
||||
slots: { default: "Wrapped" },
|
||||
});
|
||||
|
||||
expect(result).toContain("<li data-astro-source-file");
|
||||
expect(result).toContain("<a");
|
||||
expect(result).toContain("Wrapped");
|
||||
});
|
||||
|
||||
it("passes through additional props to anchor", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(NavLink, {
|
||||
props: {
|
||||
href: "/props-test",
|
||||
target: "_blank",
|
||||
rel: "noopener",
|
||||
},
|
||||
slots: { default: "External" },
|
||||
});
|
||||
|
||||
expect(result).toContain('target="_blank"');
|
||||
expect(result).toContain('rel="noopener"');
|
||||
});
|
||||
});
|
@ -1,81 +0,0 @@
|
||||
import { experimental_AstroContainer as AstroContainer } from "astro/container";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import Navbar from "../Navbar.astro";
|
||||
|
||||
describe("Navbar", () => {
|
||||
it("renders with default props", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Navbar, {
|
||||
slots: {
|
||||
brand: "Brand",
|
||||
nav: "Navigation",
|
||||
actions: "Actions",
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toContain('class="navbar"');
|
||||
expect(result).toContain("<nav");
|
||||
});
|
||||
|
||||
it("renders brand slot content", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Navbar, {
|
||||
slots: {
|
||||
brand: '<a href="/">MyBrand</a>',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toContain("MyBrand");
|
||||
});
|
||||
|
||||
it("renders navigation slot content", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Navbar, {
|
||||
slots: {
|
||||
nav: "<ul><li>Home</li><li>About</li></ul>",
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toContain("<ul><li>Home</li><li>About</li></ul>");
|
||||
});
|
||||
|
||||
it("renders actions slot content", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Navbar, {
|
||||
slots: {
|
||||
actions: "<button>Login</button>",
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toContain("<button>Login</button>");
|
||||
});
|
||||
|
||||
it("applies custom classes", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Navbar, {
|
||||
props: { class: "custom-navbar" },
|
||||
});
|
||||
|
||||
expect(result).toContain("navbar custom-navbar");
|
||||
});
|
||||
|
||||
it("has proper grid structure", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Navbar);
|
||||
|
||||
expect(result).toContain('class="container"');
|
||||
expect(result).toContain(
|
||||
'class="row justify-content-between align-items-center"',
|
||||
);
|
||||
expect(result.match(/class="col-auto"/g)?.length).toBe(3);
|
||||
});
|
||||
|
||||
it("passes through additional props", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Navbar, {
|
||||
props: { "data-nav": "main" },
|
||||
});
|
||||
|
||||
expect(result).toContain('data-nav="main"');
|
||||
});
|
||||
});
|
@ -1,60 +0,0 @@
|
||||
import { experimental_AstroContainer as AstroContainer } from "astro/container";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import NavbarBrand from "../NavbarBrand.astro";
|
||||
|
||||
describe("NavbarBrand", () => {
|
||||
it("renders with default props", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(NavbarBrand, {
|
||||
slots: { default: "Brand Name" },
|
||||
});
|
||||
|
||||
expect(result).toContain('class="navbar-brand"');
|
||||
expect(result).toContain("<a");
|
||||
expect(result).toContain("Brand Name");
|
||||
expect(result).toContain('href="/"');
|
||||
});
|
||||
|
||||
it("applies custom href", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(NavbarBrand, {
|
||||
props: { href: "/dashboard" },
|
||||
slots: { default: "Dashboard" },
|
||||
});
|
||||
|
||||
expect(result).toContain('href="/dashboard"');
|
||||
});
|
||||
|
||||
it("applies custom classes", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(NavbarBrand, {
|
||||
props: { class: "custom-brand" },
|
||||
slots: { default: "Brand" },
|
||||
});
|
||||
|
||||
expect(result).toContain("navbar-brand custom-brand");
|
||||
});
|
||||
|
||||
it("renders as anchor element", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(NavbarBrand, {
|
||||
slots: { default: "Test" },
|
||||
});
|
||||
|
||||
expect(result).toContain('<a class="navbar-brand"');
|
||||
});
|
||||
|
||||
it("passes through additional props", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(NavbarBrand, {
|
||||
props: {
|
||||
title: "Go to homepage",
|
||||
"data-brand": "main",
|
||||
},
|
||||
slots: { default: "Home" },
|
||||
});
|
||||
|
||||
expect(result).toContain('title="Go to homepage"');
|
||||
expect(result).toContain('data-brand="main"');
|
||||
});
|
||||
});
|
@ -1,91 +0,0 @@
|
||||
import { experimental_AstroContainer as AstroContainer } from "astro/container";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import Progress from "../Progress.astro";
|
||||
|
||||
describe("Progress", () => {
|
||||
it("renders with required value prop", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Progress, {
|
||||
props: { value: 50 },
|
||||
});
|
||||
|
||||
expect(result).toContain('class="progress"');
|
||||
expect(result).toContain('class="progress-bar"');
|
||||
});
|
||||
|
||||
it("calculates correct percentage with default max", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Progress, {
|
||||
props: { value: 25 },
|
||||
});
|
||||
|
||||
expect(result).toContain("width: 25%");
|
||||
});
|
||||
|
||||
it("calculates correct percentage with custom max", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Progress, {
|
||||
props: { value: 30, max: 60 },
|
||||
});
|
||||
|
||||
expect(result).toContain("width: 50%");
|
||||
});
|
||||
|
||||
it("caps percentage at 100% when value exceeds max", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Progress, {
|
||||
props: { value: 150, max: 100 },
|
||||
});
|
||||
|
||||
expect(result).toContain("width: 100%");
|
||||
});
|
||||
|
||||
it("applies correct ARIA attributes", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Progress, {
|
||||
props: { value: 40, max: 80 },
|
||||
});
|
||||
|
||||
expect(result).toContain('role="progressbar"');
|
||||
expect(result).toContain('aria-valuenow="40"');
|
||||
expect(result).toContain('aria-valuemin="0"');
|
||||
expect(result).toContain('aria-valuemax="80"');
|
||||
});
|
||||
|
||||
it("uses default max value of 100", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Progress, {
|
||||
props: { value: 75 },
|
||||
});
|
||||
|
||||
expect(result).toContain('aria-valuemax="100"');
|
||||
});
|
||||
|
||||
it("applies custom classes", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Progress, {
|
||||
props: { value: 60, class: "custom-progress" },
|
||||
});
|
||||
|
||||
expect(result).toContain("progress custom-progress");
|
||||
});
|
||||
|
||||
it("handles zero value", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Progress, {
|
||||
props: { value: 0 },
|
||||
});
|
||||
|
||||
expect(result).toContain("width: 0%");
|
||||
expect(result).toContain('aria-valuenow="0"');
|
||||
});
|
||||
|
||||
it("passes through additional props", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Progress, {
|
||||
props: { value: 45, "data-progress": "loading" },
|
||||
});
|
||||
|
||||
expect(result).toContain('data-progress="loading"');
|
||||
});
|
||||
});
|
@ -1,54 +0,0 @@
|
||||
import { experimental_AstroContainer as AstroContainer } from "astro/container";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import Row from "../Row.astro";
|
||||
|
||||
describe("Row", () => {
|
||||
it("renders with default props", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Row, {
|
||||
slots: { default: "Row content" },
|
||||
});
|
||||
|
||||
expect(result).toContain("Row content");
|
||||
expect(result).toContain('class="row"');
|
||||
});
|
||||
|
||||
it("applies custom classes", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Row, {
|
||||
props: { class: "custom-row" },
|
||||
slots: { default: "Content" },
|
||||
});
|
||||
|
||||
expect(result).toContain("row custom-row");
|
||||
});
|
||||
|
||||
it("applies inline styles", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Row, {
|
||||
props: { style: "margin-top: 20px;" },
|
||||
slots: { default: "Content" },
|
||||
});
|
||||
|
||||
expect(result).toContain('style="margin-top: 20px;"');
|
||||
});
|
||||
|
||||
it("renders as div element", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Row, {
|
||||
slots: { default: "Test" },
|
||||
});
|
||||
|
||||
expect(result).toContain('<div class="row"');
|
||||
});
|
||||
|
||||
it("passes through additional props", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Row, {
|
||||
props: { "data-row": "header" },
|
||||
slots: { default: "Test" },
|
||||
});
|
||||
|
||||
expect(result).toContain('data-row="header"');
|
||||
});
|
||||
});
|
@ -1,94 +0,0 @@
|
||||
import { experimental_AstroContainer as AstroContainer } from "astro/container";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import SearchBar from "../SearchBar.astro";
|
||||
|
||||
describe("SearchBar", () => {
|
||||
it("renders with default props", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(SearchBar);
|
||||
|
||||
expect(result).toContain('class="search-bar"');
|
||||
expect(result).toContain('type="search"');
|
||||
expect(result).toContain('class="search-icon"');
|
||||
});
|
||||
|
||||
it("applies default placeholder", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(SearchBar);
|
||||
|
||||
expect(result).toContain('placeholder="Rechercher..."');
|
||||
});
|
||||
|
||||
it("applies custom placeholder", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(SearchBar, {
|
||||
props: { placeholder: "Search movies..." },
|
||||
});
|
||||
|
||||
expect(result).toContain('placeholder="Search movies..."');
|
||||
});
|
||||
|
||||
it("applies value prop", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(SearchBar, {
|
||||
props: { value: "test query" },
|
||||
});
|
||||
|
||||
expect(result).toContain('value="test query"');
|
||||
});
|
||||
|
||||
it("applies default name and id", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(SearchBar);
|
||||
|
||||
expect(result).toContain('name="search"');
|
||||
expect(result).toContain('id="search"');
|
||||
});
|
||||
|
||||
it("applies custom name and id", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(SearchBar, {
|
||||
props: {
|
||||
name: "movie-search",
|
||||
id: "movie-search-input",
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toContain('name="movie-search"');
|
||||
expect(result).toContain('id="movie-search-input"');
|
||||
});
|
||||
|
||||
it("applies custom classes", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(SearchBar, {
|
||||
props: { class: "custom-search" },
|
||||
});
|
||||
|
||||
expect(result).toContain("search-bar custom-search");
|
||||
});
|
||||
|
||||
it("renders search icon with correct attributes", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(SearchBar);
|
||||
|
||||
expect(result).toContain('width="16"');
|
||||
expect(result).toContain('height="16"');
|
||||
expect(result).toContain('viewBox="0 0 24 24"');
|
||||
});
|
||||
|
||||
it("input has search type", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(SearchBar);
|
||||
|
||||
expect(result).toContain('type="search"');
|
||||
});
|
||||
|
||||
it("passes through additional props", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(SearchBar, {
|
||||
props: { "data-testid": "search-component" },
|
||||
});
|
||||
|
||||
expect(result).toContain('data-testid="search-component"');
|
||||
});
|
||||
});
|
@ -1,71 +0,0 @@
|
||||
import { experimental_AstroContainer as AstroContainer } from "astro/container";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import Select from "../Select.astro";
|
||||
|
||||
describe("Select", () => {
|
||||
it("renders with default props", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Select, {
|
||||
slots: {
|
||||
default:
|
||||
'<option value="1">Option 1</option><option value="2">Option 2</option>',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toContain("<select");
|
||||
expect(result).toContain('class="select"');
|
||||
expect(result).toContain("Option 1");
|
||||
expect(result).toContain("Option 2");
|
||||
});
|
||||
|
||||
it("applies name and id attributes", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Select, {
|
||||
props: { name: "category", id: "category-select" },
|
||||
});
|
||||
|
||||
expect(result).toContain('name="category"');
|
||||
expect(result).toContain('id="category-select"');
|
||||
});
|
||||
|
||||
it("applies disabled state", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Select, {
|
||||
props: { disabled: true },
|
||||
});
|
||||
|
||||
expect(result).toContain("disabled");
|
||||
});
|
||||
|
||||
it("applies required attribute", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Select, {
|
||||
props: { required: true },
|
||||
});
|
||||
|
||||
expect(result).toContain("required");
|
||||
});
|
||||
|
||||
it("applies custom classes", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Select, {
|
||||
props: { class: "custom-select" },
|
||||
});
|
||||
|
||||
expect(result).toContain("select custom-select");
|
||||
});
|
||||
|
||||
it("renders option content in slot", async () => {
|
||||
const options =
|
||||
'<option value="apple">Apple</option><option value="banana">Banana</option>';
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Select, {
|
||||
slots: { default: options },
|
||||
});
|
||||
|
||||
expect(result).toContain("Apple");
|
||||
expect(result).toContain("Banana");
|
||||
expect(result).toContain('value="apple"');
|
||||
expect(result).toContain('value="banana"');
|
||||
});
|
||||
});
|
@ -1,71 +0,0 @@
|
||||
import { experimental_AstroContainer as AstroContainer } from "astro/container";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import Spinner from "../Spinner.astro";
|
||||
|
||||
describe("Spinner", () => {
|
||||
it("renders with default props", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Spinner);
|
||||
|
||||
expect(result).toContain('<div class="spinner"');
|
||||
expect(result).toContain("></div>");
|
||||
});
|
||||
|
||||
it("renders small size", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Spinner, {
|
||||
props: { size: "sm" },
|
||||
});
|
||||
|
||||
expect(result).toContain("spinner spinner-sm");
|
||||
});
|
||||
|
||||
it("renders large size", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Spinner, {
|
||||
props: { size: "lg" },
|
||||
});
|
||||
|
||||
expect(result).toContain("spinner spinner-lg");
|
||||
});
|
||||
|
||||
it("applies custom classes", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Spinner, {
|
||||
props: { class: "loading-spinner" },
|
||||
});
|
||||
|
||||
expect(result).toContain("spinner loading-spinner");
|
||||
});
|
||||
|
||||
it("applies custom classes with size", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Spinner, {
|
||||
props: { size: "lg", class: "custom-spinner" },
|
||||
});
|
||||
|
||||
expect(result).toContain("spinner spinner-lg custom-spinner");
|
||||
});
|
||||
|
||||
it("renders as empty div", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Spinner);
|
||||
|
||||
expect(result).toContain("></div>");
|
||||
// The div should be empty (no content between opening and closing tags, except for Astro metadata)
|
||||
expect(result).toMatch(/<div[^>]*><\/div>/);
|
||||
});
|
||||
|
||||
it("passes through additional props", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Spinner, {
|
||||
props: {
|
||||
"data-testid": "loading-indicator",
|
||||
"aria-label": "Loading content",
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toContain('data-testid="loading-indicator"');
|
||||
expect(result).toContain('aria-label="Loading content"');
|
||||
});
|
||||
});
|
@ -1,78 +0,0 @@
|
||||
import { experimental_AstroContainer as AstroContainer } from "astro/container";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import Textarea from "../Textarea.astro";
|
||||
|
||||
describe("Textarea", () => {
|
||||
it("renders with default props", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Textarea);
|
||||
|
||||
expect(result).toContain("<textarea");
|
||||
expect(result).toContain('class="textarea"');
|
||||
expect(result).toContain('rows="4"');
|
||||
});
|
||||
|
||||
it("applies placeholder", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Textarea, {
|
||||
props: { placeholder: "Enter your message" },
|
||||
});
|
||||
|
||||
expect(result).toContain('placeholder="Enter your message"');
|
||||
});
|
||||
|
||||
it("applies value", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Textarea, {
|
||||
props: { value: "Initial text" },
|
||||
});
|
||||
|
||||
expect(result).toContain("Initial text");
|
||||
});
|
||||
|
||||
it("applies name and id attributes", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Textarea, {
|
||||
props: { name: "message", id: "message-input" },
|
||||
});
|
||||
|
||||
expect(result).toContain('name="message"');
|
||||
expect(result).toContain('id="message-input"');
|
||||
});
|
||||
|
||||
it("applies custom rows", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Textarea, {
|
||||
props: { rows: 8 },
|
||||
});
|
||||
|
||||
expect(result).toContain('rows="8"');
|
||||
});
|
||||
|
||||
it("applies disabled state", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Textarea, {
|
||||
props: { disabled: true },
|
||||
});
|
||||
|
||||
expect(result).toContain("disabled");
|
||||
});
|
||||
|
||||
it("applies required attribute", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Textarea, {
|
||||
props: { required: true },
|
||||
});
|
||||
|
||||
expect(result).toContain("required");
|
||||
});
|
||||
|
||||
it("applies custom classes", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Textarea, {
|
||||
props: { class: "custom-textarea" },
|
||||
});
|
||||
|
||||
expect(result).toContain("textarea custom-textarea");
|
||||
});
|
||||
});
|
@ -1,15 +0,0 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { experimental_AstroContainer as AstroContainer } from "astro/container";
|
||||
import Button from "../Button.astro";
|
||||
|
||||
describe("Basic Astro Component Test", () => {
|
||||
it("should render a simple button", async () => {
|
||||
const container = await AstroContainer.create();
|
||||
const result = await container.renderToString(Button, {
|
||||
slots: { default: "Test Button" },
|
||||
});
|
||||
|
||||
expect(result).toContain("Test Button");
|
||||
expect(result).toContain("btn");
|
||||
});
|
||||
});
|
7
src/layouts/EmptyLayout.astro
Normal file
7
src/layouts/EmptyLayout.astro
Normal file
@ -0,0 +1,7 @@
|
||||
---
|
||||
import Layout from "./Layout.astro";
|
||||
---
|
||||
|
||||
<Layout>
|
||||
<slot />
|
||||
</Layout>
|
39
src/pages/login.astro
Normal file
39
src/pages/login.astro
Normal file
@ -0,0 +1,39 @@
|
||||
---
|
||||
export const prerender = false;
|
||||
|
||||
import { actions } from "astro:actions";
|
||||
import Ascii from "../components/Ascii.astro";
|
||||
import LoginForm from "../components/forms/LoginForm.astro";
|
||||
import Card from "../components/ui/Card.astro";
|
||||
import CardBody from "../components/ui/CardBody.astro";
|
||||
import CardHeader from "../components/ui/CardHeader.astro";
|
||||
import Col from "../components/ui/Col.astro";
|
||||
import Container from "../components/ui/Container.astro";
|
||||
import Row from "../components/ui/Row.astro";
|
||||
import EmptyLayout from "../layouts/EmptyLayout.astro";
|
||||
import { isInputError } from "astro:actions";
|
||||
|
||||
const result = Astro.getActionResult(actions.login);
|
||||
---
|
||||
|
||||
<EmptyLayout>
|
||||
<Container class={"h-100"}>
|
||||
<Row class="justify-content-center align-items-center h-100">
|
||||
<Col xs={12} md={6} lg={4}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Ascii />
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<LoginForm
|
||||
action={actions.login}
|
||||
error={result?.error && isInputError(result.error)
|
||||
? result.error
|
||||
: null}
|
||||
/>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
</EmptyLayout>
|
@ -1,163 +1,163 @@
|
||||
:root {
|
||||
--primary-50: #f3f1ff;
|
||||
--primary-100: #ebe5ff;
|
||||
--primary-200: #d9ccff;
|
||||
--primary-300: #bea6ff;
|
||||
--primary-400: #9f75ff;
|
||||
--primary-500: #843dff;
|
||||
--primary-600: #7916ff;
|
||||
--primary-700: #6b04fd;
|
||||
--primary-800: #5a03d4;
|
||||
--primary-900: #4b05ad;
|
||||
--primary-950: #2c0274;
|
||||
--primary-50: #f3f1ff;
|
||||
--primary-100: #ebe5ff;
|
||||
--primary-200: #d9ccff;
|
||||
--primary-300: #bea6ff;
|
||||
--primary-400: #9f75ff;
|
||||
--primary-500: #843dff;
|
||||
--primary-600: #7916ff;
|
||||
--primary-700: #6b04fd;
|
||||
--primary-800: #5a03d4;
|
||||
--primary-900: #4b05ad;
|
||||
--primary-950: #2c0274;
|
||||
|
||||
--gray-50: #f8fafc;
|
||||
--gray-100: #f1f5f9;
|
||||
--gray-200: #e2e8f0;
|
||||
--gray-300: #cbd5e1;
|
||||
--gray-400: #94a3b8;
|
||||
--gray-500: #64748b;
|
||||
--gray-600: #475569;
|
||||
--gray-700: #334155;
|
||||
--gray-800: #1e293b;
|
||||
--gray-900: #0f172a;
|
||||
--gray-950: #020617;
|
||||
--gray-50: #f8fafc;
|
||||
--gray-100: #f1f5f9;
|
||||
--gray-200: #e2e8f0;
|
||||
--gray-300: #cbd5e1;
|
||||
--gray-400: #94a3b8;
|
||||
--gray-500: #64748b;
|
||||
--gray-600: #475569;
|
||||
--gray-700: #334155;
|
||||
--gray-800: #1e293b;
|
||||
--gray-900: #0f172a;
|
||||
--gray-950: #020617;
|
||||
|
||||
--success: #10b981;
|
||||
--warning: #f59e0b;
|
||||
--error: #ef4444;
|
||||
--info: #3b82f6;
|
||||
--success: #10b981;
|
||||
--warning: #f59e0b;
|
||||
--error: #ef4444;
|
||||
--info: #3b82f6;
|
||||
|
||||
--bg-primary: var(--gray-950);
|
||||
--bg-secondary: var(--gray-900);
|
||||
--bg-tertiary: var(--gray-800);
|
||||
--bg-card: var(--gray-900);
|
||||
--bg-hover: var(--gray-800);
|
||||
--bg-primary: var(--gray-950);
|
||||
--bg-secondary: var(--gray-900);
|
||||
--bg-tertiary: var(--gray-800);
|
||||
--bg-card: var(--gray-900);
|
||||
--bg-hover: var(--gray-800);
|
||||
|
||||
--text-primary: var(--gray-50);
|
||||
--text-secondary: var(--gray-300);
|
||||
--text-muted: var(--gray-500);
|
||||
--text-primary: var(--gray-50);
|
||||
--text-secondary: var(--gray-300);
|
||||
--text-muted: var(--gray-500);
|
||||
|
||||
--border-primary: var(--gray-700);
|
||||
--border-secondary: var(--gray-800);
|
||||
--border-primary: var(--gray-700);
|
||||
--border-secondary: var(--gray-800);
|
||||
|
||||
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
||||
--shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
||||
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||||
--shadow-lg:
|
||||
0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
||||
--shadow-xl:
|
||||
0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
|
||||
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
||||
--shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
||||
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||||
--shadow-lg:
|
||||
0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
||||
--shadow-xl:
|
||||
0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
|
||||
|
||||
--radius-sm: 0.125rem;
|
||||
--radius: 0.25rem;
|
||||
--radius-md: 0.375rem;
|
||||
--radius-lg: 0.5rem;
|
||||
--radius-xl: 0.75rem;
|
||||
--radius-2xl: 1rem;
|
||||
--radius-3xl: 1.5rem;
|
||||
--radius-full: 9999px;
|
||||
--radius-sm: 0.125rem;
|
||||
--radius: 0.25rem;
|
||||
--radius-md: 0.375rem;
|
||||
--radius-lg: 0.5rem;
|
||||
--radius-xl: 0.75rem;
|
||||
--radius-2xl: 1rem;
|
||||
--radius-3xl: 1.5rem;
|
||||
--radius-full: 9999px;
|
||||
|
||||
--spacing-1: 0.25rem;
|
||||
--spacing-2: 0.5rem;
|
||||
--spacing-3: 0.75rem;
|
||||
--spacing-4: 1rem;
|
||||
--spacing-5: 1.25rem;
|
||||
--spacing-6: 1.5rem;
|
||||
--spacing-8: 2rem;
|
||||
--spacing-10: 2.5rem;
|
||||
--spacing-12: 3rem;
|
||||
--spacing-16: 4rem;
|
||||
--spacing-20: 5rem;
|
||||
--spacing-24: 6rem;
|
||||
--spacing-32: 8rem;
|
||||
--spacing-1: 0.25rem;
|
||||
--spacing-2: 0.5rem;
|
||||
--spacing-3: 0.75rem;
|
||||
--spacing-4: 1rem;
|
||||
--spacing-5: 1.25rem;
|
||||
--spacing-6: 1.5rem;
|
||||
--spacing-8: 2rem;
|
||||
--spacing-10: 2.5rem;
|
||||
--spacing-12: 3rem;
|
||||
--spacing-16: 4rem;
|
||||
--spacing-20: 5rem;
|
||||
--spacing-24: 6rem;
|
||||
--spacing-32: 8rem;
|
||||
|
||||
--font-family-sans:
|
||||
ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||
Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
|
||||
--font-family-mono:
|
||||
ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo,
|
||||
monospace;
|
||||
--font-family-sans:
|
||||
ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||
Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
|
||||
--font-family-mono:
|
||||
ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo,
|
||||
monospace;
|
||||
|
||||
--font-size-xs: 0.75rem;
|
||||
--font-size-sm: 0.875rem;
|
||||
--font-size-base: 1rem;
|
||||
--font-size-lg: 1.125rem;
|
||||
--font-size-xl: 1.25rem;
|
||||
--font-size-2xl: 1.5rem;
|
||||
--font-size-3xl: 1.875rem;
|
||||
--font-size-4xl: 2.25rem;
|
||||
--font-size-5xl: 3rem;
|
||||
--font-size-xs: 0.75rem;
|
||||
--font-size-sm: 0.875rem;
|
||||
--font-size-base: 1rem;
|
||||
--font-size-lg: 1.125rem;
|
||||
--font-size-xl: 1.25rem;
|
||||
--font-size-2xl: 1.5rem;
|
||||
--font-size-3xl: 1.875rem;
|
||||
--font-size-4xl: 2.25rem;
|
||||
--font-size-5xl: 3rem;
|
||||
|
||||
--font-weight-normal: 400;
|
||||
--font-weight-medium: 500;
|
||||
--font-weight-semibold: 600;
|
||||
--font-weight-bold: 700;
|
||||
--font-weight-normal: 400;
|
||||
--font-weight-medium: 500;
|
||||
--font-weight-semibold: 600;
|
||||
--font-weight-bold: 700;
|
||||
|
||||
--line-height-tight: 1.25;
|
||||
--line-height-normal: 1.5;
|
||||
--line-height-relaxed: 1.625;
|
||||
--line-height-tight: 1.25;
|
||||
--line-height-normal: 1.5;
|
||||
--line-height-relaxed: 1.625;
|
||||
|
||||
--transition-fast: 150ms ease;
|
||||
--transition-normal: 300ms ease;
|
||||
--transition-slow: 500ms ease;
|
||||
--transition-fast: 150ms ease;
|
||||
--transition-normal: 300ms ease;
|
||||
--transition-slow: 500ms ease;
|
||||
|
||||
--z-dropdown: 1000;
|
||||
--z-sticky: 1020;
|
||||
--z-fixed: 1030;
|
||||
--z-modal-backdrop: 1040;
|
||||
--z-modal: 1050;
|
||||
--z-popover: 1060;
|
||||
--z-tooltip: 1070;
|
||||
--z-dropdown: 1000;
|
||||
--z-sticky: 1020;
|
||||
--z-fixed: 1030;
|
||||
--z-modal-backdrop: 1040;
|
||||
--z-modal: 1050;
|
||||
--z-popover: 1060;
|
||||
--z-tooltip: 1070;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-base);
|
||||
line-height: var(--line-height-normal);
|
||||
color: var(--text-primary);
|
||||
background-color: var(--bg-primary);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-base);
|
||||
line-height: var(--line-height-normal);
|
||||
color: var(--text-primary);
|
||||
background-color: var(--bg-primary);
|
||||
}
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Form Control Base Styles */
|
||||
.form-control-base {
|
||||
width: 100%;
|
||||
padding: var(--spacing-3) var(--spacing-4);
|
||||
font-size: var(--font-size-base);
|
||||
line-height: var(--line-height-normal);
|
||||
color: var(--text-primary);
|
||||
background-color: var(--bg-tertiary);
|
||||
border: 1px solid var(--border-primary);
|
||||
border-radius: var(--radius-md);
|
||||
transition: all var(--transition-fast);
|
||||
width: 100%;
|
||||
padding: var(--spacing-3) var(--spacing-4);
|
||||
font-size: var(--font-size-base);
|
||||
line-height: var(--line-height-normal);
|
||||
color: var(--text-primary);
|
||||
background-color: var(--bg-tertiary);
|
||||
border: 1px solid var(--border-primary);
|
||||
border-radius: var(--radius-md);
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.form-control-base:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary-600);
|
||||
box-shadow: 0 0 0 3px rgba(132, 61, 255, 0.1);
|
||||
outline: none;
|
||||
border-color: var(--primary-600);
|
||||
box-shadow: 0 0 0 3px rgba(132, 61, 255, 0.1);
|
||||
}
|
||||
|
||||
.form-control-base:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.form-control-base::placeholder {
|
||||
color: var(--text-muted);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
Reference in New Issue
Block a user