feat(ui): broad consistency sweep — sources, dates, comboboxes, milestones
Mobile + responsive - berth-form full-width on phones (was 480px fixed → overflowed iPhone) - currency-input switched to inputMode=decimal with live thousands separator - client-form Country/Timezone/Source/Preferred-Contact full-width <sm - contacts row restructured so Primary toggle + Remove get their own strip - customize-dashboard footer stacks vertically on mobile; Done full-width - interest-form client/berth pickers no longer cmdk-filter on UUID (typing "Carlos" now returns Carlos Vega instead of "No clients found") Data + consistency - SOURCES + SOURCE_LABELS + formatSource() in lib/constants; 9 surfaces now resolve interest/client source from one place - INTEREST_OUTCOMES adds lost_other (picker, badge, timeline) - Berth options natural-sort A1 → A2 → … → A10 via lib/utils/mooring-sort - archiver downgraded ^8 → ^7.0.1 so the GDPR export route compiles - TableBody last-row uses border-b-0 (not border-0); colored left-accent on the bottom berth row now renders - Hide Invite-to-Portal until port setting === true (was !== false default-show) - OwnerPicker primer query resolves entity name on first paint (no more UUID flash before the popover opens) Terminology - Replaced user-facing "Documenso" with "signing service" / "Generated EOI" / "Manual EOI" in 8 components (admin/internal references kept) - Plainer status-change copy on berth-detail-header Forms + editing - InlineEditableField gained a `date` variant (native picker); applied to company incorporation date and ready for other YYYY-MM-DD plaintext fields - Inline source picker on interest-tabs detail (was free text) - TagPicker self-hides when port has no tags AND nothing is selected - New ReminderDaysInput with preset chips (1d / 3d / 1wk / 2wk / 1mo / custom) - Compose dialog follow-up is now a toggle that reveals datetime picker Pipeline milestones - changeStageSchema accepts optional milestoneDate; service stamps it on the matching date column instead of always using now - MilestoneAdvanceButton popover collects a back-date before stage advance - Applied to every "Mark X manually" surface on the interest overview EOI / linked-berths polish - Add-bypass row aligned inline with toggle descriptions - Tooltips on "Specifically pitching" / "Mark in EOI bundle" explain their legal vs. public-map consequences Surfaces - Companies list now has the column picker + persisted hidden-column prefs - NotesList aggregate flag enabled on clients, companies, residential_clients (yachts already aggregated) ft/m unit toggle (interim, before drift fix) - "Berth size desired" gets a section-level ft/m toggle; per-field hint shows the converted value. Storage stays canonical-ft for now; the drift-safe persistence migration is the next step. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,7 +13,7 @@ import {
|
||||
Building2,
|
||||
Receipt,
|
||||
FileText,
|
||||
Bell,
|
||||
Inbox,
|
||||
Camera,
|
||||
Globe,
|
||||
Settings,
|
||||
@@ -156,10 +156,12 @@ function buildNavSections(portSlug: string | undefined): NavSection[] {
|
||||
title: 'Communication',
|
||||
marinaRequired: true,
|
||||
items: [
|
||||
// Email tab removed: we deferred building a full inbox/threading
|
||||
// Email tab removed: deferred building a full inbox/threading
|
||||
// feature (would require Google OAuth + scope review + IMAP
|
||||
// syncing infra). Reminders stays since it's already wired up.
|
||||
{ href: `${base}/reminders`, label: 'Reminders', icon: Bell },
|
||||
// syncing infra). This entry routes to the merged
|
||||
// Alerts + Reminders surface (2026-05-11) — explicit name so
|
||||
// reps don't mistake it for an email inbox.
|
||||
{ href: `${base}/inbox`, label: 'Alerts & Reminders', icon: Inbox },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -188,8 +190,8 @@ function NavItemLink({
|
||||
href={item.href as any}
|
||||
className={cn(
|
||||
'relative flex items-center gap-3 px-3 py-2 rounded-md text-sm font-medium transition-all duration-150',
|
||||
'text-[#cdcfd6] hover:bg-[#171f35] hover:text-white',
|
||||
active && 'text-white pl-[14px]',
|
||||
'text-slate-700 hover:bg-accent hover:text-foreground',
|
||||
active && 'bg-accent text-foreground pl-[14px]',
|
||||
collapsed && 'justify-center px-2',
|
||||
)}
|
||||
>
|
||||
@@ -202,7 +204,7 @@ function NavItemLink({
|
||||
<item.icon
|
||||
className={cn(
|
||||
'shrink-0',
|
||||
active ? 'text-[#3a7bc8]' : 'text-[#83aab1]',
|
||||
active ? 'text-[#3a7bc8]' : 'text-slate-500',
|
||||
collapsed ? 'w-5 h-5' : 'w-4 h-4',
|
||||
)}
|
||||
/>
|
||||
@@ -252,7 +254,7 @@ function SidebarContent({
|
||||
const [adminExpanded, setAdminExpanded] = useState(true);
|
||||
const sections = buildNavSections(portSlug);
|
||||
const umami = useUmamiActive('today');
|
||||
const umamiConfigured = umami.data?.error !== 'umami_not_configured';
|
||||
const umamiConfigured = !umami.isLoading && umami.data?.notConfigured !== true;
|
||||
|
||||
// Small label under the user identity when the user has access to more
|
||||
// than one port — disambiguates which port is currently active without
|
||||
@@ -283,12 +285,13 @@ function SidebarContent({
|
||||
// compete with the logo for attention.
|
||||
return (
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<div className="flex flex-col h-full bg-[#1e2844]">
|
||||
{/* Brand header - logo centered (large when expanded, smaller when
|
||||
collapsed). Collapse toggle floats top-right as a tiny chevron. */}
|
||||
<div className="flex flex-col h-full bg-white">
|
||||
{/* Brand header - logo centered. Soft hairline below echoes the
|
||||
inter-section separators in the nav so the eye reads the logo
|
||||
as a distinct top-row, not a floating element. */}
|
||||
<div
|
||||
className={cn(
|
||||
'relative flex items-center justify-center border-b border-[#474e66]',
|
||||
'relative flex items-center justify-center border-b border-slate-200',
|
||||
collapsed ? 'h-16 px-2' : 'h-24 px-4',
|
||||
)}
|
||||
>
|
||||
@@ -297,7 +300,7 @@ function SidebarContent({
|
||||
alt="Port Nimara"
|
||||
width={collapsed ? 40 : 72}
|
||||
height={collapsed ? 40 : 72}
|
||||
className="rounded-full shadow-md ring-2 ring-white/20"
|
||||
className="rounded-full shadow-sm"
|
||||
unoptimized
|
||||
priority
|
||||
/>
|
||||
@@ -307,7 +310,7 @@ function SidebarContent({
|
||||
onClick={onToggleCollapse}
|
||||
aria-label={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
||||
className={cn(
|
||||
'absolute right-2 flex h-6 w-6 items-center justify-center rounded-md text-[#83aab1] hover:bg-[#171f35] hover:text-white transition-colors',
|
||||
'absolute right-2 flex h-6 w-6 items-center justify-center rounded-md text-slate-400 hover:bg-slate-100 hover:text-slate-700 transition-colors',
|
||||
collapsed ? 'top-1' : 'top-2',
|
||||
)}
|
||||
>
|
||||
@@ -333,13 +336,13 @@ function SidebarContent({
|
||||
<div key={section.title}>
|
||||
{!collapsed && (
|
||||
<div className="flex items-center justify-between px-1 mb-1">
|
||||
<span className="text-[#83aab1] text-[10px] font-semibold uppercase tracking-[0.12em]">
|
||||
<span className="text-slate-500 text-[10px] font-semibold uppercase tracking-[0.12em]">
|
||||
{section.title}
|
||||
</span>
|
||||
{section.adminRequired && (
|
||||
<button
|
||||
onClick={() => setAdminExpanded((v) => !v)}
|
||||
className="text-[#71768a] hover:text-[#cdcfd6] transition-colors"
|
||||
className="text-slate-400 hover:text-slate-700 transition-colors"
|
||||
>
|
||||
{adminExpanded ? (
|
||||
<ChevronUp className="w-3 h-3" />
|
||||
@@ -363,7 +366,7 @@ function SidebarContent({
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
<Separator className="mt-3 bg-[#474e66]/50" />
|
||||
<Separator className="mt-3 bg-slate-200" />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -374,7 +377,7 @@ function SidebarContent({
|
||||
user can click their name/avatar to access Profile / Settings /
|
||||
port-switcher / sign-out. The same UserMenu component drives the
|
||||
top-right avatar dropdown, so the menu items stay consistent. */}
|
||||
<div className={cn('border-t border-[#474e66] p-2', collapsed && 'flex justify-center')}>
|
||||
<div className={cn('border-t border-slate-200 p-2', collapsed && 'flex justify-center')}>
|
||||
{collapsed ? (
|
||||
<UserMenu
|
||||
align="start"
|
||||
@@ -384,7 +387,7 @@ function SidebarContent({
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Open user menu"
|
||||
className="rounded-full focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#3a7bc8] focus-visible:ring-offset-2 focus-visible:ring-offset-[#1e2844]"
|
||||
className="rounded-full focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#3a7bc8] focus-visible:ring-offset-2 focus-visible:ring-offset-white"
|
||||
>
|
||||
<Avatar className="w-8 h-8 cursor-pointer">
|
||||
<AvatarImage src={undefined} />
|
||||
@@ -404,26 +407,26 @@ function SidebarContent({
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Open user menu"
|
||||
className="flex w-full items-center gap-3 rounded-md p-1.5 text-left transition-colors hover:bg-[#171f35] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#3a7bc8] focus-visible:ring-offset-2 focus-visible:ring-offset-[#1e2844]"
|
||||
className="flex w-full items-center gap-3 rounded-md p-1.5 text-left transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#3a7bc8] focus-visible:ring-offset-2 focus-visible:ring-offset-white"
|
||||
>
|
||||
<Avatar className="w-8 h-8 shrink-0 shadow-sm ring-2 ring-white/30">
|
||||
<Avatar className="w-8 h-8 shrink-0 shadow-sm ring-2 ring-slate-200">
|
||||
<AvatarImage src={undefined} />
|
||||
<AvatarFallback className="bg-[#3a7bc8] text-white text-xs font-semibold">
|
||||
{(user?.name ?? 'U').slice(0, 1).toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-white text-sm font-medium truncate">
|
||||
<p className="text-foreground text-sm font-medium truncate">
|
||||
{user?.name ?? 'User'}
|
||||
</p>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-[10px] px-1.5 py-0 text-[#83aab1] border-[#474e66] mt-0.5"
|
||||
className="text-[10px] px-1.5 py-0 text-slate-500 border-slate-300 mt-0.5"
|
||||
>
|
||||
{isSuperAdmin ? 'Super Admin' : humanizeRole(portRoles[0]?.role?.name)}
|
||||
</Badge>
|
||||
{currentPortName && (
|
||||
<p className="mt-1 text-[10px] text-[#71768a] truncate">{currentPortName}</p>
|
||||
<p className="mt-1 text-[10px] text-slate-400 truncate">{currentPortName}</p>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
@@ -461,10 +464,9 @@ export function Sidebar({ portRoles, isSuperAdmin = false, user, ports }: Sideba
|
||||
return (
|
||||
<aside
|
||||
className={cn(
|
||||
'relative hidden md:flex flex-col h-screen border-r border-[#474e66] transition-all duration-200 ease-in-out shrink-0',
|
||||
'relative hidden md:flex flex-col h-screen border-r border-slate-200 transition-all duration-200 ease-in-out shrink-0 bg-white',
|
||||
sidebarCollapsed ? 'w-sidebar-collapsed' : 'w-sidebar',
|
||||
)}
|
||||
style={{ backgroundColor: '#1e2844' }}
|
||||
>
|
||||
<SidebarContent
|
||||
collapsed={sidebarCollapsed}
|
||||
|
||||
Reference in New Issue
Block a user