feat: Add trip cloning from past voyages, PWA manifest, and batch pack/share actions

This commit is contained in:
airent106 committed 2026-10-15 11:21:19 -06:00
1 parent d275808711
commit 253b7f3329
5 files changed
+440 -62

No files matched your search

+7 -1
View File
@@ -2,8 +2,14 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><rect width='70' height='70' x='15' y='15' fill='%23FFE279' rx='4' transform='rotate(6 50 50)'/><rect width='65' height='65' x='18' y='18' fill='%23FFF9A6' rx='3' transform='rotate(-4 50 50)'/></svg>" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="theme-color" content="#141518" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Loadout" />
<link rel="manifest" href="/manifest.webmanifest" />
<link rel="apple-touch-icon" href="/icon.svg" />
<link rel="icon" type="image/svg+xml" href="/icon.svg" />
<title>LOADOUT // Travel Manifest & Bag Allocation</title>
<!-- Google Fonts for retro travel ephemera, boarding pass typography, and tactile monospace -->
<link rel="preconnect" href="https://fonts.googleapis.com">
+49
View File
@@ -0,0 +1,49 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Loadout">
<defs>
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#1E2633"/>
<stop offset="100%" stop-color="#0F141C"/>
</linearGradient>
<linearGradient id="luggage" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#FF5A26"/>
<stop offset="100%" stop-color="#D93510"/>
</linearGradient>
<linearGradient id="tag" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#FFE853"/>
<stop offset="100%" stop-color="#FFC700"/>
</linearGradient>
</defs>
<rect width="64" height="64" rx="16" fill="url(#bg)"/>
<rect x="1" y="1" width="62" height="62" rx="15" fill="none" stroke="#FFFFFF" stroke-opacity="0.08" stroke-width="1"/>
<!-- Luggage Handle -->
<path d="M25 21 V15 C25 13.5 26.5 12 28 12 H36 C37.5 12 39 13.5 39 15 V21" fill="none" stroke="#CBD5E1" stroke-width="3" stroke-linecap="round"/>
<rect x="23" y="19" width="4" height="4" rx="1" fill="#475569"/>
<rect x="37" y="19" width="4" height="4" rx="1" fill="#475569"/>
<!-- Suitcase Body -->
<rect x="13" y="21" width="38" height="29" rx="5" fill="url(#luggage)"/>
<!-- Suitcase Accents & Straps -->
<rect x="13" y="33" width="38" height="4" fill="#872205" opacity="0.6"/>
<rect x="22" y="21" width="3" height="29" fill="#1E2633" opacity="0.45"/>
<rect x="39" y="21" width="3" height="29" fill="#1E2633" opacity="0.45"/>
<!-- Wheels -->
<circle cx="20" cy="51" r="2.5" fill="#0F172A"/>
<circle cx="44" cy="51" r="2.5" fill="#0F172A"/>
<!-- Dangling Baggage Tag -->
<g transform="rotate(-15 36 21)">
<path d="M37 17 L37 23" stroke="#FFEBA8" stroke-width="1.3" stroke-linecap="round"/>
<polygon points="32,23 42,23 45,38 29,38" fill="url(#tag)"/>
<circle cx="37" cy="25.5" r="1.5" fill="#8B4513"/>
<line x1="32" y1="30" x2="42" y2="30" stroke="#1E2633" stroke-width="1.2" stroke-dasharray="2 1 1 1.5 2"/>
<line x1="33" y1="33" x2="41" y2="33" stroke="#1E2633" stroke-width="1.2" stroke-dasharray="1 1.5 2 1"/>
<line x1="33" y1="36" x2="41" y2="36" stroke="#1E2633" stroke-width="0.8"/>
</g>
<!-- Stamped Verified Badge -->
<circle cx="48" cy="46" r="6.5" fill="#FFD100" stroke="#1E2633" stroke-width="1"/>
<path d="M45.5 46.2 L47.2 47.8 L50.5 44.5" fill="none" stroke="#141518" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

+17
View File
@@ -0,0 +1,17 @@
{
"name": "LOADOUT // Travel Manifest & Bag Allocation",
"short_name": "Loadout",
"start_url": "/",
"display": "standalone",
"background_color": "#F4F1EA",
"theme_color": "#141518",
"description": "Tactile travel packing manifest and luggage allocation system.",
"icons": [
{
"src": "/icon.svg",
"sizes": "64x64",
"type": "image/svg+xml",
"purpose": "any maskable"
}
]
}
+155 -25
View File
@@ -32,7 +32,9 @@ import {
RotateCcw,
Plane,
List,
LayoutList
LayoutList,
Copy,
CheckCheck
} from 'lucide-react';
const CATEGORY_TABS: { id: Category | 'all'; label: string }[] = [
@@ -55,6 +57,8 @@ export function App() {
});
const [settings, setSettings] = useState<AppSettings>(StorageClient.getSettings());
const [compactView, setCompactView] = useState<boolean>(false);
const [cloneSourceTripId, setCloneSourceTripId] = useState<string | null>(null);
const [toastMessage, setToastMessage] = useState<string | null>(null);
// UI Filter states
const [selectedBag, setSelectedBag] = useState<BagType | 'all'>('all');
@@ -74,6 +78,11 @@ export function App() {
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
const [settingsTab, setSettingsTab] = useState<'general' | 'bags' | 'archetypes'>('general');
const showToast = (msg: string) => {
setToastMessage(msg);
setTimeout(() => setToastMessage(null), 2800);
};
// Load initial data
useEffect(() => {
async function loadData() {
@@ -115,6 +124,7 @@ export function App() {
setTrips(updatedTrips);
setActiveTripId(trip.id);
StorageClient.setActiveTripId(trip.id);
showToast(`Voyage "${trip.title}" manifest issued!`);
};
const handleDeleteTrip = async (tripId: string) => {
@@ -126,6 +136,14 @@ export function App() {
if (activeTripId === tripId) {
setActiveTripId(updatedTrips[0]?.id || null);
}
showToast('Voyage manifest deleted');
};
const handleCloneCurrentTrip = () => {
if (!activeTrip) return;
sound.playClick();
setCloneSourceTripId(activeTrip.id);
setIsTripModalOpen(true);
};
// Item actions
@@ -192,6 +210,55 @@ export function App() {
setNewItemName('');
};
const handleBatchTogglePacked = async (packedState: boolean) => {
if (!activeTrip || filteredItems.length === 0) return;
sound.playStamp();
const targetItemIds = new Set(filteredItems.map((i) => i.id));
const updatedItems = activeTrip.items.map((item) =>
targetItemIds.has(item.id) ? { ...item, packed: packedState } : item
);
const updatedTrip = { ...activeTrip, items: updatedItems };
await StorageClient.saveTrip(updatedTrip);
setTrips((prev) => prev.map((t) => (t.id === activeTrip.id ? updatedTrip : t)));
showToast(
packedState
? `Marked ${targetItemIds.size} item(s) as stowed!`
: `Reset ${targetItemIds.size} item(s) to unpacked.`
);
};
const handleCopyTextManifest = () => {
if (!activeTrip) return;
sound.playClick();
let text = `LOADOUT MANIFEST // ${activeTrip.routeCode}: ${activeTrip.title}\n`;
text += `Destination: ${activeTrip.destination} • ${activeTrip.durationDays}D / ${activeTrip.durationNights}N • Archetype: ${activeTrip.tripType.toUpperCase()}\n`;
text += `==========================================================\n\n`;
for (const bag of settings.customBags) {
const bagItems = activeTrip.items.filter((i) => i.bag === bag.id);
if (bagItems.length > 0) {
text += `[${bag.tagCode}] ${bag.label} (${bag.sub}):\n`;
for (const item of bagItems) {
const check = item.packed ? '[✓]' : '[ ]';
text += ` ${check} ${item.name} (${item.quantity}x)${item.essential ? ' [ESSENTIAL]' : ''}${
item.notes ? ` — Note: ${item.notes}` : ''
}\n`;
}
text += `\n`;
}
}
navigator.clipboard
.writeText(text)
.then(() => {
showToast('Text manifest copied to clipboard!');
})
.catch(() => {
showToast('Could not copy to clipboard.');
});
};
const handleSaveDebrief = async (debrief: TripDebrief) => {
if (!activeTrip) return;
const updated = await StorageClient.recordDebrief(activeTrip.id, debrief);
@@ -199,6 +266,7 @@ export function App() {
setTrips((prev) => prev.map((t) => (t.id === updated.id ? updated : t)));
const refreshedMemory = await StorageClient.getMemory();
setMemory(refreshedMemory);
showToast('Debrief saved & engine memory updated!');
}
};
@@ -243,12 +311,23 @@ export function App() {
return (
<div className="min-h-screen bg-[#F4F1EA] text-[#141518] flex flex-col font-sans">
{/* Toast Feedback Notification */}
{toastMessage && (
<div className="fixed bottom-5 right-5 z-50 bg-ink text-ticket-100 border-2 border-canary px-4 py-2 font-mono text-xs font-bold shadow-card-hard animate-in fade-in slide-in-from-bottom-2 duration-150 flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-canary animate-ping" />
<span>{toastMessage}</span>
</div>
)}
{/* Boarding Pass Telemetry Header */}
<BoardingHeader
activeTrip={activeTrip}
trips={trips}
onSelectTrip={handleSelectTrip}
onNewTrip={() => setIsTripModalOpen(true)}
onNewTrip={() => {
setCloneSourceTripId(null);
setIsTripModalOpen(true);
}}
onOpenDebrief={() => setIsDebriefModalOpen(true)}
onOpenMemory={() => setIsMemoryOpen(true)}
onOpenSettings={() => {
@@ -432,39 +511,87 @@ export function App() {
</div>
</div>
{/* Manifest Item Count & Bag Filter Notice */}
<div className="flex items-center justify-between text-xs font-mono text-ink-muted mb-3">
<span>
DISPLAYING {filteredItems.length} ITEM(S)
{selectedBag !== 'all' && (
<span className="text-gate-orange font-bold ml-1">
(FILTERED TO {selectedBag.toUpperCase()})
</span>
)}
{compactView && (
<span className="text-blue-700 font-bold ml-1">
[COMPACT MODE]
</span>
)}
</span>
{/* Manifest Item Count, Batch Actions & Toolbar */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between text-xs font-mono text-ink-muted mb-3 gap-2">
<div className="flex items-center gap-2 flex-wrap">
<span>
DISPLAYING {filteredItems.length} ITEM(S)
{selectedBag !== 'all' && (
<span className="text-gate-orange font-bold ml-1">
(FILTERED TO {selectedBag.toUpperCase()})
</span>
)}
{compactView && (
<span className="text-blue-700 font-bold ml-1">
[COMPACT MODE]
</span>
)}
</span>
<div className="flex items-center gap-3">
{/* Batch Stow / Unpack Buttons */}
{filteredItems.length > 0 && (
<div className="flex items-center gap-1 ml-1">
<button
type="button"
onClick={() => handleBatchTogglePacked(true)}
className="px-1.5 py-0.5 border border-ink bg-white hover:bg-stamp-green-light text-stamp-green font-bold flex items-center gap-1 shadow-xs text-[10px]"
title="Mark all currently displayed items as stowed"
>
<CheckCheck size={11} />
<span>PACK ALL</span>
</button>
<button
type="button"
onClick={() => handleBatchTogglePacked(false)}
className="px-1.5 py-0.5 border border-ink bg-white hover:bg-ticket-200 text-ink-muted font-bold flex items-center gap-1 shadow-xs text-[10px]"
title="Reset all displayed items to unpacked"
>
<RotateCcw size={11} />
<span>UNPACK ALL</span>
</button>
</div>
)}
</div>
<div className="flex items-center gap-2 sm:gap-3 flex-wrap">
<button
onClick={() => window.print()}
className="hover:text-ink flex items-center gap-1 font-bold"
title="Print paper boarding manifest"
type="button"
onClick={handleCloneCurrentTrip}
className="hover:text-ink flex items-center gap-1 font-bold text-ink"
title="Use this trip's packing list as the basis for a new voyage"
>
<Printer size={13} />
<span>PRINT MANIFEST</span>
<Copy size={12} />
<span>CLONE TO NEW VOYAGE</span>
</button>
<span>•</span>
<button
type="button"
onClick={handleCopyTextManifest}
className="hover:text-ink flex items-center gap-1 font-bold text-ink"
title="Copy formatted text packing list to clipboard"
>
<Share2 size={12} />
<span>SHARE / COPY TEXT</span>
</button>
<span>•</span>
<button
type="button"
onClick={() => window.print()}
className="hover:text-ink flex items-center gap-1 font-bold text-ink"
title="Print paper boarding manifest"
>
<Printer size={12} />
<span className="hidden sm:inline">PRINT</span>
</button>
<span>•</span>
<button
type="button"
onClick={() => handleDeleteTrip(activeTrip.id)}
className="hover:text-stamp-red flex items-center gap-1 font-bold text-stamp-red/70"
title="Delete voyage"
>
<Trash2 size={13} />
<span>DELETE VOYAGE</span>
<Trash2 size={12} />
<span className="hidden sm:inline">DELETE</span>
</button>
</div>
</div>
@@ -511,6 +638,7 @@ export function App() {
<button
onClick={() => {
sound.playClick();
setCloneSourceTripId(null);
setIsTripModalOpen(true);
}}
className="bg-canary hover:bg-canary-hover text-ink font-grotesk font-bold text-sm uppercase px-6 py-3 border-2 border-ink shadow-card-hard active:translate-x-0.5 active:translate-y-0.5 inline-flex items-center gap-2"
@@ -541,6 +669,8 @@ export function App() {
setSettingsTab('archetypes');
setIsSettingsOpen(true);
}}
trips={trips}
initialCloneTripId={cloneSourceTripId}
/>
{activeTrip && (
+212 -36
View File
@@ -1,8 +1,21 @@
import React, { useState } from 'react';
import { TripType, Climate, LuggageConfig, Trip, PackMemory, CustomArchetype } from '../types';
import React, { useState, useEffect } from 'react';
import { TripType, Climate, LuggageConfig, Trip, PackMemory, CustomArchetype, PackingItem } from '../types';
import { generateAirportCode, generatePackingList } from '../utils/generator';
import { sound } from '../utils/audio';
import { X, Plane, Calendar, Cloud, Luggage, Sparkles, Check, Settings2 } from 'lucide-react';
import {
X,
Plane,
Calendar,
Cloud,
Luggage,
Sparkles,
Check,
Settings2,
Copy,
Wand2,
ArrowRight,
AlertTriangle
} from 'lucide-react';
interface TripModalProps {
isOpen: boolean;
@@ -11,6 +24,8 @@ interface TripModalProps {
memory: PackMemory;
customArchetypes: CustomArchetype[];
onEditArchetypes: () => void;
trips: Trip[];
initialCloneTripId?: string | null;
}
const CLIMATES: { id: Climate; label: string; icon: string }[] = [
@@ -42,9 +57,19 @@ export const TripModal: React.FC<TripModalProps> = ({
memory,
customArchetypes,
onEditArchetypes,
trips,
initialCloneTripId = null,
}) => {
if (!isOpen) return null;
const [creationMode, setCreationMode] = useState<'generate' | 'clone'>(
initialCloneTripId ? 'clone' : 'generate'
);
const [selectedCloneId, setSelectedCloneId] = useState<string>(
initialCloneTripId || (trips[0]?.id || '')
);
const [applyDebriefLearnings, setApplyDebriefLearnings] = useState<boolean>(true);
const [destination, setDestination] = useState('');
const [title, setTitle] = useState('');
const [tripType, setTripType] = useState<TripType>(customArchetypes[0]?.id || 'international_urban');
@@ -54,6 +79,41 @@ export const TripModal: React.FC<TripModalProps> = ({
const [hasLaundry, setHasLaundry] = useState(true);
const [selectedActivities, setSelectedActivities] = useState<string[]>(['remote_work']);
// Handle pre-populating when cloning from a trip
useEffect(() => {
if (initialCloneTripId) {
setCreationMode('clone');
setSelectedCloneId(initialCloneTripId);
const src = trips.find((t) => t.id === initialCloneTripId);
if (src) {
setDestination(src.destination);
setTitle(`${src.title} (Copy)`);
setTripType(src.tripType);
setDurationDays(src.durationDays);
setClimate(src.climate);
setLuggageConfig(src.luggageConfig);
setHasLaundry(src.hasLaundry);
setSelectedActivities(src.activities || []);
}
}
}, [initialCloneTripId, isOpen]);
const handleSelectSourceTrip = (tripId: string) => {
setSelectedCloneId(tripId);
const src = trips.find((t) => t.id === tripId);
if (src) {
sound.playClick();
setDestination(src.destination);
setTitle(`${src.title} (Repack)`);
setTripType(src.tripType);
setDurationDays(src.durationDays);
setClimate(src.climate);
setLuggageConfig(src.luggageConfig);
setHasLaundry(src.hasLaundry);
setSelectedActivities(src.activities || []);
}
};
const destinationAirportCode = generateAirportCode(destination || 'DEST');
const routeCode = `DEP ➔ ${destinationAirportCode}`;
@@ -73,18 +133,58 @@ export const TripModal: React.FC<TripModalProps> = ({
const tripTitle = title.trim() || `${destination.trim() || 'Voyage'} Expedition`;
const tripId = `trip_${Date.now()}`;
const items = generatePackingList({
title: tripTitle,
destination: destination.trim() || 'New Destination',
tripType,
durationDays,
durationNights: Math.max(1, durationDays - 1),
climate,
luggageConfig,
hasLaundry,
activities: selectedActivities,
memory,
});
let items: PackingItem[] = [];
if (creationMode === 'clone') {
const sourceTrip = trips.find((t) => t.id === selectedCloneId);
if (sourceTrip) {
// Clone items with reset packed checkboxes and new IDs
items = sourceTrip.items.map((item) => ({
...item,
id: `item_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`,
packed: false,
}));
// Apply debrief learnings if selected and available
if (applyDebriefLearnings && sourceTrip.debrief) {
const excludedLower = sourceTrip.debrief.excludedItemNames.map((n) => n.toLowerCase());
// Remove deadweight items
items = items.filter((item) => !excludedLower.includes(item.name.toLowerCase()));
// Inject wishlist items
for (const wishlistName of sourceTrip.debrief.includedItemNames) {
if (!items.some((i) => i.name.toLowerCase() === wishlistName.toLowerCase())) {
items.push({
id: `item_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`,
name: wishlistName,
category: 'gear',
quantity: 1,
bag: 'carry_on',
packed: false,
source: 'learned',
learnedReason: `Included from past debrief of ${sourceTrip.title}`,
});
}
}
}
}
}
// Fallback or generator mode
if (items.length === 0) {
items = generatePackingList({
title: tripTitle,
destination: destination.trim() || 'New Destination',
tripType,
durationDays,
durationNights: Math.max(1, durationDays - 1),
climate,
luggageConfig,
hasLaundry,
activities: selectedActivities,
memory,
});
}
const newTrip: Trip = {
id: tripId,
@@ -106,9 +206,7 @@ export const TripModal: React.FC<TripModalProps> = ({
onClose();
};
// Count relevant memory items
const relevantInclusions = memory.inclusions.filter((i) => i.tripType === tripType).length;
const relevantExclusions = memory.exclusions.filter((e) => e.tripType === tripType).length;
const selectedCloneTrip = trips.find((t) => t.id === selectedCloneId);
return (
<div className="fixed inset-0 z-50 bg-ink/70 backdrop-blur-xs flex items-center justify-center p-3 sm:p-6 overflow-y-auto">
@@ -132,6 +230,45 @@ export const TripModal: React.FC<TripModalProps> = ({
</button>
</div>
{/* Creation Mode Tabs */}
<div className="bg-ticket-200 border-b-2 border-ink px-4 pt-2 flex gap-1 font-mono text-xs font-bold">
<button
type="button"
onClick={() => {
sound.playClick();
setCreationMode('generate');
}}
className={`px-3 py-2 border-t-2 border-l-2 border-r-2 border-ink transition-all flex items-center gap-1.5 ${
creationMode === 'generate'
? 'bg-ticket-100 text-ink shadow-xs -mb-[2px] z-10'
: 'bg-white/60 text-ink-muted hover:bg-white'
}`}
>
<Wand2 size={13} />
<span>SMART HEURISTIC GENERATOR</span>
</button>
{trips.length > 0 && (
<button
type="button"
onClick={() => {
sound.playClick();
setCreationMode('clone');
if (!selectedCloneId && trips[0]) {
handleSelectSourceTrip(trips[0].id);
}
}}
className={`px-3 py-2 border-t-2 border-l-2 border-r-2 border-ink transition-all flex items-center gap-1.5 ${
creationMode === 'clone'
? 'bg-ticket-100 text-ink shadow-xs -mb-[2px] z-10'
: 'bg-white/60 text-ink-muted hover:bg-white'
}`}
>
<Copy size={13} />
<span>CLONE FROM PAST VOYAGE ({trips.length})</span>
</button>
)}
</div>
{/* Route Code Banner */}
<div className="bg-canary border-b-2 border-ink px-4 py-2 flex items-center justify-between font-mono text-xs font-bold text-ink">
<span>ROUTING TELEMETRY:</span>
@@ -140,7 +277,57 @@ export const TripModal: React.FC<TripModalProps> = ({
</span>
</div>
<form onSubmit={handleGenerate} className="p-4 sm:p-6 max-h-[80vh] overflow-y-auto space-y-5">
<form onSubmit={handleGenerate} className="p-4 sm:p-6 max-h-[75vh] overflow-y-auto space-y-5">
{/* CLONE SELECTION SECTION (When cloning from a past trip) */}
{creationMode === 'clone' && (
<div className="bg-white border-2 border-ink p-3.5 shadow-card-hard-sm space-y-3">
<div className="flex items-center justify-between">
<span className="font-mono text-xs font-bold uppercase text-ink flex items-center gap-1.5">
<Copy size={14} className="text-gate-orange" />
<span>SELECT BASE VOYAGE TO RE-FLY</span>
</span>
<span className="font-mono text-[10px] bg-ticket-200 border border-ink/30 px-1.5 py-0.5 text-ink">
{selectedCloneTrip?.items.length || 0} ITEMS IN BASE
</span>
</div>
<select
value={selectedCloneId}
onChange={(e) => handleSelectSourceTrip(e.target.value)}
className="w-full bg-ticket-50 border-2 border-ink font-mono text-xs font-bold p-2 shadow-xs cursor-pointer"
>
{trips.map((t) => (
<option key={t.id} value={t.id}>
{t.routeCode} — {t.title} ({t.items.length} items, {t.durationDays}D)
{t.debrief ? ' [DEBRIEFED]' : ''}
</option>
))}
</select>
{/* Debrief Learning Filter Toggle */}
{selectedCloneTrip?.debrief && (
<div className="p-2.5 bg-purple-50 border border-purple-300 font-mono text-xs">
<label className="flex items-start gap-2 cursor-pointer">
<input
type="checkbox"
checked={applyDebriefLearnings}
onChange={(e) => setApplyDebriefLearnings(e.target.checked)}
className="mt-0.5 accent-purple-700"
/>
<div>
<span className="font-bold text-purple-900 block">
APPLY PAST DEBRIEF REFINEMENTS
</span>
<span className="font-sans text-[11px] text-purple-800 leading-tight block mt-0.5">
Automatically omit {selectedCloneTrip.debrief.excludedItemNames.length} deadweight item(s) you never used, and automatically inject {selectedCloneTrip.debrief.includedItemNames.length} wishlist item(s) you missed!
</span>
</div>
</label>
</div>
)}
</div>
)}
{/* Destination & Title */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
@@ -171,7 +358,7 @@ export const TripModal: React.FC<TripModalProps> = ({
</div>
</div>
{/* Trip Type */}
{/* Trip Type (Archetypes) */}
<div>
<div className="flex items-center justify-between mb-1.5">
<label className="font-mono text-xs font-bold uppercase text-ink">
@@ -378,21 +565,6 @@ export const TripModal: React.FC<TripModalProps> = ({
</div>
</div>
{/* Learning Memory Signal */}
{(relevantInclusions > 0 || relevantExclusions > 0) && (
<div className="bg-purple-50 border-2 border-purple-300 p-3 flex items-start gap-2.5 font-mono text-xs text-purple-900">
<Sparkles size={16} className="text-purple-600 shrink-0 mt-0.5" />
<div>
<span className="font-bold">CONTINUOUS LEARNING ACTIVE:</span>
<p className="font-sans text-[11px] text-purple-800 mt-0.5">
Your past debriefs have taught the engine for {tripType.replace('_', ' ')}:
{relevantInclusions > 0 && ` ${relevantInclusions} wishlist item(s) will be automatically injected.`}
{relevantExclusions > 0 && ` ${relevantExclusions} frequently unused deadweight item(s) will be omitted or reduced.`}
</p>
</div>
</div>
)}
{/* Form Actions */}
<div className="border-t-2 border-ink pt-4 flex items-center justify-end gap-3">
<button
@@ -410,7 +582,11 @@ export const TripModal: React.FC<TripModalProps> = ({
className="px-6 py-2.5 border-2 border-ink font-grotesk font-bold text-sm uppercase bg-gate-orange hover:bg-gate-hover text-white shadow-card-hard active:translate-x-0.5 active:translate-y-0.5 flex items-center gap-2"
>
<Plane size={16} />
<span>STAMP & ISSUE LOADOUT MANIFEST</span>
<span>
{creationMode === 'clone'
? 'CLONE & ISSUE MANIFEST'
: 'STAMP & ISSUE LOADOUT MANIFEST'}
</span>
</button>
</div>
</form>