Initial project setup with Astro framework, including configuration files, a comprehensive .gitignore, and a variety of UI components for a streaming platform. Added essential styles and layout structure, along with a README detailing project features and development guidelines.

This commit is contained in:
2025-07-30 19:14:23 +00:00
parent 0e4a09f9fc
commit 159cff7f77
49 changed files with 3749 additions and 1 deletions

1
src/assets/astro.svg Normal file
View File

@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,52 @@
---
export interface Props {
variant?: "success" | "warning" | "error" | "info";
class?: string;
}
const { variant = "info", class: className = "", ...rest } = Astro.props;
const baseClasses = "alert";
const variantClasses = `alert-${variant}`;
const classes = [baseClasses, variantClasses, className]
.filter(Boolean)
.join(" ");
---
<div class={classes} {...rest}>
<slot />
</div>
<style>
.alert {
padding: var(--spacing-4);
border: 1px solid;
border-radius: var(--radius-md);
margin-bottom: var(--spacing-4);
}
.alert-success {
background-color: rgba(16, 185, 129, 0.1);
border-color: var(--success);
color: var(--success);
}
.alert-warning {
background-color: rgba(245, 158, 11, 0.1);
border-color: var(--warning);
color: var(--warning);
}
.alert-error {
background-color: rgba(239, 68, 68, 0.1);
border-color: var(--error);
color: var(--error);
}
.alert-info {
background-color: rgba(59, 130, 246, 0.1);
border-color: var(--info);
color: var(--info);
}
</style>

View File

@ -0,0 +1,58 @@
---
export interface Props {
variant?: "primary" | "secondary" | "success" | "warning" | "error";
class?: string;
}
const { variant = "primary", class: className = "", ...rest } = Astro.props;
const baseClasses = "badge";
const variantClasses = `badge-${variant}`;
const classes = [baseClasses, variantClasses, className]
.filter(Boolean)
.join(" ");
---
<span class={classes} {...rest}>
<slot />
</span>
<style>
.badge {
display: inline-flex;
align-items: center;
padding: var(--spacing-1) var(--spacing-2);
font-size: var(--font-size-xs);
font-weight: var(--font-weight-medium);
line-height: var(--line-height-tight);
border-radius: var(--radius-full);
text-transform: uppercase;
letter-spacing: 0.025em;
}
.badge-primary {
background-color: var(--primary-600);
color: white;
}
.badge-secondary {
background-color: var(--bg-tertiary);
color: var(--text-secondary);
}
.badge-success {
background-color: var(--success);
color: white;
}
.badge-warning {
background-color: var(--warning);
color: white;
}
.badge-error {
background-color: var(--error);
color: white;
}
</style>

View File

@ -0,0 +1,150 @@
---
export interface Props {
variant?: "primary" | "secondary" | "outline" | "ghost";
size?: "sm" | "lg";
type?: "button" | "submit" | "reset";
href?: string;
disabled?: boolean;
icon?: boolean;
class?: string;
}
const {
variant = "primary",
size,
type = "button",
href,
disabled = false,
icon = false,
class: className = "",
...rest
} = Astro.props;
const baseClasses = "btn";
const variantClasses = `btn-${variant}`;
const sizeClasses = size ? `btn-${size}` : "";
const iconClasses = icon ? "btn-icon" : "";
const classes = [
baseClasses,
variantClasses,
sizeClasses,
iconClasses,
className,
]
.filter(Boolean)
.join(" ");
const Tag = href ? "a" : "button";
---
<Tag
class={classes}
type={href ? undefined : type}
href={href}
disabled={disabled}
{...rest}
>
<slot />
</Tag>
<style>
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: var(--spacing-3) var(--spacing-6);
font-size: var(--font-size-base);
font-weight: var(--font-weight-medium);
line-height: var(--line-height-tight);
border-radius: var(--radius-md);
border: 1px solid transparent;
text-decoration: none;
cursor: pointer;
transition: all var(--transition-fast);
user-select: none;
white-space: nowrap;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-primary {
background-color: var(--primary-600);
color: white;
border-color: var(--primary-600);
}
.btn-primary:hover:not(:disabled) {
background-color: var(--primary-700);
border-color: var(--primary-700);
}
.btn-primary:active:not(:disabled) {
background-color: var(--primary-800);
border-color: var(--primary-800);
}
.btn-secondary {
background-color: var(--bg-tertiary);
color: var(--text-primary);
border-color: var(--border-primary);
}
.btn-secondary:hover:not(:disabled) {
background-color: var(--bg-hover);
border-color: var(--border-secondary);
}
.btn-outline {
background-color: transparent;
color: var(--primary-400);
border-color: var(--primary-600);
}
.btn-outline:hover:not(:disabled) {
background-color: var(--primary-600);
color: white;
}
.btn-ghost {
background-color: transparent;
color: var(--text-secondary);
border-color: transparent;
}
.btn-ghost:hover:not(:disabled) {
background-color: var(--bg-hover);
color: var(--text-primary);
}
.btn-sm {
padding: var(--spacing-2) var(--spacing-4);
font-size: var(--font-size-sm);
}
.btn-lg {
padding: var(--spacing-4) var(--spacing-8);
font-size: var(--font-size-lg);
}
.btn-icon {
padding: var(--spacing-3);
width: 2.5rem;
height: 2.5rem;
}
.btn-icon.btn-sm {
padding: var(--spacing-2);
width: 2rem;
height: 2rem;
}
.btn-icon.btn-lg {
padding: var(--spacing-4);
width: 3rem;
height: 3rem;
}
</style>

View File

@ -0,0 +1,29 @@
---
export interface Props {
class?: string;
}
const { class: className = "", ...rest } = Astro.props;
const classes = ["card", className].filter(Boolean).join(" ");
---
<div class={classes} {...rest}>
<slot />
</div>
<style>
.card {
background-color: var(--bg-card);
border: 1px solid var(--border-primary);
border-radius: var(--radius-lg);
padding: var(--spacing-6);
box-shadow: var(--shadow);
transition: all var(--transition-fast);
}
.card:hover {
box-shadow: var(--shadow-md);
border-color: var(--border-secondary);
}
</style>

View File

@ -0,0 +1,19 @@
---
export interface Props {
class?: string;
}
const { class: className = "", ...rest } = Astro.props;
const classes = ["card-body", className].filter(Boolean).join(" ");
---
<div class={classes} {...rest}>
<slot />
</div>
<style>
.card-body {
padding: 0;
}
</style>

View File

@ -0,0 +1,22 @@
---
export interface Props {
class?: string;
}
const { class: className = "", ...rest } = Astro.props;
const classes = ["card-footer", className].filter(Boolean).join(" ");
---
<div class={classes} {...rest}>
<slot />
</div>
<style>
.card-footer {
padding: var(--spacing-4) var(--spacing-6) var(--spacing-6);
border-top: 1px solid var(--border-primary);
margin: var(--spacing-4) calc(var(--spacing-6) * -1)
calc(var(--spacing-6) * -1);
}
</style>

View File

@ -0,0 +1,22 @@
---
export interface Props {
class?: string;
}
const { class: className = "", ...rest } = Astro.props;
const classes = ["card-header", className].filter(Boolean).join(" ");
---
<div class={classes} {...rest}>
<slot />
</div>
<style>
.card-header {
padding: var(--spacing-6) var(--spacing-6) var(--spacing-4);
border-bottom: 1px solid var(--border-primary);
margin: calc(var(--spacing-6) * -1) calc(var(--spacing-6) * -1)
var(--spacing-4);
}
</style>

View File

@ -0,0 +1,21 @@
---
export interface Props {
class?: string;
}
const { class: className = "", ...rest } = Astro.props;
const classes = ["card-subtitle", className].filter(Boolean).join(" ");
---
<p class={classes} {...rest}>
<slot />
</p>
<style>
.card-subtitle {
font-size: var(--font-size-sm);
color: var(--text-secondary);
margin: 0 0 var(--spacing-4);
}
</style>

View File

@ -0,0 +1,22 @@
---
export interface Props {
class?: string;
}
const { class: className = "", ...rest } = Astro.props;
const classes = ["card-title", className].filter(Boolean).join(" ");
---
<h3 class={classes} {...rest}>
<slot />
</h3>
<style>
.card-title {
font-size: var(--font-size-lg);
font-weight: var(--font-weight-semibold);
color: var(--text-primary);
margin: 0 0 var(--spacing-2);
}
</style>

View File

@ -0,0 +1,66 @@
---
export interface Props {
xs?: number | "auto";
sm?: number | "auto";
md?: number | "auto";
lg?: number | "auto";
xl?: number | "auto";
xxl?: number | "auto";
offset?: {
xs?: number;
sm?: number;
md?: number;
lg?: number;
xl?: number;
xxl?: number;
};
class?: string;
}
const {
xs,
sm,
md,
lg,
xl,
xxl,
offset,
class: className = "",
...rest
} = Astro.props;
const getColClass = (size: string, value: number | "auto" | undefined) => {
if (value === undefined) return "";
return value === "auto" ? `col-${size}-auto` : `col-${size}-${value}`;
};
const getOffsetClass = (size: string, value: number | undefined) => {
if (value === undefined) return "";
return `offset-${size}-${value}`;
};
const baseClasses = xs || sm || md || lg || xl || xxl ? "" : "col";
const colClasses = [
baseClasses,
xs ? getColClass("", xs) : "",
sm ? getColClass("sm", sm) : "",
md ? getColClass("md", md) : "",
lg ? getColClass("lg", lg) : "",
xl ? getColClass("xl", xl) : "",
xxl ? getColClass("xxl", xxl) : "",
offset?.xs ? getOffsetClass("", offset.xs) : "",
offset?.sm ? getOffsetClass("sm", offset.sm) : "",
offset?.md ? getOffsetClass("md", offset.md) : "",
offset?.lg ? getOffsetClass("lg", offset.lg) : "",
offset?.xl ? getOffsetClass("xl", offset.xl) : "",
offset?.xxl ? getOffsetClass("xxl", offset.xxl) : "",
className,
]
.filter(Boolean)
.join(" ");
---
<div class={colClasses} {...rest}>
<slot />
</div>

View File

@ -0,0 +1,15 @@
---
export interface Props {
fluid?: boolean;
class?: string;
}
const { fluid = false, class: className = "", ...rest } = Astro.props;
const baseClasses = fluid ? "container-fluid" : "container";
const classes = [baseClasses, className].filter(Boolean).join(" ");
---
<div class={classes} {...rest}>
<slot />
</div>

View File

@ -0,0 +1,21 @@
---
export interface Props {
class?: string;
}
const { class: className = "", ...rest } = Astro.props;
const classes = ["form-error", className].filter(Boolean).join(" ");
---
<small class={classes} {...rest}>
<slot />
</small>
<style>
.form-error {
font-size: var(--font-size-xs);
color: var(--error);
margin-top: var(--spacing-1);
}
</style>

View File

@ -0,0 +1,19 @@
---
export interface Props {
class?: string;
}
const { class: className = "", ...rest } = Astro.props;
const classes = ["form-group", className].filter(Boolean).join(" ");
---
<div class={classes} {...rest}>
<slot />
</div>
<style>
.form-group {
margin-bottom: var(--spacing-4);
}
</style>

View File

@ -0,0 +1,24 @@
---
export interface Props {
for?: string;
class?: string;
}
const { for: htmlFor, class: className = "", ...rest } = Astro.props;
const classes = ["form-label", className].filter(Boolean).join(" ");
---
<label class={classes} for={htmlFor} {...rest}>
<slot />
</label>
<style>
.form-label {
display: block;
font-size: var(--font-size-sm);
font-weight: var(--font-weight-medium);
color: var(--text-primary);
margin-bottom: var(--spacing-2);
}
</style>

View File

@ -0,0 +1,21 @@
---
export interface Props {
class?: string;
}
const { class: className = "", ...rest } = Astro.props;
const classes = ["form-text", className].filter(Boolean).join(" ");
---
<small class={classes} {...rest}>
<slot />
</small>
<style>
.form-text {
font-size: var(--font-size-xs);
color: var(--text-muted);
margin-top: var(--spacing-1);
}
</style>

View File

@ -0,0 +1,55 @@
---
export interface Props {
type?: "text" | "email" | "password" | "number" | "tel" | "url" | "search";
size?: "sm" | "lg";
placeholder?: string;
value?: string;
name?: string;
id?: string;
disabled?: boolean;
required?: boolean;
class?: string;
}
const {
type = "text",
size,
placeholder,
value,
name,
id,
disabled = false,
required = false,
class: className = "",
...rest
} = Astro.props;
const baseClasses = "form-control-base input";
const sizeClasses = size ? `input-${size}` : "";
const classes = [baseClasses, sizeClasses, className].filter(Boolean).join(" ");
---
<input
type={type}
class={classes}
placeholder={placeholder}
value={value}
name={name}
id={id}
disabled={disabled}
required={required}
{...rest}
/>
<style>
.input-sm {
padding: var(--spacing-2) var(--spacing-3);
font-size: var(--font-size-sm);
}
.input-lg {
padding: var(--spacing-4) var(--spacing-5);
font-size: var(--font-size-lg);
}
</style>

View File

@ -0,0 +1,79 @@
---
export interface Props {
id: string;
class?: string;
}
const { id, class: className = "", ...rest } = Astro.props;
const classes = ["modal-backdrop", className].filter(Boolean).join(" ");
---
<div class={classes} id={id} style="display: none;" {...rest}>
<div class="modal">
<slot />
</div>
</div>
<script>
document.addEventListener("DOMContentLoaded", () => {
const modalTriggers = document.querySelectorAll("[data-modal-trigger]");
const modalCloses = document.querySelectorAll("[data-modal-close]");
modalTriggers.forEach((trigger) => {
trigger.addEventListener("click", (e) => {
e.preventDefault();
const modalId = trigger.getAttribute("data-modal-trigger");
const modal = document.getElementById(modalId || "");
if (modal) {
modal.style.display = "flex";
}
});
});
modalCloses.forEach((close) => {
close.addEventListener("click", (e) => {
e.preventDefault();
const modal = close.closest(".modal-backdrop");
if (modal) {
(modal as HTMLElement).style.display = "none";
}
});
});
document.addEventListener("click", (e) => {
if (
e.target &&
(e.target as HTMLElement).classList.contains("modal-backdrop")
) {
(e.target as HTMLElement).style.display = "none";
}
});
});
</script>
<style>
.modal-backdrop {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: var(--z-modal-backdrop);
display: flex;
align-items: center;
justify-content: center;
}
.modal {
background-color: var(--bg-card);
border: 1px solid var(--border-primary);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-xl);
max-width: 90vw;
max-height: 90vh;
overflow: auto;
z-index: var(--z-modal);
}
</style>

View File

@ -0,0 +1,19 @@
---
export interface Props {
class?: string;
}
const { class: className = "", ...rest } = Astro.props;
const classes = ["modal-body", className].filter(Boolean).join(" ");
---
<div class={classes} {...rest}>
<slot />
</div>
<style>
.modal-body {
padding: var(--spacing-6);
}
</style>

View File

@ -0,0 +1,23 @@
---
export interface Props {
class?: string;
}
const { class: className = "", ...rest } = Astro.props;
const classes = ["modal-footer", className].filter(Boolean).join(" ");
---
<div class={classes} {...rest}>
<slot />
</div>
<style>
.modal-footer {
padding: var(--spacing-6);
border-top: 1px solid var(--border-primary);
display: flex;
gap: var(--spacing-3);
justify-content: flex-end;
}
</style>

View File

@ -0,0 +1,28 @@
---
import Button from "./Button.astro";
export interface Props {
class?: string;
}
const { class: className = "", ...rest } = Astro.props;
const classes = ["modal-header", className].filter(Boolean).join(" ");
---
<div class={classes} {...rest}>
<slot />
<Button variant="ghost" icon data-modal-close>
<span>&times;</span>
</Button>
</div>
<style>
.modal-header {
padding: var(--spacing-6);
border-bottom: 1px solid var(--border-primary);
display: flex;
align-items: center;
justify-content: space-between;
}
</style>

View File

@ -0,0 +1,22 @@
---
export interface Props {
class?: string;
}
const { class: className = "", ...rest } = Astro.props;
const classes = ["modal-title", className].filter(Boolean).join(" ");
---
<h2 class={classes} {...rest}>
<slot />
</h2>
<style>
.modal-title {
font-size: var(--font-size-lg);
font-weight: var(--font-weight-semibold);
color: var(--text-primary);
margin: 0;
}
</style>

View File

@ -0,0 +1,115 @@
---
import Card from "./Card.astro";
export interface Props {
title: string;
poster: string;
year?: number;
genre?: string;
rating?: number;
href?: string;
class?: string;
}
const {
title,
poster,
year,
genre,
rating,
href,
class: className = "",
...rest
} = Astro.props;
const classes = ["movie-card", className].filter(Boolean).join(" ");
const Tag = href ? "a" : "div";
---
<Tag class={classes} href={href} {...rest}>
<Card class="movie-card-inner">
<img src={poster} alt={title} class="movie-poster" loading="lazy" />
<div class="movie-info">
<h3 class="movie-title">{title}</h3>
{
(year || genre) && (
<div class="movie-meta">
{year && <span>{year}</span>}
{year && genre && <span> • </span>}
{genre && <span>{genre}</span>}
</div>
)
}
{
rating && (
<div class="movie-rating">
<span>⭐</span>
<span>{rating}/10</span>
</div>
)
}
</div>
</Card>
</Tag>
<style>
.movie-card {
display: block;
text-decoration: none;
color: inherit;
height: 100%;
}
.movie-card:hover {
text-decoration: none;
color: inherit;
}
.movie-card:hover :global(.movie-card-inner) {
transform: translateY(-4px);
box-shadow: var(--shadow-lg);
}
.movie-card-inner {
overflow: hidden;
height: 100%;
}
.movie-poster {
width: 100%;
aspect-ratio: 2/3;
object-fit: cover;
display: block;
border: none;
}
.movie-info {
padding: var(--spacing-4);
}
.movie-title {
font-size: var(--font-size-base);
font-weight: var(--font-weight-semibold);
color: var(--text-primary);
margin: 0 0 var(--spacing-2);
line-height: var(--line-height-tight);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.movie-meta {
font-size: var(--font-size-sm);
color: var(--text-secondary);
margin-bottom: var(--spacing-2);
}
.movie-rating {
display: inline-flex;
align-items: center;
gap: var(--spacing-1);
font-size: var(--font-size-sm);
color: var(--primary-400);
font-weight: var(--font-weight-medium);
}
</style>

View File

@ -0,0 +1,22 @@
---
export interface Props {
href: string;
active?: boolean;
class?: string;
}
const { href, active = false, class: className = "", ...rest } = Astro.props;
const baseClasses = "nav-link";
const activeClasses = active ? "active" : "";
const classes = [baseClasses, activeClasses, className]
.filter(Boolean)
.join(" ");
---
<li>
<a class={classes} href={href} {...rest}>
<slot />
</a>
</li>

View File

@ -0,0 +1,25 @@
---
export interface Props {
class?: string;
}
const { class: className = "", ...rest } = Astro.props;
const classes = ["navbar", className].filter(Boolean).join(" ");
---
<nav class={classes} {...rest}>
<div class="container">
<div class="row justify-content-between align-items-center">
<div class="col-auto">
<slot name="brand" />
</div>
<div class="col-auto">
<slot name="nav" />
</div>
<div class="col-auto">
<slot name="actions" />
</div>
</div>
</div>
</nav>

View File

@ -0,0 +1,14 @@
---
export interface Props {
href?: string;
class?: string;
}
const { href = "/", class: className = "", ...rest } = Astro.props;
const classes = ["navbar-brand", className].filter(Boolean).join(" ");
---
<a class={classes} href={href} {...rest}>
<slot />
</a>

View File

@ -0,0 +1,13 @@
---
export interface Props {
class?: string;
}
const { class: className = "", ...rest } = Astro.props;
const classes = ["navbar-nav", className].filter(Boolean).join(" ");
---
<ul class={classes} {...rest}>
<slot />
</ul>

View File

@ -0,0 +1,40 @@
---
export interface Props {
value: number;
max?: number;
class?: string;
}
const { value, max = 100, class: className = "", ...rest } = Astro.props;
const percentage = Math.min((value / max) * 100, 100);
const classes = ["progress", className].filter(Boolean).join(" ");
---
<div class={classes} {...rest}>
<div
class="progress-bar"
style={`width: ${percentage}%`}
role="progressbar"
aria-valuenow={value}
aria-valuemin="0"
aria-valuemax={max}
>
</div>
</div>
<style>
.progress {
width: 100%;
height: 8px;
background-color: var(--bg-tertiary);
border-radius: var(--radius-full);
overflow: hidden;
}
.progress-bar {
height: 100%;
background-color: var(--primary-600);
transition: width var(--transition-normal);
}
</style>

View File

@ -0,0 +1,14 @@
---
export interface Props {
class?: string;
style?: string;
}
const { class: className = "", style, ...rest } = Astro.props;
const classes = ["row", className].filter(Boolean).join(" ");
---
<div class={classes} style={style} {...rest}>
<slot />
</div>

View File

@ -0,0 +1,65 @@
---
import Input from "./Input.astro";
export interface Props {
placeholder?: string;
value?: string;
name?: string;
id?: string;
class?: string;
}
const {
placeholder = "Rechercher...",
value,
name = "search",
id = "search",
class: className = "",
...rest
} = Astro.props;
const classes = ["search-bar", className].filter(Boolean).join(" ");
---
<div class={classes} {...rest}>
<Input
type="search"
class="search-input"
placeholder={placeholder}
value={value}
name={name}
id={id}
/>
<svg
class="search-icon"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle cx="11" cy="11" r="8"></circle>
<path d="M21 21l-4.35-4.35"></path>
</svg>
</div>
<style>
.search-bar {
position: relative;
max-width: 400px;
}
.search-bar :global(.search-input) {
padding-left: var(--spacing-10);
}
.search-icon {
position: absolute;
left: var(--spacing-3);
top: 50%;
transform: translateY(-50%);
color: var(--text-muted);
pointer-events: none;
}
</style>

View File

@ -0,0 +1,51 @@
---
export interface Props {
name?: string;
id?: string;
disabled?: boolean;
required?: boolean;
class?: string;
}
const {
name,
id,
disabled = false,
required = false,
class: className = "",
...rest
} = Astro.props;
const classes = ["select", className].filter(Boolean).join(" ");
---
<select
class={classes}
name={name}
id={id}
disabled={disabled}
required={required}
{...rest}
>
<slot />
</select>
<style>
.select {
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);
}
.select:focus {
outline: none;
border-color: var(--primary-600);
box-shadow: 0 0 0 3px rgba(132, 61, 255, 0.1);
}
</style>

View File

@ -0,0 +1,47 @@
---
export interface Props {
size?: "sm" | "lg";
class?: string;
}
const { size, class: className = "", ...rest } = Astro.props;
const baseClasses = "spinner";
const sizeClasses = size ? `spinner-${size}` : "";
const classes = [baseClasses, sizeClasses, className].filter(Boolean).join(" ");
---
<div class={classes} {...rest}></div>
<style>
.spinner {
width: 1rem;
height: 1rem;
border: 2px solid var(--border-primary);
border-top: 2px solid var(--primary-600);
border-radius: 50%;
animation: spin 1s linear infinite;
}
.spinner-sm {
width: 0.75rem;
height: 0.75rem;
border-width: 1.5px;
}
.spinner-lg {
width: 1.5rem;
height: 1.5rem;
border-width: 3px;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>

View File

@ -0,0 +1,59 @@
---
export interface Props {
placeholder?: string;
value?: string;
name?: string;
id?: string;
rows?: number;
disabled?: boolean;
required?: boolean;
class?: string;
}
const {
placeholder,
value,
name,
id,
rows = 4,
disabled = false,
required = false,
class: className = "",
...rest
} = Astro.props;
const classes = ["textarea", className].filter(Boolean).join(" ");
---
<textarea
class={classes}
placeholder={placeholder}
name={name}
id={id}
rows={rows}
disabled={disabled}
required={required}
{...rest}>{value}</textarea
>
<style>
.textarea {
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);
resize: vertical;
min-height: 100px;
}
.textarea:focus {
outline: none;
border-color: var(--primary-600);
box-shadow: 0 0 0 3px rgba(132, 61, 255, 0.1);
}
</style>

18
src/layouts/Layout.astro Normal file
View File

@ -0,0 +1,18 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="generator" content={Astro.generator} />
<title>Nixi - Streaming Films & Séries</title>
</head>
<body>
<slot />
</body>
</html>
<style is:global>
@import "../styles/variables.css";
@import "../styles/grid.css";
</style>

282
src/pages/index.astro Normal file
View File

@ -0,0 +1,282 @@
---
import Layout from "../layouts/Layout.astro";
import Button from "../components/ui/Button.astro";
import Card from "../components/ui/Card.astro";
import CardHeader from "../components/ui/CardHeader.astro";
import CardBody from "../components/ui/CardBody.astro";
import CardFooter from "../components/ui/CardFooter.astro";
import CardTitle from "../components/ui/CardTitle.astro";
import CardSubtitle from "../components/ui/CardSubtitle.astro";
import Badge from "../components/ui/Badge.astro";
import Alert from "../components/ui/Alert.astro";
import Container from "../components/ui/Container.astro";
import Row from "../components/ui/Row.astro";
import Col from "../components/ui/Col.astro";
import Input from "../components/ui/Input.astro";
import Textarea from "../components/ui/Textarea.astro";
import Select from "../components/ui/Select.astro";
import FormGroup from "../components/ui/FormGroup.astro";
import FormLabel from "../components/ui/FormLabel.astro";
import FormText from "../components/ui/FormText.astro";
import FormError from "../components/ui/FormError.astro";
import Modal from "../components/ui/Modal.astro";
import ModalHeader from "../components/ui/ModalHeader.astro";
import ModalBody from "../components/ui/ModalBody.astro";
import ModalFooter from "../components/ui/ModalFooter.astro";
import ModalTitle from "../components/ui/ModalTitle.astro";
import Progress from "../components/ui/Progress.astro";
import Spinner from "../components/ui/Spinner.astro";
import MovieCard from "../components/ui/MovieCard.astro";
import SearchBar from "../components/ui/SearchBar.astro";
---
<Layout>
<Container>
<div style="padding: 2rem 0;">
<h1
style="color: var(--primary-400); margin-bottom: 3rem; text-align: center;"
>
🎬 Nixi - Design System Demo
</h1>
<!-- Alerts -->
<section style="margin-bottom: 3rem;">
<h2 style="color: var(--text-primary); margin-bottom: 1rem;">
📢 Alertes
</h2>
<Alert variant="success"
>Succès ! Votre compte a été créé avec succès.</Alert
>
<Alert variant="warning"
>Attention ! Votre abonnement expire bientôt.</Alert
>
<Alert variant="error">Erreur ! Impossible de charger le contenu.</Alert
>
<Alert variant="info"
>Info : Nouveaux films ajoutés cette semaine.</Alert
>
</section>
<!-- Buttons -->
<section style="margin-bottom: 3rem;">
<h2 style="color: var(--text-primary); margin-bottom: 1rem;">
🔘 Boutons
</h2>
<div
style="display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: 1rem;"
>
<Button variant="primary">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="primary" disabled>Disabled</Button>
</div>
<div
style="display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: 1rem;"
>
<Button variant="primary" size="sm">Small</Button>
<Button variant="primary">Normal</Button>
<Button variant="primary" size="lg">Large</Button>
<Button variant="primary" icon>⭐</Button>
</div>
<Button variant="primary" data-modal-trigger="demo-modal"
>Ouvrir Modal</Button
>
</section>
<!-- Badges -->
<section style="margin-bottom: 3rem;">
<h2 style="color: var(--text-primary); margin-bottom: 1rem;">
🏷️ Badges
</h2>
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
<Badge variant="primary">Action</Badge>
<Badge variant="secondary">Comédie</Badge>
<Badge variant="success">HD</Badge>
<Badge variant="warning">VF</Badge>
<Badge variant="error">+18</Badge>
</div>
</section>
<!-- Progress & Spinner -->
<Row style="margin-bottom: 3rem;">
<Col md={6}>
<h2 style="color: var(--text-primary); margin-bottom: 1rem;">
📊 Progress
</h2>
<div style="margin-bottom: 1rem;">
<Progress value={25} />
</div>
<div style="margin-bottom: 1rem;">
<Progress value={60} />
</div>
<div style="margin-bottom: 1rem;">
<Progress value={90} />
</div>
</Col>
<Col md={6}>
<h2 style="color: var(--text-primary); margin-bottom: 1rem;">
⏳ Spinners
</h2>
<div style="display: flex; gap: 1rem; align-items: center;">
<Spinner size="sm" />
<Spinner />
<Spinner size="lg" />
</div>
</Col>
</Row>
<!-- Search Bar -->
<section style="margin-bottom: 3rem;">
<h2 style="color: var(--text-primary); margin-bottom: 1rem;">
🔍 Barre de recherche
</h2>
<SearchBar placeholder="Rechercher un film ou une série..." />
</section>
<!-- Forms -->
<Row style="margin-bottom: 3rem;">
<Col md={6}>
<h2 style="color: var(--text-primary); margin-bottom: 1rem;">
📝 Formulaires
</h2>
<FormGroup>
<FormLabel for="email">Email</FormLabel>
<Input type="email" id="email" placeholder="votre@email.com" />
<FormText>Nous ne partagerons jamais votre email.</FormText>
</FormGroup>
<FormGroup>
<FormLabel for="password">Mot de passe</FormLabel>
<Input type="password" id="password" placeholder="••••••••" />
<FormError
>Le mot de passe doit contenir au moins 8 caractères.</FormError
>
</FormGroup>
<FormGroup>
<FormLabel for="genre">Genre préféré</FormLabel>
<Select id="genre">
<option>Action</option>
<option>Comédie</option>
<option>Drame</option>
<option>Sci-Fi</option>
</Select>
</FormGroup>
<FormGroup>
<FormLabel for="bio">Biographie</FormLabel>
<Textarea
id="bio"
placeholder="Parlez-nous de vos goûts cinématographiques..."
rows={3}
/>
</FormGroup>
</Col>
<Col md={6}>
<h2 style="color: var(--text-primary); margin-bottom: 1rem;">
🎬 MovieCards
</h2>
<Row>
<Col md={6}>
<MovieCard
title="Inception"
poster="https://via.placeholder.com/200x300/6b04fd/ffffff?text=Inception"
year={2010}
genre="Sci-Fi"
rating={8.8}
href="/film/inception"
/>
</Col>
<Col md={6}>
<MovieCard
title="The Matrix"
poster="https://via.placeholder.com/200x300/843dff/ffffff?text=Matrix"
year={1999}
genre="Action"
rating={8.7}
href="/film/matrix"
/>
</Col>
</Row>
</Col>
</Row>
<!-- Cards -->
<section style="margin-bottom: 3rem;">
<h2 style="color: var(--text-primary); margin-bottom: 1rem;">
🃏 Cartes
</h2>
<Row>
<Col md={4}>
<Card>
<CardHeader>
<CardTitle>Carte Standard</CardTitle>
<CardSubtitle>Avec header et footer</CardSubtitle>
</CardHeader>
<CardBody>
<p>Contenu de la carte avec du texte descriptif.</p>
</CardBody>
<CardFooter>
<Row>
<Col md={8}>
<Button variant="primary" size="sm" class="w-100"
>Action</Button
>
</Col>
<Col md={4}>
<Button variant="ghost" size="sm" class="w-100"
>Annuler</Button
>
</Col>
</Row>
</CardFooter>
</Card>
</Col>
<Col md={4}>
<Card>
<CardBody>
<CardTitle>Carte Simple</CardTitle>
<p>
Une carte sans header ni footer, juste le contenu principal.
</p>
<Badge variant="success">Nouveau</Badge>
</CardBody>
</Card>
</Col>
<Col md={4}>
<Card>
<CardHeader>
<CardTitle>Streaming Stats</CardTitle>
</CardHeader>
<CardBody>
<p><strong>Films vus :</strong> 127</p>
<p><strong>Séries terminées :</strong> 23</p>
<p><strong>Temps total :</strong> 240h</p>
<Progress value={75} />
</CardBody>
</Card>
</Col>
</Row>
</section>
</div>
</Container>
<!-- Modal Demo -->
<Modal id="demo-modal">
<ModalHeader>
<ModalTitle>Exemple de Modal</ModalTitle>
</ModalHeader>
<ModalBody>
<p>
Ceci est un exemple de modal avec du contenu. Vous pouvez y mettre des
formulaires, des informations, ou tout autre contenu.
</p>
<FormGroup>
<FormLabel for="modal-input">Votre nom</FormLabel>
<Input type="text" id="modal-input" placeholder="Entrez votre nom" />
</FormGroup>
</ModalBody>
<ModalFooter>
<Button variant="primary">Confirmer</Button>
<Button variant="secondary" data-modal-close>Annuler</Button>
</ModalFooter>
</Modal>
</Layout>

934
src/styles/grid.css Normal file
View File

@ -0,0 +1,934 @@
.container {
width: 100%;
padding-right: var(--spacing-4);
padding-left: var(--spacing-4);
margin-right: auto;
margin-left: auto;
}
@media (min-width: 576px) {
.container {
max-width: 540px;
}
}
@media (min-width: 768px) {
.container {
max-width: 720px;
}
}
@media (min-width: 992px) {
.container {
max-width: 960px;
}
}
@media (min-width: 1200px) {
.container {
max-width: 1140px;
}
}
@media (min-width: 1400px) {
.container {
max-width: 1320px;
}
}
.container-fluid {
width: 100%;
padding-right: var(--spacing-4);
padding-left: var(--spacing-4);
margin-right: auto;
margin-left: auto;
}
.row {
display: flex;
flex-wrap: wrap;
margin-right: calc(var(--spacing-3) * -1);
margin-left: calc(var(--spacing-3) * -1);
}
.row > * {
padding-right: var(--spacing-3);
padding-left: var(--spacing-3);
}
.col {
flex: 1 0 0%;
}
.col-auto {
flex: 0 0 auto;
width: auto;
}
.col-1 {
flex: 0 0 auto;
width: 8.33333333%;
}
.col-2 {
flex: 0 0 auto;
width: 16.66666667%;
}
.col-3 {
flex: 0 0 auto;
width: 25%;
}
.col-4 {
flex: 0 0 auto;
width: 33.33333333%;
}
.col-5 {
flex: 0 0 auto;
width: 41.66666667%;
}
.col-6 {
flex: 0 0 auto;
width: 50%;
}
.col-7 {
flex: 0 0 auto;
width: 58.33333333%;
}
.col-8 {
flex: 0 0 auto;
width: 66.66666667%;
}
.col-9 {
flex: 0 0 auto;
width: 75%;
}
.col-10 {
flex: 0 0 auto;
width: 83.33333333%;
}
.col-11 {
flex: 0 0 auto;
width: 91.66666667%;
}
.col-12 {
flex: 0 0 auto;
width: 100%;
}
@media (min-width: 576px) {
.col-sm {
flex: 1 0 0%;
}
.col-sm-auto {
flex: 0 0 auto;
width: auto;
}
.col-sm-1 {
flex: 0 0 auto;
width: 8.33333333%;
}
.col-sm-2 {
flex: 0 0 auto;
width: 16.66666667%;
}
.col-sm-3 {
flex: 0 0 auto;
width: 25%;
}
.col-sm-4 {
flex: 0 0 auto;
width: 33.33333333%;
}
.col-sm-5 {
flex: 0 0 auto;
width: 41.66666667%;
}
.col-sm-6 {
flex: 0 0 auto;
width: 50%;
}
.col-sm-7 {
flex: 0 0 auto;
width: 58.33333333%;
}
.col-sm-8 {
flex: 0 0 auto;
width: 66.66666667%;
}
.col-sm-9 {
flex: 0 0 auto;
width: 75%;
}
.col-sm-10 {
flex: 0 0 auto;
width: 83.33333333%;
}
.col-sm-11 {
flex: 0 0 auto;
width: 91.66666667%;
}
.col-sm-12 {
flex: 0 0 auto;
width: 100%;
}
}
@media (min-width: 768px) {
.col-md {
flex: 1 0 0%;
}
.col-md-auto {
flex: 0 0 auto;
width: auto;
}
.col-md-1 {
flex: 0 0 auto;
width: 8.33333333%;
}
.col-md-2 {
flex: 0 0 auto;
width: 16.66666667%;
}
.col-md-3 {
flex: 0 0 auto;
width: 25%;
}
.col-md-4 {
flex: 0 0 auto;
width: 33.33333333%;
}
.col-md-5 {
flex: 0 0 auto;
width: 41.66666667%;
}
.col-md-6 {
flex: 0 0 auto;
width: 50%;
}
.col-md-7 {
flex: 0 0 auto;
width: 58.33333333%;
}
.col-md-8 {
flex: 0 0 auto;
width: 66.66666667%;
}
.col-md-9 {
flex: 0 0 auto;
width: 75%;
}
.col-md-10 {
flex: 0 0 auto;
width: 83.33333333%;
}
.col-md-11 {
flex: 0 0 auto;
width: 91.66666667%;
}
.col-md-12 {
flex: 0 0 auto;
width: 100%;
}
}
@media (min-width: 992px) {
.col-lg {
flex: 1 0 0%;
}
.col-lg-auto {
flex: 0 0 auto;
width: auto;
}
.col-lg-1 {
flex: 0 0 auto;
width: 8.33333333%;
}
.col-lg-2 {
flex: 0 0 auto;
width: 16.66666667%;
}
.col-lg-3 {
flex: 0 0 auto;
width: 25%;
}
.col-lg-4 {
flex: 0 0 auto;
width: 33.33333333%;
}
.col-lg-5 {
flex: 0 0 auto;
width: 41.66666667%;
}
.col-lg-6 {
flex: 0 0 auto;
width: 50%;
}
.col-lg-7 {
flex: 0 0 auto;
width: 58.33333333%;
}
.col-lg-8 {
flex: 0 0 auto;
width: 66.66666667%;
}
.col-lg-9 {
flex: 0 0 auto;
width: 75%;
}
.col-lg-10 {
flex: 0 0 auto;
width: 83.33333333%;
}
.col-lg-11 {
flex: 0 0 auto;
width: 91.66666667%;
}
.col-lg-12 {
flex: 0 0 auto;
width: 100%;
}
}
@media (min-width: 1200px) {
.col-xl {
flex: 1 0 0%;
}
.col-xl-auto {
flex: 0 0 auto;
width: auto;
}
.col-xl-1 {
flex: 0 0 auto;
width: 8.33333333%;
}
.col-xl-2 {
flex: 0 0 auto;
width: 16.66666667%;
}
.col-xl-3 {
flex: 0 0 auto;
width: 25%;
}
.col-xl-4 {
flex: 0 0 auto;
width: 33.33333333%;
}
.col-xl-5 {
flex: 0 0 auto;
width: 41.66666667%;
}
.col-xl-6 {
flex: 0 0 auto;
width: 50%;
}
.col-xl-7 {
flex: 0 0 auto;
width: 58.33333333%;
}
.col-xl-8 {
flex: 0 0 auto;
width: 66.66666667%;
}
.col-xl-9 {
flex: 0 0 auto;
width: 75%;
}
.col-xl-10 {
flex: 0 0 auto;
width: 83.33333333%;
}
.col-xl-11 {
flex: 0 0 auto;
width: 91.66666667%;
}
.col-xl-12 {
flex: 0 0 auto;
width: 100%;
}
}
@media (min-width: 1400px) {
.col-xxl {
flex: 1 0 0%;
}
.col-xxl-auto {
flex: 0 0 auto;
width: auto;
}
.col-xxl-1 {
flex: 0 0 auto;
width: 8.33333333%;
}
.col-xxl-2 {
flex: 0 0 auto;
width: 16.66666667%;
}
.col-xxl-3 {
flex: 0 0 auto;
width: 25%;
}
.col-xxl-4 {
flex: 0 0 auto;
width: 33.33333333%;
}
.col-xxl-5 {
flex: 0 0 auto;
width: 41.66666667%;
}
.col-xxl-6 {
flex: 0 0 auto;
width: 50%;
}
.col-xxl-7 {
flex: 0 0 auto;
width: 58.33333333%;
}
.col-xxl-8 {
flex: 0 0 auto;
width: 66.66666667%;
}
.col-xxl-9 {
flex: 0 0 auto;
width: 75%;
}
.col-xxl-10 {
flex: 0 0 auto;
width: 83.33333333%;
}
.col-xxl-11 {
flex: 0 0 auto;
width: 91.66666667%;
}
.col-xxl-12 {
flex: 0 0 auto;
width: 100%;
}
}
.offset-1 {
margin-left: 8.33333333%;
}
.offset-2 {
margin-left: 16.66666667%;
}
.offset-3 {
margin-left: 25%;
}
.offset-4 {
margin-left: 33.33333333%;
}
.offset-5 {
margin-left: 41.66666667%;
}
.offset-6 {
margin-left: 50%;
}
.offset-7 {
margin-left: 58.33333333%;
}
.offset-8 {
margin-left: 66.66666667%;
}
.offset-9 {
margin-left: 75%;
}
.offset-10 {
margin-left: 83.33333333%;
}
.offset-11 {
margin-left: 91.66666667%;
}
@media (min-width: 576px) {
.offset-sm-0 {
margin-left: 0;
}
.offset-sm-1 {
margin-left: 8.33333333%;
}
.offset-sm-2 {
margin-left: 16.66666667%;
}
.offset-sm-3 {
margin-left: 25%;
}
.offset-sm-4 {
margin-left: 33.33333333%;
}
.offset-sm-5 {
margin-left: 41.66666667%;
}
.offset-sm-6 {
margin-left: 50%;
}
.offset-sm-7 {
margin-left: 58.33333333%;
}
.offset-sm-8 {
margin-left: 66.66666667%;
}
.offset-sm-9 {
margin-left: 75%;
}
.offset-sm-10 {
margin-left: 83.33333333%;
}
.offset-sm-11 {
margin-left: 91.66666667%;
}
}
@media (min-width: 768px) {
.offset-md-0 {
margin-left: 0;
}
.offset-md-1 {
margin-left: 8.33333333%;
}
.offset-md-2 {
margin-left: 16.66666667%;
}
.offset-md-3 {
margin-left: 25%;
}
.offset-md-4 {
margin-left: 33.33333333%;
}
.offset-md-5 {
margin-left: 41.66666667%;
}
.offset-md-6 {
margin-left: 50%;
}
.offset-md-7 {
margin-left: 58.33333333%;
}
.offset-md-8 {
margin-left: 66.66666667%;
}
.offset-md-9 {
margin-left: 75%;
}
.offset-md-10 {
margin-left: 83.33333333%;
}
.offset-md-11 {
margin-left: 91.66666667%;
}
}
@media (min-width: 992px) {
.offset-lg-0 {
margin-left: 0;
}
.offset-lg-1 {
margin-left: 8.33333333%;
}
.offset-lg-2 {
margin-left: 16.66666667%;
}
.offset-lg-3 {
margin-left: 25%;
}
.offset-lg-4 {
margin-left: 33.33333333%;
}
.offset-lg-5 {
margin-left: 41.66666667%;
}
.offset-lg-6 {
margin-left: 50%;
}
.offset-lg-7 {
margin-left: 58.33333333%;
}
.offset-lg-8 {
margin-left: 66.66666667%;
}
.offset-lg-9 {
margin-left: 75%;
}
.offset-lg-10 {
margin-left: 83.33333333%;
}
.offset-lg-11 {
margin-left: 91.66666667%;
}
}
@media (min-width: 1200px) {
.offset-xl-0 {
margin-left: 0;
}
.offset-xl-1 {
margin-left: 8.33333333%;
}
.offset-xl-2 {
margin-left: 16.66666667%;
}
.offset-xl-3 {
margin-left: 25%;
}
.offset-xl-4 {
margin-left: 33.33333333%;
}
.offset-xl-5 {
margin-left: 41.66666667%;
}
.offset-xl-6 {
margin-left: 50%;
}
.offset-xl-7 {
margin-left: 58.33333333%;
}
.offset-xl-8 {
margin-left: 66.66666667%;
}
.offset-xl-9 {
margin-left: 75%;
}
.offset-xl-10 {
margin-left: 83.33333333%;
}
.offset-xl-11 {
margin-left: 91.66666667%;
}
}
@media (min-width: 1400px) {
.offset-xxl-0 {
margin-left: 0;
}
.offset-xxl-1 {
margin-left: 8.33333333%;
}
.offset-xxl-2 {
margin-left: 16.66666667%;
}
.offset-xxl-3 {
margin-left: 25%;
}
.offset-xxl-4 {
margin-left: 33.33333333%;
}
.offset-xxl-5 {
margin-left: 41.66666667%;
}
.offset-xxl-6 {
margin-left: 50%;
}
.offset-xxl-7 {
margin-left: 58.33333333%;
}
.offset-xxl-8 {
margin-left: 66.66666667%;
}
.offset-xxl-9 {
margin-left: 75%;
}
.offset-xxl-10 {
margin-left: 83.33333333%;
}
.offset-xxl-11 {
margin-left: 91.66666667%;
}
}
.justify-content-start {
justify-content: flex-start;
}
.justify-content-end {
justify-content: flex-end;
}
.justify-content-center {
justify-content: center;
}
.justify-content-between {
justify-content: space-between;
}
.justify-content-around {
justify-content: space-around;
}
.justify-content-evenly {
justify-content: space-evenly;
}
.align-items-start {
align-items: flex-start;
}
.align-items-end {
align-items: flex-end;
}
.align-items-center {
align-items: center;
}
.align-items-baseline {
align-items: baseline;
}
.align-items-stretch {
align-items: stretch;
}
.flex-row {
flex-direction: row;
}
.flex-column {
flex-direction: column;
}
.flex-row-reverse {
flex-direction: row-reverse;
}
.flex-column-reverse {
flex-direction: column-reverse;
}
.flex-wrap {
flex-wrap: wrap;
}
.flex-nowrap {
flex-wrap: nowrap;
}
.flex-wrap-reverse {
flex-wrap: wrap-reverse;
}
.w-100 {
width: 100% !important;
}
.w-auto {
width: auto !important;
}
.w-75 {
width: 75% !important;
}
.w-50 {
width: 50% !important;
}
.w-25 {
width: 25% !important;
}
.h-100 {
height: 100% !important;
}
.h-auto {
height: auto !important;
}
.d-block {
display: block !important;
}
.d-inline {
display: inline !important;
}
.d-inline-block {
display: inline-block !important;
}
.d-flex {
display: flex !important;
}
.d-inline-flex {
display: inline-flex !important;
}
.d-none {
display: none !important;
}
.text-left {
text-align: left !important;
}
.text-center {
text-align: center !important;
}
.text-right {
text-align: right !important;
}
.m-0 {
margin: 0 !important;
}
.m-1 {
margin: var(--spacing-1) !important;
}
.m-2 {
margin: var(--spacing-2) !important;
}
.m-3 {
margin: var(--spacing-3) !important;
}
.m-4 {
margin: var(--spacing-4) !important;
}
.m-5 {
margin: var(--spacing-5) !important;
}
.mt-0 {
margin-top: 0 !important;
}
.mt-1 {
margin-top: var(--spacing-1) !important;
}
.mt-2 {
margin-top: var(--spacing-2) !important;
}
.mt-3 {
margin-top: var(--spacing-3) !important;
}
.mt-4 {
margin-top: var(--spacing-4) !important;
}
.mt-5 {
margin-top: var(--spacing-5) !important;
}
.mb-0 {
margin-bottom: 0 !important;
}
.mb-1 {
margin-bottom: var(--spacing-1) !important;
}
.mb-2 {
margin-bottom: var(--spacing-2) !important;
}
.mb-3 {
margin-bottom: var(--spacing-3) !important;
}
.mb-4 {
margin-bottom: var(--spacing-4) !important;
}
.mb-5 {
margin-bottom: var(--spacing-5) !important;
}
.ms-0 {
margin-left: 0 !important;
}
.ms-1 {
margin-left: var(--spacing-1) !important;
}
.ms-2 {
margin-left: var(--spacing-2) !important;
}
.ms-3 {
margin-left: var(--spacing-3) !important;
}
.ms-4 {
margin-left: var(--spacing-4) !important;
}
.ms-5 {
margin-left: var(--spacing-5) !important;
}
.me-0 {
margin-right: 0 !important;
}
.me-1 {
margin-right: var(--spacing-1) !important;
}
.me-2 {
margin-right: var(--spacing-2) !important;
}
.me-3 {
margin-right: var(--spacing-3) !important;
}
.me-4 {
margin-right: var(--spacing-4) !important;
}
.me-5 {
margin-right: var(--spacing-5) !important;
}
.p-0 {
padding: 0 !important;
}
.p-1 {
padding: var(--spacing-1) !important;
}
.p-2 {
padding: var(--spacing-2) !important;
}
.p-3 {
padding: var(--spacing-3) !important;
}
.p-4 {
padding: var(--spacing-4) !important;
}
.p-5 {
padding: var(--spacing-5) !important;
}
.pt-0 {
padding-top: 0 !important;
}
.pt-1 {
padding-top: var(--spacing-1) !important;
}
.pt-2 {
padding-top: var(--spacing-2) !important;
}
.pt-3 {
padding-top: var(--spacing-3) !important;
}
.pt-4 {
padding-top: var(--spacing-4) !important;
}
.pt-5 {
padding-top: var(--spacing-5) !important;
}
.pb-0 {
padding-bottom: 0 !important;
}
.pb-1 {
padding-bottom: var(--spacing-1) !important;
}
.pb-2 {
padding-bottom: var(--spacing-2) !important;
}
.pb-3 {
padding-bottom: var(--spacing-3) !important;
}
.pb-4 {
padding-bottom: var(--spacing-4) !important;
}
.pb-5 {
padding-bottom: var(--spacing-5) !important;
}
.ps-0 {
padding-left: 0 !important;
}
.ps-1 {
padding-left: var(--spacing-1) !important;
}
.ps-2 {
padding-left: var(--spacing-2) !important;
}
.ps-3 {
padding-left: var(--spacing-3) !important;
}
.ps-4 {
padding-left: var(--spacing-4) !important;
}
.ps-5 {
padding-left: var(--spacing-5) !important;
}
.pe-0 {
padding-right: 0 !important;
}
.pe-1 {
padding-right: var(--spacing-1) !important;
}
.pe-2 {
padding-right: var(--spacing-2) !important;
}
.pe-3 {
padding-right: var(--spacing-3) !important;
}
.pe-4 {
padding-right: var(--spacing-4) !important;
}
.pe-5 {
padding-right: var(--spacing-5) !important;
}

163
src/styles/variables.css Normal file
View File

@ -0,0 +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;
--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;
--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);
--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);
--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;
--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-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;
--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;
}
* {
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);
}
html {
height: 100%;
}
body {
min-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);
}
.form-control-base:focus {
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;
}
.form-control-base::placeholder {
color: var(--text-muted);
}