diff --git a/apps/web/src/assets/styles/global-tokens.scss b/apps/web/src/assets/styles/global-tokens.scss index ad37e98..e50c62b 100644 --- a/apps/web/src/assets/styles/global-tokens.scss +++ b/apps/web/src/assets/styles/global-tokens.scss @@ -16,6 +16,7 @@ --color-success-500: #12a150; --color-warning-500: #d88a00; --color-danger-500: #e5484d; + --color-danger-solid: #b4232b; --color-info-500: #1683c4; --color-success-text: #08783a; --color-warning-text: #8a5700; diff --git a/apps/web/src/assets/styles/global.scss b/apps/web/src/assets/styles/global.scss index be18879..6f3761d 100644 --- a/apps/web/src/assets/styles/global.scss +++ b/apps/web/src/assets/styles/global.scss @@ -79,7 +79,7 @@ h1 { max-width: 760px; margin: var(--space-3) 0 var(--space-4); font-size: var(- .button { min-height: 44px; border: var(--border-thin) solid transparent; border-radius: var(--radius-md); padding: var(--space-3) var(--space-4); font-weight: var(--weight-bold); } .button--primary { background: var(--color-interactive); color: var(--color-on-interactive); } .button--secondary { border-color: var(--color-border-strong); background: var(--color-surface); color: var(--color-text); } -.button--danger { background: var(--color-danger-text); color: var(--color-white); } +.button--danger { background: var(--color-danger-solid); color: var(--color-white); } .button:disabled { cursor: not-allowed; opacity: 0.45; } .swatch { display: grid; gap: var(--space-2); color: var(--color-text-muted); font-size: var(--text-xs); } .swatch__chip { width: 72px; height: 44px; border-radius: var(--radius-md); } diff --git a/apps/web/src/components/organisms/AdminImageManager.stories.js b/apps/web/src/components/organisms/AdminImageManager.stories.js index de29cf3..9fe84f6 100644 --- a/apps/web/src/components/organisms/AdminImageManager.stories.js +++ b/apps/web/src/components/organisms/AdminImageManager.stories.js @@ -44,3 +44,14 @@ export const RetryFailedImage = { await expect(canvas.getByRole('button', { name: 'Retry processing' })).toBeInTheDocument(); }, }; + +export const DisabledAfterRefreshFailure = { + args: { images, disabled: true }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await expect(canvas.getByLabelText('Add images')).toBeDisabled(); + for (const button of canvas.getAllByRole('button', { name: /Edit alt text|Make primary|Move up|Move down|Delete image/ })) { + await expect(button).toBeDisabled(); + } + }, +}; diff --git a/apps/web/src/components/organisms/AdminImageManager.vue b/apps/web/src/components/organisms/AdminImageManager.vue index c69a5b2..80faa6b 100644 --- a/apps/web/src/components/organisms/AdminImageManager.vue +++ b/apps/web/src/components/organisms/AdminImageManager.vue @@ -1,5 +1,5 @@ @@ -17,18 +24,28 @@ onMounted(load); Back to catalog

Admin inventory

Manage images

-

Loading images...

- - +

{{ hasLoaded ? 'Refreshing images...' : 'Loading images...' }}

+ + +
+ +
+ + diff --git a/docs/IMAGES.md b/docs/IMAGES.md index 49920ea..4c4b734 100644 --- a/docs/IMAGES.md +++ b/docs/IMAGES.md @@ -108,3 +108,14 @@ Public galleries must support: - screen-reader-friendly state Primary images power cards, search results, homepage previews, and social/share metadata. + +## Admin error recovery + +Keep the loaded gallery and alt-text drafts mounted during image actions and +refreshes. Show action failures separately from list-read failures. After every +action, including a failed multi-file upload or reorder, reload image metadata to +reconcile any partial success without replaying writes. If that refresh fails, +retain the last loaded gallery but disable mutations until an explicit reload +succeeds. Failed initial loads show a reload action instead of an empty manager. +Reload restores keyboard focus to the manager or the retry control. Pending +operations block conflicting writes and uploads, including drag-and-drop. diff --git a/docs/IMPLEMENTATION-STATUS.md b/docs/IMPLEMENTATION-STATUS.md index 76a0112..7adf667 100644 --- a/docs/IMPLEMENTATION-STATUS.md +++ b/docs/IMPLEMENTATION-STATUS.md @@ -162,14 +162,22 @@ Verified locally: `npm test` passes 167 tests with no failures (23 domain, 79 ap - Storybook covers loading, load errors, successful/repeated retry, save failure/retry, saving/disabled states, mobile retry and dark-theme save recovery. - Validation: 167 API tests, 11 web tests, production web build, Storybook build, and 32 Storybook suites / 102 tests. Frontend-only change; no API or persistence changes. -### Latest Vertical Slice: Saved Views Error Recovery +### Saved Views Error Recovery - Failed list reads no longer show an empty-list message or create form; Retry restores keyboard focus on success or repeated failure. - Create/delete actions clear stale feedback, announce progress, and block conflicting actions. Create failures preserve drafts; delete failures preserve the view and focus; successful deletion announces completion and focuses the list heading. - Storybook covers populated/empty/loading/error states, load retries, create/delete recovery, invalid JSON, pending disabled states, mobile retry and dark-theme recovery. - Validation: 167 API tests, 11 web tests, production web build, Storybook build, and 32 Storybook suites / 113 tests. Frontend-only change; API and persistence unchanged. -Next bounded slice: admin image-manager error recovery. A mutation error currently replaces the whole gallery with an error message. Keep loaded images and controls available after failed edits/uploads, distinguish initial-load failure, provide reload retry, and cover retry/failure states with Storybook interactions. +### Latest Vertical Slice: Image Manager Error Recovery + +- Loaded images and alt-text drafts survive action failures and refreshes. Initial-load errors offer Reload; refresh errors retain the gallery with writes disabled until reload succeeds. +- Every action reconciles metadata even on failure, exposing partial multi-upload/reorder success without replaying mutations. Pending actions block overlapping writes and file drops. +- Added page/component stories covering load/retry, edit/delete failure and retry, successful deletion followed by refresh failure, pending/disabled states, partial upload failure, mobile and dark-theme recovery. +- Dark-theme coverage also fixed destructive-button contrast by separating its solid background token from error-text color. +- Validation: 167 API tests, 11 web tests, production web build, Storybook build, and 32 Storybook suites / 124 tests. No API or persistence changes. + +Next bounded slice: atomic image reordering. Move up/down currently sends two separate order updates; failure between them can leave a partial reorder. Add a seller-scoped API operation that updates both positions transactionally, connect the controls to it, and verify rollback and ownership with integration tests. ## Next Milestone