web: account avatars, member /profile, framed admin panel

- an account can carry a picture: POST /api/auth/avatar (raw bytes,
  sniffed, replaces and unlinks the old file) and the public
  GET /api/users/:id/avatar. It rides wherever the account is named —
  the landing chip, the studio TopBar, the profile form.
- new /profile page for members, sharing one Profile form (picture,
  email, password) with the admin drawer.
- /admin is now one bordered frame whose left column is
  Profile / User account / Pictures / Close. Pictures lists every
  photo in the system with the slot that shows it; User account lists
  each account's name, email, picture and contribution count.
- account control opens a menu: Admin page + Log out for an admin,
  Profile + Log out for a member.
This commit is contained in:
2026-09-18 08:19:36 +07:00
parent 2917c034ed
commit 6bbf77860b
15 changed files with 908 additions and 79 deletions
+90
View File
@@ -337,9 +337,99 @@ input[type="range"] { width: 100%; accent-color: var(--accent); }
.adm-danger { color: var(--danger); border-color: var(--border); }
.adm-danger:hover:not(:disabled) { border-color: var(--danger); }
/* Settings drawer: the tabs that organise the page, plus the three panels. */
/* The management frame: a bordered box whose left column is the menu and whose
right side is the pane for the chosen section. */
.adm-frame {
display: flex;
align-items: stretch;
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--bg-elev);
overflow: hidden;
}
.adm-side {
flex: 0 0 200px;
display: flex;
flex-direction: column;
gap: 2px;
padding: 12px 10px;
border-right: 1px solid var(--border);
background: var(--bg-sunken);
}
.adm-side-title {
padding: 0 8px 8px;
font-size: 11px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--text-dim);
}
.adm-side-item {
display: block;
padding: 8px 10px;
border: 0;
border-radius: var(--radius-sm);
background: transparent;
color: var(--text);
font: inherit;
text-align: left;
text-decoration: none;
cursor: pointer;
}
.adm-side-item:hover { background: var(--bg-elev); }
.adm-side-item.on { background: var(--accent); color: var(--accent-fg); font-weight: 600; }
.adm-pane { flex: 1 1 auto; min-width: 0; padding: 14px 16px; }
.adm-table-wrap { overflow-x: auto; }
.adm-face {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border-radius: 999px;
overflow: hidden;
background: var(--bg-sunken);
border: 1px solid var(--border);
color: var(--text-dim);
font-size: 11px;
font-weight: 600;
}
.adm-face img { display: block; width: 100%; height: 100%; object-fit: cover; }
.adm-filter { display: flex; align-items: center; gap: 8px; }
.adm-form { display: flex; flex-direction: column; gap: 8px; max-width: 360px; }
.field-label { font-size: 11.5px; color: var(--text-dim); }
/* --- profile picture ---------------------------------------------------- */
/* One face for every control that names the signed-in user: the workspace
TopBar, the landing chip and the profile form. No picture = initial. */
.avatar {
display: inline-flex;
align-items: center;
justify-content: center;
flex: 0 0 auto;
width: 26px;
height: 26px;
border-radius: 999px;
overflow: hidden;
background: var(--bg-sunken);
border: 1px solid var(--border);
color: var(--text-dim);
font-size: 11px;
font-weight: 600;
line-height: 1;
}
.avatar img { display: block; width: 100%; height: 100%; object-fit: cover; }
.avatar-sm { width: 20px; height: 20px; font-size: 10px; }
.avatar-row { display: flex; align-items: center; gap: 10px; }
.avatar-row .avatar { width: 46px; height: 46px; font-size: 17px; }
.account-btn { display: inline-flex; align-items: center; gap: 7px; }
/* --- responsive --------------------------------------------------------- */
@media (max-width: 860px) {
.workspace { flex-direction: column; }
/* The frame's left column becomes a wrapping row above the pane. */
.adm-frame { flex-direction: column; }
.adm-side { flex: 0 0 auto; flex-direction: row; flex-wrap: wrap; border-right: 0; border-bottom: 1px solid var(--border); }
/* The header wraps on narrow screens, so a menu-wrap can land at the start of
a row — the popover is right-anchored to its button and would hang off the
left of the screen (measured: x=-280 of 390px). Anchor it to the full-width