From 28fd7d47343a448bba1a779c35b5840de2066c71 Mon Sep 17 00:00:00 2001 From: Matt OD Date: Fri, 22 May 2026 13:08:24 -0700 Subject: [PATCH] feat(recruiting): add Recruit tab + empty view behind a feature flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First visible seam for the Recruit module (FHR-70 / Recruit S0.1). A `recruiting` tab — gated by a compile-time RECRUITING_ENABLED flag, default off — renders an empty RecruitingView in the main content area. With the flag off, Rollup tree-shakes the recruiting code out of the bundle, so this ships dark to production while S0.2/S0.3 build against the seam. - src/lib/featureFlags.ts (new): RECRUITING_ENABLED compile-time gate - src/components/recruiting/ (new): empty RecruitingView, lazy-loaded + barrel - LayoutContext: SidebarTab union += 'recruiting' - TabSwitcher: flag-gated Recruit tab - AppShell: three-case sidebar content - App.tsx: swap main area to when the tab is active Verified: tsc clean (both flag states), build clean on/off, 478 Rust tests unchanged. Decisions in DECISIONS.md: compile-time flag over settings toggle; main-area placement over sidebar. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/App.tsx | 20 ++++++++++-- src/components/conversations/TabSwitcher.tsx | 15 +++++++++ src/components/layout/AppShell.tsx | 9 +++--- src/components/recruiting/RecruitingView.tsx | 33 ++++++++++++++++++++ src/components/recruiting/index.ts | 1 + src/contexts/LayoutContext.tsx | 2 +- src/lib/featureFlags.ts | 21 +++++++++++++ 7 files changed, 93 insertions(+), 8 deletions(-) create mode 100644 src/components/recruiting/RecruitingView.tsx create mode 100644 src/components/recruiting/index.ts create mode 100644 src/lib/featureFlags.ts diff --git a/src/App.tsx b/src/App.tsx index c4404b3..fd92a0e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,5 @@ import { useState, useCallback, useEffect, useRef, lazy, Suspense, Component, type ReactNode } from 'react'; -import { LayoutProvider } from './contexts/LayoutContext'; +import { LayoutProvider, useLayout } from './contexts/LayoutContext'; import { EmployeeProvider } from './contexts/EmployeeContext'; import { ConversationProvider, @@ -17,6 +17,7 @@ import { TestDataImporter } from './components/dev/TestDataImporter'; import { OnboardingProvider, OnboardingFlow, useOnboarding } from './components/onboarding'; import { useEmployees } from './contexts/EmployeeContext'; import { useNetwork, useCommandPalette } from './hooks'; +import { RECRUITING_ENABLED } from './lib/featureFlags'; const EmployeeEdit = lazy(() => import('./components/employees/EmployeeEdit').then((module) => ({ default: module.EmployeeEdit })) @@ -31,6 +32,9 @@ const CommandPalette = lazy(() => import('./components/CommandPalette')); const UpgradePrompt = lazy(() => import('./components/trial/UpgradePrompt').then((module) => ({ default: module.UpgradePrompt })) ); +const RecruitingView = lazy(() => + import('./components/recruiting').then((module) => ({ default: module.RecruitingView })) +); // Error Boundary to catch React render errors. // Positioned at the outermost App root so first-launch failures (onboarding @@ -311,6 +315,12 @@ function MainAppContent() { const [isTestDataModalOpen, setIsTestDataModalOpen] = useState(false); const [isSettingsOpen, setIsSettingsOpen] = useState(false); const chatInputRef = useRef(null); + const { sidebarTab } = useLayout(); + + // Recruit module (FHR-70) takes over the main content area when its tab is + // active. Guarded by RECRUITING_ENABLED so an off flag can never strand the + // user on an empty view (the tab can't be selected when the flag is off). + const showRecruiting = RECRUITING_ENABLED && sidebarTab === 'recruiting'; // Command palette hook (uses useLayout internally) const { isOpen: isPaletteOpen, close: closePalette } = useCommandPalette({ @@ -337,7 +347,13 @@ function MainAppContent() { contextPanel={} onSettingsClick={() => setIsSettingsOpen(true)} > - + {showRecruiting ? ( + + + + ) : ( + + )} diff --git a/src/components/conversations/TabSwitcher.tsx b/src/components/conversations/TabSwitcher.tsx index 7783917..9628d4b 100644 --- a/src/components/conversations/TabSwitcher.tsx +++ b/src/components/conversations/TabSwitcher.tsx @@ -2,6 +2,7 @@ // Allows switching between Conversations and Employees views in sidebar import type { SidebarTab } from '../../contexts/LayoutContext'; +import { RECRUITING_ENABLED } from '../../lib/featureFlags'; interface TabSwitcherProps { value: SidebarTab; @@ -30,6 +31,20 @@ export function TabSwitcher({ value, onChange }: TabSwitcherProps) { }, ]; + // Recruit module tab (FHR-70) — only shown when the feature flag is on, so + // production (flag off) keeps exactly the Chats / People switcher it had. + if (RECRUITING_ENABLED) { + tabs.push({ + key: 'recruiting', + label: 'Recruit', + icon: ( + + + + ), + }); + } + return (
{tabs.map((tab) => ( diff --git a/src/components/layout/AppShell.tsx b/src/components/layout/AppShell.tsx index f4ba7e6..8082e3a 100644 --- a/src/components/layout/AppShell.tsx +++ b/src/components/layout/AppShell.tsx @@ -279,11 +279,10 @@ export function AppShell({ children, contextPanel, onSettingsClick }: AppShellPr {/* Tab Content */}
- {sidebarTab === 'conversations' ? ( - - ) : ( - - )} + {sidebarTab === 'conversations' && } + {sidebarTab === 'employees' && } + {/* recruiting: sidebar intentionally empty in the S0.1 skeleton — + the empty Recruiting view renders in the main content area. */}
diff --git a/src/components/recruiting/RecruitingView.tsx b/src/components/recruiting/RecruitingView.tsx new file mode 100644 index 0000000..14bd63a --- /dev/null +++ b/src/components/recruiting/RecruitingView.tsx @@ -0,0 +1,33 @@ +// People Partner — Recruit module (talent sourcing) +// +// S0.1 skeleton (FHR-70): an empty Recruiting view, rendered in the main +// content area when the `recruiting` tab is active and gated behind +// RECRUITING_ENABLED. This is the first visible seam for the module — S0.2 +// stands up the Rust `recruiting` module + first command, and S0.3 round-trips +// a live Exa search into this view. Intentionally empty until then. + +export function RecruitingView() { + return ( +
+
+ +
+

Recruit

+

+ Context-aware talent sourcing, seeded by the employee data you already + have. This module is under construction — nothing to show here yet. +

+
+ ); +} + +export default RecruitingView; diff --git a/src/components/recruiting/index.ts b/src/components/recruiting/index.ts new file mode 100644 index 0000000..899ffa5 --- /dev/null +++ b/src/components/recruiting/index.ts @@ -0,0 +1 @@ +export { RecruitingView } from './RecruitingView'; diff --git a/src/contexts/LayoutContext.tsx b/src/contexts/LayoutContext.tsx index b3d2f4d..45e9dda 100644 --- a/src/contexts/LayoutContext.tsx +++ b/src/contexts/LayoutContext.tsx @@ -1,6 +1,6 @@ import { createContext, useContext, useState, useCallback, type ReactNode } from 'react'; -export type SidebarTab = 'conversations' | 'employees'; +export type SidebarTab = 'conversations' | 'employees' | 'recruiting'; interface LayoutState { sidebarOpen: boolean; diff --git a/src/lib/featureFlags.ts b/src/lib/featureFlags.ts new file mode 100644 index 0000000..732e518 --- /dev/null +++ b/src/lib/featureFlags.ts @@ -0,0 +1,21 @@ +// Feature flags — compile-time gates for in-development modules. +// +// These are plain constants, deliberately NOT settings-backed: a flag here +// hides an unfinished module from production users while it's being built, and +// is flipped by editing this file + rebuilding. There's nothing to toggle at +// runtime yet — the gated views are empty skeletons. When a module graduates +// to real, user-facing functionality, migrate its flag to a settings-table +// toggle (mirroring the Signals / Fairness "Intelligence Features" pattern in +// SettingsPanel) so users/support can enable it without a rebuild. +// +// Typed `: boolean` (not the inferred literal `false`) on purpose — it stops +// TypeScript from narrowing reads to dead branches, so `if (RECRUITING_ENABLED)` +// and `RECRUITING_ENABLED ? … : …` type-check cleanly with the flag either way. + +/** + * Recruit module (talent sourcing) — FHR-70 → FHR-91. + * OFF in production until the module is built out. Flip to `true` locally to + * develop S0.2 (Rust `recruiting` module) and S0.3 (live Exa round-trip) + * against the UI seam established in S0.1. + */ +export const RECRUITING_ENABLED: boolean = false;