chore: добавить SLM-каркас admin

- добавлен app entrypoint, main layout и dashboard screen\n- настроены алиасы SLM-слоёв и PostCSS для CSS Modules\n- перенесены глобальные стили в shared/styles
This commit is contained in:
2026-05-05 14:28:17 +03:00
parent 2c88cc3eca
commit 72f9386f57
29 changed files with 575 additions and 163 deletions

View File

@@ -7,6 +7,6 @@
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
<script type="module" src="/src/app/main.tsx"></script>
</body>
</html>

View File

@@ -10,14 +10,19 @@
"typecheck": "tsc -b"
},
"dependencies": {
"clsx": "^2.1.1",
"react": "^19.2.5",
"react-dom": "^19.2.5"
},
"devDependencies": {
"@csstools/postcss-global-data": "^4.0.0",
"@types/node": "^24.12.2",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"autoprefixer": "^10.5.0",
"postcss-custom-media": "^12.0.1",
"postcss-nesting": "^14.0.0",
"typescript": "^5.9.3",
"vite": "^8.0.10"
}

View File

@@ -0,0 +1,10 @@
export default {
plugins: {
"@csstools/postcss-global-data": {
files: ["src/shared/styles/media.css"],
},
"postcss-custom-media": {},
"postcss-nesting": {},
autoprefixer: {},
},
}

View File

@@ -1,111 +0,0 @@
:root {
color: #171411;
background: #f7f4ee;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
* {
box-sizing: border-box;
}
body {
min-width: 320px;
min-height: 100vh;
margin: 0;
background:
radial-gradient(circle at 18% 18%, rgba(123, 76, 255, 0.18), transparent 30rem),
#f7f4ee;
}
button,
input,
textarea,
select {
font: inherit;
}
.app-shell {
min-height: 100vh;
padding: 48px 24px;
}
.hero {
max-width: 960px;
margin: 0 auto;
padding: 40px;
border: 1px solid #e4ded4;
border-radius: 32px;
background: rgba(255, 255, 255, 0.82);
box-shadow: 0 22px 80px rgba(40, 32, 21, 0.08);
}
.eyebrow {
margin: 0 0 16px;
color: #7b4cff;
font-size: 13px;
font-weight: 700;
letter-spacing: 0.22em;
text-transform: uppercase;
}
h1 {
max-width: 760px;
margin: 0;
font-size: clamp(40px, 7vw, 76px);
line-height: 0.92;
letter-spacing: -0.06em;
}
.lead {
max-width: 680px;
margin: 24px 0 0;
color: #73695d;
font-size: 18px;
line-height: 1.7;
}
.cards {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 16px;
max-width: 960px;
margin: 24px auto 0;
}
.card {
min-height: 150px;
padding: 24px;
border: 1px solid #e4ded4;
border-radius: 24px;
background: rgba(255, 255, 255, 0.76);
}
.card h2 {
margin: 0;
font-size: 18px;
}
.card p {
margin: 12px 0 0;
color: #73695d;
line-height: 1.55;
}
@media (max-width: 720px) {
.app-shell {
padding: 24px 16px;
}
.hero {
padding: 28px;
border-radius: 24px;
}
.cards {
grid-template-columns: 1fr;
}
}

View File

@@ -1,40 +0,0 @@
import "./App.css"
const cards = [
{
title: "Assets",
description: "Каталог исходных изображений и связанной metadata.",
},
{
title: "Variants",
description: "Будущие AVIF/WebP/JPEG variants, presets и статусы генерации.",
},
{
title: "Storage",
description: "PostgreSQL как source of truth, S3/MinIO как хранилище bytes.",
},
]
export function App() {
return (
<main className="app-shell">
<section className="hero">
<p className="eyebrow">Image Platform Admin</p>
<h1>чистый Vite React TS app</h1>
<p className="lead">
Это стартовая админка без UI-фреймворков. Дальше сюда добавим управление allowed hosts,
assets, variants и presets.
</p>
</section>
<section className="cards" aria-label="Будущие разделы">
{cards.map((card) => (
<article className="card" key={card.title}>
<h2>{card.title}</h2>
<p>{card.description}</p>
</article>
))}
</section>
</main>
)
}

View File

@@ -0,0 +1,10 @@
import { MainLayout } from "layouts/main"
import { DashboardScreen } from "screens/dashboard"
export function App() {
return (
<MainLayout>
<DashboardScreen />
</MainLayout>
)
}

View File

@@ -0,0 +1,18 @@
import "shared/styles/global.css"
import { StrictMode } from "react"
import { createRoot } from "react-dom/client"
import { App } from "./app"
const root = document.getElementById("root")
if (!root) {
throw new Error("Root element #root not found")
}
createRoot(root).render(
<StrictMode>
<App />
</StrictMode>,
)

View File

View File

View File

@@ -0,0 +1,2 @@
export { MainLayout } from "./main.layout"
export type { MainLayoutProps } from "./types/main.type"

View File

@@ -0,0 +1,30 @@
import cl from "clsx"
import styles from "./styles/main.module.css"
import type { MainLayoutProps } from "./types/main.type"
/**
* Базовый layout админки: задаёт page shell и общую навигационную шапку.
*
* Используется для:
* - оборачивания экранов admin-приложения
* - подключения общей структуры страницы
*/
export const MainLayout = (props: MainLayoutProps) => {
const { children, className, ...rootAttrs } = props
return (
<div {...rootAttrs} className={cl(styles.root, className)}>
<header className={styles.header}>
<a className={styles.brand} href="/" aria-label="Image Platform Admin">
<span className={styles.brandMark}>IP</span>
<span className={styles.brandText}>Image Platform</span>
</a>
<p className={styles.status}>Admin MVP</p>
</header>
<main className={styles.content}>{children}</main>
</div>
)
}

View File

@@ -0,0 +1,66 @@
.root {
min-height: 100vh;
padding: var(--space-4);
background:
radial-gradient(circle at 18% 18%, var(--color-accent-wash), transparent 30rem),
var(--color-page);
@media (--md) {
padding: var(--space-6);
}
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
max-width: var(--content-width);
margin: 0 auto var(--space-4);
}
.brand {
display: inline-flex;
align-items: center;
gap: var(--space-3);
color: var(--color-text);
font-weight: 800;
letter-spacing: -0.03em;
}
.brandMark {
display: grid;
width: 2.5rem;
height: 2.5rem;
place-items: center;
border: 1px solid var(--color-border);
border-radius: var(--radius-round);
background: var(--color-surface);
color: var(--color-accent);
box-shadow: var(--shadow-soft);
font-size: 0.8125rem;
letter-spacing: 0.08em;
}
.brandText {
display: none;
@media (--sm) {
display: inline;
}
}
.status {
margin: 0;
padding: var(--space-2) var(--space-3);
border: 1px solid var(--color-border);
border-radius: var(--radius-round);
background: var(--color-surface-muted);
color: var(--color-text-muted);
font-size: 0.8125rem;
font-weight: 700;
}
.content {
max-width: var(--content-width);
margin: 0 auto;
}

View File

@@ -0,0 +1,14 @@
import type { ComponentPropsWithoutRef, ReactNode } from "react"
/**
* Параметры MainLayout.
*/
export type MainLayoutParams = {
/** Содержимое layout. */
children?: ReactNode
}
/** Атрибуты корневого элемента без children. */
type RootAttrs = Omit<ComponentPropsWithoutRef<'div'>, 'children'>
export type MainLayoutProps = RootAttrs & MainLayoutParams

View File

@@ -1,10 +0,0 @@
import { StrictMode } from "react"
import { createRoot } from "react-dom/client"
import { App } from "./App"
createRoot(document.getElementById("root")!).render(
<StrictMode>
<App />
</StrictMode>,
)

View File

@@ -0,0 +1,16 @@
export const DASHBOARD_CARDS = [
{
title: "Assets",
description: "Каталог исходных изображений, версий и публичных identifiers.",
},
{
title: "Variants",
description: "Статусы генерации AVIF/WebP/JPEG под presets и custom transforms.",
},
{
title: "Storage",
description: "PostgreSQL как source of truth, S3/MinIO как хранилище готовых bytes.",
},
] as const
export const DASHBOARD_PIPELINE = ["Backend", "RabbitMQ", "Worker", "imgproxy", "S3"] as const

View File

@@ -0,0 +1,46 @@
import cl from "clsx"
import { DASHBOARD_CARDS, DASHBOARD_PIPELINE } from "./config/dashboard.config"
import styles from "./styles/dashboard.module.css"
import type { DashboardScreenProps } from "./types/dashboard.type"
/**
* Стартовый dashboard admin-приложения.
*
* Используется для:
* - отображения стартового состояния admin MVP
* - обзора будущих разделов и пайплайна генерации
*/
export const DashboardScreen = (props: DashboardScreenProps) => {
const { className, ...rootAttrs } = props
return (
<section {...rootAttrs} className={cl(styles.root, className)}>
<div className={styles.hero}>
<p className={styles.eyebrow}>Image Platform Admin</p>
<h1 className={styles.title}>Control plane для image delivery</h1>
<p className={styles.lead}>
Админка будет управлять allowed hosts, assets, source versions, presets и variant
generation без прямого доступа к storage-слою.
</p>
</div>
<div className={styles.grid} aria-label="Будущие разделы admin">
{DASHBOARD_CARDS.map((card) => (
<article className={styles.card} key={card.title}>
<h2 className={styles.cardTitle}>{card.title}</h2>
<p className={styles.cardDescription}>{card.description}</p>
</article>
))}
</div>
<div className={styles.pipeline} aria-label="Пайплайн генерации изображений">
{DASHBOARD_PIPELINE.map((step) => (
<span className={styles.pipelineStep} key={step}>
{step}
</span>
))}
</div>
</section>
)
}

View File

@@ -0,0 +1,2 @@
export { DashboardScreen } from "./dashboard.screen"
export type { DashboardScreenProps } from "./types/dashboard.type"

View File

@@ -0,0 +1,87 @@
.root {
display: grid;
gap: var(--space-4);
}
.hero {
padding: var(--space-6);
border: 1px solid var(--color-border);
border-radius: var(--radius-5);
background: var(--color-surface);
box-shadow: var(--shadow-panel);
@media (--md) {
padding: var(--space-8);
}
}
.eyebrow {
margin: 0 0 var(--space-4);
color: var(--color-accent);
font-size: 0.8125rem;
font-weight: 800;
letter-spacing: 0.22em;
text-transform: uppercase;
}
.title {
max-width: 48rem;
margin: 0;
font-size: clamp(2.75rem, 7vw, 5.5rem);
line-height: 0.9;
letter-spacing: -0.07em;
}
.lead {
max-width: 43rem;
margin: var(--space-5) 0 0;
color: var(--color-text-muted);
font-size: 1.0625rem;
line-height: 1.7;
}
.grid {
display: grid;
grid-template-columns: 1fr;
gap: var(--space-3);
@media (--md) {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
.card {
min-height: 9.5rem;
padding: var(--space-5);
border: 1px solid var(--color-border);
border-radius: var(--radius-4);
background: var(--color-surface-muted);
}
.cardTitle {
margin: 0;
font-size: 1.125rem;
letter-spacing: -0.03em;
}
.cardDescription {
margin: var(--space-3) 0 0;
color: var(--color-text-muted);
line-height: 1.55;
}
.pipeline {
display: flex;
flex-wrap: wrap;
gap: var(--space-2);
}
.pipelineStep {
padding: var(--space-2) var(--space-3);
border: 1px solid var(--color-border);
border-radius: var(--radius-round);
background: var(--color-surface-muted);
color: var(--color-text-muted);
font-size: 0.8125rem;
font-weight: 700;
}

View File

@@ -0,0 +1,4 @@
import type { ComponentPropsWithoutRef } from "react"
/** Параметры экрана Dashboard. */
export type DashboardScreenProps = ComponentPropsWithoutRef<"section">

View File

@@ -0,0 +1,2 @@
@import "./variables.css";
@import "./reset.css";

View File

@@ -0,0 +1,15 @@
@custom-media --xs (max-width: 35.9375rem);
@custom-media --sm (min-width: 36rem);
@custom-media --md (min-width: 48rem);
@custom-media --lg (min-width: 62rem);
@custom-media --xl (min-width: 75rem);
@custom-media --2xl (min-width: 88rem);
@custom-media --3xl (min-width: 120rem);
@custom-media --h-xs (min-height: 41.6875rem);
@custom-media --h-sm (min-height: 43.875rem);
@custom-media --h-md (min-height: 50.625rem);
@custom-media --h-lg (min-height: 56.25rem);
@custom-media --h-xl (min-height: 62.5rem);
@custom-media --h-2xl (min-height: 68.75rem);
@custom-media --h-3xl (min-height: 75rem);

View File

@@ -0,0 +1,34 @@
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
min-width: 320px;
min-height: 100vh;
margin: 0;
background: var(--color-page);
color: var(--color-text);
font-family: var(--font-sans);
font-synthesis: none;
text-rendering: optimizeLegibility;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
}
button,
input,
textarea,
select {
font: inherit;
}
a {
color: inherit;
text-decoration: none;
}
#root {
min-height: 100vh;
}

View File

@@ -0,0 +1,24 @@
:root {
--color-page: #f7f4ee;
--color-surface: rgb(255 255 255 / 82%);
--color-surface-muted: rgb(255 255 255 / 76%);
--color-border: #e4ded4;
--color-text: #171411;
--color-text-muted: #73695d;
--color-accent: #7b4cff;
--color-accent-wash: rgb(123 76 255 / 18%);
--font-sans: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--content-width: 60rem;
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-3: 0.75rem;
--space-4: 1rem;
--space-5: 1.5rem;
--space-6: 1.75rem;
--space-8: 2.5rem;
--radius-4: 1.5rem;
--radius-5: 2rem;
--radius-round: 999px;
--shadow-soft: 0 0.75rem 2rem rgb(40 32 21 / 8%);
--shadow-panel: 0 1.375rem 5rem rgb(40 32 21 / 8%);
}

View File

1
apps/admin/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

View File

@@ -16,7 +16,18 @@
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
"jsx": "react-jsx",
"baseUrl": ".",
"paths": {
"app/*": ["src/app/*"],
"layouts/*": ["src/layouts/*"],
"screens/*": ["src/screens/*"],
"widgets/*": ["src/widgets/*"],
"business/*": ["src/business/*"],
"infra/*": ["src/infra/*"],
"ui/*": ["src/ui/*"],
"shared/*": ["src/shared/*"]
}
},
"include": ["src"]
}

View File

@@ -1,6 +1,22 @@
import { fileURLToPath, URL } from "node:url"
import react from "@vitejs/plugin-react"
import { defineConfig } from "vite"
const srcPath = (path: string) => fileURLToPath(new URL(`./src/${path}`, import.meta.url))
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
app: srcPath("app"),
layouts: srcPath("layouts"),
screens: srcPath("screens"),
widgets: srcPath("widgets"),
business: srcPath("business"),
infra: srcPath("infra"),
ui: srcPath("ui"),
shared: srcPath("shared"),
},
},
})