This commit is contained in:
@@ -45,6 +45,7 @@ h1 { max-width: 760px; margin: var(--space-3) 0 var(--space-4); font-size: var(-
|
|||||||
.auth-panel h1 { font-size: var(--text-2xl); }
|
.auth-panel h1 { font-size: var(--text-2xl); }
|
||||||
.skip-link { position: absolute; left: var(--space-4); top: var(--space-2); z-index: 10; transform: translateY(-200%); background: var(--color-surface); color: var(--color-interactive-text); padding: var(--space-2) var(--space-4); border-radius: var(--radius-sm); }
|
.skip-link { position: absolute; left: var(--space-4); top: var(--space-2); z-index: 10; transform: translateY(-200%); background: var(--color-surface); color: var(--color-interactive-text); padding: var(--space-2) var(--space-4); border-radius: var(--radius-sm); }
|
||||||
.skip-link:focus { transform: translateY(0); }
|
.skip-link:focus { transform: translateY(0); }
|
||||||
|
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
|
||||||
.admin-shell { display: grid; grid-template-columns: 260px 1fr; min-height: 100vh; background: var(--color-background); }
|
.admin-shell { display: grid; grid-template-columns: 260px 1fr; min-height: 100vh; background: var(--color-background); }
|
||||||
.admin-sidebar { display: grid; align-content: start; gap: var(--space-6); border-right: var(--border-thin) solid var(--color-border); padding: var(--space-8); background: var(--color-surface); }
|
.admin-sidebar { display: grid; align-content: start; gap: var(--space-6); border-right: var(--border-thin) solid var(--color-border); padding: var(--space-8); background: var(--color-surface); }
|
||||||
.admin-sidebar h1 { font-size: var(--text-xl); }
|
.admin-sidebar h1 { font-size: var(--text-xl); }
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ const inventoryTags = ref('');
|
|||||||
const isLoading = ref(true);
|
const isLoading = ref(true);
|
||||||
const isSaving = ref(false);
|
const isSaving = ref(false);
|
||||||
const error = ref('');
|
const error = ref('');
|
||||||
|
const announcement = ref('');
|
||||||
|
|
||||||
function csrfToken() {
|
function csrfToken() {
|
||||||
return document.cookie.split('; ').find((value) => value.startsWith('pokemon_inventory_csrf='))?.split('=')[1] ?? '';
|
return document.cookie.split('; ').find((value) => value.startsWith('pokemon_inventory_csrf='))?.split('=')[1] ?? '';
|
||||||
@@ -46,6 +47,7 @@ async function archiveSelectedInventory() {
|
|||||||
const archivedById = new Map(archivedItems.map((item) => [item.id, item]));
|
const archivedById = new Map(archivedItems.map((item) => [item.id, item]));
|
||||||
inventory.value = inventory.value.map((item) => archivedById.get(item.id) ?? item);
|
inventory.value = inventory.value.map((item) => archivedById.get(item.id) ?? item);
|
||||||
selectedInventoryIds.value = [];
|
selectedInventoryIds.value = [];
|
||||||
|
announcement.value = `${archivedItems.length} inventory item${archivedItems.length === 1 ? '' : 's'} archived.`;
|
||||||
} catch (requestError) {
|
} catch (requestError) {
|
||||||
error.value = requestError.message;
|
error.value = requestError.message;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -61,6 +63,7 @@ async function refreshSelectedPricing() {
|
|||||||
const jobs = await refreshAdminInventoryPricing(selectedInventoryIds.value, 'fixture-provider', csrfToken());
|
const jobs = await refreshAdminInventoryPricing(selectedInventoryIds.value, 'fixture-provider', csrfToken());
|
||||||
pricingJobs.value = [...jobs, ...pricingJobs.value];
|
pricingJobs.value = [...jobs, ...pricingJobs.value];
|
||||||
selectedInventoryIds.value = [];
|
selectedInventoryIds.value = [];
|
||||||
|
announcement.value = `Pricing refresh queued for ${jobs.length} inventory item${jobs.length === 1 ? '' : 's'}.`;
|
||||||
} catch (requestError) {
|
} catch (requestError) {
|
||||||
error.value = requestError.message;
|
error.value = requestError.message;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -82,6 +85,7 @@ async function updateSelectedTags(action) {
|
|||||||
);
|
);
|
||||||
inventoryTags.value = '';
|
inventoryTags.value = '';
|
||||||
selectedInventoryIds.value = [];
|
selectedInventoryIds.value = [];
|
||||||
|
announcement.value = `Tags ${action === 'add' ? 'added to' : 'removed from'} selected inventory items.`;
|
||||||
} catch (requestError) {
|
} catch (requestError) {
|
||||||
error.value = requestError.message;
|
error.value = requestError.message;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -95,6 +99,7 @@ async function retryPricingJob(jobId) {
|
|||||||
try {
|
try {
|
||||||
await retryAdminPricingJob(jobId, csrfToken());
|
await retryAdminPricingJob(jobId, csrfToken());
|
||||||
pricingJobs.value = await listAdminPricingJobs();
|
pricingJobs.value = await listAdminPricingJobs();
|
||||||
|
announcement.value = 'Pricing job retry requested.';
|
||||||
} catch (requestError) {
|
} catch (requestError) {
|
||||||
error.value = requestError.message;
|
error.value = requestError.message;
|
||||||
} finally {
|
} finally {
|
||||||
@@ -111,6 +116,7 @@ onMounted(load);
|
|||||||
<h1>Physical inventory</h1>
|
<h1>Physical inventory</h1>
|
||||||
<p v-if="isLoading" class="state state--loading" role="status">Loading inventory...</p>
|
<p v-if="isLoading" class="state state--loading" role="status">Loading inventory...</p>
|
||||||
<p v-if="error" class="state state--error" role="alert">{{ error }}</p>
|
<p v-if="error" class="state state--error" role="alert">{{ error }}</p>
|
||||||
|
<p class="sr-only" role="status" aria-live="polite">{{ announcement }}</p>
|
||||||
<section v-if="!isLoading" class="admin-list" aria-labelledby="inventory-title">
|
<section v-if="!isLoading" class="admin-list" aria-labelledby="inventory-title">
|
||||||
<h2 id="inventory-title">All physical copies</h2>
|
<h2 id="inventory-title">All physical copies</h2>
|
||||||
<div class="admin-list__actions">
|
<div class="admin-list__actions">
|
||||||
|
|||||||
@@ -268,6 +268,7 @@ Remaining roadmap areas are approved external provider credentials/adapters, bro
|
|||||||
- `ListingDetailPage`, `ProductDetailPage`, and `AdminImagePage` now have Storybook coverage. Adding it surfaced and fixed a color-contrast bug shared by all three: the "Back to catalog" link, pagination controls, and the selected image-gallery thumbnail were using the non-text-safe `--color-interactive` token instead of `--color-interactive-text`.
|
- `ListingDetailPage`, `ProductDetailPage`, and `AdminImagePage` now have Storybook coverage. Adding it surfaced and fixed a color-contrast bug shared by all three: the "Back to catalog" link, pagination controls, and the selected image-gallery thumbnail were using the non-text-safe `--color-interactive` token instead of `--color-interactive-text`.
|
||||||
- Per-item image limits are now enforced atomically for seller image creation and uploads, using `inventory.maxImagesPerItem` (default 10); concurrent inserts are serialized on the physical inventory item and return `IMAGE_COUNT_LIMIT_REACHED` at the configured cap.
|
- Per-item image limits are now enforced atomically for seller image creation and uploads, using `inventory.maxImagesPerItem` (default 10); concurrent inserts are serialized on the physical inventory item and return `IMAGE_COUNT_LIMIT_REACHED` at the configured cap.
|
||||||
- Admin navigation now includes a keyboard-visible skip link to the main content region, with Storybook interaction coverage.
|
- Admin navigation now includes a keyboard-visible skip link to the main content region, with Storybook interaction coverage.
|
||||||
|
- Admin inventory bulk archive, pricing refresh, tag, and retry actions now announce successful outcomes through a polite live region.
|
||||||
|
|
||||||
## Local Commands
|
## Local Commands
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user