@syntrologie/adapt-reviews 2.55.1 → 2.56.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.
@@ -1,3 +1,7 @@
1
+ import {
2
+ external_exports
3
+ } from "./chunk-IGIU6S7P.js";
4
+
1
5
  // src/reviewDisplay.ts
2
6
  import { html, LitElement, nothing } from "lit";
3
7
  import { styleMap } from "lit/directives/style-map.js";
@@ -167,7 +171,7 @@ var SyntroReviews = class extends LitElement {
167
171
  const rootStyles = {
168
172
  display: "block",
169
173
  width: "100%",
170
- color: "var(--sc-tile-text, var(--sc-overlay-text-color, #111827))",
174
+ color: "var(--sc-tile-text-color, var(--sc-overlay-text-color, #111827))",
171
175
  fontFamily: "var(--sc-font-family, inherit)"
172
176
  };
173
177
  return html`
@@ -229,7 +233,7 @@ var SyntroReviews = class extends LitElement {
229
233
  _panelStyles() {
230
234
  return {
231
235
  boxSizing: "border-box",
232
- border: "1px solid var(--sc-tile-border-color, rgba(148, 163, 184, 0.24))",
236
+ border: "1px solid var(--sc-content-divider-color, rgba(148, 163, 184, 0.24))",
233
237
  borderRadius: "8px",
234
238
  background: "var(--sc-tile-background, #ffffff)",
235
239
  padding: "14px",
@@ -258,13 +262,13 @@ var SyntroReviews = class extends LitElement {
258
262
  display: "inline-flex",
259
263
  alignItems: "center",
260
264
  gap: "4px",
261
- color: "var(--sc-accent, #2563eb)",
265
+ color: "hsl(var(--sc-accent-color, 221 83% 53%))",
262
266
  whiteSpace: "nowrap"
263
267
  };
264
268
  }
265
269
  _reviewStyles() {
266
270
  return {
267
- borderTop: "1px solid var(--sc-tile-border-color, rgba(148, 163, 184, 0.2))",
271
+ borderTop: "1px solid var(--sc-content-divider-color, rgba(148, 163, 184, 0.2))",
268
272
  paddingTop: "10px"
269
273
  };
270
274
  }
@@ -280,7 +284,7 @@ var SyntroReviews = class extends LitElement {
280
284
  }
281
285
  _starsStyles() {
282
286
  return {
283
- color: "var(--sc-accent, #2563eb)",
287
+ color: "hsl(var(--sc-accent-color, 221 83% 53%))",
284
288
  fontSize: "13px",
285
289
  lineHeight: "18px",
286
290
  whiteSpace: "nowrap"
@@ -295,7 +299,7 @@ var SyntroReviews = class extends LitElement {
295
299
  }
296
300
  _metaStyles() {
297
301
  return {
298
- color: "var(--sc-muted-text, #64748b)",
302
+ color: "var(--sc-content-text-secondary-color, #64748b)",
299
303
  fontSize: "12px",
300
304
  lineHeight: "18px"
301
305
  };
@@ -303,7 +307,7 @@ var SyntroReviews = class extends LitElement {
303
307
  _stateStyles() {
304
308
  return {
305
309
  marginTop: "8px",
306
- color: "var(--sc-muted-text, #64748b)",
310
+ color: "var(--sc-content-text-secondary-color, #64748b)",
307
311
  fontSize: "14px",
308
312
  lineHeight: "20px"
309
313
  };
@@ -403,8 +407,8 @@ var FlipController = class {
403
407
  this.animating = false;
404
408
  this.dir = "in";
405
409
  this.settleTimer = null;
406
- this.onAnimationEnd = (e) => {
407
- if (e.animationName === `${this.keyframePrefix}-in` || e.animationName === `${this.keyframePrefix}-out`) {
410
+ this.onAnimationEnd = (e2) => {
411
+ if (e2.animationName === `${this.keyframePrefix}-in` || e2.animationName === `${this.keyframePrefix}-out`) {
408
412
  this.settle();
409
413
  }
410
414
  };
@@ -506,6 +510,542 @@ var FlipController = class {
506
510
  }
507
511
  };
508
512
 
513
+ // ../../../node_modules/@lit/context/lib/create-context.js
514
+ function n(n2) {
515
+ return n2;
516
+ }
517
+
518
+ // ../../sdk-contracts/dist/canvas-context.js
519
+ var canvasRuntimeContext = n("syntrologie:canvas-runtime");
520
+
521
+ // ../../sdk-contracts/dist/telemetry-events.js
522
+ var entry = (e2) => e2;
523
+ var TELEMETRY_EVENTS = {
524
+ // --- intervention metrics (April 2026) — wire names unchanged, all plan-scope
525
+ syntro_config_served: entry({
526
+ subject: "config",
527
+ verb: "served",
528
+ scope: "plan",
529
+ props: ["tiles", "actions"]
530
+ }),
531
+ syntro_intervention_seen: entry({
532
+ subject: "intervention",
533
+ verb: "seen",
534
+ scope: "plan",
535
+ props: ["intervention_id", "intervention_kind"]
536
+ }),
537
+ syntro_intervention_triggered: entry({
538
+ subject: "intervention",
539
+ verb: "triggered",
540
+ scope: "plan",
541
+ props: ["intervention_id", "intervention_kind"]
542
+ }),
543
+ syntro_intervention_interacted: entry({
544
+ subject: "intervention",
545
+ verb: "interacted",
546
+ scope: "plan",
547
+ props: ["intervention_id", "intervention_kind", "interaction_type"]
548
+ }),
549
+ // --- concierge (FEAT-1789150184) ---------------------------------------
550
+ syntro_concierge_opened: entry({
551
+ subject: "concierge",
552
+ verb: "opened",
553
+ scope: "open",
554
+ props: ["trigger", "page_path"]
555
+ // trigger: ConciergeOpenTrigger
556
+ }),
557
+ syntro_concierge_closed: entry({
558
+ subject: "concierge",
559
+ verb: "closed",
560
+ scope: "open",
561
+ props: ["duration_ms", "reason", "messages_sent", "tiles_dismissed", "pages_viewed"]
562
+ // reason: ConciergeCloseReason
563
+ }),
564
+ /**
565
+ * The shopper asked to leave the canvas by a link. Emitted on intent, before
566
+ * the host answers, so refused or denied opens are counted too (parity with
567
+ * syntro_concierge_closed reason navigation).
568
+ *
569
+ * On a merchant's own page a link tap closes the canvas, and
570
+ * `syntro_concierge_closed { reason: 'navigation' }` is the row that
571
+ * records it. Inside a chat app's card the frame is sandboxed: the canvas
572
+ * cannot navigate, does not close, and asks the host to open the link
573
+ * instead, so that close row never fires. This is its counterpart, and it
574
+ * carries the same `reason` so one query covers both surfaces.
575
+ */
576
+ syntro_link_opened: entry({
577
+ subject: "link",
578
+ verb: "opened",
579
+ scope: "open",
580
+ props: ["kind", "reason"]
581
+ // kind: LinkOpenKind, reason: ConciergeCloseReason
582
+ }),
583
+ /**
584
+ * A store's product page opened inside a chat-app card instead of leaving
585
+ * through the host (the card's product view). `product` is the store's
586
+ * handle, never the display name; a rail switch is a new open. Its
587
+ * counterpart when the shopper goes back to the conversation's picks is
588
+ * syntro_product_view_closed.
589
+ *
590
+ * Plan scope, like the card's own syntro_card_lifecycle_seen: an inline
591
+ * card holds an open scope only while the host has it fullscreen, and a
592
+ * view opened from a tile fires before the host answers that request (or
593
+ * after it refused), so an open-scope row here would be dropped. The core
594
+ * still stamps surface and open_id whenever an open is live.
595
+ */
596
+ syntro_product_view_opened: entry({
597
+ subject: "product_view",
598
+ verb: "opened",
599
+ scope: "plan",
600
+ props: ["product", "from"]
601
+ // from: ProductViewOpenSource
602
+ }),
603
+ syntro_product_view_closed: entry({
604
+ subject: "product_view",
605
+ verb: "closed",
606
+ scope: "plan",
607
+ props: ["product"]
608
+ }),
609
+ syntro_deck_paged: entry({
610
+ subject: "deck",
611
+ verb: "paged",
612
+ scope: "open",
613
+ props: ["from_index", "to_index", "method"]
614
+ // method: DeckPageMethod
615
+ }),
616
+ syntro_tile_dismissed: entry({
617
+ subject: "tile",
618
+ verb: "dismissed",
619
+ scope: "open",
620
+ props: ["tile_id", "tile_kind", "visible_ms"]
621
+ }),
622
+ // A tile's own data load finished: the network wait between the concierge
623
+ // mounting a tile and the shopper seeing its content (BUG-1784767817 was a
624
+ // 30 s reviews hang here). `outcome` is 'ok' | 'error'; `requested` is how
625
+ // many items the tile asked for, `received` how many came back.
626
+ syntro_tile_data_received: entry({
627
+ subject: "tile_data",
628
+ verb: "received",
629
+ scope: "open",
630
+ props: ["tile_id", "tile_kind", "total_ms", "outcome", "requested", "received"]
631
+ }),
632
+ // The three chat atoms are OPEN-scope: they carry `surface` / `open_id` and
633
+ // only make sense inside a concierge open. An INLINE chat bar (no concierge)
634
+ // still dispatches them on every turn, and the core drops each one and
635
+ // increments the `telemetry_dropped` health counter. That is EXPECTED on an
636
+ // inline-chat host page, not a bug — `telemetry_dropped` there mixes "real
637
+ // defect" with "inline chat has no open scope", so do not alert on it alone.
638
+ syntro_chat_message_sent: entry({
639
+ subject: "chat_message",
640
+ verb: "sent",
641
+ scope: "open",
642
+ props: ["turn", "chars", "via"]
643
+ // via: ChatSendVia
644
+ }),
645
+ syntro_chat_reply_received: entry({
646
+ subject: "chat_reply",
647
+ verb: "received",
648
+ scope: "open",
649
+ // mount_tools: the mount_* tool name of every mount this reply fired, in order (two product cards appear twice).
650
+ props: ["turn", "ttft_ms", "total_ms", "mounts", "cards", "mount_tools"]
651
+ }),
652
+ syntro_chat_interrupted: entry({
653
+ subject: "chat",
654
+ verb: "interrupted",
655
+ scope: "open",
656
+ props: ["turn", "after_ms"]
657
+ }),
658
+ syntro_chat_maximize_toggled: entry({
659
+ subject: "chat_maximize",
660
+ verb: "toggled",
661
+ scope: "open",
662
+ props: ["maximized"]
663
+ }),
664
+ syntro_chat_takeover_triggered: entry({
665
+ subject: "chat_takeover",
666
+ verb: "triggered",
667
+ scope: "open",
668
+ props: ["turn"]
669
+ }),
670
+ // --- search takeover (2026-09-17) ---------------------------------------
671
+ // All plan-scope: the search interceptor fires before any concierge open
672
+ // exists. No prop here ever carries query text (SEE the contract test's
673
+ // no-text-prop rule) — classification/signal_hits are the interceptor's
674
+ // own derived signals, never the string itself.
675
+ syntro_search_sent: entry({
676
+ subject: "search",
677
+ verb: "sent",
678
+ scope: "plan",
679
+ props: ["classification", "word_count", "signal_hits", "entry_point", "platform"]
680
+ // classification: SearchClassification, entry_point: SearchEntryPoint
681
+ }),
682
+ // `ms_to_skeleton` is intentionally the only timing prop: it is always
683
+ // knowable the instant the surface mounts. `ms_to_first_content` is NOT
684
+ // declared here (revision 1 defect) because a surface that never painted
685
+ // content still fires this event, and a required prop it cannot supply
686
+ // would make TelemetryCore.emit drop exactly the row worth measuring.
687
+ syntro_search_surface_seen: entry({
688
+ subject: "search_surface",
689
+ verb: "seen",
690
+ scope: "plan",
691
+ props: ["variant", "classification", "result_count", "ms_to_skeleton"]
692
+ // variant: SearchVariant, classification: SearchClassification
693
+ }),
694
+ syntro_search_result_tapped: entry({
695
+ subject: "search_result",
696
+ verb: "tapped",
697
+ scope: "plan",
698
+ props: ["variant", "region_id", "module", "position"]
699
+ // variant: SearchVariant
700
+ }),
701
+ syntro_search_followup_sent: entry({
702
+ subject: "search_followup",
703
+ verb: "sent",
704
+ scope: "plan",
705
+ props: ["delta_kind", "variant"]
706
+ // delta_kind: SearchDeltaKind, variant: SearchVariant
707
+ }),
708
+ // Declares `ms_since_boot` (not a "since surface mounted" timing prop) so a
709
+ // not-ready fallback the takeover never rendered is still measurable —
710
+ // revision 1 emitted nothing on this path.
711
+ syntro_search_native_served: entry({
712
+ subject: "search_native",
713
+ verb: "served",
714
+ scope: "plan",
715
+ props: ["reason", "platform", "ms_since_boot"]
716
+ // reason: SearchFallbackReason
717
+ }),
718
+ syntro_search_takeover_dismissed: entry({
719
+ subject: "search_takeover",
720
+ verb: "dismissed",
721
+ scope: "plan",
722
+ props: ["variant", "reason"]
723
+ // variant: SearchVariant, reason: SearchDismissReason
724
+ }),
725
+ // --- chat-app card lifecycle (2026-09-24) --------------------------------
726
+ // One row per stage a card reached, from `reportCardStage` in
727
+ // card-lifecycle.ts. Plan scope: most stages happen before a config is
728
+ // served and none happen inside a concierge open. `stage` is a
729
+ // CardLifecycleStage; `since_shell_ms` is the card shell's own clock, so a
730
+ // card's rows order themselves from the first byte of the page. The same
731
+ // stages reach the connector through the shell's beacon, which is the only
732
+ // record of a card whose SDK never booted.
733
+ syntro_card_lifecycle_seen: entry({
734
+ subject: "card_lifecycle",
735
+ verb: "seen",
736
+ scope: "plan",
737
+ props: ["stage", "since_shell_ms"]
738
+ // stage: CardLifecycleStage
739
+ })
740
+ };
741
+ var TELEMETRY_EVENT_NAMES = Object.keys(TELEMETRY_EVENTS);
742
+ var TELEMETRY_DOM_EVENT = "syntro:telemetry";
743
+
744
+ // ../../sdk-contracts/dist/card-lifecycle.js
745
+ var MCP_APP_SURFACE_ATTRIBUTES = Object.freeze({
746
+ surface_type: "mcp-app",
747
+ surface: "personal_agent"
748
+ });
749
+
750
+ // ../../sdk-contracts/dist/detector-events.js
751
+ var DETECTOR_EVENT_NAMES = [
752
+ "ui.hover",
753
+ "ui.idle",
754
+ "ui.hesitate",
755
+ "ui.rage_click",
756
+ "ui.scroll_thrash",
757
+ "ui.focus_bounce"
758
+ ];
759
+ var CANONICAL_BUS_EVENT_NAMES = [
760
+ ...DETECTOR_EVENT_NAMES,
761
+ "nav.section_viewed",
762
+ "nav.scroll_depth"
763
+ ];
764
+
765
+ // ../../sdk-contracts/dist/routes.js
766
+ var RESERVED_BYTES = /* @__PURE__ */ new Set([
767
+ 33,
768
+ // !
769
+ 35,
770
+ // #
771
+ 36,
772
+ // $
773
+ 38,
774
+ // &
775
+ 39,
776
+ // '
777
+ 40,
778
+ // (
779
+ 41,
780
+ // )
781
+ 42,
782
+ // *
783
+ 43,
784
+ // +
785
+ 44,
786
+ // ,
787
+ 47,
788
+ // /
789
+ 58,
790
+ // :
791
+ 59,
792
+ // ;
793
+ 61,
794
+ // =
795
+ 63,
796
+ // ?
797
+ 64,
798
+ // @
799
+ 91,
800
+ // [
801
+ 93
802
+ // ]
803
+ ]);
804
+ var utf8Decoder = new TextDecoder("utf-8", { fatal: false });
805
+ function decodeUnreservedOnly(input) {
806
+ let out = "";
807
+ let pending = [];
808
+ const flushPending = () => {
809
+ if (pending.length === 0)
810
+ return;
811
+ const bytes = new Uint8Array(pending);
812
+ out += utf8Decoder.decode(bytes);
813
+ pending = [];
814
+ };
815
+ let i2 = 0;
816
+ while (i2 < input.length) {
817
+ const ch = input[i2];
818
+ if (ch === "%" && i2 + 2 < input.length && isHex(input[i2 + 1]) && isHex(input[i2 + 2])) {
819
+ const byte = parseInt(input.slice(i2 + 1, i2 + 3), 16);
820
+ if (RESERVED_BYTES.has(byte)) {
821
+ flushPending();
822
+ out += `%${input.slice(i2 + 1, i2 + 3).toUpperCase()}`;
823
+ i2 += 3;
824
+ } else {
825
+ pending.push(byte);
826
+ i2 += 3;
827
+ }
828
+ } else {
829
+ flushPending();
830
+ out += ch;
831
+ i2 += 1;
832
+ }
833
+ }
834
+ flushPending();
835
+ return out;
836
+ }
837
+ function isHex(c2) {
838
+ return c2 >= "0" && c2 <= "9" || c2 >= "a" && c2 <= "f" || c2 >= "A" && c2 <= "F";
839
+ }
840
+ function stripQueryAndHash(s4) {
841
+ const q = s4.indexOf("?");
842
+ if (q !== -1)
843
+ s4 = s4.slice(0, q);
844
+ const h = s4.indexOf("#");
845
+ if (h !== -1)
846
+ s4 = s4.slice(0, h);
847
+ return s4;
848
+ }
849
+ function normalizeRoute(path) {
850
+ if (typeof path !== "string" || path.length === 0) {
851
+ throw new TypeError("normalizeRoute: input must be a non-empty string");
852
+ }
853
+ if (!path.startsWith("/")) {
854
+ throw new TypeError(`normalizeRoute: input must be absolute (start with '/'); got ${JSON.stringify(path)}`);
855
+ }
856
+ let s4 = stripQueryAndHash(path);
857
+ s4 = decodeUnreservedOnly(s4);
858
+ s4 = s4.replace(/\/+/g, "/");
859
+ if (s4.length > 1 && s4.endsWith("/"))
860
+ s4 = s4.slice(0, -1);
861
+ return s4;
862
+ }
863
+
864
+ // ../../sdk-contracts/dist/schemas.js
865
+ var NO_CSS_BREAKOUT_PATTERN = /^[^{}]*$/;
866
+ var AnchorIdZ = external_exports.object({
867
+ selector: external_exports.string().regex(NO_CSS_BREAKOUT_PATTERN, {
868
+ message: 'selector must not contain "{" or "}" \u2014 not valid CSS selector syntax, and content:hideBySelector injects this value directly into a <style> element where these characters would break out of the generated rule.'
869
+ }).describe("CSS selector for the target element"),
870
+ route: external_exports.union([external_exports.string(), external_exports.array(external_exports.string())]).superRefine((value, ctx) => {
871
+ for (const route of Array.isArray(value) ? value : [value]) {
872
+ let canonical;
873
+ try {
874
+ canonical = normalizeRoute(route);
875
+ } catch (err) {
876
+ ctx.addIssue({
877
+ code: external_exports.ZodIssueCode.custom,
878
+ message: `route must be an absolute path starting with "/" (got ${JSON.stringify(route)}): ${err instanceof Error ? err.message : String(err)}`
879
+ });
880
+ continue;
881
+ }
882
+ if (canonical !== route) {
883
+ ctx.addIssue({
884
+ code: external_exports.ZodIssueCode.custom,
885
+ message: `route ${JSON.stringify(route)} is not canonical \u2014 use ${JSON.stringify(canonical)} (this must match what the backend's RouteCanonicalityCheck accepts)`
886
+ });
887
+ }
888
+ }
889
+ }).describe("URL path(s) where this element exists")
890
+ }).strict().describe("DOM element target. selector = CSS selector, route = URL path(s) where the element exists.");
891
+ var AuthoringFieldsZ = {
892
+ id: external_exports.string().optional().describe('Stable action identifier (e.g. "act_3db6a14d2ab0").'),
893
+ title: external_exports.string().max(200).optional().describe("Authoring-only: short label shown on the action plan dashboard. Stripped before serving to the runtime SDK."),
894
+ description: external_exports.string().max(1e3).optional().describe("Authoring-only: one-sentence explanation of what this action does and why. Stripped before serving to the runtime SDK."),
895
+ validation: external_exports.array(external_exports.string().max(500)).max(10).optional().describe("Authoring-only: ordered steps a reviewer can follow to trigger this action and visually confirm it works. Each entry is one step. Stripped before serving to the runtime SDK.")
896
+ };
897
+ var COUNTABLE_EVENTS = [
898
+ // User interactions (from PostHog autocapture normalization)
899
+ "ui.click",
900
+ "ui.scroll",
901
+ "ui.input",
902
+ "ui.change",
903
+ "ui.submit",
904
+ // Behavioral detectors (from event-processor)
905
+ "ui.hover",
906
+ "ui.idle",
907
+ "ui.scroll_thrash",
908
+ "ui.focus_bounce",
909
+ "ui.hesitate",
910
+ "ui.rage_click",
911
+ // Navigation
912
+ "nav.page_view",
913
+ "nav.page_leave"
914
+ ];
915
+ var CountableEventZ = external_exports.enum(COUNTABLE_EVENTS).describe("Event name to count. ui.* = user interactions and behavioral detectors (hesitate, rage_click, scroll_thrash, focus_bounce, idle, hover); nav.* = page navigation.");
916
+ var SESSION_METRIC_KEYS = ["time_on_page", "page_views", "scroll_depth"];
917
+ var SessionMetricKeyZ = external_exports.enum(SESSION_METRIC_KEYS).describe("Session metric key. time_on_page = seconds on current page, page_views = pages visited this session, scroll_depth = 0-100 percentage.");
918
+ var PageUrlConditionZ = external_exports.object({
919
+ type: external_exports.literal("page_url"),
920
+ url: external_exports.string().describe('URL path to match (e.g. "/pricing", "/dashboard")')
921
+ }).describe('Fires when the current page URL matches. Use for page-specific actions. Example: {"type": "page_url", "url": "/pricing"}');
922
+ var RouteConditionZ = external_exports.object({
923
+ type: external_exports.literal("route"),
924
+ routeId: external_exports.string().describe("Named route ID from the route filter")
925
+ }).describe("Fires when the current route matches a named route ID.");
926
+ var AnchorVisibleConditionZ = external_exports.object({
927
+ type: external_exports.literal("anchor_visible"),
928
+ anchorId: external_exports.string().describe("CSS selector of the anchor element"),
929
+ state: external_exports.enum(["visible", "present", "absent"]).describe('"visible" = in viewport, "present" = in DOM, "absent" = not in DOM')
930
+ }).describe(`Fires based on a DOM element's visibility state. Example: {"type": "anchor_visible", "anchorId": "#cta-button", "state": "visible"}`);
931
+ var EventOccurredConditionZ = external_exports.object({
932
+ type: external_exports.literal("event_occurred"),
933
+ eventName: external_exports.string().describe('Event name (e.g. "ui.click", "$pageview")'),
934
+ withinMs: external_exports.number().optional().describe("Time window in ms. Omit = any time this session.")
935
+ }).describe('Fires when a specific event has occurred during this session. Example: {"type": "event_occurred", "eventName": "ui.click", "withinMs": 5000}');
936
+ var StateEqualsConditionZ = external_exports.object({
937
+ type: external_exports.literal("state_equals"),
938
+ key: external_exports.string().describe("Key in the SDK persistent state store (localStorage). Only valid for keys the host app explicitly sets via syntro.state.set()."),
939
+ value: external_exports.unknown().describe("Expected value to match against")
940
+ }).describe("Checks the SDK persistent state store (localStorage). ONLY for host-app state set via syntro.state.set() \u2014 NOT for user attributes like region, device, or UTM params (those are handled by segment targeting). Do NOT use this for targeting. If you do not know the valid state keys, do not use this condition type.");
941
+ var ViewportConditionZ = external_exports.object({
942
+ type: external_exports.literal("viewport"),
943
+ minWidth: external_exports.number().optional().describe("Minimum viewport width in pixels"),
944
+ maxWidth: external_exports.number().optional().describe("Maximum viewport width in pixels"),
945
+ minHeight: external_exports.number().optional().describe("Minimum viewport height in pixels"),
946
+ maxHeight: external_exports.number().optional().describe("Maximum viewport height in pixels")
947
+ }).describe('Fires based on viewport (screen) size. Use for responsive behavior. Example: {"type": "viewport", "minWidth": 768} \u2014 fires on tablet and larger.');
948
+ var SessionMetricConditionZ = external_exports.object({
949
+ type: external_exports.literal("session_metric"),
950
+ key: SessionMetricKeyZ,
951
+ operator: external_exports.enum(["gte", "lte", "eq", "gt", "lt"]),
952
+ threshold: external_exports.number().describe("Numeric threshold to compare against")
953
+ }).describe('Fires when a session metric crosses a threshold. Valid keys: "time_on_page" (seconds), "page_views" (count), "scroll_depth" (0-100). Example: {"type": "session_metric", "key": "time_on_page", "operator": "gte", "threshold": 30}');
954
+ var DismissedConditionZ = external_exports.object({
955
+ type: external_exports.literal("dismissed"),
956
+ key: external_exports.string().describe("Dismissal key (usually a tile or action ID)"),
957
+ inverted: external_exports.boolean().optional().describe("When true, fires if NOT dismissed (default behavior)")
958
+ }).describe("Checks if an item has been dismissed by the user. Use with inverted: true to show only if not dismissed.");
959
+ var CooldownActiveConditionZ = external_exports.object({
960
+ type: external_exports.literal("cooldown_active"),
961
+ key: external_exports.string().describe("Cooldown key"),
962
+ inverted: external_exports.boolean().optional().describe("When true, fires if cooldown is NOT active")
963
+ }).describe("Checks if a cooldown timer is currently active. Use to prevent showing the same intervention too frequently.");
964
+ var FrequencyLimitConditionZ = external_exports.object({
965
+ type: external_exports.literal("frequency_limit"),
966
+ key: external_exports.string().describe("Frequency counter key"),
967
+ limit: external_exports.number().describe("Maximum allowed count"),
968
+ inverted: external_exports.boolean().optional().describe("When true, fires if limit NOT reached")
969
+ }).describe("Checks if a frequency limit has been reached. Use to cap how many times an action fires per session.");
970
+ var MatchOpZ = external_exports.object({
971
+ equals: external_exports.union([external_exports.string(), external_exports.number(), external_exports.boolean()]).optional(),
972
+ contains: external_exports.string().optional()
973
+ }).refine((operator) => Number(operator.equals !== void 0) + Number(operator.contains !== void 0) === 1, {
974
+ message: "Exactly one of equals or contains must be specified."
975
+ }).describe("Match operator for counter filters. Exactly one of equals or contains must be specified.");
976
+ var CounterDefZ = external_exports.object({
977
+ events: external_exports.array(CountableEventZ).min(1).describe("Event names to count. Use values from the countable events enum."),
978
+ match: external_exports.record(external_exports.string(), MatchOpZ).optional().describe("Property filters. Keys are event prop names or element-chain fields (tag_name, $el_text, attr__*). All entries AND together.")
979
+ }).describe("Defines what events to count. Registered as an accumulator predicate at config-load time.");
980
+ var EventCountConditionZ = external_exports.object({
981
+ type: external_exports.literal("event_count"),
982
+ key: external_exports.string().describe("Unique key for this counter (used for accumulator registration)"),
983
+ operator: external_exports.enum(["gte", "lte", "eq", "gt", "lt"]),
984
+ count: external_exports.number().int().min(0).describe("Target count threshold"),
985
+ withinMs: external_exports.number().positive().optional().describe("Time window in ms. Omit = count across entire session."),
986
+ counter: CounterDefZ.optional().describe("Inline counter definition. Defines what events to count.")
987
+ }).describe('Fires when accumulated event count crosses a threshold. Most powerful trigger type. Example: {"type": "event_count", "key": "pricing-clicks", "operator": "gte", "count": 3, "counter": {"events": ["ui.click"], "match": {"attr__data-cta": {"contains": "pricing"}}}}');
988
+ var ConditionZ = external_exports.discriminatedUnion("type", [
989
+ PageUrlConditionZ,
990
+ RouteConditionZ,
991
+ AnchorVisibleConditionZ,
992
+ EventOccurredConditionZ,
993
+ StateEqualsConditionZ,
994
+ ViewportConditionZ,
995
+ SessionMetricConditionZ,
996
+ DismissedConditionZ,
997
+ CooldownActiveConditionZ,
998
+ FrequencyLimitConditionZ,
999
+ EventCountConditionZ
1000
+ ]);
1001
+ var RuleZ = external_exports.object({
1002
+ conditions: external_exports.array(ConditionZ).describe("Array of conditions \u2014 ALL must match (AND logic) for this rule to fire."),
1003
+ value: external_exports.unknown().describe("Value returned when all conditions match. For triggerWhen: true = fire the action.")
1004
+ }).describe("A single rule. ALL conditions must match (AND logic). Rules in a strategy are evaluated top-to-bottom \u2014 first rule where all conditions match wins and returns its value.");
1005
+ var RuleStrategyZ = external_exports.object({
1006
+ type: external_exports.literal("rules"),
1007
+ rules: external_exports.array(RuleZ).describe("Ordered list of rules. Evaluated top-to-bottom \u2014 first match wins."),
1008
+ default: external_exports.unknown().describe("Fallback value when no rule matches. For triggerWhen: false = do not fire by default.")
1009
+ }).describe("Rule-based strategy. Evaluates rules top-to-bottom. First rule where ALL conditions match returns its value. If no rule matches, returns default. For triggerWhen: set value=true on matching rules, default=false.");
1010
+ var ScoreStrategyZ = external_exports.object({
1011
+ type: external_exports.literal("score"),
1012
+ field: external_exports.string(),
1013
+ threshold: external_exports.number(),
1014
+ above: external_exports.unknown(),
1015
+ below: external_exports.unknown()
1016
+ }).describe("Score-based strategy. Compares a field value against a threshold.");
1017
+ var ModelStrategyZ = external_exports.object({
1018
+ type: external_exports.literal("model"),
1019
+ modelId: external_exports.string(),
1020
+ inputs: external_exports.array(external_exports.string()),
1021
+ outputMapping: external_exports.record(external_exports.string(), external_exports.unknown()),
1022
+ default: external_exports.unknown()
1023
+ }).describe("ML model strategy. Sends inputs to a model and maps outputs.");
1024
+ var ExternalStrategyZ = external_exports.object({
1025
+ type: external_exports.literal("external"),
1026
+ endpoint: external_exports.string(),
1027
+ method: external_exports.enum(["GET", "POST"]).optional(),
1028
+ default: external_exports.unknown(),
1029
+ timeoutMs: external_exports.number().optional()
1030
+ }).describe("External API strategy. Calls an endpoint to determine the value.");
1031
+ var DecisionStrategyZ = external_exports.discriminatedUnion("type", [
1032
+ RuleStrategyZ,
1033
+ ScoreStrategyZ,
1034
+ ModelStrategyZ,
1035
+ ExternalStrategyZ
1036
+ ]);
1037
+ var TriggerWhenZ = DecisionStrategyZ.nullable().optional();
1038
+ var EventScopeZ = external_exports.object({
1039
+ events: external_exports.array(external_exports.string()),
1040
+ urlContains: external_exports.string().optional(),
1041
+ props: external_exports.record(external_exports.union([external_exports.string(), external_exports.number(), external_exports.boolean()])).optional()
1042
+ });
1043
+ var NotifyZ = external_exports.object({
1044
+ title: external_exports.string().optional().describe("Notification title"),
1045
+ body: external_exports.string().optional().describe("Notification body text"),
1046
+ icon: external_exports.string().optional().describe("Notification icon (emoji or URL)")
1047
+ }).describe("Optional toast notification shown when this action triggers.").nullable().optional();
1048
+
509
1049
  // src/reviewsFlip.ts
510
1050
  import { html as html2, LitElement as LitElement2, nothing as nothing2 } from "lit";
511
1051
  import { styleMap as styleMap2 } from "lit/directives/style-map.js";
@@ -526,6 +1066,7 @@ function reviewOneLiner(review) {
526
1066
  const body = (review.body ?? "").trim().replace(/\s+/g, " ");
527
1067
  return body.length > 90 ? `${body.slice(0, 89)}\u2026` : body;
528
1068
  }
1069
+ var REVIEWS_FLIP_WIDGET_ID = "adaptive-reviews:reviews-flip";
529
1070
  function defaultLoadReviewsByIds(ids) {
530
1071
  const authedFetch = window.SynOS?.authedFetch;
531
1072
  if (!authedFetch) {
@@ -599,20 +1140,49 @@ var SyntroReviewsFlip = class extends LitElement2 {
599
1140
  }
600
1141
  this._isLoading = true;
601
1142
  this._error = null;
1143
+ const startedAt = performance.now();
602
1144
  try {
603
1145
  const loader = this.loadReviewsByIds ?? defaultLoadReviewsByIds;
604
1146
  const data = await loader(ids);
605
1147
  if (requestId !== this._requestId) return;
606
1148
  const byId = new Map(data.reviews.map((r) => [r.id, r]));
607
1149
  this._reviews = ids.map((id) => byId.get(id)).filter((r) => Boolean(r));
1150
+ this._reportLoad(startedAt, "ok", ids.length, this._reviews.length);
608
1151
  } catch {
609
1152
  if (requestId !== this._requestId) return;
610
1153
  this._reviews = [];
611
1154
  this._error = "Reviews are unavailable";
1155
+ this._reportLoad(startedAt, "error", ids.length, 0);
612
1156
  } finally {
613
1157
  if (requestId === this._requestId) this._isLoading = false;
614
1158
  }
615
1159
  }
1160
+ /**
1161
+ * Report how long this load took. The reviews fetch is the network wait
1162
+ * between the concierge mounting this tile and the shopper seeing it
1163
+ * (BUG-1784767817 was a 30 s hang here), so it goes to telemetry on every
1164
+ * load rather than living only in the promotion playbook. Superseded loads
1165
+ * report nothing: their result is never shown.
1166
+ */
1167
+ _reportLoad(startedAt, outcome, requested, received) {
1168
+ this.dispatchEvent(
1169
+ new CustomEvent(TELEMETRY_DOM_EVENT, {
1170
+ detail: {
1171
+ name: "syntro_tile_data_received",
1172
+ props: {
1173
+ tile_id: this._scopeId,
1174
+ tile_kind: REVIEWS_FLIP_WIDGET_ID,
1175
+ total_ms: Math.round(performance.now() - startedAt),
1176
+ outcome,
1177
+ requested,
1178
+ received
1179
+ }
1180
+ },
1181
+ bubbles: true,
1182
+ composed: true
1183
+ })
1184
+ );
1185
+ }
616
1186
  _applyPendingFocus() {
617
1187
  if (this._pendingFocus === null) return;
618
1188
  const target = this._pendingFocus;
@@ -814,9 +1384,11 @@ var SyntroReviewsFlip = class extends LitElement2 {
814
1384
  }
815
1385
  _starsStyles() {
816
1386
  return {
817
- // Brand accent — the SAME token the FAQ uses for its primary marks, so the
818
- // rating follows the workspace brand (not a hardcoded blue).
819
- color: "var(--sc-color-primary, #723b32)",
1387
+ // Brand accent, the SAME token the FAQ uses for its primary marks, so
1388
+ // the rating follows the workspace brand (not a hardcoded blue). On a
1389
+ // host card the accent is ink on the CHAT APP's ground, where it is only
1390
+ // a contrast risk, so the host's own ink wins when it sent one.
1391
+ color: "var(--sc-host-link-color, var(--sc-color-primary, #723b32))",
820
1392
  fontSize: "13px",
821
1393
  lineHeight: "18px",
822
1394
  whiteSpace: "nowrap"
@@ -904,7 +1476,6 @@ if (!customElements.get("syntro-reviews-flip")) {
904
1476
  // src/runtime.ts
905
1477
  var REVIEWS_ADAPTIVE_APP_ID = "adaptive-reviews";
906
1478
  var REVIEWS_ADAPTIVE_WIDGET_ID = "adaptive-reviews:reviews";
907
- var REVIEWS_FLIP_WIDGET_ID = "adaptive-reviews:reviews-flip";
908
1479
  function asPositiveLimit(value) {
909
1480
  const numeric = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN;
910
1481
  if (!Number.isFinite(numeric)) return void 0;
@@ -1008,13 +1579,41 @@ var reviewsAdaptiveRuntime = runtime;
1008
1579
  var runtime_default = runtime;
1009
1580
 
1010
1581
  export {
1582
+ REVIEWS_FLIP_WIDGET_ID,
1011
1583
  REVIEWS_ADAPTIVE_APP_ID,
1012
1584
  REVIEWS_ADAPTIVE_WIDGET_ID,
1013
- REVIEWS_FLIP_WIDGET_ID,
1014
1585
  reviewsWidget,
1015
1586
  reviewsFlipWidget,
1016
1587
  runtime,
1017
1588
  reviewsAdaptiveRuntime,
1018
1589
  runtime_default
1019
1590
  };
1020
- //# sourceMappingURL=chunk-ATGQNO24.js.map
1591
+ /*! Bundled license information:
1592
+
1593
+ @lit/context/lib/context-request-event.js:
1594
+ @lit/context/lib/create-context.js:
1595
+ @lit/context/lib/controllers/context-consumer.js:
1596
+ @lit/context/lib/value-notifier.js:
1597
+ @lit/context/lib/controllers/context-provider.js:
1598
+ @lit/context/lib/context-root.js:
1599
+ (**
1600
+ * @license
1601
+ * Copyright 2021 Google LLC
1602
+ * SPDX-License-Identifier: BSD-3-Clause
1603
+ *)
1604
+
1605
+ @lit/context/lib/decorators/provide.js:
1606
+ (**
1607
+ * @license
1608
+ * Copyright 2017 Google LLC
1609
+ * SPDX-License-Identifier: BSD-3-Clause
1610
+ *)
1611
+
1612
+ @lit/context/lib/decorators/consume.js:
1613
+ (**
1614
+ * @license
1615
+ * Copyright 2022 Google LLC
1616
+ * SPDX-License-Identifier: BSD-3-Clause
1617
+ *)
1618
+ */
1619
+ //# sourceMappingURL=chunk-M64YCKZP.js.map