feat(web): sign-in, first-run setup, app shell and user admin (M1)
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 50s
ci / ts / ts (packages + web) (push) Successful in 43s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 27s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m22s

- core: AuthSession keeps the access token in memory, refreshes it
  once for all parallel requests (single-flight) and retries them;
  refresh races between tabs are retried, reuse signs out; ApiError
  carries problem+json codes and field errors
- web: TanStack Router with guards — guests go to /login, the bootstrap
  admin to /setup/admin, temporary passwords to /setup/password, admin
  pages by role
- login, setup screens, Classic shell (top bar, role-based sidebar,
  empty player), settings: name, theme, language, password, devices
- users admin: search, role filter, create with a one-time temporary
  password, reset, block, role change, delete as the policy allows;
  audit log for admins
- ru/en strings for auth, admin and error codes; language and theme
  are saved in the profile

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mrleo1nidandClaude Opus 5.5 committed 2026-10-20 06:33:46 +03:00
1 parent f075863f15
commit ab092def54
44 files changed
+4070 -203

No files matched your search

+202
View File
@@ -0,0 +1,202 @@
.shell {
display: grid;
grid-template-columns: 240px 1fr;
grid-template-rows: 60px 1fr 88px;
grid-template-areas:
'top top'
'side content'
'player player';
height: 100%;
}
.top {
grid-area: top;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
border-bottom: 1px solid var(--line);
background: var(--bar-a);
backdrop-filter: blur(20px);
}
.logoLink {
text-decoration: none;
}
.topRight {
display: flex;
align-items: center;
gap: 12px;
}
.profileButton {
border-radius: 50%;
outline-offset: 3px;
}
.profileButton[aria-expanded='true'] {
box-shadow: 0 0 0 3px var(--accent-soft);
}
.profileHead {
display: grid;
grid-template-columns: auto 1fr;
gap: 4px 12px;
align-items: center;
padding: 10px 10px 12px;
margin-bottom: 6px;
border-bottom: 1px solid var(--line);
}
.profileHead > :last-child {
grid-column: 1 / -1;
margin-top: 10px;
justify-self: start;
}
.profileName {
font-weight: 600;
}
.profileMeta {
font-size: 12px;
color: var(--text3);
}
.side {
grid-area: side;
display: flex;
flex-direction: column;
gap: 2px;
padding: 16px 12px;
border-right: 1px solid var(--line);
overflow-y: auto;
}
.navSection {
margin: 18px 12px 6px;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--text3);
}
.navItem {
display: flex;
align-items: center;
gap: 12px;
height: 40px;
padding: 0 12px;
border-radius: 10px;
color: var(--text2);
font-weight: 500;
text-decoration: none;
}
.navItem:hover {
background: var(--hover);
color: var(--text);
}
.navItem[data-status='active'] {
background: var(--accent-soft);
color: var(--text);
}
.navIcon {
color: var(--text3);
}
.navItem[data-status='active'] .navIcon {
color: var(--accent2);
}
.content {
grid-area: content;
overflow-y: auto;
padding: 32px 36px;
animation: pipuIn 0.2s ease-out;
}
.player {
grid-area: player;
display: flex;
align-items: center;
gap: 14px;
padding: 0 20px;
border-top: 1px solid var(--line);
background: var(--bar-a);
backdrop-filter: blur(20px);
}
.cover {
display: grid;
place-items: center;
width: 52px;
height: 52px;
border-radius: 10px;
background: var(--surface2);
color: var(--text3);
}
.playerEmpty {
color: var(--text3);
font-size: 14px;
}
@media (max-width: 1023px) {
.shell {
grid-template-columns: 72px 1fr;
}
.navItem {
justify-content: center;
padding: 0;
font-size: 0;
}
.navSection {
margin: 14px 0 6px;
text-align: center;
font-size: 0;
border-top: 1px solid var(--line);
}
}
@media (max-width: 767px) {
.shell {
grid-template-columns: 1fr;
grid-template-rows: 60px 1fr 72px 64px;
grid-template-areas:
'top'
'content'
'player'
'side';
}
.side {
flex-direction: row;
justify-content: space-around;
padding: 6px 8px;
border-right: 0;
border-top: 1px solid var(--line);
}
.navSection {
display: none;
}
.navItem {
flex: 1;
}
.content {
padding: 20px 16px;
}
.player {
padding: 0 16px;
}
}