@pablo2410/shared-ui 0.24.0 → 0.25.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/tour/tourStore.ts","../src/tour/tourProgress.ts","../src/tour/tourNarration.ts","../src/tour/TourProvider.tsx","../src/tour/TakeTheTourButton.tsx","../src/tour/useSpotlight.ts","../src/tour/spotlightMath.ts","../src/tour/TourShell.tsx","../src/tour/useTour.ts","../src/tour/tourMachine.ts","../src/tour/OpiTourHost.tsx"],"sourcesContent":["/**\n * The tour registry + launcher store — the one place a module's tour\n * definition and the \"is a tour open right now\" flag live, so the header's\n * \"Take the tour\" trigger, the tour host, and the module's own page code\n * never talk to each other directly.\n *\n * A module's whole integration is a definition object (see\n * TourDefinition) passed to `useRegisterTour`, plus `data-tour-target`\n * attributes on the elements its spotlight steps point at. Everything else\n * — progress storage, the header trigger, the overlay, narration — is shared.\n *\n * Plain functions, no React import: testable in a Node environment (this\n * package has no jsdom set up — see tour.test.ts), same split as\n * tourMachine.ts. TourProvider.tsx is the React binding.\n */\nimport type { TourStep, TourStepKind } from \"./types\";\n\n/** Portal caps `lastStepId` at 120 chars (onboarding.setUserTourState). */\nconst MAX_STEP_ID_LENGTH = 120;\n/** Tour ids are a key in Portal's progress store, so keep them short and plain. */\nconst MAX_TOUR_ID_LENGTH = 60;\nconst TOUR_ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/;\nconst STEP_KINDS: readonly TourStepKind[] = [\"intro\", \"service\", \"outro\"];\n\nexport interface TourDefinition {\n /** Stable, lowercase-kebab id, e.g. \"policy-deployment\". It is the key\n * progress is stored under in Portal, so never rename a shipped tour's\n * id — users' progress and \"done\" state would reset. */\n id: string;\n /** The steps, in order. A step that should highlight a live element sets\n * `spotlight.selector` to the value of a `data-tour-target` attribute in\n * the module's DOM. */\n steps: TourStep[];\n /** Open the tour by itself for a user who has never seen it or left it\n * part-way (resuming at their last step). Off by default. Never fires\n * when the user's progress couldn't be read, and never for a tour they\n * finished or dismissed — those only reopen from \"Take the tour\". */\n autoStart?: boolean;\n}\n\nexport class InvalidTourDefinitionError extends Error {\n readonly problems: string[];\n constructor(tourId: string, problems: string[]) {\n super(`Invalid tour definition \"${tourId}\": ${problems.join(\"; \")}`);\n this.name = \"InvalidTourDefinitionError\";\n this.problems = problems;\n }\n}\n\n/** Returns a list of problems; empty means the definition is usable. */\nexport function validateTourDefinition(def: TourDefinition): string[] {\n const problems: string[] = [];\n\n if (typeof def.id !== \"string\" || def.id.length === 0) {\n problems.push(\"id is required\");\n } else {\n if (def.id.length > MAX_TOUR_ID_LENGTH) {\n problems.push(`id is longer than ${MAX_TOUR_ID_LENGTH} characters`);\n }\n if (!TOUR_ID_PATTERN.test(def.id)) {\n problems.push(\"id must be lowercase letters, digits and hyphens\");\n }\n }\n\n if (!Array.isArray(def.steps) || def.steps.length === 0) {\n problems.push(\"at least one step is required\");\n return problems;\n }\n\n const seen = new Set<string>();\n def.steps.forEach((step, i) => {\n const at = `step ${i + 1}`;\n if (!step.id) {\n problems.push(`${at} has no id`);\n } else {\n if (step.id.length > MAX_STEP_ID_LENGTH) {\n problems.push(\n `${at} id is longer than ${MAX_STEP_ID_LENGTH} characters`\n );\n }\n if (seen.has(step.id)) problems.push(`duplicate step id \"${step.id}\"`);\n seen.add(step.id);\n }\n if (!STEP_KINDS.includes(step.kind)) {\n problems.push(`${at} has an unknown kind \"${String(step.kind)}\"`);\n }\n if (!step.label) problems.push(`${at} has no label`);\n if (!step.fallbackText) problems.push(`${at} has no fallbackText`);\n if (step.spotlight && !step.spotlight.selector) {\n problems.push(`${at} has a spotlight with no selector`);\n }\n });\n\n return problems;\n}\n\nexport interface TourSession {\n /** True = start at step 1 regardless of saved progress (the header's\n * \"Take the tour\" replay). False = resume from the saved step. */\n restart: boolean;\n}\n\nexport interface TourStoreState {\n /** The tour the header trigger and host act on: the most recently\n * registered one still mounted (a page-level tour wins over a\n * module-level one while its page is open). */\n active: TourDefinition | null;\n /** Non-null while the tour overlay is open. */\n session: TourSession | null;\n}\n\nexport interface TourStore {\n /** Registers (or replaces, by id) a tour. Throws InvalidTourDefinitionError\n * for an unusable definition. Returns the unregister function. */\n register(def: TourDefinition): () => void;\n /** Opens the active tour. No-op when none is registered. */\n open(session?: TourSession): void;\n close(): void;\n getState(): TourStoreState;\n subscribe(listener: () => void): () => void;\n}\n\nexport function createTourStore(): TourStore {\n // Insertion order = registration order; the last entry is the active tour.\n const tours = new Map<string, TourDefinition>();\n let session: TourSession | null = null;\n let state: TourStoreState = { active: null, session: null };\n const listeners = new Set<() => void>();\n\n const recompute = () => {\n const last = [...tours.values()].pop() ?? null;\n // An overlay can't outlive the tour it was opened for.\n if (state.active && last?.id !== state.active.id) session = null;\n // Keep object identity stable when nothing changed, so\n // useSyncExternalStore doesn't re-render on no-op notifications.\n if (last !== state.active || session !== state.session) {\n state = { active: last, session };\n }\n listeners.forEach(l => l());\n };\n\n return {\n register(def) {\n const problems = validateTourDefinition(def);\n if (problems.length > 0) {\n throw new InvalidTourDefinitionError(def?.id ?? \"(no id)\", problems);\n }\n // Re-registering an id moves it to the end (becomes active), which is\n // also what React StrictMode's mount/unmount/mount needs.\n tours.delete(def.id);\n tours.set(def.id, def);\n recompute();\n return () => {\n // Only remove our own registration — if the same id was registered\n // again meanwhile, that newer one must survive this cleanup.\n if (tours.get(def.id) === def) {\n tours.delete(def.id);\n recompute();\n }\n };\n },\n open(next = { restart: true }) {\n if (!state.active) return;\n session = next;\n recompute();\n },\n close() {\n if (session === null) return;\n session = null;\n recompute();\n },\n getState: () => state,\n subscribe(listener) {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n };\n}\n\n/** The store modules share by default. `TourProvider` can swap in another\n * (tests, or an app that wants isolation). */\nexport const defaultTourStore: TourStore = createTourStore();\n","/**\n * Tour progress — read/write of a user's progress through one tour, stored\n * once in Portal (`onboarding.userTourState` / `onboarding.setUserTourState`,\n * server/routers/onboarding.ts in oplytics-portal) so it follows the user\n * across every module. No module keeps its own copy and none has its own\n * table.\n *\n * Two layers, both free of React so they're testable in Node:\n * - TourProgressTransport: how progress reaches Portal. The default is a\n * plain cross-origin fetch with the session cookie, the same way every\n * module already calls `ai.askOpi` (see each app's lib/opiClient.ts).\n * - TourProgressController: the per-tour state (load, optimistic record,\n * in-order writes) the `useTourProgress` hook wraps.\n *\n * PER-TOUR STORAGE NEEDS A PORTAL CHANGE. Today Portal keeps one progress\n * row per user (the Service Hub tour) and its endpoints take no tour id.\n * This transport sends `tourId`; a Portal that supports it echoes `tourId`\n * back on reads. A Portal that does not would silently drop the field and\n * write every module's progress over the Service Hub tour's, so for any\n * tour other than the Service Hub's the transport only writes once a read\n * has proven the echo — until then reads fail (so nothing auto-opens) and\n * progress isn't persisted, but the tour itself and its header replay still\n * work. See the [Portal] Epic linked from shared-ui#106.\n */\nimport { portalUrl } from \"../services/platformDomain\";\nimport type { TourProgress, TourStatus } from \"./types\";\n\n/** The id Portal's existing, tour-id-less state belongs to. */\nexport const SERVICE_HUB_TOUR_ID = \"service-hub\";\n\nexport interface TourProgressUpdate {\n status: Exclude<TourStatus, \"unseen\">;\n lastStepId: string | null;\n}\n\nexport interface TourProgressTransport {\n read(tourId: string): Promise<TourProgress>;\n write(tourId: string, update: TourProgressUpdate): Promise<void>;\n}\n\nexport class TourProgressError extends Error {\n readonly status?: number;\n constructor(message: string, status?: number) {\n super(message);\n this.name = \"TourProgressError\";\n this.status = status;\n }\n}\n\nconst UNSEEN = (): TourProgress => ({\n status: \"unseen\",\n lastStepId: null,\n updatedAt: new Date(0).toISOString(),\n});\n\nexport interface PortalTransportOptions {\n /** Portal origin. Defaults to the platform Portal (`localhost:3000` on the\n * local sandbox) — see services/platformDomain. */\n baseUrl?: string;\n fetchImpl?: typeof fetch;\n}\n\n/** tRPC + superjson wraps results as `{ result: { data: { json, meta? } } }`.\n * Tour progress holds only strings and null, so `json` is the value. */\nfunction unwrapTrpc<T>(body: unknown): T {\n const data = (body as { result?: { data?: { json?: T } } })?.result?.data;\n if (data === undefined || data.json === undefined) {\n throw new TourProgressError(\"Unexpected response from Portal\");\n }\n return data.json;\n}\n\nconst STATUSES: readonly TourStatus[] = [\n \"unseen\",\n \"in-progress\",\n \"done\",\n \"dismissed\",\n];\n\nexport function createPortalTourProgressTransport(\n opts: PortalTransportOptions = {}\n): TourProgressTransport {\n const doFetch: typeof fetch =\n opts.fetchImpl ?? ((input, init) => fetch(input, init));\n const base = () => (opts.baseUrl ?? portalUrl(\"\")).replace(/\\/$/, \"\");\n // undefined = not yet known; set by the first read.\n let perTourSupported: boolean | undefined;\n\n const read = async (tourId: string): Promise<TourProgress> => {\n const input = encodeURIComponent(JSON.stringify({ json: { tourId } }));\n const res = await doFetch(\n `${base()}/api/trpc/onboarding.userTourState?input=${input}`,\n { method: \"GET\", credentials: \"include\" }\n );\n if (!res.ok) {\n throw new TourProgressError(\n `Couldn't read tour progress (${res.status})`,\n res.status\n );\n }\n const json = unwrapTrpc<{\n tourId?: string;\n status?: string;\n lastStepId?: string | null;\n updatedAt?: string;\n }>(await res.json());\n\n perTourSupported = json.tourId === tourId;\n // A Portal without per-tour support answered with the Service Hub tour's\n // row — that is not this tour's progress. Report it as a failed read\n // (not \"unseen\") so callers don't auto-open the tour on every page load\n // for progress that can never be saved.\n if (tourId !== SERVICE_HUB_TOUR_ID && !perTourSupported) {\n throw new TourProgressError(\n \"Portal does not store per-tour progress yet\"\n );\n }\n\n return {\n status: STATUSES.includes(json.status as TourStatus)\n ? (json.status as TourStatus)\n : \"unseen\",\n lastStepId: json.lastStepId ?? null,\n updatedAt: json.updatedAt ?? UNSEEN().updatedAt,\n };\n };\n\n return {\n read,\n async write(tourId, update) {\n if (tourId !== SERVICE_HUB_TOUR_ID) {\n if (perTourSupported === undefined) await read(tourId);\n if (!perTourSupported) return;\n }\n const res = await doFetch(\n `${base()}/api/trpc/onboarding.setUserTourState`,\n {\n method: \"POST\",\n credentials: \"include\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ json: { tourId, ...update } }),\n }\n );\n if (!res.ok) {\n throw new TourProgressError(\n `Couldn't save tour progress (${res.status})`,\n res.status\n );\n }\n },\n };\n}\n\nexport interface TourProgressSnapshot {\n /** \"loading\" until the first read settles. A failed read is \"error\" with\n * `progress` falling back to unseen, so the tour is still usable. */\n phase: \"loading\" | \"ready\" | \"error\";\n progress: TourProgress;\n error: Error | null;\n}\n\nexport interface TourProgressController {\n getSnapshot(): TourProgressSnapshot;\n subscribe(listener: () => void): () => void;\n load(): Promise<void>;\n /** Updates local state at once, then persists. Writes go out in the order\n * they were recorded, so rapid Next clicks can't land reversed. A failed\n * write never throws — the tour must not block on storage. */\n record(update: TourProgressUpdate): Promise<void>;\n}\n\nexport function createTourProgressController(\n transport: TourProgressTransport,\n tourId: string\n): TourProgressController {\n let snapshot: TourProgressSnapshot = {\n phase: \"loading\",\n progress: UNSEEN(),\n error: null,\n };\n const listeners = new Set<() => void>();\n let writeChain: Promise<void> = Promise.resolve();\n // A write recorded before the first read lands must not be overwritten by\n // that (older) read.\n let recordedLocally = false;\n\n const set = (next: TourProgressSnapshot) => {\n snapshot = next;\n listeners.forEach(l => l());\n };\n\n return {\n getSnapshot: () => snapshot,\n subscribe(listener) {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n async load() {\n try {\n const progress = await transport.read(tourId);\n if (recordedLocally) return;\n set({ phase: \"ready\", progress, error: null });\n } catch (err) {\n if (recordedLocally) return;\n set({\n phase: \"error\",\n progress: UNSEEN(),\n error: err instanceof Error ? err : new Error(String(err)),\n });\n }\n },\n record(update) {\n recordedLocally = true;\n set({\n phase: snapshot.phase === \"loading\" ? \"ready\" : snapshot.phase,\n progress: { ...update, updatedAt: new Date().toISOString() },\n error: snapshot.error,\n });\n writeChain = writeChain\n .then(() => transport.write(tourId, update))\n .catch(err => {\n set({\n ...snapshot,\n error: err instanceof Error ? err : new Error(String(err)),\n });\n });\n return writeChain;\n },\n };\n}\n\n/** Whether to open the tour on its own when the module loads: only for a\n * user who has never seen it or left it part-way. Done and dismissed tours\n * only reopen from the header's \"Take the tour\". */\nexport function shouldAutoOpen(status: TourStatus): boolean {\n return status === \"unseen\" || status === \"in-progress\";\n}\n\n/** The full auto-open decision, made once when the first read settles.\n * A failed read (\"error\") never auto-opens: its fallback \"unseen\" is not\n * real, and opening on every page load for progress that can't be saved\n * would nag. */\nexport function decideAutoOpen(\n autoStart: boolean | undefined,\n snapshot: Pick<TourProgressSnapshot, \"phase\" | \"progress\">\n): boolean {\n return (\n autoStart === true &&\n snapshot.phase === \"ready\" &&\n shouldAutoOpen(snapshot.progress.status)\n );\n}\n","/**\n * Tour narration — the one call every module's tour makes to Portal's\n * `ai.askOpi`, so Opi's voice is identical everywhere.\n *\n * The persona is not defined here. Portal selects ONBOARDING_CONCIERGE_PERSONA\n * (server/routers/ai.ts) whenever `service` is the reserved value\n * \"onboarding\", and grounds it on the `insights` payload below — so a module\n * can't drift the voice, and shared-ui carries no copy of the prompt. The\n * payload keys and the user message match what Portal's own Service Hub tour\n * (UserTour.tsx) sends.\n *\n * Narration is an enhancement, never a dependency: callers render the step's\n * `fallbackText` first and treat any failure here (AI gated off, rate limit,\n * network) as \"keep the scripted text\".\n */\nimport { portalUrl } from \"../services/platformDomain\";\nimport type { TourStep } from \"./types\";\n\n/** Reserved `service` value that makes Portal use the onboarding persona. */\nexport const ONBOARDING_NARRATION_SERVICE = \"onboarding\";\n\nexport const NARRATION_PROMPT =\n \"Narrate this onboarding step for me in 2-3 sentences.\";\n\nexport interface NarrationRequestInput {\n service: typeof ONBOARDING_NARRATION_SERVICE;\n insights: {\n hook: string;\n highlights?: string[];\n features?: { name: string; detail: string }[];\n };\n messages: { role: \"user\"; content: string }[];\n}\n\nexport function buildNarrationInput(step: TourStep): NarrationRequestInput {\n return {\n service: ONBOARDING_NARRATION_SERVICE,\n insights: {\n hook: step.fallbackText,\n highlights: step.highlights,\n features: step.features,\n },\n messages: [{ role: \"user\", content: NARRATION_PROMPT }],\n };\n}\n\n/** Resolves with Opi's narration for the step, or rejects. */\nexport type TourNarrator = (step: TourStep) => Promise<string>;\n\nexport interface PortalNarratorOptions {\n baseUrl?: string;\n fetchImpl?: typeof fetch;\n}\n\nexport function createPortalNarrator(\n opts: PortalNarratorOptions = {}\n): TourNarrator {\n const doFetch: typeof fetch =\n opts.fetchImpl ?? ((input, init) => fetch(input, init));\n return async step => {\n const base = (opts.baseUrl ?? portalUrl(\"\")).replace(/\\/$/, \"\");\n const res = await doFetch(`${base}/api/trpc/ai.askOpi`, {\n method: \"POST\",\n credentials: \"include\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ json: buildNarrationInput(step) }),\n });\n if (!res.ok) throw new Error(`Opi narration failed (${res.status})`);\n const body = (await res.json()) as {\n result?: { data?: { json?: { content?: unknown } } };\n };\n const content = body.result?.data?.json?.content;\n if (typeof content !== \"string\" || content.trim() === \"\") {\n throw new Error(\"Opi narration returned no text\");\n }\n return content;\n };\n}\n","/**\n * React binding for the shared tour architecture: the context that carries\n * the tour store, the progress transport and the narrator, and the hooks a\n * module (or the shared header/host) uses.\n *\n * Every piece has a working default, so a module needs NO provider:\n *\n * useRegisterTour(myTour); // one line, plus data-tour-target attributes\n *\n * `TourProvider` exists to override the defaults (tests, an app that needs\n * an isolated store or a different Portal origin).\n */\nimport {\n createContext,\n useContext,\n useEffect,\n useMemo,\n useSyncExternalStore,\n type ReactNode,\n} from \"react\";\nimport {\n InvalidTourDefinitionError,\n defaultTourStore,\n type TourDefinition,\n type TourSession,\n type TourStore,\n type TourStoreState,\n} from \"./tourStore\";\nimport {\n createPortalTourProgressTransport,\n createTourProgressController,\n type TourProgressSnapshot,\n type TourProgressTransport,\n type TourProgressUpdate,\n} from \"./tourProgress\";\nimport { createPortalNarrator, type TourNarrator } from \"./tourNarration\";\n\ninterface TourContextValue {\n store: TourStore;\n transport: TourProgressTransport;\n narrator: TourNarrator;\n}\n\n// Created on first use rather than at import time, so importing the tour\n// entry point stays side-effect free (and safe during SSR).\nlet defaultTransport: TourProgressTransport | undefined;\nlet defaultNarrator: TourNarrator | undefined;\n\nconst TourContext = createContext<Partial<TourContextValue>>({});\n\nfunction useTourContext(): TourContextValue {\n const ctx = useContext(TourContext);\n return {\n store: ctx.store ?? defaultTourStore,\n // The default transport is shared app-wide: it remembers whether Portal\n // supports per-tour storage after the first read.\n transport:\n ctx.transport ??\n (defaultTransport ??= createPortalTourProgressTransport()),\n narrator: ctx.narrator ?? (defaultNarrator ??= createPortalNarrator()),\n };\n}\n\nexport interface TourProviderProps {\n store?: TourStore;\n transport?: TourProgressTransport;\n narrator?: TourNarrator;\n children: ReactNode;\n}\n\nexport function TourProvider({\n store,\n transport,\n narrator,\n children,\n}: TourProviderProps) {\n const value = useMemo(\n () => ({ store, transport, narrator }),\n [store, transport, narrator]\n );\n return <TourContext.Provider value={value}>{children}</TourContext.Provider>;\n}\n\n/**\n * Registers the module's tour for as long as the calling component is\n * mounted. That is all a module does to get the header's \"Take the tour\"\n * trigger, the overlay, saved progress and narration.\n *\n * Pass a definition that is stable across renders (module scope, or\n * useMemo) — a new object every render re-registers every render. Pass\n * null/undefined to register nothing (e.g. while data is loading). An\n * invalid definition is logged and ignored rather than thrown: a broken\n * tour must never take down the page it sits on.\n */\nexport function useRegisterTour(def: TourDefinition | null | undefined): void {\n const { store } = useTourContext();\n useEffect(() => {\n if (!def) return;\n try {\n return store.register(def);\n } catch (err) {\n if (err instanceof InvalidTourDefinitionError) {\n console.error(err.message);\n return;\n }\n throw err;\n }\n }, [store, def]);\n}\n\nexport function useTourState(): TourStoreState {\n const { store } = useTourContext();\n return useSyncExternalStore(store.subscribe, store.getState, store.getState);\n}\n\n/** The tour the header trigger would open now, or null when the current\n * module has registered none. */\nexport function useActiveTour(): TourDefinition | null {\n return useTourState().active;\n}\n\n/** Imperative open/close for the active tour — what the header trigger uses. */\nexport function useTourLauncher(): {\n /** Opens from step 1, regardless of saved progress. */\n start: () => void;\n /** Opens with an explicit session (e.g. resume from saved progress). */\n open: (session: TourSession) => void;\n close: () => void;\n available: boolean;\n} {\n const { store } = useTourContext();\n const available = useTourState().active !== null;\n return {\n start: () => store.open({ restart: true }),\n open: session => store.open(session),\n close: () => store.close(),\n available,\n };\n}\n\nexport interface UseTourProgressResult extends TourProgressSnapshot {\n record: (update: TourProgressUpdate) => Promise<void>;\n}\n\n/** The user's saved progress through one tour, read from and written to\n * Portal (see tourProgress.ts). */\nexport function useTourProgress(tourId: string): UseTourProgressResult {\n const { transport } = useTourContext();\n const controller = useMemo(\n () => createTourProgressController(transport, tourId),\n [transport, tourId]\n );\n useEffect(() => {\n void controller.load();\n }, [controller]);\n const snapshot = useSyncExternalStore(\n controller.subscribe,\n controller.getSnapshot,\n controller.getSnapshot\n );\n return { ...snapshot, record: controller.record };\n}\n\n/** The narrator the tour host uses — exposed so a module's own tour UI can\n * narrate in the same voice. */\nexport function useTourNarrator(): TourNarrator {\n return useTourContext().narrator;\n}\n","/**\n * TakeTheTourButton — the shared header's \"Take the tour\" entry.\n *\n * Renders nothing unless the current module has registered a tour (see\n * useRegisterTour), so it is safe to mount unconditionally in a shared\n * header: modules without a tour are unchanged. Clicking it opens the\n * module's tour from step 1 whatever the user's saved progress, i.e. it is\n * the \"replay on demand\" control. DashboardLayout mounts it; a module\n * with its own header can drop it in the same way.\n */\nimport { Sparkles } from \"lucide-react\";\nimport { cn } from \"../utils/cn\";\nimport { useTourLauncher } from \"./TourProvider\";\n\nexport interface TakeTheTourButtonProps {\n /** Defaults to \"Take the tour\". */\n label?: string;\n className?: string;\n}\n\nexport function TakeTheTourButton({\n label = \"Take the tour\",\n className,\n}: TakeTheTourButtonProps) {\n const { available, start } = useTourLauncher();\n if (!available) return null;\n return (\n <button\n type=\"button\"\n onClick={start}\n title={label}\n aria-label={label}\n className={cn(\n \"flex items-center gap-1 px-2 py-1 rounded-md text-[10px]\",\n \"text-[#8890A0] hover:text-[#E2E8F0] hover:bg-[#1E2738] transition-colors cursor-pointer\",\n \"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[#8C34E9]\",\n className\n )}\n >\n <Sparkles className=\"h-3 w-3 text-[#8C34E9]\" />\n <span className=\"shell-hidden shell-sm:inline\">{label}</span>\n </button>\n );\n}\n","/**\n * useSpotlight — resolves a `data-tour-target` element and keeps its\n * measured rect fresh across resize/scroll/layout-shift, for TourShell's\n * spotlight render path.\n *\n * This is the DOM-touching half of the spotlight primitive; the actual\n * placement geometry is pure functions in spotlightMath.ts. This package has\n * no jsdom/@testing-library/react (see tourMachine.ts's own note on the same\n * gap), so this hook's correctness is exercised for real once a consumer\n * wires it up against real markup — see the R1 plan's step 3 manual-\n * verification checkpoint, not a unit test here.\n */\nimport { useEffect, useRef, useState } from \"react\";\nimport type { Rect } from \"./spotlightMath\";\n\nexport interface UseSpotlightResult {\n /** The target's current viewport-relative rect, or null while unresolved. */\n rect: Rect | null;\n /** False if no element in the DOM currently carries this selector —\n * callers must fall back to the non-spotlight rendering path, never\n * throw or render a spotlight around nothing. */\n found: boolean;\n}\n\nconst RESIZE_DEBOUNCE_MS = 100;\n\n/**\n * @param selector The `data-tour-target` attribute value to look for\n * (e.g. \"service-card-policy-deployment\"). Pass `null` to disable\n * resolution entirely (e.g. the current step has no spotlight).\n */\nexport function useSpotlight(selector: string | null): UseSpotlightResult {\n const [rect, setRect] = useState<Rect | null>(null);\n const [found, setFound] = useState(false);\n // Guards the one-time scroll-into-view per selector, per useSpotlight's own\n // doc comment: re-measuring on resize/scroll must never re-trigger a scroll.\n const scrolledForSelector = useRef<string | null>(null);\n\n useEffect(() => {\n if (!selector) {\n setRect(null);\n setFound(false);\n return;\n }\n\n let resizeTimer: ReturnType<typeof setTimeout> | undefined;\n let cancelled = false;\n\n const measure = () => {\n if (cancelled) return;\n const el = document.querySelector<HTMLElement>(\n `[data-tour-target=\"${selector}\"]`\n );\n if (!el) {\n setRect(null);\n setFound(false);\n return;\n }\n const domRect = el.getBoundingClientRect();\n setRect({\n top: domRect.top,\n left: domRect.left,\n width: domRect.width,\n height: domRect.height,\n });\n setFound(true);\n\n if (scrolledForSelector.current !== selector) {\n scrolledForSelector.current = selector;\n el.scrollIntoView({ block: \"center\", behavior: \"smooth\" });\n }\n };\n\n // Initial measure. A freshly-mounted target may not have painted yet on\n // the very first tick (e.g. right after the tour opens) — one microtask\n // deferral covers that without a fixed arbitrary delay.\n measure();\n const initialFrame = requestAnimationFrame(measure);\n\n const onResize = () => {\n if (resizeTimer) clearTimeout(resizeTimer);\n resizeTimer = setTimeout(measure, RESIZE_DEBOUNCE_MS);\n };\n // Capture phase: catches scroll on any ancestor scroll container, not\n // just document-level scroll (document-level is what ServiceHub.tsx\n // actually uses today, per the R1 plan's grounding, but capture-phase\n // costs nothing extra and keeps this hook correct if that changes).\n const onScroll = () => measure();\n\n window.addEventListener(\"resize\", onResize);\n window.addEventListener(\"scroll\", onScroll, { capture: true });\n\n // Layout shifts with no resize/scroll event — e.g. a conditionally\n // rendered sibling card appearing above the target, or the target\n // itself not existing yet at mount (e.g. the tour opened before\n // ServiceHub's card grid finished loading its services query) and\n // appearing moments later. Observing document.body rather than the\n // target's own (possibly not-yet-existent) parent means both cases are\n // covered by the same subscription with no race between \"did the\n // target exist when we looked\" and \"attach the observer\" — `measure`\n // itself is what re-checks `found` on every mutation, so a target that\n // doesn't exist yet is simply re-resolved the moment it does.\n const observer = new MutationObserver(measure);\n observer.observe(document.body, {\n childList: true,\n subtree: true,\n attributes: true,\n });\n\n return () => {\n cancelled = true;\n cancelAnimationFrame(initialFrame);\n if (resizeTimer) clearTimeout(resizeTimer);\n window.removeEventListener(\"resize\", onResize);\n window.removeEventListener(\"scroll\", onScroll, { capture: true });\n observer?.disconnect();\n };\n }, [selector]);\n\n return { rect, found };\n}\n","/**\n * Pure geometry behind the spotlight rendering mode — no React, no DOM\n * access, just numbers in and numbers out. Factored out so it's testable\n * without jsdom (this package has none — see tourMachine.ts for the same\n * split/precedent on the step-index state machine).\n *\n * TourShell is the consumer: given a target element's measured rect (from\n * useSpotlight.ts) and the current viewport size, it needs to know (a) the\n * four backdrop-frame rectangles that surround the target without ever\n * covering it, and (b) where to place the explanation panel so it doesn't\n * overflow the viewport, honoring a preferred placement when there's room.\n */\n\nexport interface Rect {\n top: number;\n left: number;\n width: number;\n height: number;\n}\n\nexport interface Size {\n width: number;\n height: number;\n}\n\nexport type Placement = \"top\" | \"bottom\" | \"left\" | \"right\" | \"auto\";\n\n/** The four backdrop segments that frame a target rect without covering it.\n * Each is a plain rect in viewport (fixed-position) coordinates. A segment\n * with zero width or height (e.g. the target touches a viewport edge) is\n * still returned — the caller can skip rendering a degenerate rect, but\n * the geometry itself doesn't special-case it. */\nexport interface SpotlightFrame {\n top: Rect;\n bottom: Rect;\n left: Rect;\n right: Rect;\n}\n\n/** Frames `target` within a `viewport` sized 0,0 to width,height. */\nexport function computeSpotlightFrame(\n target: Rect,\n viewport: Size\n): SpotlightFrame {\n const targetBottom = target.top + target.height;\n const targetRight = target.left + target.width;\n\n return {\n // Full-width strip above the target.\n top: {\n top: 0,\n left: 0,\n width: viewport.width,\n height: Math.max(0, target.top),\n },\n // Full-width strip below the target.\n bottom: {\n top: targetBottom,\n left: 0,\n width: viewport.width,\n height: Math.max(0, viewport.height - targetBottom),\n },\n // Strip to the left of the target, spanning only the target's own row\n // (the top/bottom strips already cover the rest of those rows).\n left: {\n top: target.top,\n left: 0,\n width: Math.max(0, target.left),\n height: target.height,\n },\n // Strip to the right of the target, same row-only span as `left`.\n right: {\n top: target.top,\n left: targetRight,\n width: Math.max(0, viewport.width - targetRight),\n height: target.height,\n },\n };\n}\n\ntype ConcretePlacement = Exclude<Placement, \"auto\">;\n\nconst FALLBACK_ORDER: Record<ConcretePlacement, ConcretePlacement[]> = {\n bottom: [\"bottom\", \"top\", \"right\", \"left\"],\n top: [\"top\", \"bottom\", \"right\", \"left\"],\n right: [\"right\", \"left\", \"bottom\", \"top\"],\n left: [\"left\", \"right\", \"bottom\", \"top\"],\n};\n// \"auto\" tries the same order as the default \"bottom\" preference.\nconst AUTO_ORDER: ConcretePlacement[] = FALLBACK_ORDER.bottom;\n\nconst PANEL_GAP = 12;\n\nexport interface PanelPosition {\n top: number;\n left: number;\n placement: ConcretePlacement;\n}\n\n/**\n * Where to place a panel of `panelSize` next to `target`, preferring\n * `placement` but falling back through the other three sides (then finally\n * clamping into the viewport) if the preferred side doesn't have room.\n */\nexport function computePanelPosition(\n target: Rect,\n panelSize: Size,\n viewport: Size,\n placement: Placement = \"auto\"\n): PanelPosition {\n const order = placement === \"auto\" ? AUTO_ORDER : FALLBACK_ORDER[placement];\n\n for (const candidate of order) {\n const pos = placeAt(candidate, target, panelSize);\n if (fits(pos, panelSize, viewport)) {\n return { ...pos, placement: candidate };\n }\n }\n\n // Nothing fit cleanly (a very small viewport, or a target close to every\n // edge at once) — fall back to the first candidate's position, clamped\n // into the viewport rather than left overflowing.\n const fallback = order[0];\n const pos = placeAt(fallback, target, panelSize);\n return {\n top: clamp(pos.top, 0, Math.max(0, viewport.height - panelSize.height)),\n left: clamp(pos.left, 0, Math.max(0, viewport.width - panelSize.width)),\n placement: fallback,\n };\n}\n\nfunction placeAt(\n side: ConcretePlacement,\n target: Rect,\n panelSize: Size\n): { top: number; left: number } {\n const targetCenterX = target.left + target.width / 2;\n const targetCenterY = target.top + target.height / 2;\n\n switch (side) {\n case \"bottom\":\n return {\n top: target.top + target.height + PANEL_GAP,\n left: targetCenterX - panelSize.width / 2,\n };\n case \"top\":\n return {\n top: target.top - panelSize.height - PANEL_GAP,\n left: targetCenterX - panelSize.width / 2,\n };\n case \"right\":\n return {\n top: targetCenterY - panelSize.height / 2,\n left: target.left + target.width + PANEL_GAP,\n };\n case \"left\":\n return {\n top: targetCenterY - panelSize.height / 2,\n left: target.left - panelSize.width - PANEL_GAP,\n };\n }\n}\n\nfunction fits(\n pos: { top: number; left: number },\n panelSize: Size,\n viewport: Size\n): boolean {\n return (\n pos.top >= 0 &&\n pos.left >= 0 &&\n pos.top + panelSize.height <= viewport.height &&\n pos.left + panelSize.width <= viewport.width\n );\n}\n\nfunction clamp(value: number, min: number, max: number): number {\n return Math.min(Math.max(value, min), max);\n}\n","/**\n * TourShell — the Opi onboarding tour overlay chrome.\n *\n * Deliberately shell-only, same split as OpiInsightsShell: this owns the\n * overlay, orb, narration area, step dots, and Back/Next/Skip controls.\n * The consumer (Portal) owns the step list, narration fetching (falling\n * back to the step's scripted text), and persistence. Hand-rolled overlay\n * (no Radix Dialog) to stay dependency-free, matching OpiInsightsShell's\n * own precedent rather than pulling in /primitives.\n *\n * Two render modes, chosen automatically per step:\n * - Centered modal (original, unchanged) — used for intro/outro steps, any\n * \"service\" step authored without `step.spotlight`, and as the fallback\n * when a spotlight target can't be resolved on the page at all.\n * - Spotlight — used when `step.spotlight` is present AND useSpotlight\n * resolves a real element for it. The backdrop is drawn as four\n * rectangles framing the target (never a single element covering it),\n * so the real page element underneath stays exactly as clickable,\n * hoverable, and focusable as it already was — no coordination needed\n * with whatever onClick the consumer's own element already has. See\n * spotlightMath.ts for the geometry and useSpotlight.ts for the DOM\n * measurement this mode is built on.\n */\nimport { useEffect, useRef, useState, type ReactNode } from \"react\";\nimport {\n X,\n ChevronLeft,\n ChevronRight,\n ArrowRight,\n ExternalLink,\n} from \"lucide-react\";\nimport type { TourStep } from \"./types\";\nimport { useSpotlight } from \"./useSpotlight\";\nimport { computeSpotlightFrame, computePanelPosition } from \"./spotlightMath\";\n\nexport interface TourShellSkin {\n accent: string;\n overlayBg: string;\n panelBg: string;\n panelBorder: string;\n mutedText: string;\n bodyText: string;\n softText: string;\n}\n\nconst defaultSkin: TourShellSkin = {\n accent: \"#8C34E9\",\n overlayBg: \"rgba(10,14,26,0.72)\",\n panelBg: \"#0e1624\",\n panelBorder: \"1px solid #1e2738\",\n mutedText: \"#596475\",\n bodyText: \"#c0c6d0\",\n softText: \"#8890a0\",\n};\n\n/** Panel size assumed for spotlight placement math — the spotlight panel is\n * narrower/shorter than the centered modal's max-w-md, since it sits next\n * to a card rather than owning the whole screen. Kept as a constant (not\n * measured live via ref) because spotlightMath needs a size up front to\n * compute placement before the panel has painted — the panel's own CSS\n * width/height below must match this or the two drift apart. */\nconst SPOTLIGHT_PANEL_SIZE = { width: 320, height: 280 };\n\n/*\n * Spacing and button typography are inline styles, not Tailwind utilities.\n * Apps that import the prebuilt shell CSS inside `@layer shell-shim` (the\n * lowest layer) have their own preflight in `base`, which outranks it and\n * resets padding and margin everywhere, and a button's font, colour, radius,\n * background and opacity. A class like `px-5` or `text-[12px]` then only\n * survives if that app happens to generate the same class itself. Inline\n * styles beat every cascade layer, so the tour looks the same in Portal (which\n * scans this package's source) and in every shell-shim app, with no class\n * list for modules to carry. Colours already work this way, through `skin`.\n * Hover states that preflight would override on a button use `useHover`.\n */\nfunction useHover() {\n const [hovered, setHovered] = useState(false);\n return [\n hovered,\n {\n onMouseEnter: () => setHovered(true),\n onMouseLeave: () => setHovered(false),\n },\n ] as const;\n}\n\nexport interface TourShellProps {\n step: TourStep;\n index: number;\n total: number;\n isFirst: boolean;\n isLast: boolean;\n onNext: () => void;\n onBack: () => void;\n onDismiss: () => void;\n /** Narration to render — pass the live AI reply once resolved; while\n * pending or on failure, pass `step.fallbackText` so the tour never\n * shows a blank state. */\n narration: string;\n /** True while an AI narration pass is in flight — shows a subtle pulse, never blocks Next. */\n narrationPending?: boolean;\n /** Rendered below the narration/highlights — e.g. a docked OpiAskBar. */\n children?: ReactNode;\n skin?: TourShellSkin;\n /** Orb / avatar mark shown in the header — pass OpiOrb from the consumer. */\n orb?: ReactNode;\n /**\n * Fires when the user activates the spotlighted element itself (a\n * document-level capture-phase listener scoped to the resolved target —\n * see useSpotlight.ts), alongside — never instead of — whatever the\n * consumer's own element does on click. Only meaningful when the current\n * step has `spotlight` and a target was found; ignored otherwise. Opt-in\n * per step via the consumer wiring it up, not a built-in auto-advance —\n * a later in-app spotlight (inside a subdomain) may want \"just look\n * here,\" no navigation expected.\n */\n onSpotlightTargetActivated?: () => void;\n}\n\n/**\n * Full-viewport dismissible overlay. Esc always dismisses. In centered\n * mode, clicking the backdrop dismisses too; in spotlight mode, each of the\n * four backdrop segments independently dismisses on click, same behavior,\n * just split across four elements instead of one.\n */\nexport function TourShell({\n step,\n index,\n total,\n isFirst,\n isLast,\n onNext,\n onBack,\n onDismiss,\n narration,\n narrationPending,\n children,\n skin = defaultSkin,\n orb,\n onSpotlightTargetActivated,\n}: TourShellProps) {\n const panelRef = useRef<HTMLDivElement>(null);\n const { rect, found } = useSpotlight(step.spotlight?.selector ?? null);\n const isSpotlight = Boolean(step.spotlight) && found && rect !== null;\n\n useEffect(() => {\n const onKeyDown = (e: KeyboardEvent) => {\n if (e.key === \"Escape\") onDismiss();\n };\n document.addEventListener(\"keydown\", onKeyDown);\n return () => document.removeEventListener(\"keydown\", onKeyDown);\n }, [onDismiss]);\n\n useEffect(() => {\n panelRef.current?.focus();\n }, [step.id]);\n\n // Document-level capture listener for onSpotlightTargetActivated — fires\n // alongside the target's own click handling (never stopPropagation'd),\n // so it never fights whatever onClick the consumer's element already has.\n useEffect(() => {\n if (!isSpotlight || !onSpotlightTargetActivated || !step.spotlight) return;\n const selector = step.spotlight.selector;\n const onClickCapture = (e: MouseEvent) => {\n const target = e.target as Element | null;\n if (target?.closest(`[data-tour-target=\"${selector}\"]`)) {\n onSpotlightTargetActivated();\n }\n };\n document.addEventListener(\"click\", onClickCapture, { capture: true });\n return () =>\n document.removeEventListener(\"click\", onClickCapture, {\n capture: true,\n });\n }, [isSpotlight, onSpotlightTargetActivated, step.spotlight]);\n\n if (isSpotlight && rect) {\n const viewport = { width: window.innerWidth, height: window.innerHeight };\n const frame = computeSpotlightFrame(rect, viewport);\n const panelPos = computePanelPosition(\n rect,\n SPOTLIGHT_PANEL_SIZE,\n viewport,\n step.spotlight?.placement ?? \"auto\"\n );\n\n return (\n <>\n {([\"top\", \"bottom\", \"left\", \"right\"] as const).map(side => {\n const segment = frame[side];\n if (segment.width <= 0 || segment.height <= 0) return null;\n return (\n <div\n key={side}\n className=\"fixed z-50\"\n style={{\n top: segment.top,\n left: segment.left,\n width: segment.width,\n height: segment.height,\n background: skin.overlayBg,\n }}\n onClick={onDismiss}\n />\n );\n })}\n {/* Ring around the target — never covers it, pointer-events none so\n it can't intercept the click meant for the real element. */}\n <div\n className=\"fixed z-50 rounded-xl pointer-events-none transition-all\"\n style={{\n top: rect.top - 4,\n left: rect.left - 4,\n width: rect.width + 8,\n height: rect.height + 8,\n boxShadow: `0 0 0 2px ${skin.accent}, 0 0 24px 4px ${skin.accent}66`,\n }}\n />\n <div\n ref={panelRef}\n tabIndex={-1}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={step.label}\n className=\"fixed z-50 rounded-xl overflow-hidden flex flex-col outline-none\"\n style={{\n top: panelPos.top,\n left: panelPos.left,\n width: SPOTLIGHT_PANEL_SIZE.width,\n maxHeight: SPOTLIGHT_PANEL_SIZE.height,\n background: skin.panelBg,\n border: skin.panelBorder,\n }}\n >\n <TourShellHeader step={step} orb={orb} onDismiss={onDismiss} />\n <TourShellStepDots total={total} index={index} skin={skin} />\n <div className=\"overflow-y-auto\" style={{ padding: \"12px 16px\" }}>\n <TourShellBody\n gap={12}\n step={step}\n narration={narration}\n narrationPending={narrationPending}\n skin={skin}\n children={children}\n />\n </div>\n <TourShellFooter\n isFirst={isFirst}\n isLast={isLast}\n onNext={onNext}\n onBack={onBack}\n onDismiss={onDismiss}\n skin={skin}\n />\n </div>\n </>\n );\n }\n\n return (\n <div\n className=\"fixed inset-0 z-50 flex items-center justify-center\"\n style={{ background: skin.overlayBg, padding: 16 }}\n onClick={e => {\n if (e.target === e.currentTarget) onDismiss();\n }}\n >\n <div\n ref={panelRef}\n tabIndex={-1}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={step.label}\n className=\"w-full max-w-md rounded-xl overflow-hidden flex flex-col max-h-[85vh] outline-none\"\n style={{ background: skin.panelBg, border: skin.panelBorder }}\n >\n <TourShellHeader step={step} orb={orb} onDismiss={onDismiss} />\n <TourShellStepDots total={total} index={index} skin={skin} />\n <div className=\"overflow-y-auto\" style={{ padding: \"16px 20px\" }}>\n <TourShellBody\n gap={16}\n step={step}\n narration={narration}\n narrationPending={narrationPending}\n skin={skin}\n children={children}\n />\n </div>\n <TourShellFooter\n isFirst={isFirst}\n isLast={isLast}\n onNext={onNext}\n onBack={onBack}\n onDismiss={onDismiss}\n skin={skin}\n />\n </div>\n </div>\n );\n}\n\nfunction TourShellHeader({\n step,\n orb,\n onDismiss,\n}: {\n step: TourStep;\n orb: ReactNode;\n onDismiss: () => void;\n}) {\n const [closeHovered, closeHover] = useHover();\n return (\n <div\n className=\"flex items-center justify-between gap-3 shrink-0\"\n style={{\n background: \"linear-gradient(135deg, #2a1450 0%, #1a0e33 100%)\",\n padding: \"16px 20px\",\n }}\n >\n <div className=\"flex items-center gap-2.5 min-w-0\">\n {orb}\n <span className=\"text-sm font-bold text-white truncate\">\n {step.label}\n </span>\n </div>\n <button\n onClick={onDismiss}\n {...closeHover}\n className=\"transition-colors shrink-0\"\n style={{\n padding: 4,\n borderRadius: 4,\n background: closeHovered ? \"rgba(255,255,255,0.1)\" : \"transparent\",\n }}\n title=\"Skip tour\"\n >\n <X size={16} className=\"text-white/70\" />\n </button>\n </div>\n );\n}\n\nfunction TourShellStepDots({\n total,\n index,\n skin,\n}: {\n total: number;\n index: number;\n skin: TourShellSkin;\n}) {\n return (\n <div\n className=\"flex items-center gap-1.5 shrink-0\"\n style={{ borderBottom: skin.panelBorder, padding: \"12px 20px\" }}\n >\n {Array.from({ length: total }, (_, i) => (\n <span\n key={i}\n className=\"h-1.5 rounded-full transition-all\"\n style={{\n width: i === index ? 20 : 8,\n background: i === index ? skin.accent : skin.mutedText,\n opacity: i === index ? 1 : 0.4,\n }}\n />\n ))}\n </div>\n );\n}\n\nfunction TourShellBody({\n step,\n narration,\n narrationPending,\n skin,\n children,\n gap,\n}: {\n step: TourStep;\n narration: string;\n narrationPending?: boolean;\n skin: TourShellSkin;\n children?: ReactNode;\n /** Vertical space between the blocks below, in px. */\n gap: number;\n}) {\n const features = step.features ?? [];\n const highlights = step.highlights ?? [];\n const hasList = features.length > 0 || highlights.length > 0;\n const hasAction = Boolean(step.action);\n const hasChildren = Boolean(children);\n // Every block except the last gets `gap` below it (what `space-y` did).\n const gapIf = (hasNext: boolean) => (hasNext ? gap : 0);\n const itemGap = (i: number, count: number, px: number) =>\n i < count - 1 ? px : 0;\n return (\n <>\n <p\n className=\"text-[13px] leading-relaxed transition-opacity\"\n style={{\n color: skin.bodyText,\n opacity: narrationPending ? 0.7 : 1,\n marginBottom: gapIf(hasList || hasAction || hasChildren),\n }}\n >\n {narration}\n </p>\n\n {features.length > 0 ? (\n <ul style={{ marginBottom: gapIf(hasAction || hasChildren) }}>\n {features.map((f, i) => (\n <li\n key={i}\n className=\"text-[12.5px] leading-relaxed\"\n style={{ marginBottom: itemGap(i, features.length, 8) }}\n >\n <span className=\"font-semibold\" style={{ color: skin.accent }}>\n {f.name}\n </span>\n <span style={{ color: skin.softText }}> — {f.detail}</span>\n </li>\n ))}\n </ul>\n ) : (\n highlights.length > 0 && (\n <ul style={{ marginBottom: gapIf(hasAction || hasChildren) }}>\n {highlights.map((h, i) => (\n <li\n key={i}\n className=\"text-[12.5px] leading-relaxed relative\"\n style={{\n color: skin.softText,\n paddingLeft: 12,\n marginBottom: itemGap(i, highlights.length, 6),\n }}\n >\n <span\n className=\"absolute left-0\"\n style={{ color: skin.mutedText }}\n >\n •\n </span>\n {h}\n </li>\n ))}\n </ul>\n )\n )}\n\n {step.action && (\n <a\n href={step.action.href}\n target={step.action.external ? \"_blank\" : undefined}\n rel={step.action.external ? \"noopener noreferrer\" : undefined}\n className=\"inline-flex items-center gap-1.5 rounded-lg text-[13px] font-semibold transition-opacity hover:opacity-90\"\n style={{\n background: `${skin.accent}20`,\n color: skin.accent,\n padding: \"8px 14px\",\n marginBottom: gapIf(hasChildren),\n }}\n >\n {step.action.label}\n {step.action.external ? (\n <ExternalLink size={13} />\n ) : (\n <ArrowRight size={13} />\n )}\n </a>\n )}\n\n {children}\n </>\n );\n}\n\nfunction TourShellFooter({\n isFirst,\n isLast,\n onNext,\n onBack,\n onDismiss,\n skin,\n}: {\n isFirst: boolean;\n isLast: boolean;\n onNext: () => void;\n onBack: () => void;\n onDismiss: () => void;\n skin: TourShellSkin;\n}) {\n const [nextHovered, nextHover] = useHover();\n return (\n <div\n className=\"flex items-center justify-between gap-2 shrink-0\"\n style={{ borderTop: skin.panelBorder, padding: \"14px 20px\" }}\n >\n <button\n onClick={onDismiss}\n className=\"transition-colors hover:underline\"\n style={{ color: skin.mutedText, fontSize: 12, fontWeight: 500 }}\n >\n Skip tour\n </button>\n <div className=\"flex items-center gap-2\">\n {!isFirst && (\n <button\n onClick={onBack}\n className=\"flex items-center gap-1 transition-colors\"\n style={{\n color: skin.bodyText,\n border: skin.panelBorder,\n padding: \"6px 12px\",\n borderRadius: 6,\n fontSize: 12.5,\n fontWeight: 500,\n }}\n >\n <ChevronLeft size={13} />\n Back\n </button>\n )}\n <button\n onClick={onNext}\n {...nextHover}\n className=\"flex items-center gap-1 transition-opacity\"\n style={{\n background: \"linear-gradient(135deg, #8C34E9 0%, #5B1FA6 100%)\",\n color: \"#fff\",\n padding: \"6px 14px\",\n borderRadius: 6,\n fontSize: 12.5,\n fontWeight: 700,\n opacity: nextHovered ? 0.9 : 1,\n }}\n >\n {isLast ? \"Done\" : \"Next\"}\n {!isLast && <ChevronRight size={13} />}\n </button>\n </div>\n </div>\n );\n}\n","/**\n * useTour — pure step-index state machine for the onboarding tour.\n *\n * Deliberately dumb: it doesn't fetch steps, doesn't call any AI endpoint,\n * doesn't persist anything itself. The consumer (Portal) owns all of that\n * and is told about every state change via the on* callbacks so it can\n * write to user_settings however it likes. See OpiInsightsShell for the\n * same shell-owns-chrome/consumer-owns-data split.\n *\n * The actual transitions live in tourMachine.ts as plain functions (no\n * React import) so they're testable without a DOM renderer — this package\n * has no jsdom/@testing-library/react set up (see src/hooks/hooks.test.ts).\n * This file is just the useState wrapper around them.\n */\nimport { useCallback, useState } from \"react\";\nimport type { TourStep } from \"./types\";\nimport {\n backIndex,\n gotoIndex,\n nextIndex,\n resolveInitialIndex,\n} from \"./tourMachine\";\n\nexport interface UseTourOptions {\n /** Called whenever the current step changes (not on initial mount). */\n onStepChange?: (step: TourStep, index: number) => void;\n /** Called once when the user reaches the last step and advances past it. */\n onComplete?: () => void;\n /** Called when the user dismisses the tour before completion. */\n onDismiss?: () => void;\n /** Step id to resume on, if the tour was already in progress. */\n initialStepId?: string | null;\n}\n\nexport interface UseTourResult {\n step: TourStep;\n index: number;\n total: number;\n isFirst: boolean;\n isLast: boolean;\n next: () => void;\n back: () => void;\n goto: (stepId: string) => void;\n dismiss: () => void;\n}\n\nexport function useTour(\n steps: TourStep[],\n opts: UseTourOptions = {}\n): UseTourResult {\n if (steps.length === 0) {\n throw new Error(\"useTour requires at least one step\");\n }\n\n const [index, setIndex] = useState(() =>\n resolveInitialIndex(steps, opts.initialStepId)\n );\n\n const next = useCallback(() => {\n const result = nextIndex(steps, index);\n if (result.completed) {\n opts.onComplete?.();\n return;\n }\n setIndex(result.index);\n opts.onStepChange?.(steps[result.index], result.index);\n // eslint-disable-next-line react-hooks/exhaustive-deps -- opts callbacks read fresh via closure; steps identity is expected stable per mount\n }, [index, steps]);\n\n const back = useCallback(() => {\n const target = backIndex(index);\n if (target === index) return;\n setIndex(target);\n opts.onStepChange?.(steps[target], target);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [index, steps]);\n\n const goto = useCallback(\n (stepId: string) => {\n const target = gotoIndex(steps, stepId, index);\n if (target === index) return;\n setIndex(target);\n opts.onStepChange?.(steps[target], target);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n },\n [steps, index]\n );\n\n const dismiss = useCallback(() => {\n opts.onDismiss?.();\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n return {\n step: steps[index],\n index,\n total: steps.length,\n isFirst: index === 0,\n isLast: index === steps.length - 1,\n next,\n back,\n goto,\n dismiss,\n };\n}\n","/**\n * Pure step-index transitions behind useTour — factored out so the actual\n * state machine logic is testable in a plain Node environment, without a\n * React renderer (this package has no jsdom/@testing-library/react set up;\n * see src/hooks/hooks.test.ts's note on the same limitation). useTour.ts is\n * the thin useState wrapper; this file has no React import at all.\n */\nimport type { TourStep } from \"./types\";\n\nexport interface TourMachineResult {\n index: number;\n /** True if this call reached the end and should fire onComplete instead of moving. */\n completed: boolean;\n}\n\nexport function resolveInitialIndex(\n steps: TourStep[],\n initialStepId: string | null | undefined\n): number {\n if (!initialStepId) return 0;\n const found = steps.findIndex(s => s.id === initialStepId);\n return found === -1 ? 0 : found;\n}\n\nexport function nextIndex(\n steps: TourStep[],\n current: number\n): TourMachineResult {\n if (current >= steps.length - 1) {\n return { index: current, completed: true };\n }\n return { index: current + 1, completed: false };\n}\n\nexport function backIndex(current: number): number {\n return Math.max(0, current - 1);\n}\n\nexport function gotoIndex(\n steps: TourStep[],\n stepId: string,\n current: number\n): number {\n const target = steps.findIndex(s => s.id === stepId);\n return target === -1 ? current : target;\n}\n","/**\n * OpiTourHost — runs the active tour: loads the user's saved progress from\n * Portal, shows TourShell when the tour is opened (by the header's\n * \"Take the tour\", or on its own when the definition sets `autoStart`), narrates each step in\n * Opi's voice, and records progress as the user moves.\n *\n * Mounted once by DashboardLayout — modules don't mount their own (two hosts\n * would stack two overlays); renders nothing while the module has no\n * registered tour. Same behaviour as Portal's Service Hub tour (UserTour):\n * scripted text first with live narration replacing it when it arrives,\n * resume from the last step on auto-open, restart from step 1 on replay.\n *\n * The inner runner is mounted only once progress has loaded, because\n * useTour reads `initialStepId` at mount and would otherwise lock onto\n * step 1 whatever the saved progress says.\n */\nimport { useEffect, useRef, useState, type ReactNode } from \"react\";\nimport { Sparkles } from \"lucide-react\";\nimport { TourShell, type TourShellSkin } from \"./TourShell\";\nimport { useTour } from \"./useTour\";\nimport {\n useTourProgress,\n useTourNarrator,\n useTourState,\n useTourLauncher,\n} from \"./TourProvider\";\nimport { decideAutoOpen, type TourProgressUpdate } from \"./tourProgress\";\nimport type { TourDefinition } from \"./tourStore\";\nimport type { TourStep } from \"./types\";\n\nexport interface OpiTourHostProps {\n /** Orb/avatar shown in the tour header. Defaults to a plain mark. */\n orb?: ReactNode;\n skin?: TourShellSkin;\n}\n\nexport function OpiTourHost(props: OpiTourHostProps) {\n const { active } = useTourState();\n if (!active) return null;\n // Keyed by id so moving between pages with different tours starts clean.\n return <ActiveTourHost key={active.id} tour={active} {...props} />;\n}\n\nfunction ActiveTourHost({\n tour,\n orb,\n skin,\n}: OpiTourHostProps & { tour: TourDefinition }) {\n const { session } = useTourState();\n const { open } = useTourLauncher();\n const { phase, progress, record } = useTourProgress(tour.id);\n\n // Decide once, when the first read settles, whether to open by itself.\n const decided = useRef(false);\n useEffect(() => {\n if (phase === \"loading\" || decided.current) return;\n decided.current = true;\n if (decideAutoOpen(tour.autoStart, { phase, progress })) {\n open({ restart: false });\n }\n }, [phase, progress, tour.autoStart, open]);\n\n if (!session || phase === \"loading\") return null;\n\n return (\n <TourRunner\n steps={tour.steps}\n initialStepId={session.restart ? null : progress.lastStepId}\n onProgress={record}\n orb={orb ?? <DefaultOrb />}\n skin={skin}\n />\n );\n}\n\nfunction DefaultOrb() {\n return (\n <span\n className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-full\"\n style={{\n background: \"linear-gradient(135deg, #8C34E9 0%, #5B1FA6 100%)\",\n }}\n >\n <Sparkles className=\"h-4 w-4 text-white\" />\n </span>\n );\n}\n\nfunction TourRunner({\n steps,\n initialStepId,\n onProgress,\n orb,\n skin,\n}: {\n steps: TourStep[];\n initialStepId: string | null;\n onProgress: (update: TourProgressUpdate) => Promise<void>;\n orb: ReactNode;\n skin?: TourShellSkin;\n}) {\n const { close } = useTourLauncher();\n const narrate = useTourNarrator();\n const [narrationByStep, setNarrationByStep] = useState<\n Record<string, string>\n >({});\n const [pendingStepId, setPendingStepId] = useState<string | null>(null);\n const requested = useRef(new Set<string>());\n // useTour keeps the opts it was first given for `dismiss`, so read the\n // current step from a ref rather than from `tour` inside onDismiss.\n const currentStepId = useRef(\n steps.find(s => s.id === initialStepId)?.id ?? steps[0].id\n );\n\n const requestNarration = (step: TourStep) => {\n if (requested.current.has(step.id)) return;\n requested.current.add(step.id);\n setPendingStepId(step.id);\n narrate(step)\n .then(text => setNarrationByStep(prev => ({ ...prev, [step.id]: text })))\n // Any failure (AI gated off, rate limit, offline) just leaves the\n // scripted text in place — the tour never needs to say why.\n .catch(() => {})\n .finally(() => setPendingStepId(cur => (cur === step.id ? null : cur)));\n };\n\n const tour = useTour(steps, {\n initialStepId,\n onStepChange: step => {\n currentStepId.current = step.id;\n void onProgress({ status: \"in-progress\", lastStepId: step.id });\n requestNarration(step);\n },\n onComplete: () => {\n void onProgress({ status: \"done\", lastStepId: null });\n close();\n },\n onDismiss: () => {\n void onProgress({\n status: \"dismissed\",\n lastStepId: currentStepId.current,\n });\n close();\n },\n });\n\n // onStepChange only fires on moves, so narrate the step the tour opens on.\n // Progress is deliberately not recorded here: peeking at a replay must not\n // turn a finished tour back into \"in progress\".\n useEffect(() => {\n requestNarration(tour.step);\n // eslint-disable-next-line react-hooks/exhaustive-deps -- once per open\n }, []);\n\n return (\n <TourShell\n step={tour.step}\n index={tour.index}\n total={tour.total}\n isFirst={tour.isFirst}\n isLast={tour.isLast}\n onNext={tour.next}\n onBack={tour.back}\n onDismiss={tour.dismiss}\n narration={narrationByStep[tour.step.id] ?? tour.step.fallbackText}\n narrationPending={pendingStepId === tour.step.id}\n orb={orb}\n skin={skin}\n />\n );\n}\n"],"mappings":"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","names":["jsx","useEffect","pos","useEffect","useRef","useState","jsx","jsxs","useState","useRef","useEffect","useState","useState","useEffect","useRef","useState","Sparkles","jsx","useRef","useEffect","Sparkles","useState"]}
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  platformUrl
3
- } from "./chunk-4QJMLQSH.js";
3
+ } from "./chunk-7LI4Y6WT.js";
4
4
  import {
5
5
  appendSelectionParam
6
6
  } from "./chunk-DMUQ5WNO.js";
@@ -236,4 +236,4 @@ export {
236
236
  normalizeServiceSlug,
237
237
  buildCrossServiceLinks
238
238
  };
239
- //# sourceMappingURL=chunk-4ERWOH67.js.map
239
+ //# sourceMappingURL=chunk-MGTOQY6O.js.map
@@ -1,7 +1,11 @@
1
+ import {
2
+ OpiTourHost,
3
+ TakeTheTourButton
4
+ } from "./chunk-LUQAR6NB.js";
1
5
  import {
2
6
  platformUrl,
3
7
  portalUrl
4
- } from "./chunk-4QJMLQSH.js";
8
+ } from "./chunk-7LI4Y6WT.js";
5
9
  import {
6
10
  readActiveEnterpriseCookie
7
11
  } from "./chunk-RSFGV7CG.js";
@@ -512,6 +516,7 @@ function DashboardShell({
512
516
  hierarchyNavigator && /* @__PURE__ */ jsx4("div", { className: "min-w-0 overflow-x-auto scrollbar-none", children: hierarchyNavigator })
513
517
  ] }),
514
518
  /* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-2 shrink-0", children: [
519
+ /* @__PURE__ */ jsx4(TakeTheTourButton, {}),
515
520
  reportingToolbar,
516
521
  portalUrl2 && /* @__PURE__ */ jsxs4(
517
522
  "a",
@@ -541,6 +546,7 @@ function DashboardShell({
541
546
  ),
542
547
  /* @__PURE__ */ jsx4("main", { className: "flex-1 p-4 md:p-6 pb-14", children })
543
548
  ] }),
549
+ /* @__PURE__ */ jsx4(OpiTourHost, {}),
544
550
  footer,
545
551
  showHealthFooter && user?.role === "platform_admin" && /* @__PURE__ */ jsx4(
546
552
  HealthFooter,
@@ -1253,4 +1259,4 @@ export {
1253
1259
  policyDeploymentConfig,
1254
1260
  SERVICE_CONFIGS
1255
1261
  };
1256
- //# sourceMappingURL=chunk-S4UQSTZN.js.map
1262
+ //# sourceMappingURL=chunk-UQUMDBCZ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/layout/SharedSidebar.tsx","../src/layout/DashboardLayout.tsx","../src/layout/CrossServiceNav.tsx","../src/layout/HealthFooter.tsx","../src/layout/EnterpriseDebugLabel.tsx","../src/layout/DashboardLayoutSkeleton.tsx","../src/layout/SharedPageHeader.tsx","../src/layout/SharedFooter.tsx","../src/layout/ServiceFooter.tsx","../src/layout/createServiceLayout.tsx","../src/layout/serviceConfigs.ts"],"sourcesContent":["/**\n * SharedSidebar — Config-driven sidebar component for all Oplytics subdomains.\n *\n * Replaces per-subdomain DashboardLayout sidebar implementations with a single\n * shared component. Each subdomain provides its own configuration (menu items,\n * service name, feature flags) and gets a consistent sidebar experience.\n *\n * Features:\n * - Resizable sidebar with localStorage persistence\n * - Collapsible icon mode with tooltips\n * - Service branding (icon + name) in header\n * - \"Back to Service Hub/Portal\" link (configurable)\n * - Grouped menu sections with optional section labels\n * - Admin-only menu items (role-gated)\n * - User avatar footer with optional logout dropdown\n * - Mobile-responsive with auto-collapse\n *\n * Usage:\n * import { createServiceLayout } from \"@shared/components/SharedSidebar\";\n * const MyLayout = createServiceLayout({ serviceName: \"SQDCP\", ... });\n * // Then use <MyLayout>{children}</MyLayout> in your routes\n *\n * @module shared/components/SharedSidebar\n */\n\nimport React, { CSSProperties, useEffect, useRef, useState } from \"react\";\n\n/* ── Types ── */\n\nexport interface MenuItem {\n icon: React.ComponentType<{ className?: string }>;\n label: string;\n path: string;\n /** Optional badge text (e.g. count) */\n badge?: string | number;\n /**\n * Optional accent classes applied to this item's icon + label at all times\n * (active or not) — e.g. \"text-purple-400\" to flag a premium/upsell item.\n * Omit for the default muted/active-primary treatment every other item uses.\n */\n accentClassName?: string;\n}\n\nexport interface MenuSection {\n /** Section label displayed above items */\n label?: string;\n /** Alias for label — used by DashboardLayout */\n title?: string;\n items: MenuItem[];\n adminOnly?: boolean;\n /**\n * Optional accent classes applied to the section label text — e.g.\n * \"text-purple-400\" for a \"Premium Service\" heading. Omit for the\n * default muted-foreground treatment.\n */\n labelClassName?: string;\n}\n\nexport interface SharedSidebarConfig {\n /** Display name shown in sidebar header */\n serviceName: string;\n /** Icon component rendered next to service name */\n serviceIcon: React.ReactNode;\n /** Primary navigation items */\n menuSections: MenuSection[];\n /** Show \"Back to Service Hub\" / \"Back to Portal\" link at top of nav */\n backLink?: {\n label: string;\n path: string;\n };\n /** localStorage key prefix for sidebar width persistence */\n storageKeyPrefix?: string;\n /** Default sidebar width in pixels (default: 260) */\n defaultWidth?: number;\n /** Minimum sidebar width in pixels (default: 200) */\n minWidth?: number;\n /** Maximum sidebar width in pixels (default: 480) */\n maxWidth?: number;\n}\n\nexport interface SharedSidebarProps {\n config: SharedSidebarConfig;\n /** Current route location */\n location: string;\n /** Navigate to a path */\n setLocation: (path: string) => void;\n /** Current user object (null if not authenticated) */\n user: { name?: string; email?: string; role?: string } | null;\n /** Logout handler */\n onLogout?: () => void;\n /** Whether sidebar is collapsed */\n isCollapsed: boolean;\n /** Toggle sidebar collapsed state */\n toggleSidebar: () => void;\n /** Whether user has admin role */\n isAdmin: boolean;\n /** Children rendered in SidebarInset main area */\n children: React.ReactNode;\n}\n\n/* ── Constants ── */\n\nconst ADMIN_ROLES = [\n \"enterprise_admin\",\n \"platform_admin\",\n \"admin\",\n \"superuser\",\n];\n\n/* ── Helpers ── */\n\nexport function isAdminRole(role?: string): boolean {\n return !!role && ADMIN_ROLES.includes(role);\n}\n\nexport function getInitials(name?: string): string {\n if (!name) return \"U\";\n return name\n .split(\" \")\n .map(n => n[0])\n .join(\"\")\n .toUpperCase()\n .slice(0, 2);\n}\n\n/**\n * Determines if a menu item is active based on current location.\n * Exact match for root paths, startsWith for nested paths.\n */\nexport function isMenuItemActive(\n itemPath: string,\n location: string,\n basePath?: string\n): boolean {\n if (basePath) {\n // For subdomain routes with a base path (e.g., /app/policy-deployment)\n if (itemPath === basePath) return location === basePath;\n return location.startsWith(itemPath);\n }\n // For root-level routes\n if (itemPath === \"/\") return location === \"/\";\n return location.startsWith(itemPath);\n}\n\n/* ── Resize Hook ── */\n\nexport function useSidebarResize(config: {\n storageKey: string;\n defaultWidth: number;\n minWidth: number;\n maxWidth: number;\n}) {\n const [width, setWidth] = useState(() => {\n if (typeof window === \"undefined\") return config.defaultWidth;\n const saved = localStorage.getItem(config.storageKey);\n return saved ? parseInt(saved, 10) : config.defaultWidth;\n });\n const [isResizing, setIsResizing] = useState(false);\n const startX = useRef(0);\n const startWidth = useRef(config.defaultWidth);\n\n useEffect(() => {\n localStorage.setItem(config.storageKey, String(width));\n }, [width, config.storageKey]);\n\n useEffect(() => {\n if (!isResizing) return;\n const onMove = (e: MouseEvent) => {\n const newWidth = Math.min(\n config.maxWidth,\n Math.max(\n config.minWidth,\n startWidth.current + (e.clientX - startX.current)\n )\n );\n setWidth(newWidth);\n };\n const onUp = () => {\n setIsResizing(false);\n };\n document.addEventListener(\"mousemove\", onMove);\n document.addEventListener(\"mouseup\", onUp);\n document.body.style.cursor = \"col-resize\";\n document.body.style.userSelect = \"none\";\n return () => {\n document.removeEventListener(\"mousemove\", onMove);\n document.removeEventListener(\"mouseup\", onUp);\n document.body.style.cursor = \"\";\n document.body.style.userSelect = \"\";\n };\n }, [isResizing, config.minWidth, config.maxWidth]);\n\n const startResize = (e: React.MouseEvent) => {\n setIsResizing(true);\n startX.current = e.clientX;\n startWidth.current = width;\n };\n\n return { width, isResizing, startResize };\n}\n\n/* ── Export config type for subdomain use ── */\nexport type { SharedSidebarConfig as SidebarConfig };\n","/**\n * DashboardLayout — the canonical sidebar + header shell for every Oplytics\n * subdomain (the \"Policy Deployment\" gold-standard layout, consolidated).\n *\n * Built on the shadcn `Sidebar` primitives shipped from `@pablo2410/shared-ui/primitives`.\n * It renders an identical chrome across services:\n * - Collapsible icon sidebar with a brand header (icon + service name)\n * - \"← Service Hub\" link pinned to the top of the nav\n * - Grouped, optionally admin-gated menu sections\n * - Settings footer\n * - Sticky top header with a hierarchy-breadcrumb slot, reporting-toolbar\n * slot, a \"Take the tour\" trigger (only when the module registers a\n * tour — see @pablo2410/shared-ui/tour), and a user menu\n *\n * The component is intentionally app-agnostic: it imports no app hooks\n * (`useAuth`, `useHierarchy`, router). Apps pass identity, navigation state,\n * and their hierarchy navigator / reporting toolbar in as props/slots.\n *\n * Oplytics dark theme: #0D1220 header, #1E2738 border, #8C34E9 accent.\n */\nimport { type ReactNode } from \"react\";\nimport {\n Sidebar,\n SidebarContent,\n SidebarFooter,\n SidebarHeader,\n SidebarInset,\n SidebarMenu,\n SidebarMenuButton,\n SidebarMenuItem,\n SidebarProvider,\n SidebarTrigger,\n useSidebar,\n Avatar,\n AvatarFallback,\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from \"../primitives\";\nimport { portalUrl as buildPortalUrl } from \"../services/platformDomain\";\nimport { useIsMobile } from \"../primitives/useMobile\";\nimport {\n ArrowLeft,\n ExternalLink,\n LogOut,\n PanelLeft,\n Settings,\n} from \"lucide-react\";\nimport { cn } from \"../utils/cn\";\nimport {\n type MenuItem,\n type MenuSection,\n isMenuItemActive,\n} from \"./SharedSidebar\";\nimport { CrossServiceNav } from \"./CrossServiceNav\";\nimport type { CrossServiceLink } from \"../services/crossNav\";\nimport { HealthFooter } from \"./HealthFooter\";\nimport { EnterpriseDebugLabel } from \"./EnterpriseDebugLabel\";\nimport { TakeTheTourButton } from \"../tour/TakeTheTourButton\";\nimport { OpiTourHost } from \"../tour/OpiTourHost\";\n\n/* ── Re-export sidebar types ── */\nexport type { MenuItem, MenuSection };\n\n/* ── Types ── */\n\nexport interface DashboardLayoutUser {\n name?: string;\n email?: string;\n role?: string;\n avatarUrl?: string;\n /** The user's stable identifier (typically the JWT `openId` claim).\n * Optional — only used by the HealthFooter diagnostic strip. */\n openId?: string;\n /** The enterprise id from the JWT (`enterpriseId` claim). May be null\n * for platform_admin users who haven't selected an active enterprise.\n * Optional — only used by the HealthFooter diagnostic strip. */\n enterpriseId?: number | null;\n}\n\nexport interface DashboardLayoutEnterprise {\n /** The enterprise id the org-hierarchy is currently scoped to. Optional\n * — only used by the HealthFooter diagnostic strip; the visible header\n * badge only needs `name` (and optionally `code`). */\n id?: number;\n name: string;\n code?: string;\n}\n\nexport interface DashboardLayoutProps {\n /** Service/module name shown in the sidebar brand header (e.g. \"OEE Manager\").\n * Stays constant across navigation — for a per-route secondary label\n * (e.g. the active section name), pass `sectionLabel` instead of changing\n * this on navigate. */\n serviceName: string;\n /** Optional secondary label shown next to `serviceName`, separated by a\n * \"›\" (e.g. serviceName=\"Admin Controls\", sectionLabel=\"Users\" renders\n * \"Admin Controls › Users\"). Typically the active nav item's label —\n * update this per route instead of swapping `serviceName` itself, so the\n * primary brand name stays stable while navigating. */\n sectionLabel?: string;\n /** Service icon (a lucide icon element), shown in the sidebar brand + header. */\n serviceIcon?: ReactNode;\n /** Primary navigation sections. */\n menuSections: MenuSection[];\n /** Admin-only navigation sections, rendered when `isAdmin` is true. */\n adminSections?: MenuSection[];\n /** Whether the current user may see `adminSections`. */\n isAdmin?: boolean;\n /** Current route (the app's router supplies this). */\n activePath: string;\n /** Navigate handler (the app's router supplies this). */\n onNavigate: (path: string) => void;\n /** Optional override for active-item detection. */\n isActive?: (itemPath: string, activePath: string) => boolean;\n /** URL for the \"← Service Hub\" link at the top of the nav (full-page nav). */\n serviceHubUrl?: string;\n /** URL for the \"Portal\" link in the header (right of the reporting toolbar).\n * Omit to hide the link. Defaults to the platform portal root when not\n * explicitly disabled — pass `null` to hide it. */\n portalUrl?: string | null;\n /** Direct links to sibling services, rendered under the Service Hub link.\n * Build with `buildCrossServiceLinks` (services/crossNav) so provisioning\n * gating and the ?oh= hierarchy carry-over are handled consistently. */\n crossServiceLinks?: CrossServiceLink[];\n /** Footer \"Settings\" route. Omit to hide the footer. */\n settingsPath?: string;\n /** Authenticated user (drives the header user menu). */\n user?: DashboardLayoutUser | null;\n /** Sign-out handler invoked from the user menu. */\n onLogout?: () => void;\n /** \"User Settings\" target in the user menu (full-page nav). */\n userSettingsUrl?: string;\n /** App-provided hierarchy navigator rendered in the header breadcrumb slot. */\n hierarchyNavigator?: ReactNode;\n /** Selected enterprise — passed to the user menu's diagnostic label\n * (EnterpriseDebugLabel) and, where a hierarchy navigator is supplied,\n * already visible via the breadcrumb; no longer rendered as a separate\n * header badge (removed as redundant with both). */\n enterprise?: DashboardLayoutEnterprise | null;\n /** App-provided reporting toolbar rendered on the right of the header. */\n reportingToolbar?: ReactNode;\n /** Whether the sidebar starts expanded. Defaults to `!isMobile`. */\n defaultOpen?: boolean;\n /** Extra footer rendered fixed at the bottom (e.g. a debug/health bar).\n * Coexists with `showHealthFooter` — pass both if you want both. */\n footer?: ReactNode;\n /** When true AND the current user has `role === \"platform_admin\"`, the\n * shell renders the standardised platform-admin diagnostic strip\n * (`HealthFooter`) fixed at the bottom of the viewport. Subdomains\n * opt in with one prop and inherit any future improvements to the\n * strip without code changes. */\n showHealthFooter?: boolean;\n /**\n * Deployed-build info shown in the HealthFooter diagnostic strip\n * (build: version · commit · date). The consuming app supplies these from\n * its own build-time constants (e.g. Vite `define` of __BUILD_DATE__ /\n * __COMMIT_HASH__) and passes them down — globals can't be read from inside\n * this pre-compiled package, so they must arrive as props.\n */\n buildDate?: string;\n commitHash?: string;\n buildVersion?: string;\n /** Main content. */\n children: ReactNode;\n}\n\n/* Defaults resolve the platform domain at render time (see platformDomain). */\nconst defaultServiceHubUrl = () => buildPortalUrl(\"/app\");\nconst defaultUserSettingsUrl = () => buildPortalUrl(\"/account\");\nconst defaultPortalUrl = () => buildPortalUrl(\"/\");\n\n/* ── Header: user menu ── */\n\nfunction UserMenu({\n user,\n onLogout,\n userSettingsUrl = defaultUserSettingsUrl(),\n enterprise,\n}: {\n user: DashboardLayoutUser;\n onLogout?: () => void;\n userSettingsUrl?: string;\n /** Active enterprise, used to render the TEMP diagnostic chip inside the menu. */\n enterprise?: { id?: number | null; name?: string | null } | null;\n}) {\n const initials = (user.name || \"U\")\n .split(\" \")\n .map(n => n[0])\n .join(\"\")\n .toUpperCase()\n .slice(0, 2);\n\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <button className=\"flex items-center gap-2 rounded-md px-2 py-1 hover:bg-[#1E2738] transition-colors outline-none focus-visible:ring-1 focus-visible:ring-[#8C34E9]\">\n <Avatar className=\"h-7 w-7 border border-[#2A2A3E]\">\n <AvatarFallback className=\"text-[10px] font-medium bg-[#8C34E9]/20 text-[#C084FC]\">\n {initials}\n </AvatarFallback>\n </Avatar>\n <span className=\"shell-hidden shell-md:block text-xs text-[#E2E8F0] truncate max-w-[100px]\">\n {user.name || \"User\"}\n </span>\n </button>\n </DropdownMenuTrigger>\n <DropdownMenuContent\n align=\"end\"\n className=\"w-48 bg-[#0D1220] border-[#1E2738]\"\n >\n <div className=\"px-3 py-2 border-b border-[#1E2738]\">\n <p className=\"text-xs font-medium text-[#E2E8F0] truncate\">\n {user.name}\n </p>\n <p className=\"text-[10px] text-[#596475] truncate\">{user.role}</p>\n </div>\n {/* TEMP DIAGNOSTIC — remove once subdomain enterprise-data issue is fixed.\n Lives inside the user menu (below the name, above User Settings) to keep\n it out of the header bar. */}\n <div className=\"px-2 py-2 border-b border-[#1E2738]\">\n <EnterpriseDebugLabel\n selectionEnterpriseId={enterprise?.id ?? null}\n cookieEnterpriseName={enterprise?.name ?? null}\n />\n </div>\n <DropdownMenuItem\n onClick={() => {\n window.location.href = userSettingsUrl;\n }}\n className=\"text-xs text-[#E2E8F0] hover:bg-[#1E2738] cursor-pointer focus:bg-[#1E2738]\"\n >\n <Settings className=\"h-3.5 w-3.5 mr-2 text-[#8890A0]\" />\n User Settings\n </DropdownMenuItem>\n <DropdownMenuSeparator className=\"bg-[#1E2738]\" />\n <DropdownMenuItem\n onClick={() => onLogout?.()}\n className=\"text-xs text-[#EF4444] hover:bg-[#EF4444]/10 cursor-pointer focus:bg-[#EF4444]/10 focus:text-[#EF4444]\"\n >\n <LogOut className=\"h-3.5 w-3.5 mr-2\" />\n Sign Out\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n );\n}\n\n/* ── Sidebar nav section ── */\n\nfunction NavSection({\n section,\n marginTop,\n isCollapsed,\n resolveActive,\n onNavigate,\n}: {\n section: MenuSection;\n marginTop?: boolean;\n isCollapsed: boolean;\n resolveActive: (path: string) => boolean;\n onNavigate: (path: string) => void;\n}) {\n const label = section.title ?? section.label;\n return (\n <>\n {label && (\n <div className={cn(\"px-4 py-2\", marginTop && \"mt-2\")}>\n {!isCollapsed && (\n <span\n className={cn(\n \"text-xs font-medium uppercase tracking-wider\",\n section.labelClassName ?? \"text-muted-foreground\"\n )}\n >\n {label}\n </span>\n )}\n </div>\n )}\n <SidebarMenu className=\"px-2 py-1\">\n {section.items.map((item: MenuItem) => {\n const active = resolveActive(item.path);\n const Icon = item.icon;\n return (\n <SidebarMenuItem key={item.path}>\n <SidebarMenuButton\n isActive={active}\n onClick={() => onNavigate(item.path)}\n tooltip={item.label}\n className=\"h-10 transition-all font-normal\"\n >\n {Icon && (\n <Icon\n className={cn(\n \"h-4 w-4\",\n item.accentClassName ?? (active && \"text-primary\")\n )}\n />\n )}\n <span className={cn(item.accentClassName)}>{item.label}</span>\n </SidebarMenuButton>\n </SidebarMenuItem>\n );\n })}\n </SidebarMenu>\n </>\n );\n}\n\n/* ── Inner shell (runs inside SidebarProvider, so it can call useSidebar) ── */\n\nfunction DashboardShell({\n serviceName,\n sectionLabel,\n serviceIcon,\n menuSections,\n adminSections,\n isAdmin = false,\n activePath,\n onNavigate,\n isActive,\n serviceHubUrl = defaultServiceHubUrl(),\n portalUrl = defaultPortalUrl(),\n crossServiceLinks,\n settingsPath,\n user,\n onLogout,\n userSettingsUrl,\n hierarchyNavigator,\n enterprise,\n reportingToolbar,\n footer,\n showHealthFooter,\n buildDate,\n commitHash,\n buildVersion,\n children,\n}: DashboardLayoutProps) {\n const { toggleSidebar, state } = useSidebar();\n const isCollapsed = state === \"collapsed\";\n\n const navigate = onNavigate;\n const resolveActive = (path: string) =>\n isActive ? isActive(path, activePath) : isMenuItemActive(path, activePath);\n\n return (\n <>\n <Sidebar collapsible=\"icon\" className=\"border-r-0\">\n <SidebarHeader\n className={cn(\n sectionLabel && !isCollapsed ? \"h-16\" : \"h-16 justify-center\"\n )}\n >\n <div className=\"flex items-center gap-3 px-2 transition-all w-full\">\n <button\n onClick={toggleSidebar}\n className=\"h-8 w-8 flex items-center justify-center hover:bg-accent rounded-lg transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-ring shrink-0\"\n aria-label=\"Toggle navigation\"\n >\n <PanelLeft className=\"h-4 w-4 text-muted-foreground\" />\n </button>\n {!isCollapsed ? (\n <div className=\"flex items-center gap-2 min-w-0\">\n {serviceIcon && (\n <span className=\"text-[#8C34E9] shrink-0\">{serviceIcon}</span>\n )}\n <span className=\"flex flex-col min-w-0 leading-tight\">\n <span className=\"font-semibold tracking-tight truncate\">\n {serviceName}\n </span>\n {sectionLabel && (\n <span className=\"text-xs text-[#8890A0] truncate\">\n {sectionLabel}\n </span>\n )}\n </span>\n </div>\n ) : null}\n </div>\n </SidebarHeader>\n\n <SidebarContent className=\"gap-0\">\n {/* Service Hub — pinned to the top of the nav */}\n {serviceHubUrl && (\n <SidebarMenu className=\"px-2 pt-2 pb-1\">\n <SidebarMenuItem>\n <SidebarMenuButton\n onClick={() => {\n window.location.href = serviceHubUrl;\n }}\n tooltip=\"Back to Service Hub\"\n className=\"h-10 transition-all font-normal text-muted-foreground hover:text-foreground\"\n >\n <ArrowLeft className=\"h-4 w-4\" />\n <span>Service Hub</span>\n </SidebarMenuButton>\n </SidebarMenuItem>\n </SidebarMenu>\n )}\n\n {/* Cross-service links — jump straight to a sibling service */}\n <CrossServiceNav links={crossServiceLinks ?? []} />\n\n {/* Primary navigation */}\n {menuSections.map((section, i) => (\n <NavSection\n key={section.title ?? section.label ?? `section-${i}`}\n section={section}\n isCollapsed={isCollapsed}\n resolveActive={resolveActive}\n onNavigate={navigate}\n />\n ))}\n\n {/* Admin-only navigation */}\n {isAdmin &&\n adminSections?.map((section, i) => (\n <NavSection\n key={section.title ?? section.label ?? `admin-${i}`}\n section={section}\n marginTop\n isCollapsed={isCollapsed}\n resolveActive={resolveActive}\n onNavigate={navigate}\n />\n ))}\n </SidebarContent>\n\n {settingsPath && (\n <SidebarFooter className=\"p-2\">\n <SidebarMenu>\n <SidebarMenuItem>\n <SidebarMenuButton\n onClick={() => navigate(settingsPath)}\n tooltip=\"Settings\"\n className=\"h-10 transition-all font-normal text-muted-foreground hover:text-foreground\"\n >\n <Settings className=\"h-4 w-4\" />\n <span>Settings</span>\n </SidebarMenuButton>\n </SidebarMenuItem>\n </SidebarMenu>\n </SidebarFooter>\n )}\n </Sidebar>\n\n <SidebarInset>\n <header\n className={cn(\n \"flex items-center justify-between h-14 px-4\",\n \"bg-[#0D1220]/95 border-b border-[#1E2738]\",\n \"backdrop-blur supports-[backdrop-filter]:backdrop-blur\",\n \"sticky top-0 z-40\"\n )}\n >\n {/* LEFT: mobile trigger + service icon + hierarchy breadcrumb */}\n <div className=\"flex items-center gap-2 min-w-0 flex-1\">\n <SidebarTrigger className=\"h-9 w-9 rounded-lg bg-background shrink-0 shell-md:hidden\" />\n {serviceIcon && (\n <span className=\"text-[#8C34E9] shrink-0 shell-hidden shell-sm:block\">\n {serviceIcon}\n </span>\n )}\n {hierarchyNavigator && (\n <div className=\"min-w-0 overflow-x-auto scrollbar-none\">\n {hierarchyNavigator}\n </div>\n )}\n </div>\n\n {/* RIGHT: tour trigger + reporting toolbar + portal link + user menu */}\n <div className=\"flex items-center gap-2 shrink-0\">\n {/* Renders nothing unless this module registered a tour\n (useRegisterTour from @pablo2410/shared-ui/tour). */}\n <TakeTheTourButton />\n {reportingToolbar}\n {portalUrl && (\n <a\n href={portalUrl}\n className=\"shell-hidden shell-md:flex items-center gap-1 px-2 py-1 rounded-md text-[10px] text-[#8890A0] hover:text-[#E2E8F0] hover:bg-[#1E2738] transition-colors\"\n title=\"Back to Portal\"\n >\n <ExternalLink className=\"h-3 w-3\" />\n <span>Portal</span>\n </a>\n )}\n <div className=\"w-px h-6 bg-[#1E2738] shell-hidden shell-md:block\" />\n {user && (\n <UserMenu\n user={user}\n onLogout={onLogout}\n userSettingsUrl={userSettingsUrl}\n enterprise={enterprise}\n />\n )}\n </div>\n </header>\n\n <main className=\"flex-1 p-4 md:p-6 pb-14\">{children}</main>\n </SidebarInset>\n\n {/* The tour overlay for the module's registered tour, if any. */}\n <OpiTourHost />\n\n {footer}\n\n {/* Standardised platform-admin diagnostic strip — opt-in via prop. */}\n {showHealthFooter && user?.role === \"platform_admin\" && (\n <HealthFooter\n user={user}\n enterprise={enterprise}\n buildDate={buildDate}\n commitHash={commitHash}\n buildVersion={buildVersion}\n />\n )}\n </>\n );\n}\n\n/* ── Public component ── */\n\nexport function DashboardLayout(props: DashboardLayoutProps) {\n const isMobile = useIsMobile();\n return (\n <SidebarProvider defaultOpen={props.defaultOpen ?? !isMobile}>\n <DashboardShell {...props} />\n </SidebarProvider>\n );\n}\n","/**\n * CrossServiceNav — sidebar block of direct links to sibling Oplytics\n * services, rendered directly under the \"← Service Hub\" back link.\n *\n * Built on the same Sidebar primitives as the rest of the nav so it works\n * both inside the shared DashboardLayout shell and in bespoke sidebars\n * (e.g. Policy Deployment) — any consumer inside a <SidebarProvider>.\n *\n * Link entries come from `buildCrossServiceLinks` (services/crossNav), which\n * owns provisioning/gating and appends the ?oh= hierarchy selection param.\n * Navigation is a full page load, matching the Service Hub link.\n */\nimport {\n SidebarMenu,\n SidebarMenuButton,\n SidebarMenuItem,\n useSidebar,\n} from \"../primitives\";\nimport {\n ClipboardCheck,\n Gauge,\n LayoutGrid,\n Target,\n ExternalLink,\n type LucideIcon,\n} from \"lucide-react\";\nimport type { CrossServiceLink } from \"../services/crossNav\";\n\n/** Catalog icon names → lucide components for the live core services. */\nconst ICONS: Record<string, LucideIcon> = {\n Target,\n LayoutGrid,\n Gauge,\n ClipboardCheck,\n};\n\nexport interface CrossServiceNavProps {\n /** Display-ready links from buildCrossServiceLinks. Renders nothing when empty. */\n links: CrossServiceLink[];\n /** Section label above the links (hidden when the sidebar is collapsed). */\n label?: string;\n}\n\nexport function CrossServiceNav({\n links,\n label = \"Services\",\n}: CrossServiceNavProps) {\n const { state } = useSidebar();\n const isCollapsed = state === \"collapsed\";\n\n if (links.length === 0) return null;\n\n return (\n <>\n {!isCollapsed && (\n <div className=\"px-4 pt-2\">\n <span className=\"text-xs font-medium text-muted-foreground uppercase tracking-wider\">\n {label}\n </span>\n </div>\n )}\n <SidebarMenu className=\"px-2 py-1\">\n {links.map(link => {\n const Icon = ICONS[link.icon] ?? ExternalLink;\n return (\n <SidebarMenuItem key={link.slug}>\n <SidebarMenuButton\n onClick={() => {\n window.location.href = link.url;\n }}\n tooltip={link.name}\n className=\"h-10 transition-all font-normal text-muted-foreground hover:text-foreground\"\n >\n <Icon className=\"h-4 w-4\" />\n <span>{link.name}</span>\n </SidebarMenuButton>\n </SidebarMenuItem>\n );\n })}\n </SidebarMenu>\n </>\n );\n}\n","/**\n * HealthFooter — thin fixed strip at the bottom of the viewport that prints\n * the current platform-admin diagnostic context. Used to verify the\n * end-to-end auth + hierarchy resolution: JWT role, JWT-stamped enterprise id,\n * the enterprise the hierarchy actually selected, and the openId of the\n * signed-in user.\n *\n * Ported from oplytics-policy-deployment/client/src/components/PolicyDeploymentLayout.tsx\n * so every subdomain that adopts the canonical shell gets the same strip\n * automatically. Use `DashboardLayout` / `OplyticsServiceShell`'s\n * `showHealthFooter` prop to auto-render this for platform_admin only —\n * or render it manually via the existing `footer` slot for full control.\n *\n * role: platform_admin jwt.eid: 30002 hierarchy.eid: 30002\n * enterprise: Lincoln Electric user: KxULoAniQat2Vm7Z9Ds32m\n */\nimport type { CSSProperties } from \"react\";\n\n// Build stamp — injected at each app's build time via Vite `define`\n// (see the app's vite.config.ts). Lets the diagnostic strip show exactly which\n// build/commit is deployed and when it was built — so you can tell at a glance\n// whether the live site is running the latest code.\ndeclare const __BUILD_DATE__: string;\ndeclare const __COMMIT_HASH__: string;\ndeclare const __APP_VERSION__: string;\n\n/** Format an ISO build timestamp as a compact \"9 Jul 07:37Z\" (UTC). */\nfunction formatBuildDate(iso?: string): string | undefined {\n if (!iso) return undefined;\n const d = new Date(iso);\n if (Number.isNaN(d.getTime())) return undefined;\n const day = d.getUTCDate();\n const month = d.toLocaleString(\"en-GB\", { month: \"short\", timeZone: \"UTC\" });\n const hh = String(d.getUTCHours()).padStart(2, \"0\");\n const mm = String(d.getUTCMinutes()).padStart(2, \"0\");\n return `${day} ${month} ${hh}:${mm}Z`;\n}\n\nexport interface HealthFooterUser {\n /** JWT role string (e.g. \"platform_admin\"). */\n role?: string;\n /** The user's stable identifier (typically the JWT `openId` claim). */\n openId?: string;\n /** The enterprise id from the JWT (`enterpriseId` claim). May be null\n * for platform_admin users who haven't selected an active enterprise. */\n enterpriseId?: number | null;\n}\n\nexport interface HealthFooterEnterprise {\n /** The enterprise id the org-hierarchy is currently scoped to. */\n id?: number;\n /** Human-readable name of the scoped enterprise. */\n name?: string;\n}\n\nexport interface HealthFooterProps {\n user?: HealthFooterUser | null;\n enterprise?: HealthFooterEnterprise | null;\n /** Optional extra CSS class for the container. */\n className?: string;\n /** Optional inline style override (z-index, etc.). */\n style?: CSSProperties;\n /** Override the build version (default: build-time __APP_VERSION__). */\n buildVersion?: string;\n /** Override the build/deploy timestamp, ISO (default: build-time __BUILD_DATE__). */\n buildDate?: string;\n /** Override the deployed commit hash (default: build-time __COMMIT_HASH__). */\n commitHash?: string;\n}\n\nexport function HealthFooter({\n user,\n enterprise,\n className,\n style,\n buildVersion,\n buildDate,\n commitHash,\n}: HealthFooterProps) {\n const version =\n buildVersion ??\n (typeof __APP_VERSION__ !== \"undefined\" ? __APP_VERSION__ : undefined);\n const commit =\n commitHash ??\n (typeof __COMMIT_HASH__ !== \"undefined\" ? __COMMIT_HASH__ : undefined);\n const builtAt = formatBuildDate(\n buildDate ??\n (typeof __BUILD_DATE__ !== \"undefined\" ? __BUILD_DATE__ : undefined)\n );\n const buildStamp = [version && `v${version}`, commit, builtAt]\n .filter(Boolean)\n .join(\" · \");\n\n return (\n <div\n className={\n \"fixed bottom-0 left-0 right-0 h-8 bg-zinc-900 border-t border-zinc-800 \" +\n \"flex items-center px-4 gap-6 text-[11px] text-zinc-400 z-50 font-mono\" +\n (className ? ` ${className}` : \"\")\n }\n style={style}\n data-slot=\"health-footer\"\n >\n <span>\n role: <span className=\"text-zinc-200\">{user?.role ?? \"–\"}</span>\n </span>\n <span>\n jwt.eid:{\" \"}\n <span className=\"text-zinc-200\">{user?.enterpriseId ?? \"null\"}</span>\n </span>\n <span>\n hierarchy.eid:{\" \"}\n <span className=\"text-zinc-200\">{enterprise?.id ?? \"–\"}</span>\n </span>\n <span>\n enterprise:{\" \"}\n <span className=\"text-zinc-200\">{enterprise?.name ?? \"–\"}</span>\n </span>\n <span>\n user: <span className=\"text-zinc-200\">{user?.openId ?? \"–\"}</span>\n </span>\n {buildStamp && (\n <span\n className=\"ml-auto text-zinc-500\"\n title=\"Deployed build (version · commit · build date UTC)\"\n >\n build: <span className=\"text-zinc-300\">{buildStamp}</span>\n </span>\n )}\n </div>\n );\n}\n","/**\n * EnterpriseDebugLabel — TEMPORARY DIAGNOSTIC chip.\n *\n * ⚠️ This is a problem-fixing aid, NOT permanent UI. Remove once the\n * subdomain \"wrong enterprise data\" issue is resolved.\n *\n * Purpose: make it plainly visible, on every page (portal + all subdomains),\n * WHICH enterprise the loaded data actually belongs to — and surface the\n * desync that causes subdomains to show the wrong enterprise's data.\n *\n * It compares two ids that are supposed to agree:\n * • cookie — the active-enterprise cookie (ACTIVE_ENTERPRISE_COOKIE), the\n * single cross-subdomain source of truth that the backend uses\n * to scope ALL data (via applyEnterpriseOverride).\n * • sel — the UI hierarchy selection's enterprise id.\n * When they diverge, the chip turns red and shows \"≠\" — that's the bug.\n *\n * Self-contained: reads the cookie directly and reads the hierarchy selection\n * defensively, so it can render even on pages mounted outside a hierarchy\n * provider (it just omits the selection comparison in that case).\n */\nimport { useEffect, useState } from \"react\";\nimport { cn } from \"../utils/cn\";\nimport { readActiveEnterpriseCookie } from \"../hierarchy\";\n\nexport interface EnterpriseDebugLabelProps {\n /**\n * The selection's enterprise id, if the host app can supply it (e.g. from\n * useOrgHierarchy().selection.enterprise?.id). Optional — when omitted, only\n * the cookie id is shown. Passing it enables the cookie-vs-selection mismatch\n * check, which is the most useful diagnostic.\n */\n selectionEnterpriseId?: number | null;\n /**\n * Optional enterprise name to show alongside the cookie id for readability.\n */\n cookieEnterpriseName?: string | null;\n className?: string;\n}\n\nexport function EnterpriseDebugLabel({\n selectionEnterpriseId,\n cookieEnterpriseName,\n className,\n}: EnterpriseDebugLabelProps) {\n // Read the cookie on the client. Re-read on focus/visibility so a switch made\n // in another tab/subdomain is reflected without a manual reload.\n const [cookieId, setCookieId] = useState<string | null>(null);\n useEffect(() => {\n const read = () => setCookieId(readActiveEnterpriseCookie());\n read();\n window.addEventListener(\"focus\", read);\n document.addEventListener(\"visibilitychange\", read);\n return () => {\n window.removeEventListener(\"focus\", read);\n document.removeEventListener(\"visibilitychange\", read);\n };\n }, []);\n\n const cookieNum =\n cookieId !== null && cookieId !== \"\" ? Number(cookieId) : null;\n const hasSelection =\n selectionEnterpriseId !== undefined && selectionEnterpriseId !== null;\n const mismatch =\n hasSelection && cookieNum !== null && cookieNum !== selectionEnterpriseId;\n const noActive = cookieNum === null;\n\n return (\n <div\n className={cn(\n \"flex items-center gap-1.5 px-2 py-1 rounded-md border font-mono text-[10px] leading-none whitespace-nowrap\",\n mismatch\n ? \"bg-[#EF4444]/15 border-[#EF4444]/40 text-[#FCA5A5]\"\n : noActive\n ? \"bg-[#F59E0B]/15 border-[#F59E0B]/40 text-[#FCD34D]\"\n : \"bg-[#10B981]/10 border-[#10B981]/30 text-[#6EE7B7]\",\n className\n )}\n title=\"TEMP DIAGNOSTIC: enterprise the loaded data is scoped to (cookie) vs. the UI hierarchy selection. Remove once subdomain data issue is fixed.\"\n >\n <span aria-hidden>🔍</span>\n {noActive ? (\n <span>no active enterprise (cookie unset)</span>\n ) : (\n <>\n <span>\n data ENT #{cookieNum}\n {cookieEnterpriseName ? ` · ${cookieEnterpriseName}` : \"\"}\n </span>\n {hasSelection && (\n <span>\n {mismatch ? \" ⚠ ≠ sel #\" : \" · sel #\"}\n {selectionEnterpriseId}\n </span>\n )}\n </>\n )}\n </div>\n );\n}\n","/**\n * DashboardLayoutSkeleton — loading skeleton for DashboardLayout.\n *\n * Shows a sidebar + content skeleton while auth state is loading.\n * Oplytics dark theme.\n */\nimport { cn } from \"../utils/cn\";\n\nfunction Skeleton({ className }: { className?: string }) {\n return (\n <div className={cn(\"animate-pulse rounded-md bg-[#1E2738]\", className)} />\n );\n}\n\nexport interface DashboardLayoutSkeletonProps {\n className?: string;\n}\n\nexport function DashboardLayoutSkeleton({\n className,\n}: DashboardLayoutSkeletonProps) {\n return (\n <div className={cn(\"flex min-h-screen bg-[#0A0E1A]\", className)}>\n {/* Sidebar skeleton */}\n <div className=\"w-[260px] border-r border-[#1E2738] bg-[#0D1220] p-4 space-y-6\">\n {/* Logo area */}\n <div className=\"flex items-center gap-3 px-2\">\n <Skeleton className=\"h-8 w-8 rounded-md\" />\n <Skeleton className=\"h-4 w-24\" />\n </div>\n\n {/* Menu items */}\n <div className=\"space-y-2 px-2\">\n <Skeleton className=\"h-9 w-full rounded-lg\" />\n <Skeleton className=\"h-9 w-full rounded-lg\" />\n <Skeleton className=\"h-9 w-full rounded-lg\" />\n <Skeleton className=\"h-9 w-full rounded-lg\" />\n </div>\n\n {/* Section divider */}\n <div className=\"px-2\">\n <Skeleton className=\"h-3 w-16 mb-3\" />\n <Skeleton className=\"h-9 w-full rounded-lg\" />\n <Skeleton className=\"h-9 w-full rounded-lg mt-2\" />\n </div>\n\n {/* User profile area at bottom */}\n <div className=\"absolute bottom-4 left-4 right-4\">\n <div className=\"flex items-center gap-3 px-1\">\n <Skeleton className=\"h-8 w-8 rounded-full\" />\n <div className=\"flex-1 space-y-2\">\n <Skeleton className=\"h-3 w-20\" />\n <Skeleton className=\"h-2 w-32\" />\n </div>\n </div>\n </div>\n </div>\n\n {/* Main content skeleton */}\n <div className=\"flex-1 p-6 space-y-4\">\n {/* Page header */}\n <div className=\"flex items-center justify-between\">\n <Skeleton className=\"h-8 w-48 rounded-lg\" />\n <Skeleton className=\"h-9 w-24 rounded-lg\" />\n </div>\n\n {/* Stats cards */}\n <div className=\"grid gap-4 md:grid-cols-2 lg:grid-cols-4\">\n <Skeleton className=\"h-24 rounded-xl\" />\n <Skeleton className=\"h-24 rounded-xl\" />\n <Skeleton className=\"h-24 rounded-xl\" />\n <Skeleton className=\"h-24 rounded-xl\" />\n </div>\n\n {/* Main content area */}\n <Skeleton className=\"h-64 rounded-xl\" />\n\n {/* Table-like area */}\n <div className=\"space-y-2\">\n <Skeleton className=\"h-10 w-full rounded-lg\" />\n <Skeleton className=\"h-10 w-full rounded-lg\" />\n <Skeleton className=\"h-10 w-full rounded-lg\" />\n </div>\n </div>\n </div>\n );\n}\n","/**\n * SharedPageHeader — standard page header for all Oplytics subdomains.\n *\n * Displays: page title, optional subtitle, optional breadcrumbs,\n * optional action buttons, and optional HierarchyNavigator.\n *\n * Oplytics dark theme: #0A0E1A bg, #8C34E9 accent, #E2E8F0 text, #8890A0 muted\n */\nimport { cn } from \"../utils/cn\";\nimport { ChevronLeft } from \"lucide-react\";\nimport type { ReactNode } from \"react\";\n\nexport interface SharedPageHeaderProps {\n /** Page title */\n title: string;\n /** Optional subtitle / description */\n subtitle?: string;\n /** Optional icon to display before the title */\n icon?: ReactNode;\n /** Optional back button handler — shows a back arrow when provided */\n onBack?: () => void;\n /** Optional action buttons to render on the right side */\n actions?: ReactNode;\n /** Optional breadcrumb trail (e.g. HierarchyNavigator) */\n breadcrumbs?: ReactNode;\n /** Whether to show a bottom border (default: true) */\n showBorder?: boolean;\n /** Compact mode — smaller spacing and text (default: false) */\n compact?: boolean;\n /** Additional CSS classes */\n className?: string;\n /** Children rendered below the header row */\n children?: ReactNode;\n}\n\nexport function SharedPageHeader({\n title,\n subtitle,\n icon,\n onBack,\n actions,\n breadcrumbs,\n showBorder = true,\n compact = false,\n className,\n children,\n}: SharedPageHeaderProps) {\n return (\n <div\n className={cn(\n \"w-full\",\n showBorder && \"border-b border-[#1E2738]\",\n compact ? \"px-4 py-3\" : \"px-6 py-4\",\n className\n )}\n >\n {/* Breadcrumbs row */}\n {breadcrumbs && (\n <div className={cn(\"mb-2\", compact && \"mb-1\")}>{breadcrumbs}</div>\n )}\n\n {/* Main header row */}\n <div className=\"flex items-center justify-between gap-4\">\n <div className=\"flex items-center gap-3 min-w-0\">\n {/* Back button */}\n {onBack && (\n <button\n onClick={onBack}\n className={cn(\n \"shrink-0 p-1.5 rounded-md\",\n \"text-[#8890A0] hover:text-[#E2E8F0] hover:bg-[#1E2738]\",\n \"transition-colors cursor-pointer\",\n \"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[#8C34E9]\"\n )}\n aria-label=\"Go back\"\n >\n <ChevronLeft className={compact ? \"h-4 w-4\" : \"h-5 w-5\"} />\n </button>\n )}\n\n {/* Icon */}\n {icon && <div className=\"shrink-0 text-[#8C34E9]\">{icon}</div>}\n\n {/* Title + subtitle */}\n <div className=\"min-w-0\">\n <h1\n className={cn(\n \"font-semibold text-[#E2E8F0] truncate\",\n \"font-[Montserrat,sans-serif]\",\n compact ? \"text-lg\" : \"text-xl\"\n )}\n >\n {title}\n </h1>\n {subtitle && (\n <p\n className={cn(\n \"text-[#8890A0] truncate mt-0.5\",\n \"font-[Space_Grotesk,sans-serif]\",\n compact ? \"text-xs\" : \"text-sm\"\n )}\n >\n {subtitle}\n </p>\n )}\n </div>\n </div>\n\n {/* Action buttons */}\n {actions && (\n <div className=\"shrink-0 flex items-center gap-2\">{actions}</div>\n )}\n </div>\n\n {/* Optional children below header */}\n {children && (\n <div className={cn(\"mt-3\", compact && \"mt-2\")}>{children}</div>\n )}\n </div>\n );\n}\n","/**\n * SharedFooter — standard footer for all Oplytics subdomains.\n *\n * Displays: Oplytics branding, service links, legal links, copyright.\n * Consistent across all subdomains.\n *\n * Oplytics dark theme: #0D1220 bg, #1E2738 border, #8890A0 text, #8C34E9 accent\n */\nimport { cn } from \"../utils/cn\";\nimport { platformUrl } from \"../services/platformDomain\";\nimport type { ReactNode } from \"react\";\n\ndeclare const __APP_VERSION__: string;\n\n/* ── Service definitions ── */\n\nexport interface FooterService {\n name: string;\n slug: string;\n /** Local route within the portal (e.g. /services/sqdcp) */\n localRoute: string;\n /** External URL for the subdomain (e.g. https://sqdcp.oplyticsdigital.net) */\n externalUrl?: string;\n}\n\n/** Standard Oplytics service list */\nexport const FOOTER_SERVICES: FooterService[] = [\n {\n name: \"SQDCP\",\n slug: \"sqdcp\",\n localRoute: \"/services/sqdcp\",\n externalUrl: platformUrl(\"sqdcp\"),\n },\n {\n name: \"OEE Manager\",\n slug: \"oee-manager\",\n localRoute: \"/services/oee-manager\",\n externalUrl: platformUrl(\"oeemanager\"),\n },\n {\n name: \"Policy Deployment\",\n slug: \"policy-deployment\",\n localRoute: \"/services/policy-deployment\",\n externalUrl: platformUrl(\"policy\"),\n },\n {\n name: \"Action Manager\",\n slug: \"action-manager\",\n localRoute: \"/services/action-manager\",\n externalUrl: platformUrl(\"actionmanager\"),\n },\n {\n name: \"Safety Manager\",\n slug: \"safety-manager\",\n localRoute: \"/services/safety-manager\",\n },\n {\n name: \"Connect\",\n slug: \"connect\",\n localRoute: \"/services/connect\",\n externalUrl: platformUrl(\"connect\"),\n },\n];\n\n/* ── Footer props ── */\n\nexport interface SharedFooterProps {\n /** Override the default service list */\n services?: FooterService[];\n /** Show pricing link (default: true) */\n showPricing?: boolean;\n /** Show sign-in link (default: true) */\n showSignIn?: boolean;\n /** Handler for service link clicks — receives the service */\n onServiceClick?: (service: FooterService, e: React.MouseEvent) => void;\n /** Handler for pricing link click */\n onPricingClick?: (e: React.MouseEvent) => void;\n /** Handler for sign-in link click */\n onSignInClick?: (e: React.MouseEvent) => void;\n /** Additional content to render in the footer */\n children?: ReactNode;\n /** CSS class for the footer container */\n className?: string;\n /** Whether to use external URLs (subdomain links) or local routes */\n useExternalUrls?: boolean;\n /** Current year override (default: auto) */\n year?: number;\n}\n\n/* ── Component ── */\n\nexport function SharedFooter({\n services = FOOTER_SERVICES,\n showPricing = true,\n showSignIn = true,\n onServiceClick,\n onPricingClick,\n onSignInClick,\n children,\n className,\n useExternalUrls = false,\n year,\n}: SharedFooterProps) {\n const currentYear = year ?? new Date().getFullYear();\n\n return (\n <footer\n className={cn(\n \"w-full border-t border-[#1E2738]\",\n \"bg-[#0D1220] text-[#8890A0]\",\n \"font-[Space_Grotesk,sans-serif]\",\n className\n )}\n >\n <div className=\"max-w-7xl mx-auto px-6 py-10\">\n {/* Top section: Logo + columns */}\n <div className=\"grid grid-cols-1 md:grid-cols-4 gap-8 mb-8\">\n {/* Brand column */}\n <div className=\"md:col-span-1\">\n <div className=\"flex items-center gap-2 mb-3\">\n <svg\n className=\"w-7 h-7 shrink-0\"\n viewBox=\"0 0 120 120\"\n role=\"img\"\n aria-label=\"Oplytics\"\n >\n <defs>\n <linearGradient\n id=\"oplytics-footer-mark\"\n x1=\"0\"\n y1=\"0\"\n x2=\"1\"\n y2=\"1\"\n >\n <stop offset=\"0\" stopColor=\"#8C34E9\" />\n <stop offset=\"1\" stopColor=\"#5B1FA6\" />\n </linearGradient>\n </defs>\n <circle\n cx=\"60\"\n cy=\"60\"\n r=\"58\"\n fill=\"url(#oplytics-footer-mark)\"\n />\n <g fill=\"none\" stroke=\"#0A0E1A\" strokeWidth=\"8\">\n <circle cx=\"60\" cy=\"60\" r=\"34\" />\n <circle cx=\"60\" cy=\"60\" r=\"17\" />\n </g>\n <circle cx=\"77\" cy=\"43\" r=\"9\" fill=\"#1DB8CE\" />\n </svg>\n <span className=\"text-[#E2E8F0] font-semibold text-sm font-[Montserrat,sans-serif]\">\n Oplytics.digital\n </span>\n </div>\n <p className=\"text-xs text-[#596475] leading-relaxed\">\n Operational intelligence platform for manufacturing excellence.\n </p>\n </div>\n\n {/* Services column */}\n <div>\n <h4 className=\"text-[#E2E8F0] font-medium text-xs uppercase tracking-wider mb-3 font-[Montserrat,sans-serif]\">\n Services\n </h4>\n <ul className=\"space-y-2\">\n {services.map(service => (\n <li key={service.slug}>\n <a\n href={\n useExternalUrls && service.externalUrl\n ? service.externalUrl\n : service.localRoute\n }\n onClick={\n onServiceClick\n ? e => onServiceClick(service, e)\n : undefined\n }\n className=\"text-xs text-[#8890A0] hover:text-[#E2E8F0] transition-colors\"\n >\n {service.name}\n </a>\n </li>\n ))}\n </ul>\n </div>\n\n {/* Company column */}\n <div>\n <h4 className=\"text-[#E2E8F0] font-medium text-xs uppercase tracking-wider mb-3 font-[Montserrat,sans-serif]\">\n Company\n </h4>\n <ul className=\"space-y-2\">\n {showPricing && (\n <li>\n <a\n href=\"/pricing\"\n onClick={onPricingClick}\n className=\"text-xs text-[#8890A0] hover:text-[#E2E8F0] transition-colors\"\n >\n Pricing\n </a>\n </li>\n )}\n <li>\n <a\n href=\"/about\"\n className=\"text-xs text-[#8890A0] hover:text-[#E2E8F0] transition-colors\"\n >\n About\n </a>\n </li>\n <li>\n <a\n href=\"/contact\"\n className=\"text-xs text-[#8890A0] hover:text-[#E2E8F0] transition-colors\"\n >\n Contact\n </a>\n </li>\n </ul>\n </div>\n\n {/* Legal column */}\n <div>\n <h4 className=\"text-[#E2E8F0] font-medium text-xs uppercase tracking-wider mb-3 font-[Montserrat,sans-serif]\">\n Legal\n </h4>\n <ul className=\"space-y-2\">\n <li>\n <a\n href=\"/privacy\"\n className=\"text-xs text-[#8890A0] hover:text-[#E2E8F0] transition-colors\"\n >\n Privacy Policy\n </a>\n </li>\n <li>\n <a\n href=\"/terms\"\n className=\"text-xs text-[#8890A0] hover:text-[#E2E8F0] transition-colors\"\n >\n Terms of Service\n </a>\n </li>\n </ul>\n {showSignIn && (\n <div className=\"mt-4\">\n <a\n href=\"/login\"\n onClick={onSignInClick}\n className=\"text-xs text-[#8C34E9] hover:text-[#A855F7] transition-colors font-medium\"\n >\n Sign In\n </a>\n </div>\n )}\n </div>\n </div>\n\n {/* Custom content */}\n {children}\n\n {/* Bottom bar */}\n <div className=\"pt-6 border-t border-[#1E2738] flex flex-col sm:flex-row items-center justify-between gap-3\">\n <p className=\"text-xs text-[#596475]\">\n &copy; {currentYear} Oplytics.digital. All rights reserved.\n </p>\n <p className=\"text-xs text-[#3E4A5C]\">\n Built for manufacturing excellence\n </p>\n {typeof __APP_VERSION__ !== \"undefined\" && (\n <p className=\"text-xs text-[#3E4A5C]\">\n Platform v{__APP_VERSION__}\n </p>\n )}\n </div>\n </div>\n </footer>\n );\n}\n","/**\n * ServiceFooter — slim status bar for all Oplytics subdomain services.\n *\n * ~36px horizontal strip: [Logo + Tagline] [Health Monitor] [Copyright + Version]\n * Health monitor is visible only when showHealthMonitor is true (typically platform_admin).\n *\n * Oplytics dark theme: #0D1220 bg, #1E2738 border, #596475/#3E4A5C text, #8C34E9 accent\n */\nimport { cn } from \"../utils/cn\";\n\ndeclare const __APP_VERSION__: string;\ndeclare const __BUILD_DATE__: string;\ndeclare const __COMMIT_HASH__: string;\n\nexport interface ServiceFooterProps {\n /** Enterprise ID for health monitor */\n enterpriseId?: number | null;\n /** Enterprise display name */\n enterpriseName?: string;\n /** Current user's role */\n userRole?: string;\n /** Current user's email */\n userEmail?: string;\n /** Deployment environment */\n environment?: \"production\" | \"staging\" | \"development\";\n /** Build version / commit hash */\n buildVersion?: string;\n /**\n * Build/deploy timestamp — when the currently-served bundle was built.\n * ISO string (e.g. \"2026-07-09T07:37:31Z\"). Falls back to the build-time\n * __BUILD_DATE__ constant. Rendered as a short date+time so you can tell at\n * a glance whether the deployed code is the latest (root-cause aid).\n */\n buildDate?: string;\n /** Short commit hash of the deployed build (e.g. \"c3af23f\"). Falls back to __COMMIT_HASH__. */\n commitHash?: string;\n /** Show health monitor strip (default: false) */\n showHealthMonitor?: boolean;\n /** CSS class for the footer container */\n className?: string;\n}\n\n/** Format an ISO build timestamp as a compact \"9 Jul 07:37\" (UTC) for the footer. */\nfunction formatBuildDate(iso?: string): string | undefined {\n if (!iso) return undefined;\n const d = new Date(iso);\n if (Number.isNaN(d.getTime())) return undefined;\n const day = d.getUTCDate();\n const month = d.toLocaleString(\"en-GB\", { month: \"short\", timeZone: \"UTC\" });\n const hh = String(d.getUTCHours()).padStart(2, \"0\");\n const mm = String(d.getUTCMinutes()).padStart(2, \"0\");\n return `${day} ${month} ${hh}:${mm}Z`;\n}\n\nconst ENV_LABELS: Record<string, { label: string; color: string }> = {\n production: { label: \"prod\", color: \"#10b981\" },\n staging: { label: \"stg\", color: \"#f59e0b\" },\n development: { label: \"dev\", color: \"#8C34E9\" },\n};\n\nexport function ServiceFooter({\n enterpriseId,\n enterpriseName,\n userRole,\n userEmail,\n environment,\n buildVersion,\n buildDate,\n commitHash,\n showHealthMonitor = false,\n className,\n}: ServiceFooterProps) {\n const currentYear = new Date().getFullYear();\n const env = environment ? ENV_LABELS[environment] : null;\n const version =\n buildVersion ??\n (typeof __APP_VERSION__ !== \"undefined\" ? __APP_VERSION__ : undefined);\n const commit =\n commitHash ??\n (typeof __COMMIT_HASH__ !== \"undefined\" ? __COMMIT_HASH__ : undefined);\n const builtAt = formatBuildDate(\n buildDate ??\n (typeof __BUILD_DATE__ !== \"undefined\" ? __BUILD_DATE__ : undefined)\n );\n // A single \"build stamp\" combining version + commit + build date, e.g.\n // \"v.0.9.2 · c3af23f · 9 Jul 07:37Z\". Any missing piece is simply omitted.\n const buildStamp = [version && `v.${version}`, commit, builtAt]\n .filter(Boolean)\n .join(\" · \");\n\n return (\n <footer\n className={cn(\n \"w-full border-t border-[#1E2738] bg-[#0D1220]\",\n \"flex items-center justify-between px-4\",\n \"h-9 min-h-[36px] shrink-0\",\n \"font-[Montserrat,sans-serif]\",\n className\n )}\n >\n {/* Left: Logo + Tagline */}\n <div className=\"flex items-center gap-2 min-w-0\">\n <svg\n className=\"w-5 h-5 shrink-0\"\n viewBox=\"0 0 120 120\"\n role=\"img\"\n aria-label=\"Oplytics\"\n >\n <defs>\n <linearGradient\n id=\"oplytics-service-footer-mark\"\n x1=\"0\"\n y1=\"0\"\n x2=\"1\"\n y2=\"1\"\n >\n <stop offset=\"0\" stopColor=\"#8C34E9\" />\n <stop offset=\"1\" stopColor=\"#5B1FA6\" />\n </linearGradient>\n </defs>\n <circle\n cx=\"60\"\n cy=\"60\"\n r=\"58\"\n fill=\"url(#oplytics-service-footer-mark)\"\n />\n <g fill=\"none\" stroke=\"#0A0E1A\" strokeWidth=\"9\">\n <circle cx=\"60\" cy=\"60\" r=\"33\" />\n <circle cx=\"60\" cy=\"60\" r=\"14\" />\n </g>\n <circle cx=\"79\" cy=\"41\" r=\"11\" fill=\"#1DB8CE\" />\n </svg>\n <span className=\"text-[10px] text-[#596475] whitespace-nowrap shell-hidden shell-sm:inline\">\n Operational Excellence. One Digital Platform.\n </span>\n </div>\n\n {/* Center: Health Monitor (admin only) */}\n {showHealthMonitor && (\n <div className=\"flex items-center gap-1.5 text-[10px] text-[#3E4A5C] min-w-0 overflow-hidden\">\n {enterpriseName && (\n <>\n <span className=\"whitespace-nowrap\">\n <span className=\"text-[#596475]\">{enterpriseName}</span>\n {enterpriseId != null && (\n <span className=\"text-[#3E4A5C]\"> ({enterpriseId})</span>\n )}\n </span>\n <Separator />\n </>\n )}\n {userEmail && (\n <>\n <span className=\"whitespace-nowrap text-[#596475]\">\n {userEmail}\n </span>\n {userRole && (\n <span className=\"text-[#3E4A5C]\"> ({userRole})</span>\n )}\n <Separator />\n </>\n )}\n {env && (\n <>\n <span className=\"whitespace-nowrap\">\n ENV:{\" \"}\n <span style={{ color: env.color }} className=\"font-semibold\">\n {env.label}\n </span>\n </span>\n <Separator />\n </>\n )}\n {buildStamp && (\n <span\n className=\"whitespace-nowrap text-[#3E4A5C] font-mono\"\n title=\"Deployed build\"\n >\n {buildStamp}\n </span>\n )}\n </div>\n )}\n\n {/* Right: Copyright + build stamp (when health monitor hidden) */}\n <div className=\"flex items-center gap-2 text-[10px] text-[#3E4A5C] whitespace-nowrap shrink-0\">\n <span>&copy; {currentYear} Oplytics.digital</span>\n {!showHealthMonitor && buildStamp && (\n <>\n <Separator />\n <span className=\"font-mono\" title=\"Deployed build\">\n {buildStamp}\n </span>\n </>\n )}\n </div>\n </footer>\n );\n}\n\nfunction Separator() {\n return (\n <span className=\"text-[#1E2738] select-none\" aria-hidden>\n |\n </span>\n );\n}\n","/**\n * createServiceLayout — Factory function that generates a complete DashboardLayout\n * for any Oplytics subdomain from a SharedSidebarConfig.\n *\n * This is the primary API for subdomains. Instead of each subdomain maintaining\n * its own 200-350 line DashboardLayout, they call:\n *\n * const SQDCPLayout = createServiceLayout({ serviceName: \"SQDCP\", ... });\n *\n * And get a fully-featured layout with:\n * - Resizable sidebar with all standard features\n * - SharedPageHeader integration\n * - ReportingToolbar integration (optional)\n * - HierarchyProvider wrapping (optional)\n * - Auth gate with loading skeleton + sign-in fallback (optional)\n * - User footer with logout dropdown\n *\n * @module shared/components/createServiceLayout\n */\n\nimport React from \"react\";\nimport type { SharedSidebarConfig, MenuSection } from \"./SharedSidebar\";\nimport {\n isAdminRole,\n getInitials,\n isMenuItemActive,\n useSidebarResize,\n} from \"./SharedSidebar\";\n\n/* ── Service Layout Configuration ── */\n\nexport interface ServiceLayoutConfig extends SharedSidebarConfig {\n /** Show SharedPageHeader above content (default: true) */\n showPageHeader?: boolean;\n /** SharedPageHeader props — passed through when showPageHeader is true */\n pageHeaderProps?: {\n showHierarchy?: boolean;\n hierarchyMaxDepth?: number;\n showSidebarTrigger?: boolean;\n };\n /** Show ReportingToolbar in the header (default: false) */\n showReportingToolbar?: boolean;\n /** Module name passed to ReportingToolbar */\n reportingModuleName?: string;\n /** Wrap layout in HierarchyProvider (default: false) */\n wrapHierarchyProvider?: boolean;\n /** Show auth gate — loading skeleton + sign-in fallback (default: true) */\n showAuthGate?: boolean;\n /** Report submit handler */\n onReportSubmit?: (report: any) => Promise<void> | void;\n /** Feedback submit handler */\n onFeedbackSubmit?: (feedback: any) => Promise<void> | void;\n}\n\n/**\n * Creates a service-specific layout component from configuration.\n *\n * NOTE: This is a blueprint/factory specification. The actual React component\n * is assembled by each subdomain using their local UI primitives (Sidebar,\n * SidebarProvider, etc.) since these are not shared across subdomains yet.\n *\n * When subdomain consolidation (#311) is complete, this factory will directly\n * render the shared UI primitives. Until then, subdomains use this config\n * to standardise their DashboardLayout implementations.\n *\n * @example\n * ```tsx\n * // In SQDCP's DashboardLayout.tsx:\n * import { sqdcpLayoutConfig } from \"./sqdcp-config\";\n * // Use sqdcpLayoutConfig.menuSections, .backLink, etc. to build the layout\n * ```\n */\nexport function defineServiceLayout(\n config: ServiceLayoutConfig\n): ServiceLayoutConfig {\n return {\n defaultWidth: 260,\n minWidth: 200,\n maxWidth: 480,\n showPageHeader: true,\n showReportingToolbar: false,\n wrapHierarchyProvider: false,\n showAuthGate: true,\n ...config,\n storageKeyPrefix:\n config.storageKeyPrefix ||\n config.serviceName.toLowerCase().replace(/\\s+/g, \"-\"),\n };\n}\n\n/* ── Pre-built Service Configurations ── */\n\n// These are the canonical configurations for each subdomain.\n// Subdomains should import and use these instead of defining their own.\n\nexport { isAdminRole, getInitials, isMenuItemActive, useSidebarResize };\n","/**\n * Canonical Service Layout Configurations for all Oplytics subdomains.\n *\n * Each subdomain imports its config from here and uses it to build its\n * DashboardLayout. This ensures consistency across all services while\n * allowing per-service customisation (menu items, features, etc.).\n *\n * When subdomain consolidation (#311) is complete, these configs will\n * drive the shared createServiceLayout factory directly.\n *\n * @module shared/components/serviceConfigs\n */\n\nimport type { ServiceLayoutConfig } from \"./createServiceLayout\";\nimport { portalUrl } from \"../services/platformDomain\";\n\n/* ── Icon placeholder types ── */\n// Icons are imported locally by each subdomain from lucide-react.\n// These configs use string identifiers that map to icon components.\n\nexport interface MenuItemConfig {\n iconName: string;\n label: string;\n path: string;\n}\n\nexport interface MenuSectionConfig {\n label?: string;\n items: MenuItemConfig[];\n adminOnly?: boolean;\n}\n\nexport interface ServiceConfig {\n serviceName: string;\n serviceAbbreviation: string;\n serviceIconName: string;\n menuSections: MenuSectionConfig[];\n backLink?: {\n label: string;\n path: string;\n };\n showPageHeader: boolean;\n showReportingToolbar: boolean;\n reportingModuleName?: string;\n wrapHierarchyProvider: boolean;\n showAuthGate: boolean;\n storageKeyPrefix: string;\n defaultWidth: number;\n minWidth: number;\n maxWidth: number;\n}\n\n/* ── SQDCP ── */\n\nexport const sqdcpConfig: ServiceConfig = {\n serviceName: \"SQDCP\",\n serviceAbbreviation: \"S\",\n serviceIconName: \"BarChart3\",\n menuSections: [\n {\n items: [\n { iconName: \"LayoutDashboard\", label: \"Dashboard\", path: \"/\" },\n { iconName: \"PenLine\", label: \"Data Entry\", path: \"/entry\" },\n {\n iconName: \"ClipboardList\",\n label: \"Action Tracker\",\n path: \"/actions\",\n },\n { iconName: \"FileText\", label: \"Reports\", path: \"/reports\" },\n { iconName: \"Settings\", label: \"Admin\", path: \"/admin\" },\n ],\n },\n ],\n showPageHeader: true,\n showReportingToolbar: true,\n reportingModuleName: \"SQDCP\",\n wrapHierarchyProvider: false,\n showAuthGate: false,\n storageKeyPrefix: \"sqdcp-sidebar\",\n defaultWidth: 260,\n minWidth: 200,\n maxWidth: 480,\n};\n\n/* ── OEE Manager ── */\n\nexport const oeeManagerConfig: ServiceConfig = {\n serviceName: \"OEE Manager\",\n serviceAbbreviation: \"o\",\n serviceIconName: \"BarChart3\",\n menuSections: [\n {\n items: [\n { iconName: \"BarChart3\", label: \"Dashboard\", path: \"/\" },\n { iconName: \"Database\", label: \"Data Input\", path: \"/data-input\" },\n { iconName: \"Clock\", label: \"Downtime\", path: \"/downtime\" },\n { iconName: \"Settings\", label: \"Settings\", path: \"/settings\" },\n ],\n },\n ],\n backLink: {\n label: \"Back to Portal\",\n path: portalUrl(),\n },\n showPageHeader: true,\n showReportingToolbar: true,\n reportingModuleName: \"OEE Enterprise Manager\",\n wrapHierarchyProvider: false,\n showAuthGate: false,\n storageKeyPrefix: \"oee-sidebar\",\n defaultWidth: 240,\n minWidth: 200,\n maxWidth: 400,\n};\n\n/* ── Action Manager ── */\n\nexport const actionManagerConfig: ServiceConfig = {\n serviceName: \"Action Manager\",\n serviceAbbreviation: \"A\",\n serviceIconName: \"LayoutDashboard\",\n menuSections: [\n {\n items: [\n { iconName: \"LayoutDashboard\", label: \"Dashboard\", path: \"/\" },\n { iconName: \"Users\", label: \"Teams\", path: \"/teams\" },\n ],\n },\n ],\n showPageHeader: true,\n showReportingToolbar: false,\n reportingModuleName: \"Action Manager\",\n wrapHierarchyProvider: false,\n showAuthGate: true,\n storageKeyPrefix: \"actionmanager-sidebar\",\n defaultWidth: 280,\n minWidth: 200,\n maxWidth: 480,\n};\n\n/* ── Business Hub ── */\n\nexport const businessHubConfig: ServiceConfig = {\n serviceName: \"Business Hub\",\n serviceAbbreviation: \"B\",\n serviceIconName: \"LayoutDashboard\",\n menuSections: [\n {\n items: [\n { iconName: \"LayoutDashboard\", label: \"Dashboard\", path: \"/\" },\n { iconName: \"Users\", label: \"Analytics\", path: \"/analytics\" },\n ],\n },\n ],\n showPageHeader: true,\n showReportingToolbar: false,\n reportingModuleName: \"Business Hub\",\n wrapHierarchyProvider: false,\n showAuthGate: true,\n storageKeyPrefix: \"businesshub-sidebar\",\n defaultWidth: 280,\n minWidth: 200,\n maxWidth: 480,\n};\n\n/* ── Policy Deployment ── */\n\nexport const policyDeploymentConfig: ServiceConfig = {\n serviceName: \"Policy Deployment\",\n serviceAbbreviation: \"PD\",\n serviceIconName: \"FileStack\",\n menuSections: [\n {\n label: \"Analysis\",\n items: [\n {\n iconName: \"LayoutGrid\",\n label: \"Dashboard\",\n path: \"/app/policy-deployment\",\n },\n {\n iconName: \"Grid3X3\",\n label: \"X-Matrix\",\n path: \"/app/policy-deployment/xmatrix\",\n },\n {\n iconName: \"BarChart3\",\n label: \"Bowling Chart\",\n path: \"/app/policy-deployment/bowling\",\n },\n {\n iconName: \"FolderKanban\",\n label: \"Action Plans\",\n path: \"/app/policy-deployment/actions\",\n },\n {\n iconName: \"GitBranchPlus\",\n label: \"Catchball\",\n path: \"/app/policy-deployment/catchball\",\n },\n {\n iconName: \"Target\",\n label: \"Deployments\",\n path: \"/app/policy-deployment/deployments\",\n },\n ],\n },\n {\n label: \"Administration\",\n adminOnly: true,\n items: [\n {\n iconName: \"Settings\",\n label: \"Manage Policy\",\n path: \"/app/policy-deployment/manage\",\n },\n {\n iconName: \"Plug\",\n label: \"Integrations\",\n path: \"/app/policy-deployment/integrations\",\n },\n ],\n },\n ],\n backLink: {\n label: \"Service Hub\",\n path: \"/app\",\n },\n showPageHeader: true,\n showReportingToolbar: true,\n reportingModuleName: \"Policy Deployment\",\n wrapHierarchyProvider: true,\n showAuthGate: true,\n storageKeyPrefix: \"policy-sidebar\",\n defaultWidth: 260,\n minWidth: 200,\n maxWidth: 480,\n};\n\n/* ── All configs indexed by service key ── */\n\nexport const SERVICE_CONFIGS = {\n sqdcp: sqdcpConfig,\n oeeManager: oeeManagerConfig,\n actionManager: actionManagerConfig,\n businessHub: businessHubConfig,\n policyDeployment: policyDeploymentConfig,\n} as const;\n\nexport type ServiceKey = keyof typeof SERVICE_CONFIGS;\n"],"mappings":"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","names":["ExternalLink","jsx","jsxs","useEffect","useState","Fragment","jsx","jsxs","useState","useEffect","Fragment","jsx","jsxs","portalUrl","ExternalLink","jsx","jsxs","jsx","jsxs","jsx","jsxs","Fragment","jsx","jsxs","formatBuildDate"]}
@@ -1,6 +1,3 @@
1
- import {
2
- cn
3
- } from "../chunk-JT3XLKKD.js";
4
1
  import {
5
2
  Button,
6
3
  Dialog,
@@ -13,6 +10,9 @@ import {
13
10
  RadioGroup,
14
11
  RadioGroupItem
15
12
  } from "../chunk-BKGUIIN2.js";
13
+ import {
14
+ cn
15
+ } from "../chunk-JT3XLKKD.js";
16
16
 
17
17
  // src/components/ErrorBoundary.tsx
18
18
  import { Component } from "react";
@@ -28,8 +28,8 @@ import {
28
28
  ORG_HIERARCHY_LEVELS
29
29
  } from "../chunk-HLMIR5UV.js";
30
30
  import "../chunk-JEHQIJS2.js";
31
- import "../chunk-JT3XLKKD.js";
32
31
  import "../chunk-BKGUIIN2.js";
32
+ import "../chunk-JT3XLKKD.js";
33
33
  export {
34
34
  ACTIVE_ENTERPRISE_COOKIE,
35
35
  HierarchyNavigator,
@@ -19,15 +19,16 @@ import {
19
19
  policyDeploymentConfig,
20
20
  sqdcpConfig,
21
21
  useSidebarResize
22
- } from "../chunk-S4UQSTZN.js";
23
- import "../chunk-4QJMLQSH.js";
22
+ } from "../chunk-UQUMDBCZ.js";
23
+ import "../chunk-LUQAR6NB.js";
24
+ import "../chunk-7LI4Y6WT.js";
24
25
  import "../chunk-RSFGV7CG.js";
25
26
  import "../chunk-DMUQ5WNO.js";
26
27
  import "../chunk-A6UTXAPS.js";
27
28
  import "../chunk-HLMIR5UV.js";
28
29
  import "../chunk-JEHQIJS2.js";
29
- import "../chunk-JT3XLKKD.js";
30
30
  import "../chunk-BKGUIIN2.js";
31
+ import "../chunk-JT3XLKKD.js";
31
32
  export {
32
33
  CrossServiceNav,
33
34
  DashboardLayout,
@@ -19,9 +19,6 @@ import {
19
19
  Switch,
20
20
  Textarea
21
21
  } from "../chunk-JEHQIJS2.js";
22
- import {
23
- cn
24
- } from "../chunk-JT3XLKKD.js";
25
22
  import {
26
23
  Button,
27
24
  Dialog,
@@ -32,6 +29,9 @@ import {
32
29
  DialogTitle,
33
30
  Label
34
31
  } from "../chunk-BKGUIIN2.js";
32
+ import {
33
+ cn
34
+ } from "../chunk-JT3XLKKD.js";
35
35
 
36
36
  // src/reporting/ReportingToolbar.tsx
37
37
  import { useRef, useState } from "react";