@pablo2410/shared-ui 0.26.0 → 0.27.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  import * as React from 'react';
2
2
  import React__default, { ReactNode, CSSProperties } from 'react';
3
- import { C as CrossServiceLink } from './crossNav-Bz0RXU_F.js';
3
+ import { C as CrossServiceLink } from './crossNav-DBX4Eyx7.js';
4
4
 
5
5
  /**
6
6
  * SharedSidebar — Config-driven sidebar component for all Oplytics subdomains.
@@ -15,7 +15,6 @@ var SERVICE_CATALOG = [
15
15
  description: "Hoshin Kanri and X-matrix planning. Cascade strategic objectives through every level of your organisation with live tracking and bowling charts.",
16
16
  icon: "Target",
17
17
  accentColor: "#F59E0B",
18
- status: "live",
19
18
  category: "core",
20
19
  subdomainLabel: "policy",
21
20
  onboarding: {
@@ -50,7 +49,6 @@ var SERVICE_CATALOG = [
50
49
  description: "Digital SQDCP boards that drive daily accountability. Safety, Quality, Delivery, Cost, and People metrics at a glance for every team.",
51
50
  icon: "LayoutGrid",
52
51
  accentColor: "#1DB8CE",
53
- status: "live",
54
52
  category: "core",
55
53
  subdomainLabel: "sqdcp",
56
54
  onboarding: {
@@ -58,7 +56,6 @@ var SERVICE_CATALOG = [
58
56
  highlights: [
59
57
  "Rooms \u2014 one per team, tiered (Team Huddle \u2192 Area/Plant Review \u2192 Monthly Review), each feeding the one above.",
60
58
  "Data Entry \u2014 where actuals get logged against each pillar's metrics.",
61
- "Safety Manager / Quality Manager tabs \u2014 the deeper pillar-specific views inside SQDCP.",
62
59
  "If AI Facilitator is on, Opi runs the huddle itself \u2014 drafts the line for each red pillar, opens the action review by name."
63
60
  ],
64
61
  features: [
@@ -69,10 +66,6 @@ var SERVICE_CATALOG = [
69
66
  {
70
67
  name: "Data Entry",
71
68
  detail: "Where actuals get logged against each pillar's metrics \u2014 this is the one habit that makes everything else here true; skip it and the board just goes stale, not red."
72
- },
73
- {
74
- name: "Safety & Quality Manager",
75
- detail: "The deeper pillar-specific views inside SQDCP \u2014 ISO-aligned gap analysis for Safety and Quality, one level below the daily board for when a red needs more than a glance."
76
69
  }
77
70
  ],
78
71
  firstAction: { label: "Open Rooms", path: "/" },
@@ -86,7 +79,6 @@ var SERVICE_CATALOG = [
86
79
  description: "Monitor Overall Equipment Effectiveness in real-time. Track availability, performance, and quality metrics across your entire production line.",
87
80
  icon: "Gauge",
88
81
  accentColor: "#8C34E9",
89
- status: "live",
90
82
  category: "core",
91
83
  subdomainLabel: "oeemanager"
92
84
  // No `onboarding` block yet — held back for a later cohort (2026-09-11 decision).
@@ -98,7 +90,6 @@ var SERVICE_CATALOG = [
98
90
  description: "Capture, assign, and track corrective and preventive actions from any source. Ensure nothing falls through the cracks.",
99
91
  icon: "ClipboardCheck",
100
92
  accentColor: "#22C55E",
101
- status: "live",
102
93
  category: "core",
103
94
  subdomainLabel: "actionmanager",
104
95
  onboarding: {
@@ -125,62 +116,11 @@ var SERVICE_CATALOG = [
125
116
  firstAction: { label: "Open Board", path: "/board" },
126
117
  emptyStateHint: "Zero actions yet \u2014 say this is normal on day one, actions will start showing up as SQDCP and Policy Deployment generate them, rather than describing an empty board as broken."
127
118
  }
128
- },
129
- {
130
- slug: "connect",
131
- name: "OplyticsConnect",
132
- tagline: "Integrate machines, sensors, and systems",
133
- description: "Industrial IoT connectivity layer. Connect PLCs, sensors, and legacy systems to the Oplytics platform with zero-code configuration.",
134
- icon: "Plug",
135
- accentColor: "#1DB8CE",
136
- status: "in-development",
137
- category: "core",
138
- subdomainLabel: "connect"
139
- },
140
- // ─── HUB SERVICES ───
141
- {
142
- slug: "safety-manager",
143
- name: "Safety Manager",
144
- tagline: "Proactive safety culture, powered by data",
145
- description: "Incident reporting, hazard tracking, safety observations, and compliance management in one unified platform.",
146
- icon: "Shield",
147
- accentColor: "#EF4444",
148
- status: "coming-soon",
149
- category: "hub",
150
- subdomainLabel: "safetymanager"
151
- },
152
- {
153
- slug: "quality-manager",
154
- name: "Quality Manager",
155
- tagline: "End-to-end quality assurance",
156
- description: "Non-conformance tracking, CAPA management, audit scheduling, and quality metrics dashboards.",
157
- icon: "CheckCircle",
158
- accentColor: "#22C55E",
159
- status: "coming-soon",
160
- category: "hub",
161
- subdomainLabel: null
162
- },
163
- {
164
- slug: "certification-manager",
165
- name: "Certification Manager",
166
- tagline: "Stay audit-ready, always",
167
- description: "Manage ISO, IATF, and other certification requirements. Document control, audit trails, and compliance tracking.",
168
- icon: "Award",
169
- accentColor: "#F97316",
170
- status: "coming-soon",
171
- category: "hub",
172
- subdomainLabel: null
173
119
  }
174
120
  ];
175
121
  var coreServices = SERVICE_CATALOG.filter((s) => s.category === "core");
176
122
  var hubServices = SERVICE_CATALOG.filter((s) => s.category === "hub");
177
- var liveServices = SERVICE_CATALOG.filter((s) => s.status === "live");
178
- var inDevServices = SERVICE_CATALOG.filter(
179
- (s) => s.status === "in-development"
180
- );
181
- var comingSoonServices = SERVICE_CATALOG.filter(
182
- (s) => s.status === "coming-soon"
183
- );
123
+ var liveServices = SERVICE_CATALOG;
184
124
  var onboardingTourServices = SERVICE_CATALOG.filter(
185
125
  (s) => s.onboarding !== void 0
186
126
  );
@@ -211,7 +151,7 @@ function buildCrossServiceLinks(options) {
211
151
  const granted = new Set(grantedSlugs.map(normalizeServiceSlug));
212
152
  const current = normalizeServiceSlug(currentSlug);
213
153
  return SERVICE_CATALOG.filter(
214
- (svc) => svc.status === "live" && svc.subdomainLabel !== null && svc.slug !== current && (isAdmin || granted.has(svc.slug))
154
+ (svc) => svc.subdomainLabel !== null && svc.slug !== current && (isAdmin || granted.has(svc.slug))
215
155
  ).map((svc) => ({
216
156
  slug: svc.slug,
217
157
  name: svc.name,
@@ -226,8 +166,6 @@ export {
226
166
  coreServices,
227
167
  hubServices,
228
168
  liveServices,
229
- inDevServices,
230
- comingSoonServices,
231
169
  onboardingTourServices,
232
170
  getServiceBySlug,
233
171
  getServiceUrl,
@@ -236,4 +174,4 @@ export {
236
174
  normalizeServiceSlug,
237
175
  buildCrossServiceLinks
238
176
  };
239
- //# sourceMappingURL=chunk-MGTOQY6O.js.map
177
+ //# sourceMappingURL=chunk-45ZQDLPA.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/services/catalog.ts","../src/services/crossNav.ts"],"sourcesContent":["/**\n * Shared Service Catalog — Single source of truth for all Oplytics.digital services.\n *\n * Consumed by:\n * - oplytics-subdomains (ServiceHub.tsx) — service cards, access gating\n * - oplytics-marketing-site (services.ts) — marketing pages, hero sections\n *\n * Each consumer extends with context-specific fields locally:\n * - Platform adds: route, external URL, React icon component\n * - Marketing adds: heroImage, demoImage, problem, howItWorks, results, aiFeatures\n */\n\nimport { platformUrl } from \"./platformDomain\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport type ServiceCategory = \"core\" | \"hub\";\n\nexport interface ServiceDefinition {\n /** Unique identifier used for DB lookups, URL slugs, and cross-referencing */\n slug: string;\n /** Display name shown in cards, headers, and navigation */\n name: string;\n /** Short one-line value proposition */\n tagline: string;\n /** Longer description (1-2 sentences) for cards and summaries */\n description: string;\n /** Lucide icon name (string) — consumers resolve to component locally */\n icon: string;\n /** Hex accent color for cards, badges, and theming */\n accentColor: string;\n /** Core = standalone product, Hub = extension/add-on module */\n category: ServiceCategory;\n /**\n * Subdomain label on the platform domain (e.g. \"sqdcp\") — null if hosted\n * on portal. Full URLs are built via getServiceUrl(), which resolves the\n * platform domain at runtime (see ./platformDomain).\n */\n subdomainLabel: string | null;\n /**\n * Scripted content for the Opi onboarding tour (see @pablo2410/shared-ui/tour).\n * Optional and deliberately sparse — only services with a real, verified\n * first-run path should have this filled in. A service with no `onboarding`\n * block is skipped by the tour entirely rather than shown with placeholder\n * copy. Marketing/other consumers ignore this field.\n */\n onboarding?: ServiceOnboardingContent;\n}\n\nexport interface ServiceOnboardingContent {\n /** Opi's scripted opening line for this step — the always-available fallback if the live LLM narration call fails or AI is gated off. */\n hook: string;\n /** 2-4 short bullets: what's actually here, in plain language. Superseded\n * by `features` for display when both are present — kept for back-compat\n * and as the fallback shape for any service not yet upgraded. */\n highlights: string[];\n /**\n * Named feature call-outs, richer than a flat bullet — one sentence each\n * on what it does AND why the user should care, not just what it's\n * called. Convention (not enforced): aim for 3 per service. Each `name`\n * doubles as the natural spotlight-target label a later in-app tour\n * (inside the service's own subdomain, once the tour follows the user\n * in) can reuse — this is deliberately the same data written once.\n */\n features?: ServiceOnboardingFeature[];\n /** The one thing the step ends on. */\n firstAction: {\n label: string;\n /** Path relative to the service's own root (e.g. \"/xmatrix\"). */\n path: string;\n };\n /** What Opi should say instead when the service has no data yet for this enterprise (empty-state framing), rather than pointing at the normal firstAction. */\n emptyStateHint?: string;\n}\n\nexport interface ServiceOnboardingFeature {\n /** Short feature name, e.g. \"X-Matrix\". */\n name: string;\n /** One deeper sentence — what it does and why the user should care. */\n detail: string;\n}\n\n// ---------------------------------------------------------------------------\n// Catalog\n// ---------------------------------------------------------------------------\n\nexport const SERVICE_CATALOG: ServiceDefinition[] = [\n // ─── CORE SERVICES ───\n {\n slug: \"policy-deployment\",\n name: \"Policy Deployment\",\n tagline: \"Align strategy to execution\",\n description:\n \"Hoshin Kanri and X-matrix planning. Cascade strategic objectives through every level of your organisation with live tracking and bowling charts.\",\n icon: \"Target\",\n accentColor: \"#F59E0B\",\n category: \"core\",\n subdomainLabel: \"policy\",\n onboarding: {\n hook: \"This is where your strategy becomes a plan everyone can see. Long-term goals break into this year's objectives, objectives break into projects, and projects carry the metrics that prove it's working.\",\n highlights: [\n \"X-Matrix — the rollup view: goals, objectives, and projects, with dots showing which project moves which goal.\",\n \"Catchball — where the plan actually gets built: negotiate objectives and projects up and down the org, add a metric to a project.\",\n \"Actions — every task that comes out of the plan, tracked to closure.\",\n ],\n features: [\n {\n name: \"X-Matrix\",\n detail:\n \"The whole plan at a glance — goals, objectives, and projects on one grid, with dots showing exactly which project moves which goal, so nobody's guessing why a project exists.\",\n },\n {\n name: \"Catchball\",\n detail:\n \"Where the plan actually gets negotiated — objectives and projects get pitched up and down the org until everyone agrees, and that's also where a project picks up the metric it'll be measured on.\",\n },\n {\n name: \"Actions\",\n detail:\n \"Every task the plan generates, tracked to closure by pillar — so a strategic project never quietly stalls because nobody was assigned the next step.\",\n },\n ],\n firstAction: { label: \"Open X-Matrix\", path: \"/xmatrix\" },\n emptyStateHint:\n \"No plan yet for this enterprise — say so plainly and point at Catchball's \\\"Create your first plan\\\" instead of X-Matrix's empty state.\",\n },\n },\n {\n slug: \"sqdcp\",\n name: \"SQDCP Dashboard\",\n tagline: \"Daily management boards, digitised\",\n description:\n \"Digital SQDCP boards that drive daily accountability. Safety, Quality, Delivery, Cost, and People metrics at a glance for every team.\",\n icon: \"LayoutGrid\",\n accentColor: \"#1DB8CE\",\n category: \"core\",\n subdomainLabel: \"sqdcp\",\n onboarding: {\n hook: \"This is your daily board — Safety, Quality, Delivery, Cost, People, live. Every team has a room; the huddle happens here, and the numbers you log in Data Entry are what turn the board green or red.\",\n highlights: [\n \"Rooms — one per team, tiered (Team Huddle → Area/Plant Review → Monthly Review), each feeding the one above.\",\n \"Data Entry — where actuals get logged against each pillar's metrics.\",\n \"If AI Facilitator is on, Opi runs the huddle itself — drafts the line for each red pillar, opens the action review by name.\",\n ],\n features: [\n {\n name: \"Rooms\",\n detail:\n \"One room per team, tiered from Team Huddle up to Area/Plant Review to Monthly Review — each level feeds the one above, so a problem raised on the floor is visible all the way up without anyone re-typing it into a slide deck.\",\n },\n {\n name: \"Data Entry\",\n detail:\n \"Where actuals get logged against each pillar's metrics — this is the one habit that makes everything else here true; skip it and the board just goes stale, not red.\",\n },\n ],\n firstAction: { label: \"Open Rooms\", path: \"/\" },\n emptyStateHint:\n \"No rooms configured yet — this needs an admin to set up rooms first; point at Admin rather than Rooms.\",\n },\n },\n {\n slug: \"oee-manager\",\n name: \"OEE Manager\",\n tagline: \"Real-time equipment effectiveness tracking\",\n description:\n \"Monitor Overall Equipment Effectiveness in real-time. Track availability, performance, and quality metrics across your entire production line.\",\n icon: \"Gauge\",\n accentColor: \"#8C34E9\",\n category: \"core\",\n subdomainLabel: \"oeemanager\",\n // No `onboarding` block yet — held back for a later cohort (2026-09-11 decision).\n },\n {\n slug: \"action-manager\",\n name: \"Action Manager\",\n tagline: \"Track every action to closure\",\n description:\n \"Capture, assign, and track corrective and preventive actions from any source. Ensure nothing falls through the cracks.\",\n icon: \"ClipboardCheck\",\n accentColor: \"#22C55E\",\n category: \"core\",\n subdomainLabel: \"actionmanager\",\n onboarding: {\n hook: \"Every action from every service lands here — SQDCP huddles, Policy Deployment projects, safety reports, all in one place, tracked until someone closes it out.\",\n highlights: [\n \"Board — kanban view, the default way most people work actions day to day.\",\n \"List and Calendar — the same actions, sorted or scheduled instead.\",\n \"Improvement (A3) — structured 7-box root-cause work for the actions that need more than a quick fix.\",\n ],\n features: [\n {\n name: \"Board\",\n detail:\n \"The default working view — a kanban of every open action, so a team can see what's moving and what's stuck without opening a single report.\",\n },\n {\n name: \"List & Calendar\",\n detail:\n \"The same actions, just sorted or scheduled instead of columned — List for triaging a long backlog, Calendar for seeing what's due this week before it becomes overdue.\",\n },\n {\n name: \"Improvement (A3)\",\n detail:\n \"Structured 7-box root-cause work for the actions that keep coming back — Background through Follow-up, one page, so a real fix gets the same rigor a quick fix doesn't need.\",\n },\n ],\n firstAction: { label: \"Open Board\", path: \"/board\" },\n emptyStateHint:\n \"Zero actions yet — say this is normal on day one, actions will start showing up as SQDCP and Policy Deployment generate them, rather than describing an empty board as broken.\",\n },\n },\n];\n\n// ---------------------------------------------------------------------------\n// Derived helpers\n// ---------------------------------------------------------------------------\n\nexport const coreServices = SERVICE_CATALOG.filter(s => s.category === \"core\");\nexport const hubServices = SERVICE_CATALOG.filter(s => s.category === \"hub\");\n/** Every service in the catalog is live: an entry is only added once it ships. */\nexport const liveServices = SERVICE_CATALOG;\n/** Services with a scripted onboarding tour step, in catalog order. */\nexport const onboardingTourServices = SERVICE_CATALOG.filter(\n s => s.onboarding !== undefined\n);\n\nexport function getServiceBySlug(slug: string): ServiceDefinition | undefined {\n return SERVICE_CATALOG.find(s => s.slug === slug);\n}\n\nexport function getServiceUrl(service: ServiceDefinition): string | null {\n if (service.subdomainLabel) {\n return platformUrl(service.subdomainLabel);\n }\n return null;\n}\n","/**\n * crossNav — build the sidebar cross-service links (service → service jump\n * without going through the Portal Service Hub).\n *\n * Visibility mirrors the Portal ServiceHub rule: platform_admin and\n * enterprise_admin see every service; everyone else only sees the\n * services granted in the portal `user_services` table. The current service\n * never links to itself. Each link URL carries the caller's current\n * org-hierarchy selection (?oh=…) so the breadcrumb arrives pre-aligned.\n */\nimport type { OrgHierarchySelection } from \"../hierarchy/orgHierarchy\";\nimport { appendSelectionParam } from \"../hierarchy/selectionParams\";\nimport { SERVICE_CATALOG, getServiceUrl } from \"./catalog\";\n\n/** Roles that see every service regardless of individual grants. */\nexport const ADMIN_SEE_ALL_ROLES = [\n \"platform_admin\",\n \"enterprise_admin\",\n] as const;\n\n/**\n * Legacy grant slugs → canonical catalog slugs. Auto-provisioned accounts\n * were granted \"sqdcp-logic\" while the catalog slug is \"sqdcp\".\n */\nexport const SLUG_ALIASES: Record<string, string> = {\n \"sqdcp-logic\": \"sqdcp\",\n};\n\nexport function normalizeServiceSlug(slug: string): string {\n return SLUG_ALIASES[slug] ?? slug;\n}\n\n/** Display-ready cross-service link entry. */\nexport interface CrossServiceLink {\n slug: string;\n name: string;\n /** Lucide icon name from the service catalog (consumers resolve locally). */\n icon: string;\n accentColor: string;\n /** Absolute URL including the ?oh= hierarchy selection param. */\n url: string;\n}\n\nexport interface BuildCrossServiceLinksOptions {\n /** Catalog slug of the service doing the rendering (excluded from links). */\n currentSlug: string;\n /** Grant slugs from the portal (raw — aliases are normalized here). */\n grantedSlugs: string[];\n /** Portal role of the current user (drives the admin see-all rule). */\n role?: string | null;\n /** Current org-hierarchy selection to carry across (may be empty/null). */\n selection?: OrgHierarchySelection | null;\n}\n\nexport function buildCrossServiceLinks(\n options: BuildCrossServiceLinksOptions\n): CrossServiceLink[] {\n const { currentSlug, grantedSlugs, role, selection } = options;\n\n const isAdmin =\n !!role && (ADMIN_SEE_ALL_ROLES as readonly string[]).includes(role);\n const granted = new Set(grantedSlugs.map(normalizeServiceSlug));\n const current = normalizeServiceSlug(currentSlug);\n\n return SERVICE_CATALOG.filter(\n svc =>\n svc.subdomainLabel !== null &&\n svc.slug !== current &&\n (isAdmin || granted.has(svc.slug))\n ).map(svc => ({\n slug: svc.slug,\n name: svc.name,\n icon: svc.icon,\n accentColor: svc.accentColor,\n url: appendSelectionParam(getServiceUrl(svc)!, selection),\n }));\n}\n"],"mappings":"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","names":[]}
@@ -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