feat(uat-batch-10): copy polish, TTL trim, and a11y discrete fixes
- Supplemental-info link TTL trimmed from 30 → 14 days (single constant in supplemental-forms.service). - LinkedBerthsList toggle renamed "Mark in EOI bundle" → "Include in EOI"; tooltip aria-label updated to match. - Icon-only row-action triggers on the interest / client / berth list tables gain aria-label (Row actions for <name>) so SR users hear the row context. - Table / Board view toggle on interest list gains aria-label + aria-pressed on each variant; wrapper gets role="group". - Upcoming-milestones disclosure on interest-tabs gains aria-expanded + aria-controls; recommender Hide/Add filters button matches. - BrandedAuthShell logo alt no longer defaults to "Sign in" — uses the configured `appName` when known, empty string otherwise so screen readers don't announce "Sign in" on password-reset / set-password pages. tsc clean. 1419/1419 vitest pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -176,6 +176,7 @@ function ActionsCell({ row }: { row: { original: BerthRow } }) {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-8 w-8"
|
className="h-8 w-8"
|
||||||
|
aria-label={`Row actions for berth ${berth.mooringNumber ?? ''}`.trim()}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<MoreHorizontal className="h-4 w-4" aria-hidden />
|
<MoreHorizontal className="h-4 w-4" aria-hidden />
|
||||||
|
|||||||
@@ -302,6 +302,7 @@ export function getClientColumns({
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-7 w-7"
|
className="h-7 w-7"
|
||||||
|
aria-label={`Row actions for ${row.original.fullName ?? 'client'}`}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<MoreHorizontal className="h-4 w-4" aria-hidden />
|
<MoreHorizontal className="h-4 w-4" aria-hidden />
|
||||||
|
|||||||
@@ -503,6 +503,8 @@ export function BerthRecommenderPanel({
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => setFiltersOpen((v) => !v)}
|
onClick={() => setFiltersOpen((v) => !v)}
|
||||||
disabled={!hasDimensions}
|
disabled={!hasDimensions}
|
||||||
|
aria-expanded={filtersOpen}
|
||||||
|
aria-controls="recommender-filters-body"
|
||||||
>
|
>
|
||||||
<Filter className="mr-1.5 size-3.5" aria-hidden />
|
<Filter className="mr-1.5 size-3.5" aria-hidden />
|
||||||
{filtersOpen ? 'Hide filters' : 'Add filters'}
|
{filtersOpen ? 'Hide filters' : 'Add filters'}
|
||||||
@@ -542,7 +544,9 @@ export function BerthRecommenderPanel({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!collapsed && filtersOpen && hasDimensions ? (
|
{!collapsed && filtersOpen && hasDimensions ? (
|
||||||
|
<div id="recommender-filters-body">
|
||||||
<AmenityFilterForm filters={amenityFilters} onChange={setAmenityFilters} />
|
<AmenityFilterForm filters={amenityFilters} onChange={setAmenityFilters} />
|
||||||
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{!collapsed && hasDimensions && areaChips.length > 1 ? (
|
{!collapsed && hasDimensions && areaChips.length > 1 ? (
|
||||||
<div className="flex flex-wrap items-center gap-1.5 pt-1">
|
<div className="flex flex-wrap items-center gap-1.5 pt-1">
|
||||||
|
|||||||
@@ -297,6 +297,7 @@ export function getInterestColumns({
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-7 w-7"
|
className="h-7 w-7"
|
||||||
|
aria-label={`Row actions for ${row.original.clientName ?? 'interest'}`}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<MoreHorizontal className="h-4 w-4" aria-hidden />
|
<MoreHorizontal className="h-4 w-4" aria-hidden />
|
||||||
|
|||||||
@@ -183,11 +183,17 @@ export function InterestList() {
|
|||||||
{/* Kanban view is desktop-only — mobile drops the toggle and
|
{/* Kanban view is desktop-only — mobile drops the toggle and
|
||||||
falls back to the list/cards view (the board's column
|
falls back to the list/cards view (the board's column
|
||||||
horizontal-scroll model is unusable at phone widths). */}
|
horizontal-scroll model is unusable at phone widths). */}
|
||||||
<div className="hidden sm:flex items-center border rounded-md overflow-hidden">
|
<div
|
||||||
|
className="hidden sm:flex items-center border rounded-md overflow-hidden"
|
||||||
|
role="group"
|
||||||
|
aria-label="View mode"
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant={viewMode === 'table' ? 'default' : 'ghost'}
|
variant={viewMode === 'table' ? 'default' : 'ghost'}
|
||||||
className="rounded-none"
|
className="rounded-none"
|
||||||
|
aria-label="Table view"
|
||||||
|
aria-pressed={viewMode === 'table'}
|
||||||
onClick={() => setViewMode('table')}
|
onClick={() => setViewMode('table')}
|
||||||
>
|
>
|
||||||
<LayoutList className="h-4 w-4" aria-hidden />
|
<LayoutList className="h-4 w-4" aria-hidden />
|
||||||
@@ -196,6 +202,8 @@ export function InterestList() {
|
|||||||
size="sm"
|
size="sm"
|
||||||
variant={viewMode === 'board' ? 'default' : 'ghost'}
|
variant={viewMode === 'board' ? 'default' : 'ghost'}
|
||||||
className="rounded-none"
|
className="rounded-none"
|
||||||
|
aria-label="Board view"
|
||||||
|
aria-pressed={viewMode === 'board'}
|
||||||
onClick={() => setViewMode('board')}
|
onClick={() => setViewMode('board')}
|
||||||
>
|
>
|
||||||
<Kanban className="h-4 w-4" aria-hidden />
|
<Kanban className="h-4 w-4" aria-hidden />
|
||||||
|
|||||||
@@ -555,6 +555,8 @@ function FutureMilestones({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setExpanded((v) => !v)}
|
onClick={() => setExpanded((v) => !v)}
|
||||||
|
aria-expanded={expanded}
|
||||||
|
aria-controls="future-milestones-body"
|
||||||
className="flex w-full items-center justify-between gap-2 px-4 py-2.5 text-sm text-muted-foreground hover:text-foreground hover:bg-muted/30 transition-colors"
|
className="flex w-full items-center justify-between gap-2 px-4 py-2.5 text-sm text-muted-foreground hover:text-foreground hover:bg-muted/30 transition-colors"
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
@@ -565,6 +567,7 @@ function FutureMilestones({
|
|||||||
</button>
|
</button>
|
||||||
{expanded && (
|
{expanded && (
|
||||||
<div
|
<div
|
||||||
|
id="future-milestones-body"
|
||||||
className={cn(
|
className={cn(
|
||||||
'grid grid-cols-1 gap-4 p-4 pt-0',
|
'grid grid-cols-1 gap-4 p-4 pt-0',
|
||||||
milestones.length === 1 ? '' : 'lg:grid-cols-2',
|
milestones.length === 1 ? '' : 'lg:grid-cols-2',
|
||||||
|
|||||||
@@ -374,14 +374,14 @@ function LinkedBerthRowItem({
|
|||||||
htmlFor={`bundle-${row.berthId}`}
|
htmlFor={`bundle-${row.berthId}`}
|
||||||
className="text-sm font-medium cursor-pointer"
|
className="text-sm font-medium cursor-pointer"
|
||||||
>
|
>
|
||||||
Mark in EOI bundle
|
Include in EOI
|
||||||
</Label>
|
</Label>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="inline-flex h-5 w-5 items-center justify-center rounded-full text-muted-foreground hover:bg-muted/60 hover:text-foreground"
|
className="inline-flex h-5 w-5 items-center justify-center rounded-full text-muted-foreground hover:bg-muted/60 hover:text-foreground"
|
||||||
aria-label="What does Mark in EOI bundle do?"
|
aria-label="What does Include in EOI do?"
|
||||||
>
|
>
|
||||||
<HelpCircle className="h-3.5 w-3.5" aria-hidden />
|
<HelpCircle className="h-3.5 w-3.5" aria-hidden />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -29,7 +29,11 @@ export function BrandedAuthShell({ children, branding }: BrandedAuthShellProps)
|
|||||||
const ctx = useAuthBranding();
|
const ctx = useAuthBranding();
|
||||||
const logoUrl = branding?.logoUrl ?? ctx?.logoUrl ?? null;
|
const logoUrl = branding?.logoUrl ?? ctx?.logoUrl ?? null;
|
||||||
const backgroundUrl = branding?.backgroundUrl ?? ctx?.backgroundUrl ?? null;
|
const backgroundUrl = branding?.backgroundUrl ?? ctx?.backgroundUrl ?? null;
|
||||||
const altText = branding?.appName ?? ctx?.appName ?? 'Sign in';
|
// When no port name is known, treat the logo as decorative — "Sign in"
|
||||||
|
// as alt text was being read on every auth page even when the page
|
||||||
|
// itself isn't a sign-in surface (e.g. password reset, set-password).
|
||||||
|
const appName = branding?.appName ?? ctx?.appName ?? null;
|
||||||
|
const altText = appName ?? '';
|
||||||
// fixed inset-0 anchors the auth surface to the viewport directly —
|
// fixed inset-0 anchors the auth surface to the viewport directly —
|
||||||
// iOS Safari ignores overflow-hidden on inner divs for body-level
|
// iOS Safari ignores overflow-hidden on inner divs for body-level
|
||||||
// scrolling, so a regular `h-dvh overflow-hidden` wrapper doesn't
|
// scrolling, so a regular `h-dvh overflow-hidden` wrapper doesn't
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
} from '@/lib/db/schema';
|
} from '@/lib/db/schema';
|
||||||
import { ConflictError, NotFoundError, ValidationError } from '@/lib/errors';
|
import { ConflictError, NotFoundError, ValidationError } from '@/lib/errors';
|
||||||
|
|
||||||
const TOKEN_TTL_DAYS = 30;
|
const TOKEN_TTL_DAYS = 14;
|
||||||
const TOKEN_BYTES = 32; // 256-bit → ~43 base64url chars; brute-force infeasible.
|
const TOKEN_BYTES = 32; // 256-bit → ~43 base64url chars; brute-force infeasible.
|
||||||
|
|
||||||
function generateToken(): string {
|
function generateToken(): string {
|
||||||
|
|||||||
Reference in New Issue
Block a user