@pablo2410/shared-ui 0.27.0 → 0.28.0

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.
package/README.md CHANGED
@@ -12,15 +12,15 @@ pnpm add @oplytics/shared-ui
12
12
 
13
13
  ## Modules
14
14
 
15
- | Module | Import | Description |
16
- | -------------- | -------------------------------- | ---------------------------------------------------------- |
17
- | **Layout** | `@oplytics/shared-ui/layout` | Config-driven sidebar, page header, footer, layout factory |
18
- | **Hierarchy** | `@oplytics/shared-ui/hierarchy` | Org hierarchy types, selection state, enterprise override |
19
- | **RBAC** | `@oplytics/shared-ui/rbac` | 7-role model, permission checks, error constants |
20
- | **Reporting** | `@oplytics/shared-ui/reporting` | Incident reporting and feedback toolbar types/config |
21
- | **Components** | `@oplytics/shared-ui/components` | SharedFooter, NotFound, ErrorBoundary, ViewOnlyBadge types |
22
- | **Hooks** | `@oplytics/shared-ui/hooks` | useMobile, useComposition, usePersistFn |
23
- | **Theme** | `@oplytics/shared-ui/theme` | Design tokens, color palette, typography, CSS variables |
15
+ | Module | Import | Description |
16
+ | -------------- | -------------------------------- | --------------------------------------------------------------------------- |
17
+ | **Layout** | `@oplytics/shared-ui/layout` | Config-driven sidebar, page header, footer, layout factory |
18
+ | **Hierarchy** | `@oplytics/shared-ui/hierarchy` | Org hierarchy types, selection state, enterprise override |
19
+ | **RBAC** | `@oplytics/shared-ui/rbac` | 7-role model, permission checks, error constants |
20
+ | **Reporting** | `@oplytics/shared-ui/reporting` | Incident reporting and feedback toolbar types/config |
21
+ | **Components** | `@oplytics/shared-ui/components` | SharedFooter, NotFound, ErrorBoundary, ViewOnlyBadge, AcceptanceBadge types |
22
+ | **Hooks** | `@oplytics/shared-ui/hooks` | useMobile, useComposition, usePersistFn |
23
+ | **Theme** | `@oplytics/shared-ui/theme` | Design tokens, color palette, typography, CSS variables |
24
24
 
25
25
  ## Quick Start
26
26
 
@@ -512,18 +512,42 @@ var PANEL_GAP = 12;
512
512
  function computePanelPosition(target, panelSize, viewport, placement = "auto") {
513
513
  const order = placement === "auto" ? AUTO_ORDER : FALLBACK_ORDER[placement];
514
514
  for (const candidate of order) {
515
- const pos2 = placeAt(candidate, target, panelSize);
516
- if (fits(pos2, panelSize, viewport)) {
517
- return { ...pos2, placement: candidate };
515
+ const pos = placeAt(candidate, target, panelSize);
516
+ if (fits(pos, panelSize, viewport)) {
517
+ return { ...pos, placement: candidate };
518
518
  }
519
519
  }
520
- const fallback = order[0];
521
- const pos = placeAt(fallback, target, panelSize);
522
- return {
523
- top: clamp(pos.top, 0, Math.max(0, viewport.height - panelSize.height)),
524
- left: clamp(pos.left, 0, Math.max(0, viewport.width - panelSize.width)),
525
- placement: fallback
520
+ const clampedAt = (side) => {
521
+ const pos = placeAt(side, target, panelSize);
522
+ return {
523
+ top: clamp(pos.top, 0, Math.max(0, viewport.height - panelSize.height)),
524
+ left: clamp(pos.left, 0, Math.max(0, viewport.width - panelSize.width)),
525
+ placement: side
526
+ };
526
527
  };
528
+ for (const candidate of order) {
529
+ const pos = clampedAt(candidate);
530
+ if (!overlapsTarget(pos, panelSize, target)) return pos;
531
+ }
532
+ return clampedAt(order[0]);
533
+ }
534
+ function maxPanelHeight(target, panelWidth, viewport) {
535
+ const targetRight = target.left + target.width;
536
+ const targetBottom = target.top + target.height;
537
+ const heights = [
538
+ viewport.height - (targetBottom + PANEL_GAP),
539
+ // below
540
+ target.top - PANEL_GAP
541
+ // above
542
+ ];
543
+ if (targetRight + PANEL_GAP + panelWidth <= viewport.width || // right
544
+ target.left - PANEL_GAP - panelWidth >= 0) {
545
+ heights.push(viewport.height);
546
+ }
547
+ return Math.max(0, ...heights);
548
+ }
549
+ function overlapsTarget(pos, panelSize, target) {
550
+ return pos.left < target.left + target.width && pos.left + panelSize.width > target.left && pos.top < target.top + target.height && pos.top + panelSize.height > target.top;
527
551
  }
528
552
  function placeAt(side, target, panelSize) {
529
553
  const targetCenterX = target.left + target.width / 2;
@@ -559,7 +583,12 @@ function clamp(value, min, max) {
559
583
  }
560
584
 
561
585
  // src/tour/TourShell.tsx
562
- import { useEffect as useEffect3, useRef as useRef2, useState as useState2 } from "react";
586
+ import {
587
+ useEffect as useEffect3,
588
+ useLayoutEffect,
589
+ useRef as useRef2,
590
+ useState as useState2
591
+ } from "react";
563
592
  import {
564
593
  X,
565
594
  ChevronLeft,
@@ -577,7 +606,11 @@ var defaultSkin = {
577
606
  bodyText: "#c0c6d0",
578
607
  softText: "#8890a0"
579
608
  };
580
- var SPOTLIGHT_PANEL_SIZE = { width: 320, height: 280 };
609
+ var SPOTLIGHT_PANEL_WIDTH = 320;
610
+ var SPOTLIGHT_PANEL_ESTIMATED_HEIGHT = 280;
611
+ var SPOTLIGHT_PANEL_MAX_HEIGHT = 560;
612
+ var SPOTLIGHT_PANEL_MIN_HEIGHT = 220;
613
+ var SPOTLIGHT_VIEWPORT_MARGIN = 16;
581
614
  function useHover() {
582
615
  const [hovered, setHovered] = useState2(false);
583
616
  return [
@@ -588,6 +621,39 @@ function useHover() {
588
621
  }
589
622
  ];
590
623
  }
624
+ function useMeasuredHeight(ref, active, estimate) {
625
+ const [height, setHeight] = useState2(estimate);
626
+ useLayoutEffect(() => {
627
+ const el = ref.current;
628
+ if (!active || !el) return;
629
+ const measure = () => {
630
+ const next = Math.ceil(el.getBoundingClientRect().height);
631
+ setHeight((prev) => next > 0 && next !== prev ? next : prev);
632
+ };
633
+ measure();
634
+ if (typeof ResizeObserver === "undefined") return;
635
+ const observer = new ResizeObserver(measure);
636
+ observer.observe(el);
637
+ return () => observer.disconnect();
638
+ }, [ref, active]);
639
+ return height;
640
+ }
641
+ function scrollCue(background, accent) {
642
+ const shadow = `${accent}88`;
643
+ return {
644
+ backgroundColor: background,
645
+ backgroundImage: [
646
+ `linear-gradient(${background} 30%, transparent)`,
647
+ `linear-gradient(transparent, ${background} 70%)`,
648
+ `radial-gradient(farthest-side at 50% 0, ${shadow}, transparent)`,
649
+ `radial-gradient(farthest-side at 50% 100%, ${shadow}, transparent)`
650
+ ].join(", "),
651
+ backgroundRepeat: "no-repeat",
652
+ backgroundPosition: "center top, center bottom, center top, center bottom",
653
+ backgroundSize: "100% 40px, 100% 40px, 100% 20px, 100% 20px",
654
+ backgroundAttachment: "local, local, scroll, scroll"
655
+ };
656
+ }
591
657
  function TourShell({
592
658
  step,
593
659
  index,
@@ -607,6 +673,11 @@ function TourShell({
607
673
  const panelRef = useRef2(null);
608
674
  const { rect, found } = useSpotlight(step.spotlight?.selector ?? null);
609
675
  const isSpotlight = Boolean(step.spotlight) && found && rect !== null;
676
+ const panelHeight = useMeasuredHeight(
677
+ panelRef,
678
+ isSpotlight,
679
+ SPOTLIGHT_PANEL_ESTIMATED_HEIGHT
680
+ );
610
681
  useEffect3(() => {
611
682
  const onKeyDown = (e) => {
612
683
  if (e.key === "Escape") onDismiss();
@@ -634,12 +705,36 @@ function TourShell({
634
705
  if (isSpotlight && rect) {
635
706
  const viewport = { width: window.innerWidth, height: window.innerHeight };
636
707
  const frame = computeSpotlightFrame(rect, viewport);
637
- const panelPos = computePanelPosition(
638
- rect,
639
- SPOTLIGHT_PANEL_SIZE,
640
- viewport,
708
+ const inset = SPOTLIGHT_VIEWPORT_MARGIN;
709
+ const insetTarget = {
710
+ ...rect,
711
+ top: rect.top - inset,
712
+ left: rect.left - inset
713
+ };
714
+ const insetViewport = {
715
+ width: viewport.width - 2 * inset,
716
+ height: viewport.height - 2 * inset
717
+ };
718
+ const room = maxPanelHeight(
719
+ insetTarget,
720
+ SPOTLIGHT_PANEL_WIDTH,
721
+ insetViewport
722
+ );
723
+ const maxHeight = Math.min(
724
+ SPOTLIGHT_PANEL_MAX_HEIGHT,
725
+ insetViewport.height,
726
+ room >= SPOTLIGHT_PANEL_MIN_HEIGHT ? room : Infinity
727
+ );
728
+ const placed = computePanelPosition(
729
+ insetTarget,
730
+ {
731
+ width: SPOTLIGHT_PANEL_WIDTH,
732
+ height: Math.min(panelHeight, maxHeight)
733
+ },
734
+ insetViewport,
641
735
  step.spotlight?.placement ?? "auto"
642
736
  );
737
+ const panelPos = { top: placed.top + inset, left: placed.left + inset };
643
738
  return /* @__PURE__ */ jsxs2(Fragment, { children: [
644
739
  ["top", "bottom", "left", "right"].map((side) => {
645
740
  const segment = frame[side];
@@ -685,25 +780,35 @@ function TourShell({
685
780
  style: {
686
781
  top: panelPos.top,
687
782
  left: panelPos.left,
688
- width: SPOTLIGHT_PANEL_SIZE.width,
689
- maxHeight: SPOTLIGHT_PANEL_SIZE.height,
783
+ width: SPOTLIGHT_PANEL_WIDTH,
784
+ maxHeight,
690
785
  background: skin.panelBg,
691
786
  border: skin.panelBorder
692
787
  },
693
788
  children: [
694
789
  /* @__PURE__ */ jsx3(TourShellHeader, { step, orb, onDismiss }),
695
790
  /* @__PURE__ */ jsx3(TourShellStepDots, { total, index, skin }),
696
- /* @__PURE__ */ jsx3("div", { className: "overflow-y-auto", style: { padding: "12px 16px" }, children: /* @__PURE__ */ jsx3(
697
- TourShellBody,
791
+ /* @__PURE__ */ jsx3(
792
+ "div",
698
793
  {
699
- gap: 12,
700
- step,
701
- narration,
702
- narrationPending,
703
- skin,
704
- children
794
+ className: "overflow-y-auto",
795
+ style: {
796
+ padding: "12px 16px",
797
+ ...scrollCue(skin.panelBg, skin.accent)
798
+ },
799
+ children: /* @__PURE__ */ jsx3(
800
+ TourShellBody,
801
+ {
802
+ gap: 12,
803
+ step,
804
+ narration,
805
+ narrationPending,
806
+ skin,
807
+ children
808
+ }
809
+ )
705
810
  }
706
- ) }),
811
+ ),
707
812
  /* @__PURE__ */ jsx3(
708
813
  TourShellFooter,
709
814
  {
@@ -1203,4 +1308,4 @@ export {
1203
1308
  useTour,
1204
1309
  OpiTourHost
1205
1310
  };
1206
- //# sourceMappingURL=chunk-LUQAR6NB.js.map
1311
+ //# sourceMappingURL=chunk-BG4DVZAY.js.map
@@ -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, a tall panel, or a target\n // close to every edge at once). Clamp each candidate into the viewport and\n // take the first that still clears the target; if none does, fall back to\n // the first candidate's clamped position, as before.\n const clampedAt = (side: ConcretePlacement): PanelPosition => {\n const pos = placeAt(side, 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: side,\n };\n };\n for (const candidate of order) {\n const pos = clampedAt(candidate);\n if (!overlapsTarget(pos, panelSize, target)) return pos;\n }\n return clampedAt(order[0]);\n}\n\n/**\n * The tallest a panel of `panelWidth` can be and still be placed clear of\n * `target` by computePanelPosition, strictly or after clamping. A panel capped\n * to this height (its body scrolls past it) can never land on the element\n * being pointed at. Beside the target the panel can slide vertically, so any\n * height up to the viewport's works there; above or below it is limited by the\n * room on that side. Uses the same gap and clamping rules as\n * computePanelPosition, so keep the two in step.\n */\nexport function maxPanelHeight(\n target: Rect,\n panelWidth: number,\n viewport: Size\n): number {\n const targetRight = target.left + target.width;\n const targetBottom = target.top + target.height;\n\n const heights = [\n viewport.height - (targetBottom + PANEL_GAP), // below\n target.top - PANEL_GAP, // above\n ];\n if (\n targetRight + PANEL_GAP + panelWidth <= viewport.width || // right\n target.left - PANEL_GAP - panelWidth >= 0 // left\n ) {\n heights.push(viewport.height);\n }\n return Math.max(0, ...heights);\n}\n\nfunction overlapsTarget(\n pos: { top: number; left: number },\n panelSize: Size,\n target: Rect\n): boolean {\n return (\n pos.left < target.left + target.width &&\n pos.left + panelSize.width > target.left &&\n pos.top < target.top + target.height &&\n pos.top + panelSize.height > target.top\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 {\n useEffect,\n useLayoutEffect,\n useRef,\n useState,\n type CSSProperties,\n type ReactNode,\n type RefObject,\n} 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 {\n computeSpotlightFrame,\n computePanelPosition,\n maxPanelHeight,\n} 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/** Spotlight panel width — narrower than the centered modal's max-w-md, since\n * it sits next to a card rather than owning the whole screen. The panel's CSS\n * width and spotlightMath's placement width both read this. */\nconst SPOTLIGHT_PANEL_WIDTH = 320;\n/** Height assumed for the first placement pass only. The panel's real height\n * depends on its content (narration length, bullets), so it is measured in a\n * layout effect and placement is recomputed before the first paint. */\nconst SPOTLIGHT_PANEL_ESTIMATED_HEIGHT = 280;\n/** The panel grows with its content up to this height (or the viewport less a\n * margin, if that is smaller). Past it the body scrolls, with a visible cue. */\nconst SPOTLIGHT_PANEL_MAX_HEIGHT = 560;\n/** Below this, capping to the room beside the target would leave the body a\n * sliver, so the panel keeps its normal cap and the placement fallback\n * decides (as it always did on a viewport this small). */\nconst SPOTLIGHT_PANEL_MIN_HEIGHT = 220;\nconst SPOTLIGHT_VIEWPORT_MARGIN = 16;\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 * The height the element actually renders at, kept current as its content\n * changes (a live narration reply replacing the scripted text, a step with\n * more bullets). Measured in a layout effect so a placement computed from it\n * lands before the first paint rather than flashing at the estimate.\n */\nfunction useMeasuredHeight(\n ref: RefObject<HTMLElement | null>,\n active: boolean,\n estimate: number\n) {\n const [height, setHeight] = useState(estimate);\n useLayoutEffect(() => {\n const el = ref.current;\n if (!active || !el) return;\n const measure = () => {\n const next = Math.ceil(el.getBoundingClientRect().height);\n setHeight(prev => (next > 0 && next !== prev ? next : prev));\n };\n measure();\n if (typeof ResizeObserver === \"undefined\") return;\n const observer = new ResizeObserver(measure);\n observer.observe(el);\n return () => observer.disconnect();\n }, [ref, active]);\n return height;\n}\n\n/**\n * Edge shadows on a scroll container that show only while there is more\n * content in that direction, so a body that overflows never hides bullets\n * silently. Pure CSS (the `local` covers scroll with the content and hide the\n * fixed shadows at the ends), and inline like the rest of the tour's styling.\n */\nfunction scrollCue(background: string, accent: string): CSSProperties {\n const shadow = `${accent}88`;\n return {\n backgroundColor: background,\n backgroundImage: [\n `linear-gradient(${background} 30%, transparent)`,\n `linear-gradient(transparent, ${background} 70%)`,\n `radial-gradient(farthest-side at 50% 0, ${shadow}, transparent)`,\n `radial-gradient(farthest-side at 50% 100%, ${shadow}, transparent)`,\n ].join(\", \"),\n backgroundRepeat: \"no-repeat\",\n backgroundPosition: \"center top, center bottom, center top, center bottom\",\n backgroundSize: \"100% 40px, 100% 40px, 100% 20px, 100% 20px\",\n backgroundAttachment: \"local, local, scroll, scroll\",\n };\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 const panelHeight = useMeasuredHeight(\n panelRef,\n isSpotlight,\n SPOTLIGHT_PANEL_ESTIMATED_HEIGHT\n );\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 // Place inside a viewport inset by the margin, so a tall panel never sits\n // flush against an edge. spotlightMath works from (0, 0), so shift the\n // target into the inset space and the result back out.\n const inset = SPOTLIGHT_VIEWPORT_MARGIN;\n const insetTarget = {\n ...rect,\n top: rect.top - inset,\n left: rect.left - inset,\n };\n const insetViewport = {\n width: viewport.width - 2 * inset,\n height: viewport.height - 2 * inset,\n };\n // Cap the height to the room that exists beside the target, so the body\n // scrolls (with a cue) rather than the panel landing on the target.\n const room = maxPanelHeight(\n insetTarget,\n SPOTLIGHT_PANEL_WIDTH,\n insetViewport\n );\n const maxHeight = Math.min(\n SPOTLIGHT_PANEL_MAX_HEIGHT,\n insetViewport.height,\n room >= SPOTLIGHT_PANEL_MIN_HEIGHT ? room : Infinity\n );\n const placed = computePanelPosition(\n insetTarget,\n {\n width: SPOTLIGHT_PANEL_WIDTH,\n height: Math.min(panelHeight, maxHeight),\n },\n insetViewport,\n step.spotlight?.placement ?? \"auto\"\n );\n const panelPos = { top: placed.top + inset, left: placed.left + inset };\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_WIDTH,\n maxHeight,\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\n className=\"overflow-y-auto\"\n style={{\n padding: \"12px 16px\",\n ...scrollCue(skin.panelBg, skin.accent),\n }}\n >\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":";;;;;;;;AAkBA,IAAM,qBAAqB;AAE3B,IAAM,qBAAqB;AAC3B,IAAM,kBAAkB;AACxB,IAAM,aAAsC,CAAC,SAAS,WAAW,OAAO;AAkBjE,IAAM,6BAAN,cAAyC,MAAM;AAAA,EAC3C;AAAA,EACT,YAAY,QAAgB,UAAoB;AAC9C,UAAM,4BAA4B,MAAM,MAAM,SAAS,KAAK,IAAI,CAAC,EAAE;AACnE,SAAK,OAAO;AACZ,SAAK,WAAW;AAAA,EAClB;AACF;AAGO,SAAS,uBAAuB,KAA+B;AACpE,QAAM,WAAqB,CAAC;AAE5B,MAAI,OAAO,IAAI,OAAO,YAAY,IAAI,GAAG,WAAW,GAAG;AACrD,aAAS,KAAK,gBAAgB;AAAA,EAChC,OAAO;AACL,QAAI,IAAI,GAAG,SAAS,oBAAoB;AACtC,eAAS,KAAK,qBAAqB,kBAAkB,aAAa;AAAA,IACpE;AACA,QAAI,CAAC,gBAAgB,KAAK,IAAI,EAAE,GAAG;AACjC,eAAS,KAAK,kDAAkD;AAAA,IAClE;AAAA,EACF;AAEA,MAAI,CAAC,MAAM,QAAQ,IAAI,KAAK,KAAK,IAAI,MAAM,WAAW,GAAG;AACvD,aAAS,KAAK,+BAA+B;AAC7C,WAAO;AAAA,EACT;AAEA,QAAM,OAAO,oBAAI,IAAY;AAC7B,MAAI,MAAM,QAAQ,CAAC,MAAM,MAAM;AAC7B,UAAM,KAAK,QAAQ,IAAI,CAAC;AACxB,QAAI,CAAC,KAAK,IAAI;AACZ,eAAS,KAAK,GAAG,EAAE,YAAY;AAAA,IACjC,OAAO;AACL,UAAI,KAAK,GAAG,SAAS,oBAAoB;AACvC,iBAAS;AAAA,UACP,GAAG,EAAE,sBAAsB,kBAAkB;AAAA,QAC/C;AAAA,MACF;AACA,UAAI,KAAK,IAAI,KAAK,EAAE,EAAG,UAAS,KAAK,sBAAsB,KAAK,EAAE,GAAG;AACrE,WAAK,IAAI,KAAK,EAAE;AAAA,IAClB;AACA,QAAI,CAAC,WAAW,SAAS,KAAK,IAAI,GAAG;AACnC,eAAS,KAAK,GAAG,EAAE,yBAAyB,OAAO,KAAK,IAAI,CAAC,GAAG;AAAA,IAClE;AACA,QAAI,CAAC,KAAK,MAAO,UAAS,KAAK,GAAG,EAAE,eAAe;AACnD,QAAI,CAAC,KAAK,aAAc,UAAS,KAAK,GAAG,EAAE,sBAAsB;AACjE,QAAI,KAAK,aAAa,CAAC,KAAK,UAAU,UAAU;AAC9C,eAAS,KAAK,GAAG,EAAE,mCAAmC;AAAA,IACxD;AAAA,EACF,CAAC;AAED,SAAO;AACT;AA4BO,SAAS,kBAA6B;AAE3C,QAAM,QAAQ,oBAAI,IAA4B;AAC9C,MAAI,UAA8B;AAClC,MAAI,QAAwB,EAAE,QAAQ,MAAM,SAAS,KAAK;AAC1D,QAAM,YAAY,oBAAI,IAAgB;AAEtC,QAAM,YAAY,MAAM;AACtB,UAAM,OAAO,CAAC,GAAG,MAAM,OAAO,CAAC,EAAE,IAAI,KAAK;AAE1C,QAAI,MAAM,UAAU,MAAM,OAAO,MAAM,OAAO,GAAI,WAAU;AAG5D,QAAI,SAAS,MAAM,UAAU,YAAY,MAAM,SAAS;AACtD,cAAQ,EAAE,QAAQ,MAAM,QAAQ;AAAA,IAClC;AACA,cAAU,QAAQ,OAAK,EAAE,CAAC;AAAA,EAC5B;AAEA,SAAO;AAAA,IACL,SAAS,KAAK;AACZ,YAAM,WAAW,uBAAuB,GAAG;AAC3C,UAAI,SAAS,SAAS,GAAG;AACvB,cAAM,IAAI,2BAA2B,KAAK,MAAM,WAAW,QAAQ;AAAA,MACrE;AAGA,YAAM,OAAO,IAAI,EAAE;AACnB,YAAM,IAAI,IAAI,IAAI,GAAG;AACrB,gBAAU;AACV,aAAO,MAAM;AAGX,YAAI,MAAM,IAAI,IAAI,EAAE,MAAM,KAAK;AAC7B,gBAAM,OAAO,IAAI,EAAE;AACnB,oBAAU;AAAA,QACZ;AAAA,MACF;AAAA,IACF;AAAA,IACA,KAAK,OAAO,EAAE,SAAS,KAAK,GAAG;AAC7B,UAAI,CAAC,MAAM,OAAQ;AACnB,gBAAU;AACV,gBAAU;AAAA,IACZ;AAAA,IACA,QAAQ;AACN,UAAI,YAAY,KAAM;AACtB,gBAAU;AACV,gBAAU;AAAA,IACZ;AAAA,IACA,UAAU,MAAM;AAAA,IAChB,UAAU,UAAU;AAClB,gBAAU,IAAI,QAAQ;AACtB,aAAO,MAAM;AACX,kBAAU,OAAO,QAAQ;AAAA,MAC3B;AAAA,IACF;AAAA,EACF;AACF;AAIO,IAAM,mBAA8B,gBAAgB;;;AC3JpD,IAAM,sBAAsB;AAY5B,IAAM,oBAAN,cAAgC,MAAM;AAAA,EAClC;AAAA,EACT,YAAY,SAAiB,QAAiB;AAC5C,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,SAAS;AAAA,EAChB;AACF;AAEA,IAAM,SAAS,OAAqB;AAAA,EAClC,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,YAAW,oBAAI,KAAK,CAAC,GAAE,YAAY;AACrC;AAWA,SAAS,WAAc,MAAkB;AACvC,QAAM,OAAQ,MAA+C,QAAQ;AACrE,MAAI,SAAS,UAAa,KAAK,SAAS,QAAW;AACjD,UAAM,IAAI,kBAAkB,iCAAiC;AAAA,EAC/D;AACA,SAAO,KAAK;AACd;AAEA,IAAM,WAAkC;AAAA,EACtC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEO,SAAS,kCACd,OAA+B,CAAC,GACT;AACvB,QAAM,UACJ,KAAK,cAAc,CAAC,OAAO,SAAS,MAAM,OAAO,IAAI;AACvD,QAAM,OAAO,OAAO,KAAK,WAAW,UAAU,EAAE,GAAG,QAAQ,OAAO,EAAE;AAEpE,MAAI;AAEJ,QAAM,OAAO,OAAO,WAA0C;AAC5D,UAAM,QAAQ,mBAAmB,KAAK,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC;AACrE,UAAM,MAAM,MAAM;AAAA,MAChB,GAAG,KAAK,CAAC,4CAA4C,KAAK;AAAA,MAC1D,EAAE,QAAQ,OAAO,aAAa,UAAU;AAAA,IAC1C;AACA,QAAI,CAAC,IAAI,IAAI;AACX,YAAM,IAAI;AAAA,QACR,gCAAgC,IAAI,MAAM;AAAA,QAC1C,IAAI;AAAA,MACN;AAAA,IACF;AACA,UAAM,OAAO,WAKV,MAAM,IAAI,KAAK,CAAC;AAEnB,uBAAmB,KAAK,WAAW;AAKnC,QAAI,WAAW,uBAAuB,CAAC,kBAAkB;AACvD,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AAEA,WAAO;AAAA,MACL,QAAQ,SAAS,SAAS,KAAK,MAAoB,IAC9C,KAAK,SACN;AAAA,MACJ,YAAY,KAAK,cAAc;AAAA,MAC/B,WAAW,KAAK,aAAa,OAAO,EAAE;AAAA,IACxC;AAAA,EACF;AAEA,SAAO;AAAA,IACL;AAAA,IACA,MAAM,MAAM,QAAQ,QAAQ;AAC1B,UAAI,WAAW,qBAAqB;AAClC,YAAI,qBAAqB,OAAW,OAAM,KAAK,MAAM;AACrD,YAAI,CAAC,iBAAkB;AAAA,MACzB;AACA,YAAM,MAAM,MAAM;AAAA,QAChB,GAAG,KAAK,CAAC;AAAA,QACT;AAAA,UACE,QAAQ;AAAA,UACR,aAAa;AAAA,UACb,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,UAC9C,MAAM,KAAK,UAAU,EAAE,MAAM,EAAE,QAAQ,GAAG,OAAO,EAAE,CAAC;AAAA,QACtD;AAAA,MACF;AACA,UAAI,CAAC,IAAI,IAAI;AACX,cAAM,IAAI;AAAA,UACR,gCAAgC,IAAI,MAAM;AAAA,UAC1C,IAAI;AAAA,QACN;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAoBO,SAAS,6BACd,WACA,QACwB;AACxB,MAAI,WAAiC;AAAA,IACnC,OAAO;AAAA,IACP,UAAU,OAAO;AAAA,IACjB,OAAO;AAAA,EACT;AACA,QAAM,YAAY,oBAAI,IAAgB;AACtC,MAAI,aAA4B,QAAQ,QAAQ;AAGhD,MAAI,kBAAkB;AAEtB,QAAM,MAAM,CAAC,SAA+B;AAC1C,eAAW;AACX,cAAU,QAAQ,OAAK,EAAE,CAAC;AAAA,EAC5B;AAEA,SAAO;AAAA,IACL,aAAa,MAAM;AAAA,IACnB,UAAU,UAAU;AAClB,gBAAU,IAAI,QAAQ;AACtB,aAAO,MAAM;AACX,kBAAU,OAAO,QAAQ;AAAA,MAC3B;AAAA,IACF;AAAA,IACA,MAAM,OAAO;AACX,UAAI;AACF,cAAM,WAAW,MAAM,UAAU,KAAK,MAAM;AAC5C,YAAI,gBAAiB;AACrB,YAAI,EAAE,OAAO,SAAS,UAAU,OAAO,KAAK,CAAC;AAAA,MAC/C,SAAS,KAAK;AACZ,YAAI,gBAAiB;AACrB,YAAI;AAAA,UACF,OAAO;AAAA,UACP,UAAU,OAAO;AAAA,UACjB,OAAO,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC;AAAA,QAC3D,CAAC;AAAA,MACH;AAAA,IACF;AAAA,IACA,OAAO,QAAQ;AACb,wBAAkB;AAClB,UAAI;AAAA,QACF,OAAO,SAAS,UAAU,YAAY,UAAU,SAAS;AAAA,QACzD,UAAU,EAAE,GAAG,QAAQ,YAAW,oBAAI,KAAK,GAAE,YAAY,EAAE;AAAA,QAC3D,OAAO,SAAS;AAAA,MAClB,CAAC;AACD,mBAAa,WACV,KAAK,MAAM,UAAU,MAAM,QAAQ,MAAM,CAAC,EAC1C,MAAM,SAAO;AACZ,YAAI;AAAA,UACF,GAAG;AAAA,UACH,OAAO,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC;AAAA,QAC3D,CAAC;AAAA,MACH,CAAC;AACH,aAAO;AAAA,IACT;AAAA,EACF;AACF;AAKO,SAAS,eAAe,QAA6B;AAC1D,SAAO,WAAW,YAAY,WAAW;AAC3C;AAMO,SAAS,eACd,WACA,UACS;AACT,SACE,cAAc,QACd,SAAS,UAAU,WACnB,eAAe,SAAS,SAAS,MAAM;AAE3C;;;AC1OO,IAAM,+BAA+B;AAErC,IAAM,mBACX;AAYK,SAAS,oBAAoB,MAAuC;AACzE,SAAO;AAAA,IACL,SAAS;AAAA,IACT,UAAU;AAAA,MACR,MAAM,KAAK;AAAA,MACX,YAAY,KAAK;AAAA,MACjB,UAAU,KAAK;AAAA,IACjB;AAAA,IACA,UAAU,CAAC,EAAE,MAAM,QAAQ,SAAS,iBAAiB,CAAC;AAAA,EACxD;AACF;AAUO,SAAS,qBACd,OAA8B,CAAC,GACjB;AACd,QAAM,UACJ,KAAK,cAAc,CAAC,OAAO,SAAS,MAAM,OAAO,IAAI;AACvD,SAAO,OAAM,SAAQ;AACnB,UAAM,QAAQ,KAAK,WAAW,UAAU,EAAE,GAAG,QAAQ,OAAO,EAAE;AAC9D,UAAM,MAAM,MAAM,QAAQ,GAAG,IAAI,uBAAuB;AAAA,MACtD,QAAQ;AAAA,MACR,aAAa;AAAA,MACb,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,MAC9C,MAAM,KAAK,UAAU,EAAE,MAAM,oBAAoB,IAAI,EAAE,CAAC;AAAA,IAC1D,CAAC;AACD,QAAI,CAAC,IAAI,GAAI,OAAM,IAAI,MAAM,yBAAyB,IAAI,MAAM,GAAG;AACnE,UAAM,OAAQ,MAAM,IAAI,KAAK;AAG7B,UAAM,UAAU,KAAK,QAAQ,MAAM,MAAM;AACzC,QAAI,OAAO,YAAY,YAAY,QAAQ,KAAK,MAAM,IAAI;AACxD,YAAM,IAAI,MAAM,gCAAgC;AAAA,IAClD;AACA,WAAO;AAAA,EACT;AACF;;;ACjEA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AA6DE;AAnCT,IAAI;AACJ,IAAI;AAEJ,IAAM,cAAc,cAAyC,CAAC,CAAC;AAE/D,SAAS,iBAAmC;AAC1C,QAAM,MAAM,WAAW,WAAW;AAClC,SAAO;AAAA,IACL,OAAO,IAAI,SAAS;AAAA;AAAA;AAAA,IAGpB,WACE,IAAI,cACH,qBAAqB,kCAAkC;AAAA,IAC1D,UAAU,IAAI,aAAa,oBAAoB,qBAAqB;AAAA,EACtE;AACF;AASO,SAAS,aAAa;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAsB;AACpB,QAAM,QAAQ;AAAA,IACZ,OAAO,EAAE,OAAO,WAAW,SAAS;AAAA,IACpC,CAAC,OAAO,WAAW,QAAQ;AAAA,EAC7B;AACA,SAAO,oBAAC,YAAY,UAAZ,EAAqB,OAAe,UAAS;AACvD;AAaO,SAAS,gBAAgB,KAA8C;AAC5E,QAAM,EAAE,MAAM,IAAI,eAAe;AACjC,YAAU,MAAM;AACd,QAAI,CAAC,IAAK;AACV,QAAI;AACF,aAAO,MAAM,SAAS,GAAG;AAAA,IAC3B,SAAS,KAAK;AACZ,UAAI,eAAe,4BAA4B;AAC7C,gBAAQ,MAAM,IAAI,OAAO;AACzB;AAAA,MACF;AACA,YAAM;AAAA,IACR;AAAA,EACF,GAAG,CAAC,OAAO,GAAG,CAAC;AACjB;AAEO,SAAS,eAA+B;AAC7C,QAAM,EAAE,MAAM,IAAI,eAAe;AACjC,SAAO,qBAAqB,MAAM,WAAW,MAAM,UAAU,MAAM,QAAQ;AAC7E;AAIO,SAAS,gBAAuC;AACrD,SAAO,aAAa,EAAE;AACxB;AAGO,SAAS,kBAOd;AACA,QAAM,EAAE,MAAM,IAAI,eAAe;AACjC,QAAM,YAAY,aAAa,EAAE,WAAW;AAC5C,SAAO;AAAA,IACL,OAAO,MAAM,MAAM,KAAK,EAAE,SAAS,KAAK,CAAC;AAAA,IACzC,MAAM,aAAW,MAAM,KAAK,OAAO;AAAA,IACnC,OAAO,MAAM,MAAM,MAAM;AAAA,IACzB;AAAA,EACF;AACF;AAQO,SAAS,gBAAgB,QAAuC;AACrE,QAAM,EAAE,UAAU,IAAI,eAAe;AACrC,QAAM,aAAa;AAAA,IACjB,MAAM,6BAA6B,WAAW,MAAM;AAAA,IACpD,CAAC,WAAW,MAAM;AAAA,EACpB;AACA,YAAU,MAAM;AACd,SAAK,WAAW,KAAK;AAAA,EACvB,GAAG,CAAC,UAAU,CAAC;AACf,QAAM,WAAW;AAAA,IACf,WAAW;AAAA,IACX,WAAW;AAAA,IACX,WAAW;AAAA,EACb;AACA,SAAO,EAAE,GAAG,UAAU,QAAQ,WAAW,OAAO;AAClD;AAIO,SAAS,kBAAgC;AAC9C,SAAO,eAAe,EAAE;AAC1B;;;AC7JA,SAAS,gBAAgB;AAiBrB,SAYE,OAAAA,MAZF;AAPG,SAAS,kBAAkB;AAAA,EAChC,QAAQ;AAAA,EACR;AACF,GAA2B;AACzB,QAAM,EAAE,WAAW,MAAM,IAAI,gBAAgB;AAC7C,MAAI,CAAC,UAAW,QAAO;AACvB,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,SAAS;AAAA,MACT,OAAO;AAAA,MACP,cAAY;AAAA,MACZ,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,MAEA;AAAA,wBAAAA,KAAC,YAAS,WAAU,0BAAyB;AAAA,QAC7C,gBAAAA,KAAC,UAAK,WAAU,gCAAgC,iBAAM;AAAA;AAAA;AAAA,EACxD;AAEJ;;;AC/BA,SAAS,aAAAC,YAAW,QAAQ,gBAAgB;AAY5C,IAAM,qBAAqB;AAOpB,SAAS,aAAa,UAA6C;AACxE,QAAM,CAAC,MAAM,OAAO,IAAI,SAAsB,IAAI;AAClD,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAS,KAAK;AAGxC,QAAM,sBAAsB,OAAsB,IAAI;AAEtD,EAAAA,WAAU,MAAM;AACd,QAAI,CAAC,UAAU;AACb,cAAQ,IAAI;AACZ,eAAS,KAAK;AACd;AAAA,IACF;AAEA,QAAI;AACJ,QAAI,YAAY;AAEhB,UAAM,UAAU,MAAM;AACpB,UAAI,UAAW;AACf,YAAM,KAAK,SAAS;AAAA,QAClB,sBAAsB,QAAQ;AAAA,MAChC;AACA,UAAI,CAAC,IAAI;AACP,gBAAQ,IAAI;AACZ,iBAAS,KAAK;AACd;AAAA,MACF;AACA,YAAM,UAAU,GAAG,sBAAsB;AACzC,cAAQ;AAAA,QACN,KAAK,QAAQ;AAAA,QACb,MAAM,QAAQ;AAAA,QACd,OAAO,QAAQ;AAAA,QACf,QAAQ,QAAQ;AAAA,MAClB,CAAC;AACD,eAAS,IAAI;AAEb,UAAI,oBAAoB,YAAY,UAAU;AAC5C,4BAAoB,UAAU;AAC9B,WAAG,eAAe,EAAE,OAAO,UAAU,UAAU,SAAS,CAAC;AAAA,MAC3D;AAAA,IACF;AAKA,YAAQ;AACR,UAAM,eAAe,sBAAsB,OAAO;AAElD,UAAM,WAAW,MAAM;AACrB,UAAI,YAAa,cAAa,WAAW;AACzC,oBAAc,WAAW,SAAS,kBAAkB;AAAA,IACtD;AAKA,UAAM,WAAW,MAAM,QAAQ;AAE/B,WAAO,iBAAiB,UAAU,QAAQ;AAC1C,WAAO,iBAAiB,UAAU,UAAU,EAAE,SAAS,KAAK,CAAC;AAY7D,UAAM,WAAW,IAAI,iBAAiB,OAAO;AAC7C,aAAS,QAAQ,SAAS,MAAM;AAAA,MAC9B,WAAW;AAAA,MACX,SAAS;AAAA,MACT,YAAY;AAAA,IACd,CAAC;AAED,WAAO,MAAM;AACX,kBAAY;AACZ,2BAAqB,YAAY;AACjC,UAAI,YAAa,cAAa,WAAW;AACzC,aAAO,oBAAoB,UAAU,QAAQ;AAC7C,aAAO,oBAAoB,UAAU,UAAU,EAAE,SAAS,KAAK,CAAC;AAChE,gBAAU,WAAW;AAAA,IACvB;AAAA,EACF,GAAG,CAAC,QAAQ,CAAC;AAEb,SAAO,EAAE,MAAM,MAAM;AACvB;;;AChFO,SAAS,sBACd,QACA,UACgB;AAChB,QAAM,eAAe,OAAO,MAAM,OAAO;AACzC,QAAM,cAAc,OAAO,OAAO,OAAO;AAEzC,SAAO;AAAA;AAAA,IAEL,KAAK;AAAA,MACH,KAAK;AAAA,MACL,MAAM;AAAA,MACN,OAAO,SAAS;AAAA,MAChB,QAAQ,KAAK,IAAI,GAAG,OAAO,GAAG;AAAA,IAChC;AAAA;AAAA,IAEA,QAAQ;AAAA,MACN,KAAK;AAAA,MACL,MAAM;AAAA,MACN,OAAO,SAAS;AAAA,MAChB,QAAQ,KAAK,IAAI,GAAG,SAAS,SAAS,YAAY;AAAA,IACpD;AAAA;AAAA;AAAA,IAGA,MAAM;AAAA,MACJ,KAAK,OAAO;AAAA,MACZ,MAAM;AAAA,MACN,OAAO,KAAK,IAAI,GAAG,OAAO,IAAI;AAAA,MAC9B,QAAQ,OAAO;AAAA,IACjB;AAAA;AAAA,IAEA,OAAO;AAAA,MACL,KAAK,OAAO;AAAA,MACZ,MAAM;AAAA,MACN,OAAO,KAAK,IAAI,GAAG,SAAS,QAAQ,WAAW;AAAA,MAC/C,QAAQ,OAAO;AAAA,IACjB;AAAA,EACF;AACF;AAIA,IAAM,iBAAiE;AAAA,EACrE,QAAQ,CAAC,UAAU,OAAO,SAAS,MAAM;AAAA,EACzC,KAAK,CAAC,OAAO,UAAU,SAAS,MAAM;AAAA,EACtC,OAAO,CAAC,SAAS,QAAQ,UAAU,KAAK;AAAA,EACxC,MAAM,CAAC,QAAQ,SAAS,UAAU,KAAK;AACzC;AAEA,IAAM,aAAkC,eAAe;AAEvD,IAAM,YAAY;AAaX,SAAS,qBACd,QACA,WACA,UACA,YAAuB,QACR;AACf,QAAM,QAAQ,cAAc,SAAS,aAAa,eAAe,SAAS;AAE1E,aAAW,aAAa,OAAO;AAC7B,UAAM,MAAM,QAAQ,WAAW,QAAQ,SAAS;AAChD,QAAI,KAAK,KAAK,WAAW,QAAQ,GAAG;AAClC,aAAO,EAAE,GAAG,KAAK,WAAW,UAAU;AAAA,IACxC;AAAA,EACF;AAMA,QAAM,YAAY,CAAC,SAA2C;AAC5D,UAAM,MAAM,QAAQ,MAAM,QAAQ,SAAS;AAC3C,WAAO;AAAA,MACL,KAAK,MAAM,IAAI,KAAK,GAAG,KAAK,IAAI,GAAG,SAAS,SAAS,UAAU,MAAM,CAAC;AAAA,MACtE,MAAM,MAAM,IAAI,MAAM,GAAG,KAAK,IAAI,GAAG,SAAS,QAAQ,UAAU,KAAK,CAAC;AAAA,MACtE,WAAW;AAAA,IACb;AAAA,EACF;AACA,aAAW,aAAa,OAAO;AAC7B,UAAM,MAAM,UAAU,SAAS;AAC/B,QAAI,CAAC,eAAe,KAAK,WAAW,MAAM,EAAG,QAAO;AAAA,EACtD;AACA,SAAO,UAAU,MAAM,CAAC,CAAC;AAC3B;AAWO,SAAS,eACd,QACA,YACA,UACQ;AACR,QAAM,cAAc,OAAO,OAAO,OAAO;AACzC,QAAM,eAAe,OAAO,MAAM,OAAO;AAEzC,QAAM,UAAU;AAAA,IACd,SAAS,UAAU,eAAe;AAAA;AAAA,IAClC,OAAO,MAAM;AAAA;AAAA,EACf;AACA,MACE,cAAc,YAAY,cAAc,SAAS;AAAA,EACjD,OAAO,OAAO,YAAY,cAAc,GACxC;AACA,YAAQ,KAAK,SAAS,MAAM;AAAA,EAC9B;AACA,SAAO,KAAK,IAAI,GAAG,GAAG,OAAO;AAC/B;AAEA,SAAS,eACP,KACA,WACA,QACS;AACT,SACE,IAAI,OAAO,OAAO,OAAO,OAAO,SAChC,IAAI,OAAO,UAAU,QAAQ,OAAO,QACpC,IAAI,MAAM,OAAO,MAAM,OAAO,UAC9B,IAAI,MAAM,UAAU,SAAS,OAAO;AAExC;AAEA,SAAS,QACP,MACA,QACA,WAC+B;AAC/B,QAAM,gBAAgB,OAAO,OAAO,OAAO,QAAQ;AACnD,QAAM,gBAAgB,OAAO,MAAM,OAAO,SAAS;AAEnD,UAAQ,MAAM;AAAA,IACZ,KAAK;AACH,aAAO;AAAA,QACL,KAAK,OAAO,MAAM,OAAO,SAAS;AAAA,QAClC,MAAM,gBAAgB,UAAU,QAAQ;AAAA,MAC1C;AAAA,IACF,KAAK;AACH,aAAO;AAAA,QACL,KAAK,OAAO,MAAM,UAAU,SAAS;AAAA,QACrC,MAAM,gBAAgB,UAAU,QAAQ;AAAA,MAC1C;AAAA,IACF,KAAK;AACH,aAAO;AAAA,QACL,KAAK,gBAAgB,UAAU,SAAS;AAAA,QACxC,MAAM,OAAO,OAAO,OAAO,QAAQ;AAAA,MACrC;AAAA,IACF,KAAK;AACH,aAAO;AAAA,QACL,KAAK,gBAAgB,UAAU,SAAS;AAAA,QACxC,MAAM,OAAO,OAAO,UAAU,QAAQ;AAAA,MACxC;AAAA,EACJ;AACF;AAEA,SAAS,KACP,KACA,WACA,UACS;AACT,SACE,IAAI,OAAO,KACX,IAAI,QAAQ,KACZ,IAAI,MAAM,UAAU,UAAU,SAAS,UACvC,IAAI,OAAO,UAAU,SAAS,SAAS;AAE3C;AAEA,SAAS,MAAM,OAAe,KAAa,KAAqB;AAC9D,SAAO,KAAK,IAAI,KAAK,IAAI,OAAO,GAAG,GAAG,GAAG;AAC3C;;;AC7MA;AAAA,EACE,aAAAC;AAAA,EACA;AAAA,EACA,UAAAC;AAAA,EACA,YAAAC;AAAA,OAIK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AA+PD,mBAKM,OAAAC,MA0BJ,QAAAC,aA/BF;AA5ON,IAAM,cAA6B;AAAA,EACjC,QAAQ;AAAA,EACR,WAAW;AAAA,EACX,SAAS;AAAA,EACT,aAAa;AAAA,EACb,WAAW;AAAA,EACX,UAAU;AAAA,EACV,UAAU;AACZ;AAKA,IAAM,wBAAwB;AAI9B,IAAM,mCAAmC;AAGzC,IAAM,6BAA6B;AAInC,IAAM,6BAA6B;AACnC,IAAM,4BAA4B;AAclC,SAAS,WAAW;AAClB,QAAM,CAAC,SAAS,UAAU,IAAIC,UAAS,KAAK;AAC5C,SAAO;AAAA,IACL;AAAA,IACA;AAAA,MACE,cAAc,MAAM,WAAW,IAAI;AAAA,MACnC,cAAc,MAAM,WAAW,KAAK;AAAA,IACtC;AAAA,EACF;AACF;AAyCA,SAAS,kBACP,KACA,QACA,UACA;AACA,QAAM,CAAC,QAAQ,SAAS,IAAIA,UAAS,QAAQ;AAC7C,kBAAgB,MAAM;AACpB,UAAM,KAAK,IAAI;AACf,QAAI,CAAC,UAAU,CAAC,GAAI;AACpB,UAAM,UAAU,MAAM;AACpB,YAAM,OAAO,KAAK,KAAK,GAAG,sBAAsB,EAAE,MAAM;AACxD,gBAAU,UAAS,OAAO,KAAK,SAAS,OAAO,OAAO,IAAK;AAAA,IAC7D;AACA,YAAQ;AACR,QAAI,OAAO,mBAAmB,YAAa;AAC3C,UAAM,WAAW,IAAI,eAAe,OAAO;AAC3C,aAAS,QAAQ,EAAE;AACnB,WAAO,MAAM,SAAS,WAAW;AAAA,EACnC,GAAG,CAAC,KAAK,MAAM,CAAC;AAChB,SAAO;AACT;AAQA,SAAS,UAAU,YAAoB,QAA+B;AACpE,QAAM,SAAS,GAAG,MAAM;AACxB,SAAO;AAAA,IACL,iBAAiB;AAAA,IACjB,iBAAiB;AAAA,MACf,mBAAmB,UAAU;AAAA,MAC7B,gCAAgC,UAAU;AAAA,MAC1C,2CAA2C,MAAM;AAAA,MACjD,8CAA8C,MAAM;AAAA,IACtD,EAAE,KAAK,IAAI;AAAA,IACX,kBAAkB;AAAA,IAClB,oBAAoB;AAAA,IACpB,gBAAgB;AAAA,IAChB,sBAAsB;AAAA,EACxB;AACF;AAQO,SAAS,UAAU;AAAA,EACxB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,OAAO;AAAA,EACP;AAAA,EACA;AACF,GAAmB;AACjB,QAAM,WAAWC,QAAuB,IAAI;AAC5C,QAAM,EAAE,MAAM,MAAM,IAAI,aAAa,KAAK,WAAW,YAAY,IAAI;AACrE,QAAM,cAAc,QAAQ,KAAK,SAAS,KAAK,SAAS,SAAS;AACjE,QAAM,cAAc;AAAA,IAClB;AAAA,IACA;AAAA,IACA;AAAA,EACF;AAEA,EAAAC,WAAU,MAAM;AACd,UAAM,YAAY,CAAC,MAAqB;AACtC,UAAI,EAAE,QAAQ,SAAU,WAAU;AAAA,IACpC;AACA,aAAS,iBAAiB,WAAW,SAAS;AAC9C,WAAO,MAAM,SAAS,oBAAoB,WAAW,SAAS;AAAA,EAChE,GAAG,CAAC,SAAS,CAAC;AAEd,EAAAA,WAAU,MAAM;AACd,aAAS,SAAS,MAAM;AAAA,EAC1B,GAAG,CAAC,KAAK,EAAE,CAAC;AAKZ,EAAAA,WAAU,MAAM;AACd,QAAI,CAAC,eAAe,CAAC,8BAA8B,CAAC,KAAK,UAAW;AACpE,UAAM,WAAW,KAAK,UAAU;AAChC,UAAM,iBAAiB,CAAC,MAAkB;AACxC,YAAM,SAAS,EAAE;AACjB,UAAI,QAAQ,QAAQ,sBAAsB,QAAQ,IAAI,GAAG;AACvD,mCAA2B;AAAA,MAC7B;AAAA,IACF;AACA,aAAS,iBAAiB,SAAS,gBAAgB,EAAE,SAAS,KAAK,CAAC;AACpE,WAAO,MACL,SAAS,oBAAoB,SAAS,gBAAgB;AAAA,MACpD,SAAS;AAAA,IACX,CAAC;AAAA,EACL,GAAG,CAAC,aAAa,4BAA4B,KAAK,SAAS,CAAC;AAE5D,MAAI,eAAe,MAAM;AACvB,UAAM,WAAW,EAAE,OAAO,OAAO,YAAY,QAAQ,OAAO,YAAY;AACxE,UAAM,QAAQ,sBAAsB,MAAM,QAAQ;AAIlD,UAAM,QAAQ;AACd,UAAM,cAAc;AAAA,MAClB,GAAG;AAAA,MACH,KAAK,KAAK,MAAM;AAAA,MAChB,MAAM,KAAK,OAAO;AAAA,IACpB;AACA,UAAM,gBAAgB;AAAA,MACpB,OAAO,SAAS,QAAQ,IAAI;AAAA,MAC5B,QAAQ,SAAS,SAAS,IAAI;AAAA,IAChC;AAGA,UAAM,OAAO;AAAA,MACX;AAAA,MACA;AAAA,MACA;AAAA,IACF;AACA,UAAM,YAAY,KAAK;AAAA,MACrB;AAAA,MACA,cAAc;AAAA,MACd,QAAQ,6BAA6B,OAAO;AAAA,IAC9C;AACA,UAAM,SAAS;AAAA,MACb;AAAA,MACA;AAAA,QACE,OAAO;AAAA,QACP,QAAQ,KAAK,IAAI,aAAa,SAAS;AAAA,MACzC;AAAA,MACA;AAAA,MACA,KAAK,WAAW,aAAa;AAAA,IAC/B;AACA,UAAM,WAAW,EAAE,KAAK,OAAO,MAAM,OAAO,MAAM,OAAO,OAAO,MAAM;AAEtE,WACE,gBAAAH,MAAA,YACI;AAAA,OAAC,OAAO,UAAU,QAAQ,OAAO,EAAY,IAAI,UAAQ;AACzD,cAAM,UAAU,MAAM,IAAI;AAC1B,YAAI,QAAQ,SAAS,KAAK,QAAQ,UAAU,EAAG,QAAO;AACtD,eACE,gBAAAD;AAAA,UAAC;AAAA;AAAA,YAEC,WAAU;AAAA,YACV,OAAO;AAAA,cACL,KAAK,QAAQ;AAAA,cACb,MAAM,QAAQ;AAAA,cACd,OAAO,QAAQ;AAAA,cACf,QAAQ,QAAQ;AAAA,cAChB,YAAY,KAAK;AAAA,YACnB;AAAA,YACA,SAAS;AAAA;AAAA,UATJ;AAAA,QAUP;AAAA,MAEJ,CAAC;AAAA,MAGD,gBAAAA;AAAA,QAAC;AAAA;AAAA,UACC,WAAU;AAAA,UACV,OAAO;AAAA,YACL,KAAK,KAAK,MAAM;AAAA,YAChB,MAAM,KAAK,OAAO;AAAA,YAClB,OAAO,KAAK,QAAQ;AAAA,YACpB,QAAQ,KAAK,SAAS;AAAA,YACtB,WAAW,aAAa,KAAK,MAAM,kBAAkB,KAAK,MAAM;AAAA,UAClE;AAAA;AAAA,MACF;AAAA,MACA,gBAAAC;AAAA,QAAC;AAAA;AAAA,UACC,KAAK;AAAA,UACL,UAAU;AAAA,UACV,MAAK;AAAA,UACL,cAAW;AAAA,UACX,cAAY,KAAK;AAAA,UACjB,WAAU;AAAA,UACV,OAAO;AAAA,YACL,KAAK,SAAS;AAAA,YACd,MAAM,SAAS;AAAA,YACf,OAAO;AAAA,YACP;AAAA,YACA,YAAY,KAAK;AAAA,YACjB,QAAQ,KAAK;AAAA,UACf;AAAA,UAEA;AAAA,4BAAAD,KAAC,mBAAgB,MAAY,KAAU,WAAsB;AAAA,YAC7D,gBAAAA,KAAC,qBAAkB,OAAc,OAAc,MAAY;AAAA,YAC3D,gBAAAA;AAAA,cAAC;AAAA;AAAA,gBACC,WAAU;AAAA,gBACV,OAAO;AAAA,kBACL,SAAS;AAAA,kBACT,GAAG,UAAU,KAAK,SAAS,KAAK,MAAM;AAAA,gBACxC;AAAA,gBAEA,0BAAAA;AAAA,kBAAC;AAAA;AAAA,oBACC,KAAK;AAAA,oBACL;AAAA,oBACA;AAAA,oBACA;AAAA,oBACA;AAAA,oBACA;AAAA;AAAA,gBACF;AAAA;AAAA,YACF;AAAA,YACA,gBAAAA;AAAA,cAAC;AAAA;AAAA,gBACC;AAAA,gBACA;AAAA,gBACA;AAAA,gBACA;AAAA,gBACA;AAAA,gBACA;AAAA;AAAA,YACF;AAAA;AAAA;AAAA,MACF;AAAA,OACF;AAAA,EAEJ;AAEA,SACE,gBAAAA;AAAA,IAAC;AAAA;AAAA,MACC,WAAU;AAAA,MACV,OAAO,EAAE,YAAY,KAAK,WAAW,SAAS,GAAG;AAAA,MACjD,SAAS,OAAK;AACZ,YAAI,EAAE,WAAW,EAAE,cAAe,WAAU;AAAA,MAC9C;AAAA,MAEA,0BAAAC;AAAA,QAAC;AAAA;AAAA,UACC,KAAK;AAAA,UACL,UAAU;AAAA,UACV,MAAK;AAAA,UACL,cAAW;AAAA,UACX,cAAY,KAAK;AAAA,UACjB,WAAU;AAAA,UACV,OAAO,EAAE,YAAY,KAAK,SAAS,QAAQ,KAAK,YAAY;AAAA,UAE5D;AAAA,4BAAAD,KAAC,mBAAgB,MAAY,KAAU,WAAsB;AAAA,YAC7D,gBAAAA,KAAC,qBAAkB,OAAc,OAAc,MAAY;AAAA,YAC3D,gBAAAA,KAAC,SAAI,WAAU,mBAAkB,OAAO,EAAE,SAAS,YAAY,GAC7D,0BAAAA;AAAA,cAAC;AAAA;AAAA,gBACC,KAAK;AAAA,gBACL;AAAA,gBACA;AAAA,gBACA;AAAA,gBACA;AAAA,gBACA;AAAA;AAAA,YACF,GACF;AAAA,YACA,gBAAAA;AAAA,cAAC;AAAA;AAAA,gBACC;AAAA,gBACA;AAAA,gBACA;AAAA,gBACA;AAAA,gBACA;AAAA,gBACA;AAAA;AAAA,YACF;AAAA;AAAA;AAAA,MACF;AAAA;AAAA,EACF;AAEJ;AAEA,SAAS,gBAAgB;AAAA,EACvB;AAAA,EACA;AAAA,EACA;AACF,GAIG;AACD,QAAM,CAAC,cAAc,UAAU,IAAI,SAAS;AAC5C,SACE,gBAAAC;AAAA,IAAC;AAAA;AAAA,MACC,WAAU;AAAA,MACV,OAAO;AAAA,QACL,YAAY;AAAA,QACZ,SAAS;AAAA,MACX;AAAA,MAEA;AAAA,wBAAAA,MAAC,SAAI,WAAU,qCACZ;AAAA;AAAA,UACD,gBAAAD,KAAC,UAAK,WAAU,yCACb,eAAK,OACR;AAAA,WACF;AAAA,QACA,gBAAAA;AAAA,UAAC;AAAA;AAAA,YACC,SAAS;AAAA,YACR,GAAG;AAAA,YACJ,WAAU;AAAA,YACV,OAAO;AAAA,cACL,SAAS;AAAA,cACT,cAAc;AAAA,cACd,YAAY,eAAe,0BAA0B;AAAA,YACvD;AAAA,YACA,OAAM;AAAA,YAEN,0BAAAA,KAAC,KAAE,MAAM,IAAI,WAAU,iBAAgB;AAAA;AAAA,QACzC;AAAA;AAAA;AAAA,EACF;AAEJ;AAEA,SAAS,kBAAkB;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AACF,GAIG;AACD,SACE,gBAAAA;AAAA,IAAC;AAAA;AAAA,MACC,WAAU;AAAA,MACV,OAAO,EAAE,cAAc,KAAK,aAAa,SAAS,YAAY;AAAA,MAE7D,gBAAM,KAAK,EAAE,QAAQ,MAAM,GAAG,CAAC,GAAG,MACjC,gBAAAA;AAAA,QAAC;AAAA;AAAA,UAEC,WAAU;AAAA,UACV,OAAO;AAAA,YACL,OAAO,MAAM,QAAQ,KAAK;AAAA,YAC1B,YAAY,MAAM,QAAQ,KAAK,SAAS,KAAK;AAAA,YAC7C,SAAS,MAAM,QAAQ,IAAI;AAAA,UAC7B;AAAA;AAAA,QANK;AAAA,MAOP,CACD;AAAA;AAAA,EACH;AAEJ;AAEA,SAAS,cAAc;AAAA,EACrB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAQG;AACD,QAAM,WAAW,KAAK,YAAY,CAAC;AACnC,QAAM,aAAa,KAAK,cAAc,CAAC;AACvC,QAAM,UAAU,SAAS,SAAS,KAAK,WAAW,SAAS;AAC3D,QAAM,YAAY,QAAQ,KAAK,MAAM;AACrC,QAAM,cAAc,QAAQ,QAAQ;AAEpC,QAAM,QAAQ,CAAC,YAAsB,UAAU,MAAM;AACrD,QAAM,UAAU,CAAC,GAAW,OAAe,OACzC,IAAI,QAAQ,IAAI,KAAK;AACvB,SACE,gBAAAC,MAAA,YACE;AAAA,oBAAAD;AAAA,MAAC;AAAA;AAAA,QACC,WAAU;AAAA,QACV,OAAO;AAAA,UACL,OAAO,KAAK;AAAA,UACZ,SAAS,mBAAmB,MAAM;AAAA,UAClC,cAAc,MAAM,WAAW,aAAa,WAAW;AAAA,QACzD;AAAA,QAEC;AAAA;AAAA,IACH;AAAA,IAEC,SAAS,SAAS,IACjB,gBAAAA,KAAC,QAAG,OAAO,EAAE,cAAc,MAAM,aAAa,WAAW,EAAE,GACxD,mBAAS,IAAI,CAAC,GAAG,MAChB,gBAAAC;AAAA,MAAC;AAAA;AAAA,QAEC,WAAU;AAAA,QACV,OAAO,EAAE,cAAc,QAAQ,GAAG,SAAS,QAAQ,CAAC,EAAE;AAAA,QAEtD;AAAA,0BAAAD,KAAC,UAAK,WAAU,iBAAgB,OAAO,EAAE,OAAO,KAAK,OAAO,GACzD,YAAE,MACL;AAAA,UACA,gBAAAC,MAAC,UAAK,OAAO,EAAE,OAAO,KAAK,SAAS,GAAG;AAAA;AAAA,YAAI,EAAE;AAAA,aAAO;AAAA;AAAA;AAAA,MAP/C;AAAA,IAQP,CACD,GACH,IAEA,WAAW,SAAS,KAClB,gBAAAD,KAAC,QAAG,OAAO,EAAE,cAAc,MAAM,aAAa,WAAW,EAAE,GACxD,qBAAW,IAAI,CAAC,GAAG,MAClB,gBAAAC;AAAA,MAAC;AAAA;AAAA,QAEC,WAAU;AAAA,QACV,OAAO;AAAA,UACL,OAAO,KAAK;AAAA,UACZ,aAAa;AAAA,UACb,cAAc,QAAQ,GAAG,WAAW,QAAQ,CAAC;AAAA,QAC/C;AAAA,QAEA;AAAA,0BAAAD;AAAA,YAAC;AAAA;AAAA,cACC,WAAU;AAAA,cACV,OAAO,EAAE,OAAO,KAAK,UAAU;AAAA,cAChC;AAAA;AAAA,UAED;AAAA,UACC;AAAA;AAAA;AAAA,MAdI;AAAA,IAeP,CACD,GACH;AAAA,IAIH,KAAK,UACJ,gBAAAC;AAAA,MAAC;AAAA;AAAA,QACC,MAAM,KAAK,OAAO;AAAA,QAClB,QAAQ,KAAK,OAAO,WAAW,WAAW;AAAA,QAC1C,KAAK,KAAK,OAAO,WAAW,wBAAwB;AAAA,QACpD,WAAU;AAAA,QACV,OAAO;AAAA,UACL,YAAY,GAAG,KAAK,MAAM;AAAA,UAC1B,OAAO,KAAK;AAAA,UACZ,SAAS;AAAA,UACT,cAAc,MAAM,WAAW;AAAA,QACjC;AAAA,QAEC;AAAA,eAAK,OAAO;AAAA,UACZ,KAAK,OAAO,WACX,gBAAAD,KAAC,gBAAa,MAAM,IAAI,IAExB,gBAAAA,KAAC,cAAW,MAAM,IAAI;AAAA;AAAA;AAAA,IAE1B;AAAA,IAGD;AAAA,KACH;AAEJ;AAEA,SAAS,gBAAgB;AAAA,EACvB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAOG;AACD,QAAM,CAAC,aAAa,SAAS,IAAI,SAAS;AAC1C,SACE,gBAAAC;AAAA,IAAC;AAAA;AAAA,MACC,WAAU;AAAA,MACV,OAAO,EAAE,WAAW,KAAK,aAAa,SAAS,YAAY;AAAA,MAE3D;AAAA,wBAAAD;AAAA,UAAC;AAAA;AAAA,YACC,SAAS;AAAA,YACT,WAAU;AAAA,YACV,OAAO,EAAE,OAAO,KAAK,WAAW,UAAU,IAAI,YAAY,IAAI;AAAA,YAC/D;AAAA;AAAA,QAED;AAAA,QACA,gBAAAC,MAAC,SAAI,WAAU,2BACZ;AAAA,WAAC,WACA,gBAAAA;AAAA,YAAC;AAAA;AAAA,cACC,SAAS;AAAA,cACT,WAAU;AAAA,cACV,OAAO;AAAA,gBACL,OAAO,KAAK;AAAA,gBACZ,QAAQ,KAAK;AAAA,gBACb,SAAS;AAAA,gBACT,cAAc;AAAA,gBACd,UAAU;AAAA,gBACV,YAAY;AAAA,cACd;AAAA,cAEA;AAAA,gCAAAD,KAAC,eAAY,MAAM,IAAI;AAAA,gBAAE;AAAA;AAAA;AAAA,UAE3B;AAAA,UAEF,gBAAAC;AAAA,YAAC;AAAA;AAAA,cACC,SAAS;AAAA,cACR,GAAG;AAAA,cACJ,WAAU;AAAA,cACV,OAAO;AAAA,gBACL,YAAY;AAAA,gBACZ,OAAO;AAAA,gBACP,SAAS;AAAA,gBACT,cAAc;AAAA,gBACd,UAAU;AAAA,gBACV,YAAY;AAAA,gBACZ,SAAS,cAAc,MAAM;AAAA,cAC/B;AAAA,cAEC;AAAA,yBAAS,SAAS;AAAA,gBAClB,CAAC,UAAU,gBAAAD,KAAC,gBAAa,MAAM,IAAI;AAAA;AAAA;AAAA,UACtC;AAAA,WACF;AAAA;AAAA;AAAA,EACF;AAEJ;;;ACjoBA,SAAS,aAAa,YAAAK,iBAAgB;;;ACC/B,SAAS,oBACd,OACA,eACQ;AACR,MAAI,CAAC,cAAe,QAAO;AAC3B,QAAM,QAAQ,MAAM,UAAU,OAAK,EAAE,OAAO,aAAa;AACzD,SAAO,UAAU,KAAK,IAAI;AAC5B;AAEO,SAAS,UACd,OACA,SACmB;AACnB,MAAI,WAAW,MAAM,SAAS,GAAG;AAC/B,WAAO,EAAE,OAAO,SAAS,WAAW,KAAK;AAAA,EAC3C;AACA,SAAO,EAAE,OAAO,UAAU,GAAG,WAAW,MAAM;AAChD;AAEO,SAAS,UAAU,SAAyB;AACjD,SAAO,KAAK,IAAI,GAAG,UAAU,CAAC;AAChC;AAEO,SAAS,UACd,OACA,QACA,SACQ;AACR,QAAM,SAAS,MAAM,UAAU,OAAK,EAAE,OAAO,MAAM;AACnD,SAAO,WAAW,KAAK,UAAU;AACnC;;;ADCO,SAAS,QACd,OACA,OAAuB,CAAC,GACT;AACf,MAAI,MAAM,WAAW,GAAG;AACtB,UAAM,IAAI,MAAM,oCAAoC;AAAA,EACtD;AAEA,QAAM,CAAC,OAAO,QAAQ,IAAIC;AAAA,IAAS,MACjC,oBAAoB,OAAO,KAAK,aAAa;AAAA,EAC/C;AAEA,QAAM,OAAO,YAAY,MAAM;AAC7B,UAAM,SAAS,UAAU,OAAO,KAAK;AACrC,QAAI,OAAO,WAAW;AACpB,WAAK,aAAa;AAClB;AAAA,IACF;AACA,aAAS,OAAO,KAAK;AACrB,SAAK,eAAe,MAAM,OAAO,KAAK,GAAG,OAAO,KAAK;AAAA,EAEvD,GAAG,CAAC,OAAO,KAAK,CAAC;AAEjB,QAAM,OAAO,YAAY,MAAM;AAC7B,UAAM,SAAS,UAAU,KAAK;AAC9B,QAAI,WAAW,MAAO;AACtB,aAAS,MAAM;AACf,SAAK,eAAe,MAAM,MAAM,GAAG,MAAM;AAAA,EAE3C,GAAG,CAAC,OAAO,KAAK,CAAC;AAEjB,QAAM,OAAO;AAAA,IACX,CAAC,WAAmB;AAClB,YAAM,SAAS,UAAU,OAAO,QAAQ,KAAK;AAC7C,UAAI,WAAW,MAAO;AACtB,eAAS,MAAM;AACf,WAAK,eAAe,MAAM,MAAM,GAAG,MAAM;AAAA,IAE3C;AAAA,IACA,CAAC,OAAO,KAAK;AAAA,EACf;AAEA,QAAM,UAAU,YAAY,MAAM;AAChC,SAAK,YAAY;AAAA,EAEnB,GAAG,CAAC,CAAC;AAEL,SAAO;AAAA,IACL,MAAM,MAAM,KAAK;AAAA,IACjB;AAAA,IACA,OAAO,MAAM;AAAA,IACb,SAAS,UAAU;AAAA,IACnB,QAAQ,UAAU,MAAM,SAAS;AAAA,IACjC;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;;;AExFA,SAAS,aAAAC,YAAW,UAAAC,SAAQ,YAAAC,iBAAgC;AAC5D,SAAS,YAAAC,iBAAgB;AAuBhB,gBAAAC,YAAA;AAJF,SAAS,YAAY,OAAyB;AACnD,QAAM,EAAE,OAAO,IAAI,aAAa;AAChC,MAAI,CAAC,OAAQ,QAAO;AAEpB,SAAO,gBAAAA,KAAC,kBAA+B,MAAM,QAAS,GAAG,SAA7B,OAAO,EAA6B;AAClE;AAEA,SAAS,eAAe;AAAA,EACtB;AAAA,EACA;AAAA,EACA;AACF,GAAgD;AAC9C,QAAM,EAAE,QAAQ,IAAI,aAAa;AACjC,QAAM,EAAE,KAAK,IAAI,gBAAgB;AACjC,QAAM,EAAE,OAAO,UAAU,OAAO,IAAI,gBAAgB,KAAK,EAAE;AAG3D,QAAM,UAAUC,QAAO,KAAK;AAC5B,EAAAC,WAAU,MAAM;AACd,QAAI,UAAU,aAAa,QAAQ,QAAS;AAC5C,YAAQ,UAAU;AAClB,QAAI,eAAe,KAAK,WAAW,EAAE,OAAO,SAAS,CAAC,GAAG;AACvD,WAAK,EAAE,SAAS,MAAM,CAAC;AAAA,IACzB;AAAA,EACF,GAAG,CAAC,OAAO,UAAU,KAAK,WAAW,IAAI,CAAC;AAE1C,MAAI,CAAC,WAAW,UAAU,UAAW,QAAO;AAE5C,SACE,gBAAAF;AAAA,IAAC;AAAA;AAAA,MACC,OAAO,KAAK;AAAA,MACZ,eAAe,QAAQ,UAAU,OAAO,SAAS;AAAA,MACjD,YAAY;AAAA,MACZ,KAAK,OAAO,gBAAAA,KAAC,cAAW;AAAA,MACxB;AAAA;AAAA,EACF;AAEJ;AAEA,SAAS,aAAa;AACpB,SACE,gBAAAA;AAAA,IAAC;AAAA;AAAA,MACC,WAAU;AAAA,MACV,OAAO;AAAA,QACL,YAAY;AAAA,MACd;AAAA,MAEA,0BAAAA,KAACG,WAAA,EAAS,WAAU,sBAAqB;AAAA;AAAA,EAC3C;AAEJ;AAEA,SAAS,WAAW;AAAA,EAClB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAMG;AACD,QAAM,EAAE,MAAM,IAAI,gBAAgB;AAClC,QAAM,UAAU,gBAAgB;AAChC,QAAM,CAAC,iBAAiB,kBAAkB,IAAIC,UAE5C,CAAC,CAAC;AACJ,QAAM,CAAC,eAAe,gBAAgB,IAAIA,UAAwB,IAAI;AACtE,QAAM,YAAYH,QAAO,oBAAI,IAAY,CAAC;AAG1C,QAAM,gBAAgBA;AAAA,IACpB,MAAM,KAAK,OAAK,EAAE,OAAO,aAAa,GAAG,MAAM,MAAM,CAAC,EAAE;AAAA,EAC1D;AAEA,QAAM,mBAAmB,CAAC,SAAmB;AAC3C,QAAI,UAAU,QAAQ,IAAI,KAAK,EAAE,EAAG;AACpC,cAAU,QAAQ,IAAI,KAAK,EAAE;AAC7B,qBAAiB,KAAK,EAAE;AACxB,YAAQ,IAAI,EACT,KAAK,UAAQ,mBAAmB,WAAS,EAAE,GAAG,MAAM,CAAC,KAAK,EAAE,GAAG,KAAK,EAAE,CAAC,EAGvE,MAAM,MAAM;AAAA,IAAC,CAAC,EACd,QAAQ,MAAM,iBAAiB,SAAQ,QAAQ,KAAK,KAAK,OAAO,GAAI,CAAC;AAAA,EAC1E;AAEA,QAAM,OAAO,QAAQ,OAAO;AAAA,IAC1B;AAAA,IACA,cAAc,UAAQ;AACpB,oBAAc,UAAU,KAAK;AAC7B,WAAK,WAAW,EAAE,QAAQ,eAAe,YAAY,KAAK,GAAG,CAAC;AAC9D,uBAAiB,IAAI;AAAA,IACvB;AAAA,IACA,YAAY,MAAM;AAChB,WAAK,WAAW,EAAE,QAAQ,QAAQ,YAAY,KAAK,CAAC;AACpD,YAAM;AAAA,IACR;AAAA,IACA,WAAW,MAAM;AACf,WAAK,WAAW;AAAA,QACd,QAAQ;AAAA,QACR,YAAY,cAAc;AAAA,MAC5B,CAAC;AACD,YAAM;AAAA,IACR;AAAA,EACF,CAAC;AAKD,EAAAC,WAAU,MAAM;AACd,qBAAiB,KAAK,IAAI;AAAA,EAE5B,GAAG,CAAC,CAAC;AAEL,SACE,gBAAAF;AAAA,IAAC;AAAA;AAAA,MACC,MAAM,KAAK;AAAA,MACX,OAAO,KAAK;AAAA,MACZ,OAAO,KAAK;AAAA,MACZ,SAAS,KAAK;AAAA,MACd,QAAQ,KAAK;AAAA,MACb,QAAQ,KAAK;AAAA,MACb,QAAQ,KAAK;AAAA,MACb,WAAW,KAAK;AAAA,MAChB,WAAW,gBAAgB,KAAK,KAAK,EAAE,KAAK,KAAK,KAAK;AAAA,MACtD,kBAAkB,kBAAkB,KAAK,KAAK;AAAA,MAC9C;AAAA,MACA;AAAA;AAAA,EACF;AAEJ;","names":["jsx","useEffect","useEffect","useRef","useState","jsx","jsxs","useState","useRef","useEffect","useState","useState","useEffect","useRef","useState","Sparkles","jsx","useRef","useEffect","Sparkles","useState"]}
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  OpiTourHost,
3
3
  TakeTheTourButton
4
- } from "./chunk-LUQAR6NB.js";
4
+ } from "./chunk-BG4DVZAY.js";
5
5
  import {
6
6
  platformUrl,
7
7
  portalUrl
@@ -1249,4 +1249,4 @@ export {
1249
1249
  policyDeploymentConfig,
1250
1250
  SERVICE_CONFIGS
1251
1251
  };
1252
- //# sourceMappingURL=chunk-YCTIC6XW.js.map
1252
+ //# sourceMappingURL=chunk-HWPEU5YY.js.map
@@ -148,6 +148,20 @@ interface ViewOnlyBadgeProps {
148
148
  }
149
149
  declare function ViewOnlyBadge({ label, size, className, }: ViewOnlyBadgeProps): React.JSX.Element;
150
150
 
151
+ type AcceptanceStatus = "pending" | "accepted" | "rejected";
152
+ interface AcceptanceBadgeProps {
153
+ status: AcceptanceStatus;
154
+ /** True when it was accepted because the 24 hours ran out. Accepted only. */
155
+ autoAccepted?: boolean;
156
+ /** Pending only: when it will be accepted automatically. */
157
+ deadlineAt?: Date | string;
158
+ /** Size variant (default: "sm") */
159
+ size?: "sm" | "md";
160
+ /** CSS class */
161
+ className?: string;
162
+ }
163
+ declare function AcceptanceBadge({ status, autoAccepted, deadlineAt, size, className, }: AcceptanceBadgeProps): React.JSX.Element;
164
+
151
165
  type ExportFormat = "csv" | "xlsx";
152
166
  interface ExportResult {
153
167
  /** File contents to save. */
@@ -175,4 +189,4 @@ interface ExportDialogProps {
175
189
  }
176
190
  declare function ExportDialog({ open, onOpenChange, title, description, onExport, formats, }: ExportDialogProps): React.JSX.Element;
177
191
 
178
- export { AIChatBox, type AIChatBoxProps, type ChatMessage, ErrorBoundary, type ErrorBoundaryProps, ExportDialog, type ExportDialogProps, type ExportFormat, type ExportResult, ManusDialog, type ManusDialogProps, MapView, type MapViewProps, NotFoundPage, type NotFoundPageProps, ViewOnlyBadge, type ViewOnlyBadgeProps };
192
+ export { AIChatBox, type AIChatBoxProps, AcceptanceBadge, type AcceptanceBadgeProps, type AcceptanceStatus, type ChatMessage, ErrorBoundary, type ErrorBoundaryProps, ExportDialog, type ExportDialogProps, type ExportFormat, type ExportResult, ManusDialog, type ManusDialogProps, MapView, type MapViewProps, NotFoundPage, type NotFoundPageProps, ViewOnlyBadge, type ViewOnlyBadgeProps };