@pablo2410/shared-ui 0.21.0 → 0.22.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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/tour/TourShell.tsx","../../src/tour/useTour.ts","../../src/tour/tourMachine.ts","../../src/tour/useSpotlight.ts","../../src/tour/spotlightMath.ts"],"sourcesContent":["/**\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 */\nimport { useEffect, useRef, type ReactNode } from \"react\";\nimport {\n X,\n ChevronLeft,\n ChevronRight,\n ArrowRight,\n ExternalLink,\n} from \"lucide-react\";\nimport type { TourStep } from \"./types\";\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\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\n/**\n * Full-viewport dismissible overlay with a centered panel. Esc and the\n * backdrop both dismiss; focus is trapped to the panel while open.\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}: TourShellProps) {\n const panelRef = useRef<HTMLDivElement>(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 return (\n <div\n className=\"fixed inset-0 z-50 flex items-center justify-center p-4\"\n style={{ background: skin.overlayBg }}\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 {/* Header */}\n <div\n className=\"flex items-center justify-between gap-3 px-5 py-4 shrink-0\"\n style={{\n background: \"linear-gradient(135deg, #2a1450 0%, #1a0e33 100%)\",\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 className=\"p-1 rounded hover:bg-white/10 transition-colors shrink-0\"\n title=\"Skip tour\"\n >\n <X size={16} className=\"text-white/70\" />\n </button>\n </div>\n\n {/* Step dots */}\n <div\n className=\"flex items-center gap-1.5 px-5 py-3 shrink-0\"\n style={{ borderBottom: skin.panelBorder }}\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 {/* Body */}\n <div className=\"overflow-y-auto px-5 py-4 space-y-4\">\n <p\n className=\"text-[13px] leading-relaxed transition-opacity\"\n style={{\n color: skin.bodyText,\n opacity: narrationPending ? 0.7 : 1,\n }}\n >\n {narration}\n </p>\n\n {step.highlights && step.highlights.length > 0 && (\n <ul className=\"space-y-1.5\">\n {step.highlights.map((h, i) => (\n <li\n key={i}\n className=\"text-[12.5px] leading-relaxed pl-3 relative\"\n style={{ color: skin.softText }}\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 {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 px-3.5 py-2 rounded-lg text-[13px] font-semibold transition-opacity hover:opacity-90\"\n style={{ background: `${skin.accent}20`, color: skin.accent }}\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 </div>\n\n {/* Footer controls */}\n <div\n className=\"flex items-center justify-between gap-2 px-5 py-3.5 shrink-0\"\n style={{ borderTop: skin.panelBorder }}\n >\n <button\n onClick={onDismiss}\n className=\"text-[12px] font-medium transition-colors hover:underline\"\n style={{ color: skin.mutedText }}\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 px-3 py-1.5 rounded-md text-[12.5px] font-medium transition-colors\"\n style={{ color: skin.bodyText, border: skin.panelBorder }}\n >\n <ChevronLeft size={13} />\n Back\n </button>\n )}\n <button\n onClick={onNext}\n className=\"flex items-center gap-1 px-3.5 py-1.5 rounded-md text-[12.5px] font-bold text-white transition-opacity hover:opacity-90\"\n style={{\n background: \"linear-gradient(135deg, #8C34E9 0%, #5B1FA6 100%)\",\n }}\n >\n {isLast ? \"Done\" : \"Next\"}\n {!isLast && <ChevronRight size={13} />}\n </button>\n </div>\n </div>\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 * 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"],"mappings":"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","names":["useEffect","useRef","useState","pos"]}
1
+ {"version":3,"sources":["../../src/tour/TourShell.tsx","../../src/tour/useSpotlight.ts","../../src/tour/spotlightMath.ts","../../src/tour/useTour.ts","../../src/tour/tourMachine.ts"],"sourcesContent":["/**\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, 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\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 px-4 py-3 space-y-3\">\n <TourShellBody\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 p-4\"\n style={{ background: skin.overlayBg }}\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 px-5 py-4 space-y-4\">\n <TourShellBody\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 return (\n <div\n className=\"flex items-center justify-between gap-3 px-5 py-4 shrink-0\"\n style={{\n background: \"linear-gradient(135deg, #2a1450 0%, #1a0e33 100%)\",\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 className=\"p-1 rounded hover:bg-white/10 transition-colors shrink-0\"\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 px-5 py-3 shrink-0\"\n style={{ borderBottom: skin.panelBorder }}\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}: {\n step: TourStep;\n narration: string;\n narrationPending?: boolean;\n skin: TourShellSkin;\n children?: ReactNode;\n}) {\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 }}\n >\n {narration}\n </p>\n\n {step.features && step.features.length > 0 ? (\n <ul className=\"space-y-2\">\n {step.features.map((f, i) => (\n <li key={i} className=\"text-[12.5px] leading-relaxed\">\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 step.highlights &&\n step.highlights.length > 0 && (\n <ul className=\"space-y-1.5\">\n {step.highlights.map((h, i) => (\n <li\n key={i}\n className=\"text-[12.5px] leading-relaxed pl-3 relative\"\n style={{ color: skin.softText }}\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 px-3.5 py-2 rounded-lg text-[13px] font-semibold transition-opacity hover:opacity-90\"\n style={{ background: `${skin.accent}20`, color: skin.accent }}\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 return (\n <div\n className=\"flex items-center justify-between gap-2 px-5 py-3.5 shrink-0\"\n style={{ borderTop: skin.panelBorder }}\n >\n <button\n onClick={onDismiss}\n className=\"text-[12px] font-medium transition-colors hover:underline\"\n style={{ color: skin.mutedText }}\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 px-3 py-1.5 rounded-md text-[12.5px] font-medium transition-colors\"\n style={{ color: skin.bodyText, border: skin.panelBorder }}\n >\n <ChevronLeft size={13} />\n Back\n </button>\n )}\n <button\n onClick={onNext}\n className=\"flex items-center gap-1 px-3.5 py-1.5 rounded-md text-[12.5px] font-bold text-white transition-opacity hover:opacity-90\"\n style={{\n background: \"linear-gradient(135deg, #8C34E9 0%, #5B1FA6 100%)\",\n }}\n >\n {isLast ? \"Done\" : \"Next\"}\n {!isLast && <ChevronRight size={13} />}\n </button>\n </div>\n </div>\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 * 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"],"mappings":"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","names":["useEffect","useRef","pos","useRef","useEffect","useState","useState"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pablo2410/shared-ui",
3
- "version": "0.21.0",
3
+ "version": "0.22.1",
4
4
  "description": "Shared UI component library for all Oplytics.digital subdomains",
5
5
  "type": "module",
6
6
  "exports": {
@@ -131,7 +131,7 @@
131
131
  "lucide-react": "^0.460.0",
132
132
  "next-themes": "^0.4.6",
133
133
  "react-day-picker": "^10.0.1",
134
- "react-hook-form": "^7.87.0",
134
+ "react-hook-form": "^7.88.0",
135
135
  "react-resizable-panels": "^4.12.4",
136
136
  "recharts": "^3.10.1",
137
137
  "sonner": "^2.0.8",
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/services/catalog.ts","../src/services/crossNav.ts"],"sourcesContent":["/**\n * Shared Service Catalog — Single source of truth for all Oplytics.digital services.\n *\n * Consumed by:\n * - oplytics-subdomains (ServiceHub.tsx) — service cards, access gating\n * - oplytics-marketing-site (services.ts) — marketing pages, hero sections\n *\n * Each consumer extends with context-specific fields locally:\n * - Platform adds: route, external URL, React icon component\n * - Marketing adds: heroImage, demoImage, problem, howItWorks, results, aiFeatures\n */\n\nimport { platformUrl } from \"./platformDomain\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport type ServiceStatus = \"live\" | \"in-development\" | \"coming-soon\";\nexport type ServiceCategory = \"core\" | \"hub\";\n\nexport interface ServiceDefinition {\n /** Unique identifier used for DB lookups, URL slugs, and cross-referencing */\n slug: string;\n /** Display name shown in cards, headers, and navigation */\n name: string;\n /** Short one-line value proposition */\n tagline: string;\n /** Longer description (1-2 sentences) for cards and summaries */\n description: string;\n /** Lucide icon name (string) — consumers resolve to component locally */\n icon: string;\n /** Hex accent color for cards, badges, and theming */\n accentColor: string;\n /** Current availability status */\n status: ServiceStatus;\n /** Core = standalone product, Hub = extension/add-on module */\n category: ServiceCategory;\n /**\n * Subdomain label on the platform domain (e.g. \"sqdcp\") — null if hosted\n * on portal. Full URLs are built via getServiceUrl(), which resolves the\n * platform domain at runtime (see ./platformDomain).\n */\n subdomainLabel: string | null;\n /**\n * Scripted content for the Opi onboarding tour (see @pablo2410/shared-ui/tour).\n * Optional and deliberately sparse — only services with a real, verified\n * first-run path should have this filled in. A service with no `onboarding`\n * block is skipped by the tour entirely rather than shown with placeholder\n * copy. Marketing/other consumers ignore this field.\n */\n onboarding?: ServiceOnboardingContent;\n}\n\nexport interface ServiceOnboardingContent {\n /** Opi's scripted opening line for this step — the always-available fallback if the live LLM narration call fails or AI is gated off. */\n hook: string;\n /** 2-4 short bullets: what's actually here, in plain language. */\n highlights: string[];\n /** The one thing the step ends on. */\n firstAction: {\n label: string;\n /** Path relative to the service's own root (e.g. \"/xmatrix\"). */\n path: string;\n };\n /** What Opi should say instead when the service has no data yet for this enterprise (empty-state framing), rather than pointing at the normal firstAction. */\n emptyStateHint?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Catalog\n// ---------------------------------------------------------------------------\n\nexport const SERVICE_CATALOG: ServiceDefinition[] = [\n // ─── CORE SERVICES ───\n {\n slug: \"policy-deployment\",\n name: \"Policy Deployment\",\n tagline: \"Align strategy to execution\",\n description:\n \"Hoshin Kanri and X-matrix planning. Cascade strategic objectives through every level of your organisation with live tracking and bowling charts.\",\n icon: \"Target\",\n accentColor: \"#F59E0B\",\n status: \"live\",\n category: \"core\",\n subdomainLabel: \"policy\",\n onboarding: {\n hook: \"This is where your strategy becomes a plan everyone can see. Long-term goals break into this year's objectives, objectives break into projects, and projects carry the metrics that prove it's working.\",\n highlights: [\n \"X-Matrix — the rollup view: goals, objectives, and projects, with dots showing which project moves which goal.\",\n \"Catchball — where the plan actually gets built: negotiate objectives and projects up and down the org, add a metric to a project.\",\n \"Actions — every task that comes out of the plan, tracked to closure.\",\n ],\n firstAction: { label: \"Open X-Matrix\", path: \"/xmatrix\" },\n emptyStateHint:\n \"No plan yet for this enterprise — say so plainly and point at Catchball's \\\"Create your first plan\\\" instead of X-Matrix's empty state.\",\n },\n },\n {\n slug: \"sqdcp\",\n name: \"SQDCP Dashboard\",\n tagline: \"Daily management boards, digitised\",\n description:\n \"Digital SQDCP boards that drive daily accountability. Safety, Quality, Delivery, Cost, and People metrics at a glance for every team.\",\n icon: \"LayoutGrid\",\n accentColor: \"#1DB8CE\",\n status: \"live\",\n category: \"core\",\n subdomainLabel: \"sqdcp\",\n onboarding: {\n hook: \"This is your daily board — Safety, Quality, Delivery, Cost, People, live. Every team has a room; the huddle happens here, and the numbers you log in Data Entry are what turn the board green or red.\",\n highlights: [\n \"Rooms — one per team, tiered (Team Huddle → Area/Plant Review → Monthly Review), each feeding the one above.\",\n \"Data Entry — where actuals get logged against each pillar's metrics.\",\n \"Safety Manager / Quality Manager tabs — the deeper pillar-specific views inside SQDCP.\",\n \"If AI Facilitator is on, Opi runs the huddle itself — drafts the line for each red pillar, opens the action review by name.\",\n ],\n firstAction: { label: \"Open Rooms\", path: \"/\" },\n emptyStateHint:\n \"No rooms configured yet — this needs an admin to set up rooms first; point at Admin rather than Rooms.\",\n },\n },\n {\n slug: \"oee-manager\",\n name: \"OEE Manager\",\n tagline: \"Real-time equipment effectiveness tracking\",\n description:\n \"Monitor Overall Equipment Effectiveness in real-time. Track availability, performance, and quality metrics across your entire production line.\",\n icon: \"Gauge\",\n accentColor: \"#8C34E9\",\n status: \"live\",\n category: \"core\",\n subdomainLabel: \"oeemanager\",\n // No `onboarding` block yet — held back for a later cohort (2026-09-11 decision).\n },\n {\n slug: \"action-manager\",\n name: \"Action Manager\",\n tagline: \"Track every action to closure\",\n description:\n \"Capture, assign, and track corrective and preventive actions from any source. Ensure nothing falls through the cracks.\",\n icon: \"ClipboardCheck\",\n accentColor: \"#22C55E\",\n status: \"live\",\n category: \"core\",\n subdomainLabel: \"actionmanager\",\n onboarding: {\n hook: \"Every action from every service lands here — SQDCP huddles, Policy Deployment projects, safety reports, all in one place, tracked until someone closes it out.\",\n highlights: [\n \"Board — kanban view, the default way most people work actions day to day.\",\n \"List and Calendar — the same actions, sorted or scheduled instead.\",\n \"Improvement (A3) — structured 7-box root-cause work for the actions that need more than a quick fix.\",\n ],\n firstAction: { label: \"Open Board\", path: \"/board\" },\n emptyStateHint:\n \"Zero actions yet — say this is normal on day one, actions will start showing up as SQDCP and Policy Deployment generate them, rather than describing an empty board as broken.\",\n },\n },\n {\n slug: \"connect\",\n name: \"OplyticsConnect\",\n tagline: \"Integrate machines, sensors, and systems\",\n description:\n \"Industrial IoT connectivity layer. Connect PLCs, sensors, and legacy systems to the Oplytics platform with zero-code configuration.\",\n icon: \"Plug\",\n accentColor: \"#1DB8CE\",\n status: \"in-development\",\n category: \"core\",\n subdomainLabel: \"connect\",\n },\n\n // ─── HUB SERVICES ───\n {\n slug: \"safety-manager\",\n name: \"Safety Manager\",\n tagline: \"Proactive safety culture, powered by data\",\n description:\n \"Incident reporting, hazard tracking, safety observations, and compliance management in one unified platform.\",\n icon: \"Shield\",\n accentColor: \"#EF4444\",\n status: \"coming-soon\",\n category: \"hub\",\n subdomainLabel: \"safetymanager\",\n },\n {\n slug: \"quality-manager\",\n name: \"Quality Manager\",\n tagline: \"End-to-end quality assurance\",\n description:\n \"Non-conformance tracking, CAPA management, audit scheduling, and quality metrics dashboards.\",\n icon: \"CheckCircle\",\n accentColor: \"#22C55E\",\n status: \"coming-soon\",\n category: \"hub\",\n subdomainLabel: null,\n },\n {\n slug: \"certification-manager\",\n name: \"Certification Manager\",\n tagline: \"Stay audit-ready, always\",\n description:\n \"Manage ISO, IATF, and other certification requirements. Document control, audit trails, and compliance tracking.\",\n icon: \"Award\",\n accentColor: \"#F97316\",\n status: \"coming-soon\",\n category: \"hub\",\n subdomainLabel: null,\n },\n];\n\n// ---------------------------------------------------------------------------\n// Derived helpers\n// ---------------------------------------------------------------------------\n\nexport const coreServices = SERVICE_CATALOG.filter(s => s.category === \"core\");\nexport const hubServices = SERVICE_CATALOG.filter(s => s.category === \"hub\");\nexport const liveServices = SERVICE_CATALOG.filter(s => s.status === \"live\");\nexport const inDevServices = SERVICE_CATALOG.filter(\n s => s.status === \"in-development\"\n);\nexport const comingSoonServices = SERVICE_CATALOG.filter(\n s => s.status === \"coming-soon\"\n);\n/** Services with a scripted onboarding tour step, in catalog order. */\nexport const onboardingTourServices = SERVICE_CATALOG.filter(\n s => s.onboarding !== undefined\n);\n\nexport function getServiceBySlug(slug: string): ServiceDefinition | undefined {\n return SERVICE_CATALOG.find(s => s.slug === slug);\n}\n\nexport function getServiceUrl(service: ServiceDefinition): string | null {\n if (service.subdomainLabel) {\n return platformUrl(service.subdomainLabel);\n }\n return null;\n}\n","/**\n * crossNav — build the sidebar cross-service links (service → service jump\n * without going through the Portal Service Hub).\n *\n * Visibility mirrors the Portal ServiceHub rule: platform_admin and\n * enterprise_admin see every live service; everyone else only sees the\n * services granted in the portal `user_services` table. The current service\n * never links to itself. Each link URL carries the caller's current\n * org-hierarchy selection (?oh=…) so the breadcrumb arrives pre-aligned.\n */\nimport type { OrgHierarchySelection } from \"../hierarchy/orgHierarchy\";\nimport { appendSelectionParam } from \"../hierarchy/selectionParams\";\nimport { SERVICE_CATALOG, getServiceUrl } from \"./catalog\";\n\n/** Roles that see every live service regardless of individual grants. */\nexport const ADMIN_SEE_ALL_ROLES = [\n \"platform_admin\",\n \"enterprise_admin\",\n] as const;\n\n/**\n * Legacy grant slugs → canonical catalog slugs. Auto-provisioned accounts\n * were granted \"sqdcp-logic\" while the catalog slug is \"sqdcp\".\n */\nexport const SLUG_ALIASES: Record<string, string> = {\n \"sqdcp-logic\": \"sqdcp\",\n};\n\nexport function normalizeServiceSlug(slug: string): string {\n return SLUG_ALIASES[slug] ?? slug;\n}\n\n/** Display-ready cross-service link entry. */\nexport interface CrossServiceLink {\n slug: string;\n name: string;\n /** Lucide icon name from the service catalog (consumers resolve locally). */\n icon: string;\n accentColor: string;\n /** Absolute URL including the ?oh= hierarchy selection param. */\n url: string;\n}\n\nexport interface BuildCrossServiceLinksOptions {\n /** Catalog slug of the service doing the rendering (excluded from links). */\n currentSlug: string;\n /** Grant slugs from the portal (raw — aliases are normalized here). */\n grantedSlugs: string[];\n /** Portal role of the current user (drives the admin see-all rule). */\n role?: string | null;\n /** Current org-hierarchy selection to carry across (may be empty/null). */\n selection?: OrgHierarchySelection | null;\n}\n\nexport function buildCrossServiceLinks(\n options: BuildCrossServiceLinksOptions\n): CrossServiceLink[] {\n const { currentSlug, grantedSlugs, role, selection } = options;\n\n const isAdmin =\n !!role && (ADMIN_SEE_ALL_ROLES as readonly string[]).includes(role);\n const granted = new Set(grantedSlugs.map(normalizeServiceSlug));\n const current = normalizeServiceSlug(currentSlug);\n\n return SERVICE_CATALOG.filter(\n svc =>\n svc.status === \"live\" &&\n svc.subdomainLabel !== null &&\n svc.slug !== current &&\n (isAdmin || granted.has(svc.slug))\n ).map(svc => ({\n slug: svc.slug,\n name: svc.name,\n icon: svc.icon,\n accentColor: svc.accentColor,\n url: appendSelectionParam(getServiceUrl(svc)!, selection),\n }));\n}\n"],"mappings":"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","names":[]}