Срез 0: скелет tabular-parser — без фронта и БД, воркер NATS без JetStream, парсер-заглушка, контракт v1.0 #3

Merged
aep merged 50 commits from feat/1-slice-0-skeleton into main 2026-10-04 18:41:02 +03:00
37 changed files with 3 additions and 1072 deletions
Showing only changes of commit b428f68e9e - Show all commits

1
.gitignore vendored
View File

@ -322,7 +322,6 @@ dist
# Custom ignore
.DS_Store
Dockerfile
docker-compose.yml
.dockerignore
.vscode

View File

@ -1,149 +0,0 @@
# A generic, single database configuration.
[alembic]
# path to migration scripts.
# this is typically a path given in POSIX (e.g. forward slashes)
# format, relative to the token %(here)s which refers to the location of this
# ini file
script_location = %(here)s/alembic
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
# Uncomment the line below if you want the files to be prepended with date and time
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
# for all available tokens
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
# Or organize into date-based subdirectories (requires recursive_version_locations = true)
# file_template = %%(year)d/%%(month).2d/%%(day).2d_%%(hour).2d%%(minute).2d_%%(second).2d_%%(rev)s_%%(slug)s
# sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory. for multiple paths, the path separator
# is defined by "path_separator" below.
prepend_sys_path = .
# timezone to use when rendering the date within the migration file
# as well as the filename.
# If specified, requires the tzdata library which can be installed by adding
# `alembic[tz]` to the pip requirements.
# string value is passed to ZoneInfo()
# leave blank for localtime
# timezone =
# max length of characters to apply to the "slug" field
# truncate_slug_length = 40
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false
# version location specification; This defaults
# to <script_location>/versions. When using multiple version
# directories, initial revisions must be specified with --version-path.
# The path separator used here should be the separator specified by "path_separator"
# below.
# version_locations = %(here)s/bar:%(here)s/bat:%(here)s/alembic/versions
# path_separator; This indicates what character is used to split lists of file
# paths, including version_locations and prepend_sys_path within configparser
# files such as alembic.ini.
# The default rendered in new alembic.ini files is "os", which uses os.pathsep
# to provide os-dependent path splitting.
#
# Note that in order to support legacy alembic.ini files, this default does NOT
# take place if path_separator is not present in alembic.ini. If this
# option is omitted entirely, fallback logic is as follows:
#
# 1. Parsing of the version_locations option falls back to using the legacy
# "version_path_separator" key, which if absent then falls back to the legacy
# behavior of splitting on spaces and/or commas.
# 2. Parsing of the prepend_sys_path option falls back to the legacy
# behavior of splitting on spaces, commas, or colons.
#
# Valid values for path_separator are:
#
# path_separator = :
# path_separator = ;
# path_separator = space
# path_separator = newline
#
# Use os.pathsep. Default configuration used for new projects.
path_separator = os
# set to 'true' to search source files recursively
# in each "version_locations" directory
# new in Alembic version 1.10
# recursive_version_locations = false
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
# database URL. This is consumed by the user-maintained env.py script only.
# other means of configuring database URLs may be customized within the env.py
# file.
sqlalchemy.url = driver://user:pass@localhost/dbname
[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run
# on newly generated revision scripts. See the documentation for further
# detail and examples
# format using "black" - use the console_scripts runner, against the "black" entrypoint
# hooks = black
# black.type = console_scripts
# black.entrypoint = black
# black.options = -l 79 REVISION_SCRIPT_FILENAME
# lint with attempts to fix using "ruff" - use the module runner, against the "ruff" module
# hooks = ruff
# ruff.type = module
# ruff.module = ruff
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
# Alternatively, use the exec runner to execute a binary found on your PATH
# hooks = ruff
# ruff.type = exec
# ruff.executable = ruff
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
# Logging configuration. This is also consumed by the user-maintained
# env.py script only.
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARNING
handlers = console
qualname =
[logger_sqlalchemy]
level = WARNING
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

View File

@ -1 +0,0 @@
Generic single-database configuration.

View File

@ -1,102 +0,0 @@
import asyncio
from logging.config import fileConfig
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from sqlalchemy.ext.asyncio import AsyncEngine
from alembic import context
from src.core.config import settings
from src.models import Base # noqa: F401 — модели регистрируются в src/models/__init__.py
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
if config.config_file_name is not None:
fileConfig(config.config_file_name)
config.set_main_option("sqlalchemy.url", settings.OPENBAO.DATABASE.DATABASE_URL)
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
target_metadata = Base.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def include_name(name, type_, parent_names):
if type_ == 'schema':
return name == 'dkb'
return True
def do_run_migrations(connection) -> None:
context.configure(
connection=connection,
target_metadata=target_metadata,
compare_type=True,
version_table_schema="dkb",
include_schemas=True,
include_name=include_name
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
version_table_schema="dkb",
include_schemas=True,
include_name=include_name
)
with context.begin_transaction():
context.run_migrations()
async def run_migrations_online() -> None:
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
connectable = AsyncEngine(
engine_from_config(
config.get_section(config.config_ini_section),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
future=True
)
)
async with connectable.connect() as connection:
await connection.run_sync(do_run_migrations)
await connectable.dispose()
if context.is_offline_mode():
run_migrations_offline()
else:
asyncio.run(run_migrations_online())

View File

@ -1,28 +0,0 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision: str = ${repr(up_revision)}
down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)}
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
def upgrade() -> None:
"""Upgrade schema."""
${upgrades if upgrades else "pass"}
def downgrade() -> None:
"""Downgrade schema."""
${downgrades if downgrades else "pass"}

View File

@ -1 +0,0 @@
from .db import get_async_session

View File

@ -1,16 +0,0 @@
from contextlib import asynccontextmanager
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine, AsyncSession
from fastapi import Depends
# from fastapi_users.db import SQLAlchemyUserDatabase, SQLAlchemyBaseUserTableUUID
from src.core.config import settings
engine = create_async_engine(settings.OPENBAO.DATABASE.DATABASE_URL, connect_args={"server_settings": {"search_path": 'dkb'}})
async_session_maker = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
# @asynccontextmanager
async def get_async_session():
async with async_session_maker() as session:
yield session

View File

@ -1,4 +0,0 @@
"""ORM-модели. Источник истины — docs/model/data-model-v5.13.dbml; заполняется в Срезе 0."""
from .base import Base, UUIDPrimaryMixin
__all__ = ["Base", "UUIDPrimaryMixin"]

View File

@ -1,14 +0,0 @@
import uuid
from sqlalchemy import MetaData
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import DeclarativeBase, Mapped, declarative_mixin, mapped_column
class Base(DeclarativeBase):
metadata = MetaData(schema="dkb")
@declarative_mixin
class UUIDPrimaryMixin:
id: Mapped[uuid.UUID] = mapped_column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)

View File

@ -19,7 +19,7 @@
### Task 1: [1.1] Удалить `web/`, `setup/.npmrc`, npm-шаги из `setup/setup.sh`, `api/alembic/`, `a
- [ ] Удалить `web/`, `setup/.npmrc`, npm-шаги из `setup/setup.sh`, `api/alembic/`, `api/alembic.ini`, `api/src/db/`, `api/src/models/`, `api/src/repositories/`; из `.gitignore` убрать `Dockerfile`; проверка: `git status` показывает только удаления и `.gitignore`; `python -c "import src"` пока падает ожидаемо (чинится в 1.2)
- [x] Удалить `web/`, `setup/.npmrc`, npm-шаги из `setup/setup.sh`, `api/alembic/`, `api/alembic.ini`, `api/src/db/`, `api/src/models/`, `api/src/repositories/`; из `.gitignore` убрать `Dockerfile`; проверка: `git status` показывает только удаления и `.gitignore`; `python -c "import src"` пока падает ожидаемо (чинится в 1.2)
### Task 2: [1.2] `src/app.py`

View File

@ -5,7 +5,7 @@
## 1. Вырезать фронт и БД из шаблона
- [ ] 1.1 Удалить `web/`, `setup/.npmrc`, npm-шаги из `setup/setup.sh`, `api/alembic/`, `api/alembic.ini`, `api/src/db/`,
- [x] 1.1 Удалить `web/`, `setup/.npmrc`, npm-шаги из `setup/setup.sh`, `api/alembic/`, `api/alembic.ini`, `api/src/db/`,
`api/src/models/`, `api/src/repositories/`; из `.gitignore` убрать `Dockerfile`; проверка: `git status` показывает
только удаления и `.gitignore`; `python -c "import src"` пока падает ожидаемо (чинится в 1.2)
- [ ] 1.2 `src/app.py`: убрать mount SPA/`os.makedirs("web")`, CORS, SMB-сессию, импорт `raisa_fastapi_protected_api`;

View File

@ -1,2 +0,0 @@
registry=http://nexus.dev.raisa.go.rshbank.ru/repository/npm/
strict-ssl=false

View File

@ -9,15 +9,5 @@ source ./../api/.venv/bin/activate
pip install -r ./../api/requirements.txt
echo "complete setup development api"
echo "setup web application"
cp ./.npmrc ./../web/.npmrc
cd ./../web
npm i
echo "complete setup web application"
echo "To start api, cd to api directory, and use venv, start run_server.py"
echo "To start api, cd to web directory, and use command npm run start!"
echo "Happy Hacking! :)"
echo "Happy Hacking! :)"

View File

@ -1,91 +0,0 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"files": {
"ignoreUnknown": true,
"ignore": ["node_modules"]
},
"formatter": {
"enabled": true,
"lineWidth": 140,
"formatWithErrors": true
},
"organizeImports": {
"enabled": true
},
"linter": {
"ignore": ["*.json"],
"enabled": true,
"rules": {
"recommended": true,
"a11y": {
"useButtonType": "off",
"noSvgWithoutTitle": "off"
},
"complexity": {
"noForEach": "off",
"noUselessTypeConstraint": "warn"
},
"correctness": {
"noUnusedVariables": "error",
"noUnusedImports": "error",
"useExhaustiveDependencies": "info"
},
"nursery": {
"noCommonJs": "error",
"noDuplicateElseIf": "error",
"noDuplicateProperties": "error",
"noNestedTernary": "error",
"useAtIndex": "error",
"useCollapsedIf": "error",
"useConsistentCurlyBraces": "error"
},
"performance": {
"noDelete": "off",
"noAccumulatingSpread": "off"
},
"style": {
"useDefaultSwitchClause": "error",
"useForOf": "error",
"useFragmentSyntax": "error",
"useConsistentArrayType": "error",
"useShorthandAssign": "error"
},
"suspicious": {
"noConsole": "warn",
"noEmptyBlockStatements": "error",
"noEvolvingTypes": "error",
"noExplicitAny": "warn",
"noSkippedTests": "warn",
"useAwait": "error"
}
}
},
"javascript": {
"formatter": {
"quoteStyle": "single",
"jsxQuoteStyle": "single",
"bracketSameLine": true
},
"linter": {
"enabled": true
}
},
"json": {
"parser": {
"allowComments": true
}
},
"css": {
"formatter": {
"enabled": true
},
"parser": {
"cssModules": true
}
}
}

View File

@ -1,36 +0,0 @@
{
"name": "frontend",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"build": "rsbuild build",
"start": "rsbuild dev --open",
"preview": "rsbuild preview"
},
"dependencies": {
"@ant-design/icons": "^6.0.0",
"@reduxjs/toolkit": "^2.8.2",
"antd": "^6.5.0",
"markdown-to-jsx": "^9.1.2",
"pdfjs-dist": "^5.7.284",
"raisa-fastapi-protected-web": "^1.0.0",
"raisa-ui": "^1.0.0",
"react": "^18.3.1",
"react-daisyui": "^5.0.5",
"react-dom": "^18.3.1",
"react-redux": "^9.2.0",
"react-router-dom": "^7.8.0"
},
"devDependencies": {
"@biomejs/biome": "1.9.4",
"@biomejs/cli-linux-x64-musl": "^1.9.4",
"@rsbuild/core": "1.6.7",
"@rsbuild/plugin-react": "^1.1.0",
"@types/react": "^18.3.1",
"@types/react-dom": "^18.3.1",
"daisyui": "^4.12.10",
"tailwindcss": "^3.4.13",
"typescript": "^5.7.3"
}
}

View File

@ -1,5 +0,0 @@
module.exports = {
plugins: {
tailwindcss: {},
}
}

View File

View File

@ -1,13 +0,0 @@
<!DOCTYPE html>
<html lang="ru-RU">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<title>Пример React-приложения</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>

View File

@ -1,32 +0,0 @@
import { defineConfig, loadEnv } from '@rsbuild/core';
import { pluginReact } from '@rsbuild/plugin-react';
const { publicVars, rawPublicVars } = loadEnv({prefixes: ['REACT_APP_']});
export default defineConfig({
server: {
base: (rawPublicVars['REACT_APP_ROOT_PATH'] !== undefined) ? rawPublicVars['REACT_APP_ROOT_PATH'] : '/'
},
output: {
filename: {
js: '[name]_script.[contenthash:8].js',
css: '[name]_style.[contenthash:8].css',
},
distPath: {
favicon: 'static/image',
},
assetPrefix: (rawPublicVars['REACT_APP_ROOT_PATH'] !== undefined) ? rawPublicVars['REACT_APP_ROOT_PATH'] : './'
},
html: {
template: './public/index.html',
favicon: './src/assets/favicon.ico',
},
source: {
include: [{ not: /[\\/]core-js[\\/]/ }],
define: {
...publicVars,
'process.env': JSON.stringify(rawPublicVars),
}
},
plugins: [pluginReact()],
});

View File

View File

@ -1,28 +0,0 @@
import './App.css';
import { Layout } from 'antd';
import { Provider } from 'react-redux';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import { Config } from './conf/config';
import { Main } from './layouts/Main';
import { store } from './store/store';
import { Sidebar } from './widgets/Sidebar/Sidebar';
// Каркас: экраны по docs/context/10-user-journey.md добавляются срезами.
function App() {
return (
<Provider store={store}>
<BrowserRouter basename={Config.APP_ADDRESS}>
<Layout hasSider={true} style={{ height: '100vh' }}>
<Sidebar />
<Layout.Content>
<Routes>
<Route path='/' element={<Main />} />
</Routes>
</Layout.Content>
</Layout>
</BrowserRouter>
</Provider>
);
}
export default App;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

View File

@ -1,7 +0,0 @@
// biome-ignore lint/suspicious/noExplicitAny: process.env подставляется rsbuild на этапе сборки
const environment = (process as any).env;
export const Config = {
API_URL: (environment.REACT_APP_API_URL ?? environment.REACT_APP_ROOT_PATH ?? '') as string,
APP_ADDRESS: `${environment.REACT_APP_ROOT_PATH || '/aurora/apps/fastapi-dkb-armka'}` as string,
};

1
web/src/env.d.ts vendored
View File

@ -1 +0,0 @@
/// <reference types="@rsbuild/core/types" />

View File

@ -1,116 +0,0 @@
/* :root {
--text: #6b6375;
--text-h: #08060d;
--bg: #fff;
--border: #e5e4e7;
--code-bg: #f4f3ec;
--accent: #aa3bff;
--accent-bg: rgba(170, 59, 255, 0.1);
--accent-border: rgba(170, 59, 255, 0.5);
--social-bg: rgba(244, 243, 236, 0.5);
--shadow:
rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
--sans: system-ui, 'Segoe UI', Roboto, sans-serif;
--heading: system-ui, 'Segoe UI', Roboto, sans-serif;
--mono: ui-monospace, Consolas, monospace;
font: 18px/145% var(--sans);
letter-spacing: 0.18px;
color-scheme: light dark;
color: var(--text);
background: var(--bg);
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
@media (max-width: 1024px) {
font-size: 16px;
}
}
@media (prefers-color-scheme: dark) {
:root {
--text: #9ca3af;
--text-h: #f3f4f6;
--bg: #16171d;
--border: #2e303a;
--code-bg: #1f2028;
--accent: #c084fc;
--accent-bg: rgba(192, 132, 252, 0.15);
--accent-border: rgba(192, 132, 252, 0.5);
--social-bg: rgba(47, 48, 58, 0.5);
--shadow:
rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
}
#social .button-icon {
filter: invert(1) brightness(2);
}
}
#root {
width: 1126px;
max-width: 100%;
margin: 0 auto;
text-align: center;
border-inline: 1px solid var(--border);
min-height: 100svh;
display: flex;
flex-direction: column;
box-sizing: border-box;
}
body {
margin: 0;
}
h1,
h2 {
font-family: var(--heading);
font-weight: 500;
color: var(--text-h);
}
h1 {
font-size: 56px;
letter-spacing: -1.68px;
margin: 32px 0;
@media (max-width: 1024px) {
font-size: 36px;
margin: 20px 0;
}
}
h2 {
font-size: 24px;
line-height: 118%;
letter-spacing: -0.24px;
margin: 0 0 8px;
@media (max-width: 1024px) {
font-size: 20px;
}
}
p {
margin: 0;
}
code,
.counter {
font-family: var(--mono);
display: inline-flex;
border-radius: 4px;
color: var(--text-h);
}
code {
font-size: 15px;
line-height: 135%;
padding: 4px 8px;
background: var(--code-bg);
} */
body {
font-family: "Gill Sans", "Gill Sans MT", Calibri, "Trebuchet MS", sans-serif;
margin: 0px;
}

View File

@ -1,41 +0,0 @@
import { createRoot } from 'react-dom/client';
import './index.css';
import { ConfigProvider } from 'antd';
import App from './App';
const rootElement = document.getElementById('root');
if (!rootElement) {
throw new Error('Элемент #root не найден');
}
createRoot(rootElement).render(
// <StrictMode>
<ConfigProvider
theme={{
token: {
colorPrimary: 'rgba(0, 255, 0, 0.5)',
colorPrimaryHover: 'rgba(0, 255, 0, 1)',
colorBgLayout: 'rgb(247, 248, 250)',
},
components: {
Layout: {
siderBg: '#fff',
},
Button: {
// textHoverBg: "green",
colorBorder: 'rgba(0,0,0,0.5)',
defaultActiveBorderColor: 'rgba(0,0,0,0.5)',
defaultBorderColor: 'rgba(0,0,0,0.5)',
},
Input: {
activeBorderColor: 'rgba(46, 166, 75, 0.35)',
activeShadow: '0 0 0 1px #2ea64b59,0 1px 2px #0000000f',
hoverBorderColor: 'rgba(46, 166, 75, 0.35)',
},
},
}}
>
<App />
</ConfigProvider>,
// </StrictMode>,
);

View File

@ -1,15 +0,0 @@
// Каркас: стартовый экран. Содержимое — по docs/context/10-user-journey.md.
export function Main() {
return (
<div
style={{
margin: 10,
padding: 16,
background: 'white',
borderRadius: 10,
height: 'calc(100% - 20px)',
}}>
<h3>АРМ КА</h3>
</div>
);
}

View File

@ -1,12 +0,0 @@
import { configureStore } from '@reduxjs/toolkit';
import { useDispatch, useSelector } from 'react-redux';
// Каркас: слайсы добавляются срезами в ./slices.
export const store = configureStore({
reducer: {},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
export const useAppDispatch = useDispatch.withTypes<AppDispatch>();
export const useAppSelector = useSelector.withTypes<RootState>();

View File

View File

@ -1,90 +0,0 @@
import { HomeOutlined, LeftOutlined, RightOutlined } from '@ant-design/icons';
import { Divider, Layout, Tooltip } from 'antd';
import { type ReactNode, memo, useState } from 'react';
import { NavLink } from 'react-router-dom';
import styles from './styles.module.css';
type NavItem = {
link: string;
label: string;
icon: ReactNode;
};
type Group = {
title: string;
key: string;
children: NavItem[];
};
// Каркас: разделы меню — по карте экранов docs/context/10-user-journey.md, добавляются срезами.
const config: Group[] = [
{
title: 'Главная',
key: 'home',
children: [{ link: '/', icon: <HomeOutlined />, label: 'Главная' }],
},
];
function CollapsedIcon({ icon, title, to }: { icon: ReactNode; title: string; to: string }) {
return (
<Tooltip placement='right' title={title}>
<NavLink to={to} className={({ isActive }) => `${styles.iconBtn} ${isActive ? styles.iconBtnActive : styles.iconBtnNav}`}>
{icon}
</NavLink>
</Tooltip>
);
}
function NavLinkStyled({ to, children }: { to: string; children: ReactNode }) {
return (
<NavLink className={({ isActive }) => `${styles.linkBase} ${isActive ? styles.linkActive : ''}`} to={to}>
{children}
</NavLink>
);
}
const Section = memo(function Section({ isCollapsed, group }: { isCollapsed: boolean; group: Group }) {
return !isCollapsed ? (
<div className={styles.section}>
<div className={styles.sectionTitle}>{group.title}</div>
{group.children.map((i) => (
<NavLinkStyled key={i.link} to={i.link}>
{i.icon}
{i.label}
</NavLinkStyled>
))}
<Divider size='small' />
</div>
) : (
<>
{group.children.map((i) => (
<CollapsedIcon key={i.link} icon={i.icon} to={i.link} title={i.label} />
))}
</>
);
});
export function Sidebar() {
const [isCollapsed, setIsCollapsed] = useState(false);
return (
<Layout.Sider
collapsed={isCollapsed}
style={{ height: 'auto', margin: '10px', borderRadius: 10 }}
width={280}
collapsedWidth={64}
collapsible
trigger={null}>
<div className={`${styles.wrapper} ${isCollapsed ? styles.wrapperCollapsed : ''}`}>
<div className={styles.topbar}>
<button type='button' className={styles.collapseBtn} onClick={() => setIsCollapsed(!isCollapsed)}>
{isCollapsed ? <RightOutlined /> : <LeftOutlined />}
</button>
</div>
{config.map((g) => (
<Section key={g.key} group={g} isCollapsed={isCollapsed} />
))}
</div>
</Layout.Sider>
);
}

View File

@ -1,188 +0,0 @@
.company_group_item {
border: 2px solid grey;
border-radius: 2px;
height: 50px;
width: 90%;
cursor: pointer;
background-color: lightgray;
}
.active_cg {
background-color: white;
}
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.brand {
font-weight: 700;
white-space: nowrap;
}
.collapsedList {
display: flex;
flex-direction: column;
gap: 8px;
align-items: center;
margin-top: 8px;
}
.section {
margin-bottom: 0px;
padding: 12px;
border-radius: 10px;
}
.sectionTitle {
font-size: 12px;
font-weight: 700;
color: #8c8c8c;
text-transform: uppercase;
letter-spacing: 0.04em;
margin-bottom: 10px;
}
.search {
margin-bottom: 10px;
}
.subgroupToggle {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
border: 0;
background: transparent;
padding: 6px 10px;
margin-bottom: 6px;
cursor: pointer;
color: rgba(0, 0, 0, 0.88);
font-weight: 600;
}
.subgroup {
/* padding-left: 8px; */
display: flex;
flex-direction: column;
/* gap: 10px; */
margin-bottom: 12px;
}
.linkBase {
display: inline-flex;
gap: 10px;
padding: 8px 10px;
width: 100%;
border-radius: 8px;
color: rgba(55, 65, 81, 0.88);
text-decoration: none;
background: transparent;
transition: background 0.2s, color 0.2s;
}
/* .linkActive:hover {
color: rgb(31, 114, 55);
} */
.linkBase:hover {
color: rgba(55, 65, 81, 0.88);
background: rgb(249, 250, 251);
}
.linkBase.linkActive {
color: rgb(31, 114, 55);
background: #edf8ee;
font-weight: 600;
}
.state {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 0;
}
.emptyWrap {
padding-top: 8;
}
.wrapper {
height: 100%;
display: flex;
flex-direction: column;
padding: 12px;
overflow: hidden;
transition: width 0.2s ease;
}
.wrapperCollapsed {
padding: 12px 0;
align-items: center;
}
.collapsedList {
display: flex;
flex-direction: column;
gap: 8px;
width: 100%;
align-items: center;
margin-top: 8px;
}
.iconBtn {
width: 40px;
height: 40px;
border: 0;
border-radius: 12px;
background: transparent;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: rgba(0, 0, 0, 0.75);
transition: background 0.2s, color 0.2s;
font-size: 22px;
}
.iconBtnActive,
.iconBtnActive:hover {
background: rgba(46, 166, 75, 0.16);
color: rgb(31, 114, 55);
}
.iconBtnNav:hover {
color: #101828;
background: #f2f4f7;
}
.iconBtnAction {
background: linear-gradient(160deg, #2ea64b, #1f7237);
font-size: small;
box-shadow: 0 1px 3px #1f723740;
color: white;
}
.iconBtnAction:hover {
background: linear-gradient(160deg, #268f40, #185c2b);
}
/* .iconBtn. */
.collapseBtn {
width: 40px;
height: 40px;
border: 1px solid rgba(0, 0, 0, 0.25);
border-radius: 12px;
background: transparent;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: rgba(0, 0, 0, 0.75);
transition: background 0.2s, color 0.2s;
font-size: 12px;
padding: 0;
}

View File

@ -1,43 +0,0 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/**/*.{html,js,jsx,ts,tsx}",
"node_modules/daisyui/dist/**/*.js",
"node_modules/react-daisyui/dist/**/*.js",
"node_modules/raisa-ui/dist/**/*.js"
],
theme: {
extend: {},
},
plugins: [require('daisyui')],
daisyui: {
themes: [
{
rshb: {
...require('daisyui/src/theming/themes')['light'],
"primary": "#171A1C",
"primary-content": "#fcfcfd",
"secondary": "#ECEDEF",
"secondary-content": "#687178",
"accent": "#8ecaff",
"accent-content": "#fcfcfd",
"neutral": "#ecedef",
"neutral-content": "#171A1C",
"base-100": "#FCFCFD",
"base-200": "#F6F7F8",
"base-300": "#ECEDEF",
"base-content": "#171A1C",
"info": "#0894FF",
"info-content": "#E3E5E8",
"success": "#39B33D",
"success-content": "#E3E5E8",
"warning": "#E6B200",
"warning-content": "#fcfcfd",
"error": "#FF371A",
"error-content": "#fcfcfd",
}
}
]
}
}

View File

@ -1,23 +0,0 @@
{
"compilerOptions": {
"lib": ["DOM", "ES2020"],
"jsx": "react-jsx",
"target": "ES2020",
"noEmit": true,
"skipLibCheck": true,
"useDefineForClassFields": true,
/* modules */
"module": "ESNext",
"isolatedModules": true,
"resolveJsonModule": true,
"moduleResolution": "Bundler",
"allowImportingTsExtensions": true,
/* type checking */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true
},
"include": ["src"]
}