Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions apps/web/src/components/FirstMateIcon.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { ShipWheelIcon, type LucideProps } from "lucide-react";
import { ShipIcon, type LucideProps } from "lucide-react";

import { cn } from "~/lib/utils";
import { FIRST_MATE_TONE_CLASS } from "./sidebar/fleetSidebar.logic";

/** First Mate's mark: a pink ship's wheel, shown wherever other rows show a project. */
/** First Mate's mark: a ship in First Mate's colour. */
export function FirstMateIcon({ className, ...props }: LucideProps) {
return (
<ShipWheelIcon
<ShipIcon
aria-hidden
{...props}
className={cn("size-4 shrink-0 text-pink-500 dark:text-pink-400", className)}
className={cn("size-4 shrink-0", FIRST_MATE_TONE_CLASS, className)}
/>
);
}
20 changes: 20 additions & 0 deletions apps/web/src/components/FleetRoleIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { FleetRole } from "@t3tools/contracts";
import { PickaxeIcon, SailboatIcon, ShipIcon, type LucideProps } from "lucide-react";

import { cn } from "~/lib/utils";

const FLEET_ROLE_ICONS = {
"first-mate": ShipIcon,
"second-mate": SailboatIcon,
worker: PickaxeIcon,
} as const;

/**
* A fleet thread's role mark, drawn beside its project icon: a ship for First
* Mate, a sailboat for a second mate, a pickaxe for a worker. The caller gives
* it the thread's text colour.
*/
export function FleetRoleIcon({ role, className, ...props }: LucideProps & { role: FleetRole }) {
const Icon = FLEET_ROLE_ICONS[role];
return <Icon aria-hidden {...props} className={cn("size-3.5 shrink-0", className)} />;
}
4 changes: 2 additions & 2 deletions apps/web/src/components/Sidebar.logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2587,7 +2587,7 @@ describe("partitionFirstMateThreads", () => {
readonly fleetRepo?: string | null;
readonly archivedAt?: string | null;
} = {},
) => ({ id, archivedAt: null, ...fleet });
) => ({ id, archivedAt: null, createdAt: "2026-09-24T00:00:00Z", pinnedAt: null, ...fleet });

it("puts the live First Mate in its own slot, outside every section", () => {
const firstMate = thread("fm", { fleetRole: "first-mate", fleetRepo: null });
Expand All @@ -2596,7 +2596,7 @@ describe("partitionFirstMateThreads", () => {
const plain = thread("plain");
const { slot, rest } = partitionFirstMateThreads([secondMate, firstMate, worker, plain]);
expect(slot).toEqual([firstMate]);
// Second mates and workers stay with ordinary threads, under their project.
// Second mates and workers pass on for the fleet tree to place.
expect(rest).toEqual([secondMate, worker, plain]);
});

Expand Down
31 changes: 18 additions & 13 deletions apps/web/src/components/Sidebar.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
effectiveSnoozed,
type ThreadSnoozeShell,
} from "@t3tools/client-runtime/state/thread-settled";
import { planFirstMateSlot } from "./sidebar/fleetSidebar.logic";
import {
isFirstMateThread,
threadDisplayTitle,
Expand Down Expand Up @@ -904,22 +905,26 @@ export { sortPinnedThreadsByOrderKey as sortPinnedThreadsForSidebar } from "@t3t
const EMPTY_CONTENT_MATCH_KEYS: ReadonlySet<string> = new Set<string>();

/**
* Split the First Mate thread out of the sidebar's thread list. First Mate
* belongs to no project, so it sits in its own slot above every section and
* ignores the project scope: `slot` holds the live First Mate threads (the
* server keeps that to one per environment) and `rest` holds every other
* thread, for the pinned, active, snoozed, and settled sections to share.
* Split the First Mate threads out of the sidebar's thread list. First Mate
* always sits in its own slot above every section and ignores the project
* scope: `slot` holds the live First Mate threads, the main one first (see
* planFirstMateSlot), and `rest` holds every other thread, for the pinned,
* active, snoozed, and settled sections to share. An archived First Mate
* leaves both.
*/
export function partitionFirstMateThreads<
T extends FleetThreadFields & { readonly archivedAt: string | null },
T extends FleetThreadFields & {
readonly id: string;
readonly archivedAt: string | null;
readonly createdAt: string;
readonly pinnedAt?: string | null | undefined;
},
>(threads: readonly T[]): { readonly slot: T[]; readonly rest: T[] } {
const slot: T[] = [];
const rest: T[] = [];
for (const thread of threads) {
if (!isFirstMateThread(thread)) rest.push(thread);
else if (thread.archivedAt === null) slot.push(thread);
}
return { slot, rest };
const plan = planFirstMateSlot(threads);
return {
slot: plan.kind === "first-mates" ? [plan.main, ...plan.others] : [],
rest: threads.filter((thread) => !isFirstMateThread(thread)),
};
}

/**
Expand Down
Loading
Loading