refactor: marketplace release polish

Sprint 28, scoped to admin/* (user decision — full marketplace audit
declined in favor of a bounded pass over the 8 admin features from
Sprints 20-27).

- a11y: aria-label added to every bare <select> not already inside a
  <label> across categories/products/orders/transactions/users/monitoring
- loading states: app-skeleton rows/cards added to list pages that
  previously rendered blank during the initial fetch (categories, orders,
  transactions, users, monitoring's event feed, analytics summary cards)
- admin-dashboard-card's custom shimmer CSS replaced with the shared
  SkeletonComponent (same visual result, one less duplicated animation)
- bundle-size budget warning (~198kB over) confirmed pre-existing —
  present at Sprint 20's first build before any admin/* code existed,
  and new admin pages are all lazy-loaded — documented as out of scope
  for this pass rather than chased

docs/ADMIN.md + docs/SPRINT-PLAN.md updated with the scope decision and
what was explicitly not done (Lighthouse, animations, SEO/sitemap,
storefront/editor a11y).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
sdarbinyan
2026-07-16 00:14:46 +04:00
parent 1db63ac99d
commit 576f2600a5
27 changed files with 223 additions and 127 deletions

View File

@@ -1,6 +1,13 @@
<section class="admin-users-page">
<div class="card">
<h2>{{ 'adminUsers.title' | translate }}</h2>
@if (facade.loading()) {
<div class="skeleton-rows">
@for (i of [1,2,3]; track i) { <app-skeleton shape="rect" height="40px" /> }
</div>
} @else if (facade.users().length === 0) {
<app-empty-state [title]="'adminUsers.emptyTitle' | translate" [description]="'adminUsers.emptyDescription' | translate" />
} @else {
<app-table>
<thead>
<tr>
@@ -18,7 +25,7 @@
<td>{{ user.name }}<br /><small>{{ user.telegramUsername }}</small></td>
<td>{{ ('adminUsers.scopeValue.' + user.scope) | translate }}</td>
<td>
<select [ngModel]="user.roleId" (ngModelChange)="facade.setRole(user.id, $event)">
<select [attr.aria-label]="'adminUsers.role' | translate" [ngModel]="user.roleId" (ngModelChange)="facade.setRole(user.id, $event)">
@for (role of facade.roles(); track role.id) {
<option [value]="role.id">{{ role.name }}</option>
}
@@ -35,6 +42,7 @@
}
</tbody>
</app-table>
}
</div>
<div class="card">
@@ -42,12 +50,12 @@
<p class="hint">{{ 'adminUsers.passwordlessHint' | translate }}</p>
<div class="invite-form">
<app-input type="email" [ngModel]="inviteEmail()" (ngModelChange)="inviteEmail.set($event)" [placeholder]="'adminUsers.email' | translate" />
<select [ngModel]="inviteRoleId()" (ngModelChange)="inviteRoleId.set($event)">
<select [attr.aria-label]="'adminUsers.role' | translate" [ngModel]="inviteRoleId()" (ngModelChange)="inviteRoleId.set($event)">
@for (role of facade.roles(); track role.id) {
<option [value]="role.id">{{ role.name }}</option>
}
</select>
<select [ngModel]="inviteScope()" (ngModelChange)="inviteScope.set($event)">
<select [attr.aria-label]="'adminUsers.scope' | translate" [ngModel]="inviteScope()" (ngModelChange)="inviteScope.set($event)">
<option value="marketplace">{{ 'adminUsers.scopeValue.marketplace' | translate }}</option>
<option value="office">{{ 'adminUsers.scopeValue.office' | translate }}</option>
</select>
@@ -79,6 +87,8 @@
}
</tbody>
</app-table>
} @else {
<app-empty-state [title]="'adminUsers.invitationsEmptyTitle' | translate" />
}
</div>