chore(autonomous-session): consolidate uncommitted work from prior session
Bundles the prior autonomous-session output that was sitting unstaged: - Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances) - country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk after the per-subpath dynamic-import approach silently failed in webpack) - Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index, redirects (ocr to ai, reports to dashboard, invitations to users), docs/admin-ia-proposal.md - Per-template email tester (registry + endpoint + UI on Email admin page) - Cancel-document mode picker (delete-from-Documenso vs keep-for-audit) - Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers - Customize-widgets per-region sortables at xl+ (charts/rails/feed); single flat sortable below xl when the layout stacks; per-viewport saved orders - Audit doc updates capturing each shipped item - Lint fixes: react-compiler immutability in DonutChart (reduce instead of let-reassign), set-state-in-effect disables in CountryFlag and UploadForSigning preview-bytes effect, unused 'confirm' destructures in interest contract + reservation tabs, unescaped apostrophe in test-template card copy
This commit is contained in:
@@ -79,7 +79,7 @@ const BUCKETS: BucketConfig[] = [
|
||||
{ type: 'stages', label: 'Stages', icon: TrendingUp },
|
||||
// Notes are noisy content search.
|
||||
{ type: 'notes', label: 'Notes', icon: MessageSquare },
|
||||
// Navigation (settings pages + admin sub-cards) lives at the very bottom —
|
||||
// Navigation (settings pages + admin sub-cards) lives at the very bottom -
|
||||
// users open the search to find entity records first; pages/settings are
|
||||
// the long-tail jump targets.
|
||||
{ type: 'navigation', label: 'Settings', icon: SettingsIcon },
|
||||
@@ -119,7 +119,7 @@ export function CommandSearch() {
|
||||
|
||||
const { results, isFetching, recentSearches, recentlyViewed } = useSearch(query, {
|
||||
type: activeBucket === 'all' ? undefined : activeBucket,
|
||||
// Slightly higher cap when narrowed to one bucket — gives the user
|
||||
// Slightly higher cap when narrowed to one bucket - gives the user
|
||||
// room to scan more matches without paging out to /search.
|
||||
limit: activeBucket === 'all' ? 5 : 15,
|
||||
});
|
||||
@@ -127,9 +127,9 @@ export function CommandSearch() {
|
||||
// Persist the totals from the last "all" query so the filter chips stay
|
||||
// populated when the user narrows to a single bucket. Without this, the
|
||||
// narrowed query only returns counts for the active bucket and every
|
||||
// other chip would vanish — making it impossible to swap between
|
||||
// other chip would vanish - making it impossible to swap between
|
||||
// filters without clearing back to "All" first. Backed by state (not
|
||||
// a ref) so render reads are pure — React Compiler-safe.
|
||||
// a ref) so render reads are pure - React Compiler-safe.
|
||||
const [lastAllTotals, setLastAllTotals] = useState<SearchResults['totals'] | null>(null);
|
||||
useEffect(() => {
|
||||
if (activeBucket === 'all' && results?.totals) {
|
||||
@@ -208,7 +208,7 @@ export function CommandSearch() {
|
||||
closeAndNavigate(res.href);
|
||||
}
|
||||
} catch {
|
||||
// Best-effort — fall through to normal search.
|
||||
// Best-effort - fall through to normal search.
|
||||
}
|
||||
},
|
||||
[closeAndNavigate],
|
||||
@@ -229,7 +229,7 @@ export function CommandSearch() {
|
||||
}, [showDropdown, query, results, recentlyViewed, recentSearches, activeBucket, portSlug]);
|
||||
|
||||
// Reset focus index when the visible row set changes. The set-state
|
||||
// is intentional — external state (bucket / query) drives focus.
|
||||
// is intentional - external state (bucket / query) drives focus.
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setFocusIndex(-1);
|
||||
@@ -263,7 +263,7 @@ export function CommandSearch() {
|
||||
}
|
||||
closeAndNavigate(row.href);
|
||||
}
|
||||
// Enter without a focused row is a no-op — the dropdown is already
|
||||
// Enter without a focused row is a no-op - the dropdown is already
|
||||
// showing every relevant match (filter chips raise the cap when the
|
||||
// user wants to see more of one bucket). No standalone /search
|
||||
// page; refining the query is faster than scrolling further.
|
||||
@@ -326,7 +326,7 @@ export function CommandSearch() {
|
||||
'max-lg:fixed max-lg:inset-x-2 max-lg:top-16 max-lg:bottom-2 max-lg:max-w-none',
|
||||
)}
|
||||
>
|
||||
{/* Filter chip row — always visible while the dropdown is open. */}
|
||||
{/* Filter chip row - always visible while the dropdown is open. */}
|
||||
<FilterChipRow
|
||||
totals={chipTotals}
|
||||
active={activeBucket}
|
||||
@@ -335,7 +335,7 @@ export function CommandSearch() {
|
||||
/>
|
||||
|
||||
<div className="max-h-[60vh] max-lg:max-h-[calc(100%-3rem)] overflow-y-auto py-1">
|
||||
{/* No query yet — recently viewed + recent terms. */}
|
||||
{/* No query yet - recently viewed + recent terms. */}
|
||||
{query.length < 2 && (
|
||||
<EmptyStateBeforeSearch
|
||||
listboxId={listboxId}
|
||||
@@ -348,7 +348,7 @@ export function CommandSearch() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Active query — results or zero-state. */}
|
||||
{/* Active query - results or zero-state. */}
|
||||
{query.length >= 2 && (
|
||||
<ResultsRegion
|
||||
listboxId={listboxId}
|
||||
@@ -363,7 +363,7 @@ export function CommandSearch() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer — keyboard hint only. */}
|
||||
{/* Footer - keyboard hint only. */}
|
||||
{query.length >= 2 && (
|
||||
<div className="border-t bg-muted/30 px-3 py-2 text-xs text-muted-foreground">
|
||||
↑↓ navigate · ↵ open · esc close
|
||||
@@ -408,7 +408,7 @@ function FilterChipRow({
|
||||
{BUCKETS.map((b) => {
|
||||
const count = totals?.[b.type] ?? 0;
|
||||
// Hide chips for buckets with zero matches in the last "all"
|
||||
// snapshot — keeps the row tight and avoids dead-end clicks.
|
||||
// snapshot - keeps the row tight and avoids dead-end clicks.
|
||||
// Always show the active chip + every chip before a query has run.
|
||||
if (!disabled && count === 0 && active !== b.type) return null;
|
||||
return (
|
||||
@@ -1145,7 +1145,7 @@ export function buildFlatRows(args: BuildFlatRowsArgs): FlatRow[] {
|
||||
});
|
||||
}
|
||||
}
|
||||
// Notes — content matches inside free-text notes are noisy by nature, so
|
||||
// Notes - content matches inside free-text notes are noisy by nature, so
|
||||
// the user sees them after the entity-specific buckets above have
|
||||
// surfaced their tighter matches.
|
||||
if (include('notes')) {
|
||||
@@ -1169,7 +1169,7 @@ export function buildFlatRows(args: BuildFlatRowsArgs): FlatRow[] {
|
||||
});
|
||||
}
|
||||
}
|
||||
// Navigation (settings pages + admin section cards) goes LAST — these
|
||||
// Navigation (settings pages + admin section cards) goes LAST - these
|
||||
// are jump targets, not the primary thing a user opens the search to
|
||||
// find. Surfacing them after entity matches keeps the top of the
|
||||
// dropdown focused on records.
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Fragment, type ReactNode } from 'react';
|
||||
* can see why a result matched. Case-insensitive, escapes regex meta-
|
||||
* chars in the query so a paste of "INV-2025" doesn't blow up.
|
||||
*
|
||||
* Tokenized matching — splits the query on whitespace and highlights
|
||||
* Tokenized matching - splits the query on whitespace and highlights
|
||||
* each token independently, so "joh smi" highlights both "Joh" and
|
||||
* "Smi" in "John Smith". Mirrors the prefix-tsquery the server uses.
|
||||
*/
|
||||
|
||||
@@ -13,7 +13,7 @@ import { useUIStore } from '@/stores/ui-store';
|
||||
import { buildFlatRows, type FlatRow } from './command-search';
|
||||
import { HighlightMatch } from './highlight-match';
|
||||
|
||||
// Match the desktop bucket order — feels consistent when reps switch contexts.
|
||||
// Match the desktop bucket order - feels consistent when reps switch contexts.
|
||||
const BUCKETS: { type: BucketType; label: string }[] = [
|
||||
{ type: 'clients', label: 'Clients' },
|
||||
{ type: 'yachts', label: 'Yachts' },
|
||||
@@ -59,7 +59,7 @@ export function MobileSearchOverlay({ open, onOpenChange }: MobileSearchOverlayP
|
||||
|
||||
// The overlay is mounted once at the layout root, so the recently-
|
||||
// viewed query won't refetch via the usual mount path. Bump it every
|
||||
// time the drawer opens — the user is about to look at it, and the
|
||||
// time the drawer opens - the user is about to look at it, and the
|
||||
// staleTime cache may have missed an entity view that happened in a
|
||||
// route that doesn't render <TrackEntityView>.
|
||||
useEffect(() => {
|
||||
@@ -76,7 +76,7 @@ export function MobileSearchOverlay({ open, onOpenChange }: MobileSearchOverlayP
|
||||
}
|
||||
const vv = window.visualViewport;
|
||||
if (!vv) return;
|
||||
// Subscribing to the visualViewport external store — canonical
|
||||
// Subscribing to the visualViewport external store - canonical
|
||||
// useEffect+setState shape for that pattern.
|
||||
const update = () => setVisibleHeight(vv.height);
|
||||
update();
|
||||
@@ -112,7 +112,7 @@ export function MobileSearchOverlay({ open, onOpenChange }: MobileSearchOverlayP
|
||||
// `autoFocus` attribute (synchronous in-gesture, which iOS Safari
|
||||
// requires before it'll pop the keyboard on programmatic focus).
|
||||
// A useEffect setTimeout was the previous approach but broke the
|
||||
// user-gesture chain — input was focused, keyboard stayed hidden.
|
||||
// user-gesture chain - input was focused, keyboard stayed hidden.
|
||||
|
||||
// Body scroll lock is delegated to Vaul (modal=true + noBodyStyles=false
|
||||
// defaults). Manual position:fixed locking caused a visible scroll-then-
|
||||
@@ -124,7 +124,7 @@ export function MobileSearchOverlay({ open, onOpenChange }: MobileSearchOverlayP
|
||||
|
||||
// Reset query when the drawer closes. Without this, reopening the
|
||||
// overlay would flash stale results before the empty state renders.
|
||||
// setState in effect is intentional — the trigger is a discrete
|
||||
// setState in effect is intentional - the trigger is a discrete
|
||||
// open/close transition.
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
@@ -162,7 +162,7 @@ export function MobileSearchOverlay({ open, onOpenChange }: MobileSearchOverlayP
|
||||
navigate(res.href);
|
||||
}
|
||||
} catch {
|
||||
// Best-effort — fall through to text search.
|
||||
// Best-effort - fall through to text search.
|
||||
}
|
||||
},
|
||||
[navigate],
|
||||
@@ -195,7 +195,7 @@ export function MobileSearchOverlay({ open, onOpenChange }: MobileSearchOverlayP
|
||||
// - shouldScaleBackground=false: page doesn't shrink behind us;
|
||||
// feels in-app rather than card-over-page.
|
||||
// - repositionInputs=false: don't let Vaul jiggle the viewport
|
||||
// when the input autofocuses and the keyboard appears — that
|
||||
// when the input autofocuses and the keyboard appears - that
|
||||
// was the source of the "scroll then jump back" we were seeing.
|
||||
// Vaul still locks scroll via its modal=true default.
|
||||
// - autoFocus=true: Vaul focuses the input synchronously inside
|
||||
@@ -212,7 +212,7 @@ export function MobileSearchOverlay({ open, onOpenChange }: MobileSearchOverlayP
|
||||
// `bottom: 0` on position:fixed inconsistently when the
|
||||
// keyboard is up (sometimes layout viewport, sometimes visual);
|
||||
// anchoring by top + height removes that ambiguity. Height
|
||||
// comes from visualViewport.height — the only iOS-reliable
|
||||
// comes from visualViewport.height - the only iOS-reliable
|
||||
// source for "visible area above keyboard". 12px gap at the
|
||||
// top keeps a strip of backdrop visible.
|
||||
style={
|
||||
@@ -403,7 +403,7 @@ function RowList({
|
||||
onSelect: (href: string) => void;
|
||||
variant: 'empty' | 'results';
|
||||
}) {
|
||||
// Split rows by section header — "Recently viewed", "Recent searches",
|
||||
// Split rows by section header - "Recently viewed", "Recent searches",
|
||||
// "Results". Headers live inside the row list so they scroll with their
|
||||
// content (instead of sticky-positioning, which adds visual noise).
|
||||
const recentViews = rows.filter((r) => r.kind === 'recent-view');
|
||||
@@ -437,7 +437,7 @@ function RowList({
|
||||
type="button"
|
||||
onClick={() => {
|
||||
// Recent-term taps populate the input rather than
|
||||
// navigating — the rep usually wants to refine, not
|
||||
// navigating - the rep usually wants to refine, not
|
||||
// jump straight back to the previous result.
|
||||
const input = document.querySelector<HTMLInputElement>(
|
||||
'input[aria-label="Search"]',
|
||||
@@ -469,7 +469,7 @@ function RowList({
|
||||
* Walk the flat result rows, inserting a small section header above the
|
||||
* first row of each bucket so reps know exactly what kind of entity
|
||||
* each result points to ("CLIENTS", "INTERESTS", "BERTHS", …). Bucket
|
||||
* order follows `buildFlatRows`'s ordering — most-likely matches first.
|
||||
* order follows `buildFlatRows`'s ordering - most-likely matches first.
|
||||
*/
|
||||
function renderResultRows(
|
||||
rows: FlatRow[],
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useTrackEntityView } from '@/hooks/use-track-entity-view';
|
||||
* Render-only client component that records "the user opened this
|
||||
* entity" via the global-search recently-viewed Redis log. Drop into a
|
||||
* server-rendered detail page as `<TrackEntityView type="client" id={…} />`
|
||||
* — the component renders nothing.
|
||||
* - the component renders nothing.
|
||||
*
|
||||
* Centralises the tracking call so future changes (debounce, schema,
|
||||
* batching) only need to touch one file rather than every detail page.
|
||||
|
||||
Reference in New Issue
Block a user