Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e62cfc815 | ||
|
|
1501e4bf62 | ||
|
|
32122c5112 | ||
|
|
78b2afb747 | ||
|
|
9ec5e852c6 | ||
|
|
2048c17eee | ||
|
|
b98b531162 | ||
|
|
6770eba5b6 |
No files matched your search
Binary file not shown.
|
After Width: | Height: | Size: 193 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 450 KiB |
@@ -0,0 +1,100 @@
|
||||
.card {
|
||||
position: relative;
|
||||
display: grid;
|
||||
min-height: 220px;
|
||||
gap: var(--space-4);
|
||||
align-content: start;
|
||||
padding: var(--space-8);
|
||||
overflow: hidden;
|
||||
text-align: left;
|
||||
color: var(--color-text);
|
||||
cursor: pointer;
|
||||
background:
|
||||
linear-gradient(135deg, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0)),
|
||||
var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-sm);
|
||||
transition:
|
||||
border-color var(--duration-fast) var(--ease-standard),
|
||||
box-shadow var(--duration-fast) var(--ease-standard),
|
||||
transform var(--duration-fast) var(--ease-standard);
|
||||
}
|
||||
|
||||
.card::before {
|
||||
position: absolute;
|
||||
inset: auto -28px -58px auto;
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
content: "";
|
||||
background: radial-gradient(circle, var(--card-accent), transparent 66%);
|
||||
opacity: 0.24;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
border-color: var(--card-accent);
|
||||
box-shadow: var(--shadow-lg);
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
.card:focus-visible {
|
||||
outline: 3px solid var(--card-focus);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
.cyan {
|
||||
--card-accent: var(--color-primary);
|
||||
--card-focus: var(--color-primary-soft);
|
||||
}
|
||||
|
||||
.violet {
|
||||
--card-accent: var(--color-accent);
|
||||
--card-focus: rgba(143, 84, 255, 0.24);
|
||||
}
|
||||
|
||||
.kicker {
|
||||
width: fit-content;
|
||||
padding: var(--space-2) var(--space-3);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
color: var(--color-black);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0;
|
||||
background: var(--card-accent);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.title {
|
||||
max-width: 12ch;
|
||||
font-size: clamp(28px, 5vw, 48px);
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.description {
|
||||
max-width: 34ch;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.action {
|
||||
display: inline-flex;
|
||||
width: fit-content;
|
||||
align-items: center;
|
||||
padding-block: var(--space-2);
|
||||
margin-top: auto;
|
||||
font-weight: 800;
|
||||
color: var(--card-accent);
|
||||
}
|
||||
|
||||
.action::after {
|
||||
margin-left: var(--space-2);
|
||||
content: "->";
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.card {
|
||||
min-height: 190px;
|
||||
padding: var(--space-6);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import styles from "./GeneratorCard.module.css";
|
||||
|
||||
interface GeneratorCardProps{
|
||||
title: string;
|
||||
description: string;
|
||||
label: string;
|
||||
accent: "cyan" | "violet";
|
||||
onClick: () => void;
|
||||
};
|
||||
|
||||
export default function GeneratorCard({
|
||||
title,
|
||||
description,
|
||||
label,
|
||||
accent,
|
||||
onClick,
|
||||
}: GeneratorCardProps) {
|
||||
return (
|
||||
<button
|
||||
className={`${styles.card} ${styles[accent]}`}
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
aria-label={label}
|
||||
>
|
||||
<span className={styles.kicker}>{label}</span>
|
||||
<span className={styles.title}>{title}</span>
|
||||
<span className={styles.description}>{description}</span>
|
||||
<span className={styles.action}>Открыть</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,7 @@ export interface Request{
|
||||
id: number;
|
||||
text: string;
|
||||
files: File[];
|
||||
ratio: string;
|
||||
}
|
||||
|
||||
export interface Response{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.layout {
|
||||
min-height: 100vh;
|
||||
padding-block: var(--space-8);
|
||||
/*padding-block: var(--space-8);*/
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Navigate, Outlet } from "react-router-dom";
|
||||
import { useAppSelector } from "../../store/hooks";
|
||||
import { Outlet } from "react-router-dom";
|
||||
import styles from "./Layout.module.css";
|
||||
|
||||
export default function Layout() {
|
||||
|
||||
@@ -1,15 +1,48 @@
|
||||
.page {
|
||||
display: grid;
|
||||
gap: var(--space-12);
|
||||
min-height: 100vh;
|
||||
padding: var(--space-10);
|
||||
align-content: center;
|
||||
background: var(--color-bg);
|
||||
}
|
||||
|
||||
.hero {
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
gap: var(--space-4);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: min(540px, 86vw);
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
filter: drop-shadow(0 24px 56px rgba(53, 216, 255, 0.2));
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: 32px;
|
||||
line-height: 1.15;
|
||||
font-size: clamp(40px, 8vw, 88px);
|
||||
font-weight: 900;
|
||||
line-height: 0.95;
|
||||
color: var(--color-text);
|
||||
text-shadow: 0 0 32px rgba(53, 216, 255, 0.28);
|
||||
}
|
||||
|
||||
.text {
|
||||
margin: 0;
|
||||
color: var(--color-text-muted);
|
||||
.cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: var(--space-6);
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.page {
|
||||
gap: var(--space-8);
|
||||
padding-block: var(--space-8);
|
||||
}
|
||||
|
||||
.cards {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
+32
-5
@@ -1,15 +1,42 @@
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import GeneratorCard from "../../components/GeneratorCard/GeneratorCard";
|
||||
import logo from "../../assets/common/HITGENERATOR-Logo.png";
|
||||
import styles from "./Main.module.css";
|
||||
import {useNavigate} from "react-router-dom";
|
||||
|
||||
export default function Main() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<main className={styles.page}>
|
||||
<h1 className={styles.title}>Главная</h1>
|
||||
<p className={styles.text}>Базовая страница приложения готова.</p>
|
||||
<button onClick={() => navigate('/picture')}>Go to Picture</button>
|
||||
<button onClick={() => navigate('/video')}>Go to Video</button>
|
||||
<section className={styles.hero} aria-labelledby="main-title">
|
||||
<img
|
||||
className={styles.logo}
|
||||
src={logo}
|
||||
alt="ShitGenerator"
|
||||
width="1080"
|
||||
height="608"
|
||||
/>
|
||||
<h1 className={styles.title} id="main-title">
|
||||
ShitGenerator
|
||||
</h1>
|
||||
</section>
|
||||
|
||||
<section className={styles.cards} aria-label="Выбор генератора">
|
||||
<GeneratorCard
|
||||
title="Генерация видео"
|
||||
description="Собирай видеоролики из идеи, промпта и нужного визуального направления."
|
||||
label="Видео"
|
||||
accent="cyan"
|
||||
onClick={() => navigate("/video")}
|
||||
/>
|
||||
<GeneratorCard
|
||||
title="Генерация картинок"
|
||||
description="Создавай изображения под задачу, стиль и настроение за пару кликов."
|
||||
label="Картинки"
|
||||
accent="violet"
|
||||
onClick={() => navigate("/picture")}
|
||||
/>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,540 @@
|
||||
.page {
|
||||
display: grid;
|
||||
min-height: 100vh;
|
||||
padding: var(--space-6) var(--space-10) var(--space-10);
|
||||
color: var(--color-text);
|
||||
background: var(--color-bg);
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-height: 52px;
|
||||
padding-bottom: var(--space-4);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.backButton,
|
||||
.removeFileButton {
|
||||
display: inline-grid;
|
||||
flex: 0 0 auto;
|
||||
place-items: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
padding: 0;
|
||||
color: var(--color-text);
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
transition:
|
||||
color var(--duration-fast) var(--ease-standard),
|
||||
border-color var(--duration-fast) var(--ease-standard),
|
||||
background-color var(--duration-fast) var(--ease-standard);
|
||||
}
|
||||
|
||||
.backButton {
|
||||
font-size: 22px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.backButton:hover,
|
||||
.removeFileButton:hover {
|
||||
color: var(--color-black);
|
||||
background: var(--color-primary);
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.backButton:focus-visible,
|
||||
.removeFileButton:focus-visible,
|
||||
.uploadButton:focus-within,
|
||||
.segmentedControl button:focus-visible,
|
||||
.styleChoices button:focus-visible,
|
||||
.generateButton:focus-visible {
|
||||
outline: 3px solid var(--color-primary-soft);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 150px;
|
||||
max-height: 38px;
|
||||
object-fit: contain;
|
||||
object-position: left center;
|
||||
}
|
||||
|
||||
.mode,
|
||||
.eyebrow,
|
||||
.labelRow,
|
||||
.previewHeader,
|
||||
.previewFooter {
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
line-height: 1.2;
|
||||
letter-spacing: 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.mode,
|
||||
.eyebrow {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.workspace {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(300px, 0.86fr) minmax(380px, 1.14fr);
|
||||
gap: var(--space-6);
|
||||
width: min(100%, 1120px);
|
||||
margin: auto;
|
||||
padding-block: var(--space-8);
|
||||
}
|
||||
|
||||
.controls,
|
||||
.previewPanel {
|
||||
min-width: 0;
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-6);
|
||||
padding: var(--space-8);
|
||||
}
|
||||
|
||||
.heading {
|
||||
display: grid;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.heading h1 {
|
||||
margin: 0;
|
||||
font-size: clamp(30px, 4vw, 44px);
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.field {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
gap: var(--space-3);
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.field legend {
|
||||
padding: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.labelRow {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-3);
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.labelRow span:first-child {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.field textarea {
|
||||
width: 100%;
|
||||
min-height: 132px;
|
||||
padding: var(--space-4);
|
||||
resize: vertical;
|
||||
color: var(--color-text);
|
||||
font: inherit;
|
||||
line-height: 1.45;
|
||||
background: rgba(3, 6, 16, 0.42);
|
||||
border: 1px solid rgba(216, 227, 251, 0.16);
|
||||
border-radius: var(--radius-sm);
|
||||
outline: 0;
|
||||
transition: border-color var(--duration-fast) var(--ease-standard);
|
||||
}
|
||||
|
||||
.field textarea::placeholder {
|
||||
color: var(--color-gray-500);
|
||||
}
|
||||
|
||||
.field textarea:focus {
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.uploadButton,
|
||||
.fileSelected {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 48px;
|
||||
padding: var(--space-2) var(--space-3);
|
||||
background: rgba(3, 6, 16, 0.42);
|
||||
border: 1px dashed rgba(216, 227, 251, 0.22);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.uploadButton {
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
color: var(--color-primary);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
color var(--duration-fast) var(--ease-standard),
|
||||
border-color var(--duration-fast) var(--ease-standard),
|
||||
background-color var(--duration-fast) var(--ease-standard);
|
||||
}
|
||||
|
||||
.uploadButton:hover {
|
||||
color: var(--color-black);
|
||||
background: var(--color-primary);
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.uploadButton input {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0 0 0 0);
|
||||
clip-path: inset(50%);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.fileSelected {
|
||||
justify-content: space-between;
|
||||
gap: var(--space-3);
|
||||
border-style: solid;
|
||||
border-color: rgba(143, 84, 255, 0.5);
|
||||
}
|
||||
|
||||
.fileName {
|
||||
overflow: hidden;
|
||||
font-size: 14px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.removeFileButton {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.segmentedControl,
|
||||
.styleChoices {
|
||||
display: grid;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.segmentedControl,
|
||||
.styleChoices {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.segmentedControl button,
|
||||
.styleChoices button {
|
||||
min-width: 0;
|
||||
padding: var(--space-3);
|
||||
font: inherit;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
color: var(--color-text-muted);
|
||||
cursor: pointer;
|
||||
background: rgba(3, 6, 16, 0.42);
|
||||
border: 1px solid rgba(216, 227, 251, 0.16);
|
||||
border-radius: var(--radius-sm);
|
||||
transition:
|
||||
color var(--duration-fast) var(--ease-standard),
|
||||
border-color var(--duration-fast) var(--ease-standard),
|
||||
background-color var(--duration-fast) var(--ease-standard);
|
||||
}
|
||||
|
||||
.segmentedControl button:hover,
|
||||
.styleChoices button:hover {
|
||||
color: var(--color-text);
|
||||
border-color: var(--color-accent);
|
||||
}
|
||||
|
||||
.segmentedControl .selected,
|
||||
.styleChoices .styleSelected {
|
||||
color: var(--color-text);
|
||||
background: var(--color-primary-soft);
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.styleChoices button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.swatch {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.synthesis {
|
||||
background: var(--color-primary);
|
||||
box-shadow: 10px 0 0 var(--color-accent);
|
||||
}
|
||||
|
||||
.neon {
|
||||
background: var(--color-accent);
|
||||
box-shadow: 10px 0 0 var(--color-accent-warm);
|
||||
}
|
||||
|
||||
.cinema {
|
||||
background: var(--color-accent-gold);
|
||||
box-shadow: 10px 0 0 var(--color-accent-warm);
|
||||
}
|
||||
|
||||
.error {
|
||||
padding: var(--space-3);
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: var(--color-rose-200);
|
||||
background: rgba(190, 18, 60, 0.16);
|
||||
border: 1px solid rgba(254, 205, 211, 0.36);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.generateButton {
|
||||
min-height: 52px;
|
||||
padding: var(--space-3) var(--space-5);
|
||||
margin-top: auto;
|
||||
font: inherit;
|
||||
font-weight: 900;
|
||||
color: var(--color-black);
|
||||
cursor: pointer;
|
||||
background: var(--color-primary);
|
||||
border: 1px solid var(--color-primary);
|
||||
border-radius: var(--radius-sm);
|
||||
transition:
|
||||
background-color var(--duration-fast) var(--ease-standard),
|
||||
border-color var(--duration-fast) var(--ease-standard),
|
||||
transform var(--duration-fast) var(--ease-standard);
|
||||
}
|
||||
|
||||
.generateButton:hover:not(:disabled) {
|
||||
background: var(--color-primary-hover);
|
||||
border-color: var(--color-primary-hover);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.generateButton:disabled {
|
||||
cursor: wait;
|
||||
opacity: 0.66;
|
||||
}
|
||||
|
||||
.previewPanel {
|
||||
display: grid;
|
||||
grid-template-rows: auto minmax(0, 1fr) auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.previewHeader,
|
||||
.previewFooter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-height: 56px;
|
||||
padding-inline: var(--space-6);
|
||||
}
|
||||
|
||||
.previewHeader {
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.previewFooter {
|
||||
color: var(--color-text-muted);
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.statusDot {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
background: var(--color-gray-500);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.statusDot[data-active="true"] {
|
||||
background: var(--color-primary);
|
||||
box-shadow: 0 0 0 5px var(--color-primary-soft);
|
||||
}
|
||||
|
||||
.previewArea {
|
||||
display: grid;
|
||||
min-height: 460px;
|
||||
padding: var(--space-6);
|
||||
overflow: auto;
|
||||
place-items: center;
|
||||
background:
|
||||
linear-gradient(rgba(53, 216, 255, 0.04) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(53, 216, 255, 0.04) 1px, transparent 1px),
|
||||
rgba(3, 6, 16, 0.3);
|
||||
background-size: 24px 24px;
|
||||
}
|
||||
|
||||
.previewCanvas {
|
||||
position: relative;
|
||||
display: grid;
|
||||
width: min(100%, 560px);
|
||||
overflow: hidden;
|
||||
place-items: center;
|
||||
background: #0a1430;
|
||||
border: 1px solid rgba(53, 216, 255, 0.34);
|
||||
border-radius: var(--radius-sm);
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.square {
|
||||
aspect-ratio: 1;
|
||||
}
|
||||
|
||||
.portrait {
|
||||
width: min(80%, 430px);
|
||||
aspect-ratio: 4 / 5;
|
||||
}
|
||||
|
||||
.wide {
|
||||
aspect-ratio: 16 / 9;
|
||||
}
|
||||
|
||||
.referenceImage {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.emptyState {
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
gap: var(--space-4);
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
color: var(--color-text-muted);
|
||||
letter-spacing: 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.emptyMark {
|
||||
position: relative;
|
||||
width: 74px;
|
||||
height: 74px;
|
||||
border: 2px solid var(--color-primary);
|
||||
border-radius: var(--radius-sm);
|
||||
transform: rotate(8deg);
|
||||
}
|
||||
|
||||
.emptyMark::before,
|
||||
.emptyMark::after {
|
||||
position: absolute;
|
||||
content: "";
|
||||
}
|
||||
|
||||
.emptyMark::before {
|
||||
inset: 12px;
|
||||
border: 2px solid var(--color-accent);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.emptyMark::after {
|
||||
right: -12px;
|
||||
bottom: -12px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: var(--color-accent-warm);
|
||||
border: 2px solid var(--color-bg);
|
||||
}
|
||||
|
||||
.scanLine {
|
||||
position: absolute;
|
||||
inset: 0 auto auto 0;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: var(--color-primary);
|
||||
box-shadow: 0 0 18px var(--color-primary);
|
||||
animation: scan 1.8s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes scan {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateY(300px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 820px) {
|
||||
.page {
|
||||
padding: var(--space-4) var(--space-6) var(--space-6);
|
||||
}
|
||||
|
||||
.workspace {
|
||||
grid-template-columns: 1fr;
|
||||
padding-block: var(--space-6);
|
||||
}
|
||||
|
||||
.controls {
|
||||
padding: var(--space-6);
|
||||
}
|
||||
|
||||
.previewArea {
|
||||
min-height: 360px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.page {
|
||||
padding-inline: var(--space-4);
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 118px;
|
||||
}
|
||||
|
||||
.mode {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
gap: var(--space-5);
|
||||
padding: var(--space-5);
|
||||
}
|
||||
|
||||
.styleChoices button {
|
||||
padding-inline: var(--space-2);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.previewHeader,
|
||||
.previewFooter {
|
||||
padding-inline: var(--space-4);
|
||||
}
|
||||
|
||||
.previewArea {
|
||||
min-height: 300px;
|
||||
padding: var(--space-4);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,212 @@
|
||||
import { useEffect, useRef, useState, type ChangeEvent, type FormEvent } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import logo from "../../assets/common/HITGENERATOR-Logo.png";
|
||||
import { useAppDispatch, useAppSelector } from "../../store/hooks";
|
||||
import { clearRequestError, request, resetState } from "../../store/slices/slice";
|
||||
import styles from "./Picture.module.css";
|
||||
|
||||
type AspectRatio = "1:1" | "4:5" | "16:9";
|
||||
|
||||
const aspectRatios: AspectRatio[] = ["1:1", "4:5", "16:9"];
|
||||
|
||||
export default function Picture() {
|
||||
return (
|
||||
<main>
|
||||
<h1>Picture</h1>
|
||||
</main>
|
||||
);
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useAppDispatch();
|
||||
const { loading, error: requestError } = useAppSelector((state) => state.request);
|
||||
|
||||
const [prompt, setPrompt] = useState("");
|
||||
const [reference, setReference] = useState<File[]>([]);
|
||||
const [aspectRatio, setAspectRatio] = useState<AspectRatio>("1:1");
|
||||
const [formError, setFormError] = useState<string | null>(null);
|
||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(clearRequestError());
|
||||
}, [dispatch]);
|
||||
|
||||
function handleReferenceChange(event: ChangeEvent<HTMLInputElement>) {
|
||||
const nextFile = event.currentTarget.files?.[0] ?? null;
|
||||
|
||||
if (nextFile && !nextFile.type.startsWith("image/")) {
|
||||
setFormError("Выберите файл изображения.");
|
||||
return;
|
||||
}
|
||||
|
||||
setFormError(null);
|
||||
//updateReference(nextFile);
|
||||
setIsSubmitted(false);
|
||||
setReference(nextFile ? [...reference, nextFile] : []);
|
||||
|
||||
}
|
||||
|
||||
function removeReference(index: number) {
|
||||
//updateReference(null);
|
||||
setIsSubmitted(false);
|
||||
setReference(reference.filter((_, i) => i !== index));
|
||||
|
||||
}
|
||||
|
||||
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
|
||||
const text = prompt.trim();
|
||||
|
||||
if (!text) {
|
||||
setFormError("Добавьте описание картинки.");
|
||||
return;
|
||||
}
|
||||
|
||||
setFormError(null);
|
||||
setIsSubmitted(false);
|
||||
|
||||
await dispatch(
|
||||
request({
|
||||
id: Date.now(),
|
||||
text,
|
||||
files: reference ? reference : [],
|
||||
ratio: aspectRatio,
|
||||
}),
|
||||
).unwrap();
|
||||
setIsSubmitted(true);
|
||||
|
||||
}
|
||||
|
||||
const previewClassName = [
|
||||
styles.previewCanvas,
|
||||
aspectRatio === "1:1"
|
||||
? styles.square
|
||||
: aspectRatio === "4:5"
|
||||
? styles.portrait
|
||||
: styles.wide,
|
||||
].join(" ");
|
||||
const status = loading ? "Генерируем" : isSubmitted ? "Запрос принят" : "Ожидание";
|
||||
|
||||
return (
|
||||
<div className={styles.page}>
|
||||
<header className={styles.toolbar}>
|
||||
<div className={styles.brand}>
|
||||
<button
|
||||
className={styles.backButton}
|
||||
type="button"
|
||||
aria-label="На главную"
|
||||
title="На главную"
|
||||
onClick={() => {
|
||||
navigate("/")
|
||||
resetState();
|
||||
}
|
||||
}
|
||||
>
|
||||
<span aria-hidden="true">←</span>
|
||||
</button>
|
||||
<img className={styles.logo} src={logo} alt="ShitGenerator" />
|
||||
</div>
|
||||
<span className={styles.mode}>PICTURE</span>
|
||||
</header>
|
||||
|
||||
<main className={styles.workspace}>
|
||||
<form className={styles.controls} onSubmit={handleSubmit}>
|
||||
<div className={styles.heading}>
|
||||
<p className={styles.eyebrow}>IMAGE LAB</p>
|
||||
<h1>Генерация картинки</h1>
|
||||
</div>
|
||||
|
||||
<label className={styles.field}>
|
||||
<span className={styles.labelRow}>
|
||||
<span>Описание</span>
|
||||
<span>{prompt.length}/800</span>
|
||||
</span>
|
||||
<textarea
|
||||
value={prompt}
|
||||
maxLength={800}
|
||||
placeholder="Опишите изображение"
|
||||
onChange={(event) => {
|
||||
setFormError(null);
|
||||
setPrompt(event.target.value);
|
||||
setIsSubmitted(false);
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<section className={styles.field} aria-labelledby="reference-label">
|
||||
<span className={styles.labelRow} id="reference-label">
|
||||
<span>Референс</span>
|
||||
<span>Необязательно</span>
|
||||
</span>
|
||||
{reference.length > 0 && (
|
||||
reference.map((file, index) => (
|
||||
<div className={styles.fileSelected} key={index}>
|
||||
<span className={styles.fileName}>{file.name}</span>
|
||||
<button
|
||||
className={styles.removeFileButton}
|
||||
type="button"
|
||||
aria-label="Удалить референс"
|
||||
title="Удалить референс"
|
||||
onClick={() => removeReference(index)}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
<label className={styles.uploadButton}>
|
||||
<input type="file" accept="image/*" onChange={handleReferenceChange} />
|
||||
<span>Выбрать файл</span>
|
||||
</label>
|
||||
|
||||
</section>
|
||||
|
||||
<fieldset className={styles.field}>
|
||||
<span className={styles.labelRow}>Формат</span>
|
||||
<div className={styles.segmentedControl}>
|
||||
{aspectRatios.map((ratio) => (
|
||||
<button
|
||||
className={ratio === aspectRatio ? styles.selected : undefined}
|
||||
type="button"
|
||||
key={ratio}
|
||||
aria-pressed={ratio === aspectRatio}
|
||||
onClick={() => {
|
||||
setAspectRatio(ratio);
|
||||
setIsSubmitted(false);
|
||||
}}
|
||||
>
|
||||
{ratio}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
{(formError || requestError) && (
|
||||
<p className={styles.error} role="alert">
|
||||
{formError || requestError}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<button className={styles.generateButton} type="submit" disabled={loading}>
|
||||
{loading ? "Генерируем..." : "Сгенерировать"}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<section className={styles.previewPanel} aria-label="Область результата">
|
||||
<div className={styles.previewHeader}>
|
||||
<span>{status}</span>
|
||||
<span className={styles.statusDot} data-active={loading || isSubmitted} aria-hidden="true" />
|
||||
</div>
|
||||
|
||||
<div className={styles.previewArea}>
|
||||
<div className={previewClassName}>
|
||||
<div className={styles.emptyState}>
|
||||
<span className={styles.emptyMark} aria-hidden="true" />
|
||||
<span>{loading ? "Генерируем" : isSubmitted ? "Запрос принят" : "Ожидаемое изображение"}</span>
|
||||
</div>
|
||||
{loading && <span className={styles.scanLine} aria-hidden="true" />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer className={styles.previewFooter}>
|
||||
<span>{aspectRatio}</span>
|
||||
</footer>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
.videoMark::before {
|
||||
inset: 18px 15px 18px 22px;
|
||||
background: var(--color-accent);
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
clip-path: polygon(0 0, 100% 50%, 0 100%);
|
||||
}
|
||||
@@ -1,6 +1,300 @@
|
||||
import { useEffect, useState, type ChangeEvent, type FormEvent } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import logo from "../../assets/common/HITGENERATOR-Logo.png";
|
||||
import { useAppDispatch, useAppSelector } from "../../store/hooks";
|
||||
import { clearRequestError, request, resetState } from "../../store/slices/slice";
|
||||
import pictureStyles from "../Picture/Picture.module.css";
|
||||
import styles from "./VideoPage.module.css";
|
||||
|
||||
type AspectRatio = "1:1" | "4:5" | "16:9";
|
||||
|
||||
const aspectRatios: AspectRatio[] = ["1:1", "4:5", "16:9"];
|
||||
|
||||
export default function Video() {
|
||||
return (
|
||||
<div>video</div>
|
||||
)
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useAppDispatch();
|
||||
const { loading, error: requestError } = useAppSelector((state) => state.request);
|
||||
|
||||
const [prompt, setPrompt] = useState("");
|
||||
const [characters, setCharacters] = useState<File[]>([]);
|
||||
const [scenes, setScenes] = useState<File[]>([]);
|
||||
const [screenplays, setScreenplays] = useState<File[]>([]);
|
||||
const [aspectRatio, setAspectRatio] = useState<AspectRatio>("16:9");
|
||||
const [formError, setFormError] = useState<string | null>(null);
|
||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(clearRequestError());
|
||||
}, [dispatch]);
|
||||
|
||||
function resetErrors() {
|
||||
setFormError(null);
|
||||
dispatch(clearRequestError());
|
||||
}
|
||||
|
||||
function addImageFiles(
|
||||
event: ChangeEvent<HTMLInputElement>,
|
||||
setFiles: React.Dispatch<React.SetStateAction<File[]>>,) {
|
||||
const nextFiles = event.currentTarget.files?.[0] ?? null;
|
||||
|
||||
if (nextFiles && !nextFiles.type.startsWith("image/")) {
|
||||
setFormError("Выберите файлы изображений.");
|
||||
return;
|
||||
}
|
||||
|
||||
resetErrors();
|
||||
setFiles((currentFiles) => nextFiles ? [...currentFiles, nextFiles] : currentFiles);
|
||||
setIsSubmitted(false);
|
||||
}
|
||||
|
||||
function addScreenplayFiles(event: ChangeEvent<HTMLInputElement>) {
|
||||
const nextFiles = Array.from(event.currentTarget.files ?? []);
|
||||
const isTextFile = (file: File) => {
|
||||
return (file.type.startsWith("text/") || /\.(txt|md)$/i.test(file.name)) || file.type.startsWith("image/");
|
||||
|
||||
};
|
||||
if (nextFiles.some((file) => !isTextFile(file))) {
|
||||
setFormError("Выберите текстовые файлы в формате TXT или MD.");
|
||||
return;
|
||||
}
|
||||
|
||||
resetErrors();
|
||||
setScreenplays((currentFiles) => [...currentFiles, ...nextFiles]);
|
||||
event.currentTarget.value = "";
|
||||
setIsSubmitted(false);
|
||||
}
|
||||
|
||||
function removeFile(
|
||||
index: number,
|
||||
setFiles: React.Dispatch<React.SetStateAction<File[]>>,
|
||||
) {
|
||||
setFiles((currentFiles) => currentFiles.filter((_, fileIndex) => fileIndex !== index));
|
||||
setIsSubmitted(false);
|
||||
}
|
||||
|
||||
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
|
||||
const text = prompt.trim();
|
||||
|
||||
if (!text) {
|
||||
setFormError("Добавьте описание видео.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
|
||||
resetErrors();
|
||||
setIsSubmitted(false);
|
||||
|
||||
await dispatch(
|
||||
request({
|
||||
id: Date.now(),
|
||||
text,
|
||||
files: [...characters, ...scenes, ...screenplays],
|
||||
ratio: aspectRatio,
|
||||
}),
|
||||
).unwrap();
|
||||
setIsSubmitted(true);
|
||||
}
|
||||
|
||||
const previewClassName = [
|
||||
pictureStyles.previewCanvas,
|
||||
aspectRatio === "1:1"
|
||||
? pictureStyles.square
|
||||
: aspectRatio === "4:5"
|
||||
? pictureStyles.portrait
|
||||
: pictureStyles.wide,
|
||||
].join(" ");
|
||||
const status = loading ? "Генерируем" : isSubmitted ? "Запрос принят" : "Ожидание";
|
||||
//const attachmentsCount = characters.length + scenes.length + screenplays.length;
|
||||
|
||||
return (
|
||||
<div className={pictureStyles.page}>
|
||||
<header className={pictureStyles.toolbar}>
|
||||
<div className={pictureStyles.brand}>
|
||||
<button
|
||||
className={pictureStyles.backButton}
|
||||
type="button"
|
||||
aria-label="На главную"
|
||||
title="На главную"
|
||||
onClick={() => {
|
||||
navigate("/");
|
||||
resetState();
|
||||
}}
|
||||
>
|
||||
<span aria-hidden="true">←</span>
|
||||
</button>
|
||||
<img className={pictureStyles.logo} src={logo} alt="ShitGenerator" />
|
||||
</div>
|
||||
<span className={pictureStyles.mode}>VIDEO</span>
|
||||
</header>
|
||||
|
||||
<main className={pictureStyles.workspace}>
|
||||
<form className={pictureStyles.controls} onSubmit={handleSubmit}>
|
||||
<div className={pictureStyles.heading}>
|
||||
<p className={pictureStyles.eyebrow}>VIDEO LAB</p>
|
||||
<h1>Генерация видео</h1>
|
||||
</div>
|
||||
|
||||
<label className={pictureStyles.field}>
|
||||
<span className={pictureStyles.labelRow}>
|
||||
<span>Описание</span>
|
||||
<span>{prompt.length}/800</span>
|
||||
</span>
|
||||
<textarea
|
||||
value={prompt}
|
||||
maxLength={800}
|
||||
placeholder="Опишите видео"
|
||||
onChange={(event) => {
|
||||
resetErrors();
|
||||
setPrompt(event.target.value);
|
||||
setIsSubmitted(false);
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<section className={pictureStyles.field} aria-labelledby="characters-label">
|
||||
<span className={pictureStyles.labelRow} id="characters-label">
|
||||
<span>Персонажи</span>
|
||||
<span>Изображения</span>
|
||||
</span>
|
||||
{characters.map((file, index) => (
|
||||
<div className={pictureStyles.fileSelected} key={`${file.name}-${index}`}>
|
||||
<span className={pictureStyles.fileName}>{file.name}</span>
|
||||
<button
|
||||
className={pictureStyles.removeFileButton}
|
||||
type="button"
|
||||
aria-label={`Удалить файл ${file.name}`}
|
||||
title="Удалить файл"
|
||||
onClick={() => removeFile(index, setCharacters)}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<label className={pictureStyles.uploadButton}>
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={(file) => addImageFiles(file, setCharacters)}
|
||||
/>
|
||||
<span>Добавить файлы</span>
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<section className={pictureStyles.field} aria-labelledby="scenes-label">
|
||||
<span className={pictureStyles.labelRow} id="scenes-label">
|
||||
<span>Сцены и локации</span>
|
||||
<span>Изображения</span>
|
||||
</span>
|
||||
{scenes.map((file, index) => (
|
||||
<div className={pictureStyles.fileSelected} key={`${file.name}-${index}`}>
|
||||
<span className={pictureStyles.fileName}>{file.name}</span>
|
||||
<button
|
||||
className={pictureStyles.removeFileButton}
|
||||
type="button"
|
||||
aria-label={`Удалить файл ${file.name}`}
|
||||
title="Удалить файл"
|
||||
onClick={() => removeFile(index, setScenes)}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<label className={pictureStyles.uploadButton}>
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
multiple
|
||||
onChange={(event) => addImageFiles(event, setScenes)}
|
||||
/>
|
||||
<span>Добавить файлы</span>
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<section className={pictureStyles.field} aria-labelledby="screenplays-label">
|
||||
<span className={pictureStyles.labelRow} id="screenplays-label">
|
||||
<span>Сценарий</span>
|
||||
<span>TXT, MD</span>
|
||||
</span>
|
||||
{screenplays.map((file, index) => (
|
||||
<div className={pictureStyles.fileSelected} key={`${file.name}-${index}`}>
|
||||
<span className={pictureStyles.fileName}>{file.name}</span>
|
||||
<button
|
||||
className={pictureStyles.removeFileButton}
|
||||
type="button"
|
||||
aria-label={`Удалить файл ${file.name}`}
|
||||
title="Удалить файл"
|
||||
onClick={() => removeFile(index, setScreenplays)}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<label className={pictureStyles.uploadButton}>
|
||||
<input
|
||||
type="file"
|
||||
accept=".txt,.md,text/plain,text/markdown,image/*"
|
||||
multiple
|
||||
onChange={addScreenplayFiles}
|
||||
/>
|
||||
<span>Добавить файлы</span>
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<fieldset className={pictureStyles.field}>
|
||||
<legend>Формат</legend>
|
||||
<div className={pictureStyles.segmentedControl}>
|
||||
{aspectRatios.map((ratio) => (
|
||||
<button
|
||||
className={ratio === aspectRatio ? pictureStyles.selected : undefined}
|
||||
type="button"
|
||||
key={ratio}
|
||||
aria-pressed={ratio === aspectRatio}
|
||||
onClick={() => {
|
||||
setAspectRatio(ratio);
|
||||
setIsSubmitted(false);
|
||||
}}
|
||||
>
|
||||
{ratio}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
{(formError || requestError) && (
|
||||
<p className={pictureStyles.error} role="alert">
|
||||
{formError || requestError}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<button className={pictureStyles.generateButton} type="submit" disabled={loading}>
|
||||
{loading ? "Генерируем..." : "Сгенерировать"}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<section className={pictureStyles.previewPanel} aria-label="Область результата">
|
||||
<div className={pictureStyles.previewHeader}>
|
||||
<span>{status}</span>
|
||||
<span className={pictureStyles.statusDot} data-active={loading || isSubmitted} aria-hidden="true" />
|
||||
</div>
|
||||
|
||||
<div className={pictureStyles.previewArea}>
|
||||
<div className={previewClassName}>
|
||||
<div className={pictureStyles.emptyState}>
|
||||
<span className={`${pictureStyles.emptyMark} ${styles.videoMark}`} aria-hidden="true" />
|
||||
<span>{loading ? "Генерируем" : isSubmitted ? "Запрос принят" : "Ожидаемый ролик"}</span>
|
||||
</div>
|
||||
{loading && <span className={pictureStyles.scanLine} aria-hidden="true" />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer className={pictureStyles.previewFooter}>
|
||||
<span>{aspectRatio}</span>
|
||||
</footer>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -38,6 +38,12 @@ export const requestSlice = createSlice({
|
||||
reducers: {
|
||||
clearRequestError(state){
|
||||
state.error = null;
|
||||
},
|
||||
resetState(state){
|
||||
state.files = [];
|
||||
state.text = '';
|
||||
state.loading = false;
|
||||
state.error = null;
|
||||
}
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
@@ -57,6 +63,6 @@ export const requestSlice = createSlice({
|
||||
}
|
||||
})
|
||||
|
||||
export const {clearRequestError} = requestSlice.actions;
|
||||
export const {clearRequestError, resetState} = requestSlice.actions;
|
||||
|
||||
export default requestSlice.reducer;
|
||||
@@ -1,5 +1,9 @@
|
||||
body {
|
||||
background: var(--color-bg);
|
||||
background-image: url('../assets/common/background.jpeg');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-attachment: fixed;
|
||||
color: var(--color-text);
|
||||
font-family: var(--font-family-base);
|
||||
line-height: 1.5;
|
||||
|
||||
+30
-24
@@ -1,23 +1,26 @@
|
||||
:root {
|
||||
color-scheme: light;
|
||||
color-scheme: dark;
|
||||
|
||||
--font-family-base:
|
||||
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||
sans-serif;
|
||||
|
||||
--color-white: #ffffff;
|
||||
--color-black: #0b1220;
|
||||
--color-gray-50: #f8fafc;
|
||||
--color-gray-100: #f1f5f9;
|
||||
--color-gray-200: #e2e8f0;
|
||||
--color-gray-300: #cbd5e1;
|
||||
--color-gray-500: #64748b;
|
||||
--color-gray-600: #475569;
|
||||
--color-gray-700: #334155;
|
||||
--color-gray-900: #0f172a;
|
||||
--color-teal-50: #ecfeff;
|
||||
--color-teal-600: #0d9488;
|
||||
--color-teal-700: #0f766e;
|
||||
--color-black: #030610;
|
||||
--color-gray-50: #f6f8ff;
|
||||
--color-gray-100: #ecf2ff;
|
||||
--color-gray-200: #d8e3fb;
|
||||
--color-gray-300: #b8c7e7;
|
||||
--color-gray-500: #7888aa;
|
||||
--color-gray-600: #52617f;
|
||||
--color-gray-700: #243452;
|
||||
--color-gray-900: #07142d;
|
||||
--color-cyan-50: #e9fbff;
|
||||
--color-cyan-400: #35d8ff;
|
||||
--color-cyan-600: #0899d8;
|
||||
--color-violet-500: #8f54ff;
|
||||
--color-coral-500: #ff6b79;
|
||||
--color-gold-400: #ffc96b;
|
||||
--color-rose-50: #fff1f2;
|
||||
--color-rose-200: #fecdd3;
|
||||
--color-rose-700: #be123c;
|
||||
@@ -25,15 +28,18 @@
|
||||
--color-green-200: #bbf7d0;
|
||||
--color-green-800: #166534;
|
||||
|
||||
--color-bg: var(--color-gray-50);
|
||||
--color-surface: var(--color-white);
|
||||
--color-surface-muted: var(--color-gray-100);
|
||||
--color-text: var(--color-gray-900);
|
||||
--color-text-muted: var(--color-gray-600);
|
||||
--color-border: var(--color-gray-200);
|
||||
--color-primary: var(--color-teal-700);
|
||||
--color-primary-hover: #115e59;
|
||||
--color-primary-soft: rgba(13, 148, 136, 0.12);
|
||||
--color-bg: #050914;
|
||||
--color-surface: rgba(9, 18, 42, 0.86);
|
||||
--color-surface-muted: rgba(17, 32, 67, 0.78);
|
||||
--color-text: #f7fbff;
|
||||
--color-text-muted: #aab8d8;
|
||||
--color-border: rgba(82, 208, 255, 0.22);
|
||||
--color-primary: var(--color-cyan-400);
|
||||
--color-primary-hover: var(--color-gold-400);
|
||||
--color-primary-soft: rgba(53, 216, 255, 0.16);
|
||||
--color-accent: var(--color-violet-500);
|
||||
--color-accent-warm: var(--color-coral-500);
|
||||
--color-accent-gold: var(--color-gold-400);
|
||||
--color-danger-bg: var(--color-rose-50);
|
||||
--color-danger-border: var(--color-rose-200);
|
||||
--color-danger-text: var(--color-rose-700);
|
||||
@@ -55,8 +61,8 @@
|
||||
--radius-md: 8px;
|
||||
--radius-lg: 12px;
|
||||
|
||||
--shadow-sm: 0 1px 2px rgba(15, 23, 42, 0.08);
|
||||
--shadow-lg: 0 24px 80px rgba(15, 23, 42, 0.14);
|
||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.24);
|
||||
--shadow-lg: 0 24px 80px rgba(0, 0, 0, 0.36);
|
||||
|
||||
--duration-fast: 160ms;
|
||||
--ease-standard: ease;
|
||||
|
||||
Reference in new issue
Block a user