diff --git a/src/features/catalogue/CatalogueBrowser.tsx b/src/features/catalogue/CatalogueBrowser.tsx index af2ec5f..476fd55 100644 --- a/src/features/catalogue/CatalogueBrowser.tsx +++ b/src/features/catalogue/CatalogueBrowser.tsx @@ -377,18 +377,11 @@ export function CatalogueBrowser({ It stays above both columns rather than inside the rail: the search narrows the whole catalogue, and the rail only lists brands. */} - {/* Pinned to the right-hand corner, and stated inline so it cannot be - undone by a stylesheet. - - `justify="end"` was already here and already resolves to - `justify-content: flex-end` — HStack maps `justify` to the main axis, - and the rule is in the bundle — yet the row was reported rendering at - the top LEFT of the platform catalogue. A utility class landing after - the component's own is the only thing that explains that, and an - inline declaration is the one thing such a class cannot outrank. - `width` is explicit for the same reason rather than relying on the - flex default. */} - + {/* Right-hand corner. The filter toggle and the search belong at the end + of the row, matching where search sits on Sales, Reports and Products, + rather than above the brand rail where they read as the rail's own + controls instead of the whole catalogue's. */} +
-
-
+
+
diff --git a/src/features/console/sections/StoreHealth.tsx b/src/features/console/sections/StoreHealth.tsx index f4bd2ac..9832d5f 100644 --- a/src/features/console/sections/StoreHealth.tsx +++ b/src/features/console/sections/StoreHealth.tsx @@ -1,7 +1,7 @@ import type { ReactNode } from 'react'; import { Link } from 'react-router-dom'; import { Boxes, ClipboardList, Monitor, Store } from 'lucide-react'; -import { count } from '@/features/store-admin/format'; +import { count, plural } from '@/features/store-admin/format'; import type { BranchRow, ConsoleTotals, HealthTone } from '../consoleModel'; /** @@ -96,7 +96,7 @@ export function StoreHealth({ detail={ productCount === 0 ? 'No products yet, so shoppers find the shop empty' - : `${count(productCount)} products` + : `${plural(productCount, 'product')}` } tone={productCount === 0 ? 'attention' : withoutTill > 0 ? 'attention' : 'healthy'} action="Manage" diff --git a/src/features/store-admin/format.test.ts b/src/features/store-admin/format.test.ts new file mode 100644 index 0000000..e6c3638 --- /dev/null +++ b/src/features/store-admin/format.test.ts @@ -0,0 +1,35 @@ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import { plural } from './format'; + +test('one is singular, everything else is not', () => { + // The whole reason this exists: a shop whose first order just landed. + assert.equal(plural(1, 'order'), '1 order'); + assert.equal(plural(0, 'order'), '0 orders'); + assert.equal(plural(2, 'order'), '2 orders'); + assert.equal(plural(234, 'order'), '234 orders'); +}); + +test('a missing count keeps its noun', () => { + // `count` renders an em dash; "— orders" is the honest reading — the noun is + // known, the number is not. + assert.equal(plural(undefined, 'order'), '— orders'); + assert.equal(plural(null, 'bill'), '— bills'); +}); + +test('an irregular plural can be given', () => { + assert.equal(plural(1, 'entry', 'entries'), '1 entry'); + assert.equal(plural(3, 'entry', 'entries'), '3 entries'); +}); + +test('the nouns actually used on the console', () => { + assert.equal(plural(1, 'bill'), '1 bill'); + assert.equal(plural(1, 'product'), '1 product'); + assert.equal(plural(1, 'app order'), '1 app order'); + assert.equal(plural(7, 'app order'), '7 app orders'); +}); + +test('negative one reads as singular too', () => { + // Nothing produces one today, but "-1 orders" would be the same bug. + assert.equal(plural(-1, 'order'), '-1 order'); +}); diff --git a/src/features/store-admin/format.ts b/src/features/store-admin/format.ts index 52695d9..7e37526 100644 --- a/src/features/store-admin/format.ts +++ b/src/features/store-admin/format.ts @@ -38,6 +38,32 @@ export const moneyExact = (value: number | undefined | null): string => export const count = (value: number | undefined | null): string => value === undefined || value === null || !Number.isFinite(value) ? '—' : COUNT.format(value); +/** + * A count and its noun, agreeing. + * + * Every tile note on the console was a template literal with the plural typed + * into it — `${count(n)} orders` — which is right for every number except one. + * A shop with a single order read "1 orders" on the Console, on Sales and on + * Reports. It is a small thing that makes a page look untended, and it only + * shows on the quietest shops: a merchant whose first order has just landed is + * exactly who sees it. + * + * The plural is derived rather than passed, since English regulars are the + * whole of this vocabulary — orders, bills, products, stops. `many` is there + * for when that stops being true. + * + * `count` still renders `—` for a missing value, and `— orders` is the right + * reading of that: the noun is known, the number is not. + */ +export function plural( + value: number | undefined | null, + singular: string, + many?: string, +): string { + const noun = value === 1 || value === -1 ? singular : (many ?? `${singular}s`); + return `${count(value)} ${noun}`; +} + /** * A percentage of a total, guarded. * diff --git a/src/features/store-admin/offlineSales/CounterSalesDrawer.tsx b/src/features/store-admin/offlineSales/CounterSalesDrawer.tsx index e7e7251..8b756ec 100644 --- a/src/features/store-admin/offlineSales/CounterSalesDrawer.tsx +++ b/src/features/store-admin/offlineSales/CounterSalesDrawer.tsx @@ -11,7 +11,7 @@ import { Download, Upload } from 'lucide-react'; import { errorMessage } from '@/api/client'; import { offlineSalesApi, type OfflineSalesUploadResponse } from '@/api/offlineSales'; import { useAuth } from '@/auth/AuthContext'; -import { count, money } from '../format'; +import { count, money, plural } from '../format'; import { Drawer } from '../Drawer'; import { TablePager } from '@/components/TablePager'; import { usePaged } from '@/components/usePaged'; @@ -314,7 +314,7 @@ export function CounterSalesDrawer({ }} />