@syntrologie/adapt-search 2.8.0-canary.568
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/dist/SearchSurfaceElement.d.ts +140 -0
- package/dist/SearchSurfaceElement.d.ts.map +1 -0
- package/dist/cdn.d.ts +52 -0
- package/dist/cdn.d.ts.map +1 -0
- package/dist/cdn.js +33 -0
- package/dist/cdn.js.map +7 -0
- package/dist/chunk-3KPQ5AOG.js +1389 -0
- package/dist/chunk-3KPQ5AOG.js.map +7 -0
- package/dist/chunk-NKOTQPVZ.js +4071 -0
- package/dist/chunk-NKOTQPVZ.js.map +7 -0
- package/dist/hrefSafety.d.ts +32 -0
- package/dist/hrefSafety.d.ts.map +1 -0
- package/dist/regions.d.ts +93 -0
- package/dist/regions.d.ts.map +1 -0
- package/dist/runtime.d.ts +45 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +12 -0
- package/dist/runtime.js.map +7 -0
- package/dist/schema.d.ts +49 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +66 -0
- package/dist/schema.js.map +7 -0
- package/dist/variants.d.ts +52 -0
- package/dist/variants.d.ts.map +1 -0
- package/package.json +58 -0
|
@@ -0,0 +1,1389 @@
|
|
|
1
|
+
import {
|
|
2
|
+
external_exports,
|
|
3
|
+
skeletonRegions
|
|
4
|
+
} from "./chunk-NKOTQPVZ.js";
|
|
5
|
+
|
|
6
|
+
// ../../../node_modules/@lit/context/lib/create-context.js
|
|
7
|
+
function n(n2) {
|
|
8
|
+
return n2;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// ../../sdk-contracts/dist/canvas-context.js
|
|
12
|
+
var canvasRuntimeContext = n("syntrologie:canvas-runtime");
|
|
13
|
+
|
|
14
|
+
// ../../sdk-contracts/dist/detector-events.js
|
|
15
|
+
var DETECTOR_EVENT_NAMES = [
|
|
16
|
+
"ui.hover",
|
|
17
|
+
"ui.idle",
|
|
18
|
+
"ui.hesitate",
|
|
19
|
+
"ui.rage_click",
|
|
20
|
+
"ui.scroll_thrash",
|
|
21
|
+
"ui.focus_bounce"
|
|
22
|
+
];
|
|
23
|
+
var CANONICAL_BUS_EVENT_NAMES = [
|
|
24
|
+
...DETECTOR_EVENT_NAMES,
|
|
25
|
+
"nav.section_viewed",
|
|
26
|
+
"nav.scroll_depth"
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
// ../../sdk-contracts/dist/mount-plumbing.js
|
|
30
|
+
var MOUNT_PLUMBING_KEYS = ["instanceId", "runtime", "tileId"];
|
|
31
|
+
function stripMountPlumbing(config) {
|
|
32
|
+
if (!config || typeof config !== "object") {
|
|
33
|
+
return {};
|
|
34
|
+
}
|
|
35
|
+
const out = { ...config };
|
|
36
|
+
for (const key of MOUNT_PLUMBING_KEYS) {
|
|
37
|
+
delete out[key];
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ../../sdk-contracts/dist/routes.js
|
|
43
|
+
var RESERVED_BYTES = /* @__PURE__ */ new Set([
|
|
44
|
+
33,
|
|
45
|
+
// !
|
|
46
|
+
35,
|
|
47
|
+
// #
|
|
48
|
+
36,
|
|
49
|
+
// $
|
|
50
|
+
38,
|
|
51
|
+
// &
|
|
52
|
+
39,
|
|
53
|
+
// '
|
|
54
|
+
40,
|
|
55
|
+
// (
|
|
56
|
+
41,
|
|
57
|
+
// )
|
|
58
|
+
42,
|
|
59
|
+
// *
|
|
60
|
+
43,
|
|
61
|
+
// +
|
|
62
|
+
44,
|
|
63
|
+
// ,
|
|
64
|
+
47,
|
|
65
|
+
// /
|
|
66
|
+
58,
|
|
67
|
+
// :
|
|
68
|
+
59,
|
|
69
|
+
// ;
|
|
70
|
+
61,
|
|
71
|
+
// =
|
|
72
|
+
63,
|
|
73
|
+
// ?
|
|
74
|
+
64,
|
|
75
|
+
// @
|
|
76
|
+
91,
|
|
77
|
+
// [
|
|
78
|
+
93
|
|
79
|
+
// ]
|
|
80
|
+
]);
|
|
81
|
+
var utf8Decoder = new TextDecoder("utf-8", { fatal: false });
|
|
82
|
+
function decodeUnreservedOnly(input) {
|
|
83
|
+
let out = "";
|
|
84
|
+
let pending = [];
|
|
85
|
+
const flushPending = () => {
|
|
86
|
+
if (pending.length === 0)
|
|
87
|
+
return;
|
|
88
|
+
const bytes = new Uint8Array(pending);
|
|
89
|
+
out += utf8Decoder.decode(bytes);
|
|
90
|
+
pending = [];
|
|
91
|
+
};
|
|
92
|
+
let i2 = 0;
|
|
93
|
+
while (i2 < input.length) {
|
|
94
|
+
const ch = input[i2];
|
|
95
|
+
if (ch === "%" && i2 + 2 < input.length && isHex(input[i2 + 1]) && isHex(input[i2 + 2])) {
|
|
96
|
+
const byte = parseInt(input.slice(i2 + 1, i2 + 3), 16);
|
|
97
|
+
if (RESERVED_BYTES.has(byte)) {
|
|
98
|
+
flushPending();
|
|
99
|
+
out += `%${input.slice(i2 + 1, i2 + 3).toUpperCase()}`;
|
|
100
|
+
i2 += 3;
|
|
101
|
+
} else {
|
|
102
|
+
pending.push(byte);
|
|
103
|
+
i2 += 3;
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
flushPending();
|
|
107
|
+
out += ch;
|
|
108
|
+
i2 += 1;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
flushPending();
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
function isHex(c2) {
|
|
115
|
+
return c2 >= "0" && c2 <= "9" || c2 >= "a" && c2 <= "f" || c2 >= "A" && c2 <= "F";
|
|
116
|
+
}
|
|
117
|
+
function stripQueryAndHash(s4) {
|
|
118
|
+
const q = s4.indexOf("?");
|
|
119
|
+
if (q !== -1)
|
|
120
|
+
s4 = s4.slice(0, q);
|
|
121
|
+
const h = s4.indexOf("#");
|
|
122
|
+
if (h !== -1)
|
|
123
|
+
s4 = s4.slice(0, h);
|
|
124
|
+
return s4;
|
|
125
|
+
}
|
|
126
|
+
function normalizeRoute(path) {
|
|
127
|
+
if (typeof path !== "string" || path.length === 0) {
|
|
128
|
+
throw new TypeError("normalizeRoute: input must be a non-empty string");
|
|
129
|
+
}
|
|
130
|
+
if (!path.startsWith("/")) {
|
|
131
|
+
throw new TypeError(`normalizeRoute: input must be absolute (start with '/'); got ${JSON.stringify(path)}`);
|
|
132
|
+
}
|
|
133
|
+
let s4 = stripQueryAndHash(path);
|
|
134
|
+
s4 = decodeUnreservedOnly(s4);
|
|
135
|
+
s4 = s4.replace(/\/+/g, "/");
|
|
136
|
+
if (s4.length > 1 && s4.endsWith("/"))
|
|
137
|
+
s4 = s4.slice(0, -1);
|
|
138
|
+
return s4;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ../../sdk-contracts/dist/schemas.js
|
|
142
|
+
var NO_CSS_BREAKOUT_PATTERN = /^[^{}]*$/;
|
|
143
|
+
var AnchorIdZ = external_exports.object({
|
|
144
|
+
selector: external_exports.string().regex(NO_CSS_BREAKOUT_PATTERN, {
|
|
145
|
+
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.'
|
|
146
|
+
}).describe("CSS selector for the target element"),
|
|
147
|
+
route: external_exports.union([external_exports.string(), external_exports.array(external_exports.string())]).superRefine((value, ctx) => {
|
|
148
|
+
for (const route of Array.isArray(value) ? value : [value]) {
|
|
149
|
+
let canonical;
|
|
150
|
+
try {
|
|
151
|
+
canonical = normalizeRoute(route);
|
|
152
|
+
} catch (err) {
|
|
153
|
+
ctx.addIssue({
|
|
154
|
+
code: external_exports.ZodIssueCode.custom,
|
|
155
|
+
message: `route must be an absolute path starting with "/" (got ${JSON.stringify(route)}): ${err instanceof Error ? err.message : String(err)}`
|
|
156
|
+
});
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (canonical !== route) {
|
|
160
|
+
ctx.addIssue({
|
|
161
|
+
code: external_exports.ZodIssueCode.custom,
|
|
162
|
+
message: `route ${JSON.stringify(route)} is not canonical \u2014 use ${JSON.stringify(canonical)} (this must match what the backend's RouteCanonicalityCheck accepts)`
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}).describe("URL path(s) where this element exists")
|
|
167
|
+
}).strict().describe("DOM element target. selector = CSS selector, route = URL path(s) where the element exists.");
|
|
168
|
+
var AuthoringFieldsZ = {
|
|
169
|
+
id: external_exports.string().optional().describe('Stable action identifier (e.g. "act_3db6a14d2ab0").'),
|
|
170
|
+
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."),
|
|
171
|
+
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."),
|
|
172
|
+
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.")
|
|
173
|
+
};
|
|
174
|
+
var COUNTABLE_EVENTS = [
|
|
175
|
+
// User interactions (from PostHog autocapture normalization)
|
|
176
|
+
"ui.click",
|
|
177
|
+
"ui.scroll",
|
|
178
|
+
"ui.input",
|
|
179
|
+
"ui.change",
|
|
180
|
+
"ui.submit",
|
|
181
|
+
// Behavioral detectors (from event-processor)
|
|
182
|
+
"ui.hover",
|
|
183
|
+
"ui.idle",
|
|
184
|
+
"ui.scroll_thrash",
|
|
185
|
+
"ui.focus_bounce",
|
|
186
|
+
"ui.hesitate",
|
|
187
|
+
"ui.rage_click",
|
|
188
|
+
// Navigation
|
|
189
|
+
"nav.page_view",
|
|
190
|
+
"nav.page_leave"
|
|
191
|
+
];
|
|
192
|
+
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.");
|
|
193
|
+
var SESSION_METRIC_KEYS = ["time_on_page", "page_views", "scroll_depth"];
|
|
194
|
+
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.");
|
|
195
|
+
var PageUrlConditionZ = external_exports.object({
|
|
196
|
+
type: external_exports.literal("page_url"),
|
|
197
|
+
url: external_exports.string().describe('URL path to match (e.g. "/pricing", "/dashboard")')
|
|
198
|
+
}).describe('Fires when the current page URL matches. Use for page-specific actions. Example: {"type": "page_url", "url": "/pricing"}');
|
|
199
|
+
var RouteConditionZ = external_exports.object({
|
|
200
|
+
type: external_exports.literal("route"),
|
|
201
|
+
routeId: external_exports.string().describe("Named route ID from the route filter")
|
|
202
|
+
}).describe("Fires when the current route matches a named route ID.");
|
|
203
|
+
var AnchorVisibleConditionZ = external_exports.object({
|
|
204
|
+
type: external_exports.literal("anchor_visible"),
|
|
205
|
+
anchorId: external_exports.string().describe("CSS selector of the anchor element"),
|
|
206
|
+
state: external_exports.enum(["visible", "present", "absent"]).describe('"visible" = in viewport, "present" = in DOM, "absent" = not in DOM')
|
|
207
|
+
}).describe(`Fires based on a DOM element's visibility state. Example: {"type": "anchor_visible", "anchorId": "#cta-button", "state": "visible"}`);
|
|
208
|
+
var EventOccurredConditionZ = external_exports.object({
|
|
209
|
+
type: external_exports.literal("event_occurred"),
|
|
210
|
+
eventName: external_exports.string().describe('Event name (e.g. "ui.click", "$pageview")'),
|
|
211
|
+
withinMs: external_exports.number().optional().describe("Time window in ms. Omit = any time this session.")
|
|
212
|
+
}).describe('Fires when a specific event has occurred during this session. Example: {"type": "event_occurred", "eventName": "ui.click", "withinMs": 5000}');
|
|
213
|
+
var StateEqualsConditionZ = external_exports.object({
|
|
214
|
+
type: external_exports.literal("state_equals"),
|
|
215
|
+
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()."),
|
|
216
|
+
value: external_exports.unknown().describe("Expected value to match against")
|
|
217
|
+
}).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.");
|
|
218
|
+
var ViewportConditionZ = external_exports.object({
|
|
219
|
+
type: external_exports.literal("viewport"),
|
|
220
|
+
minWidth: external_exports.number().optional().describe("Minimum viewport width in pixels"),
|
|
221
|
+
maxWidth: external_exports.number().optional().describe("Maximum viewport width in pixels"),
|
|
222
|
+
minHeight: external_exports.number().optional().describe("Minimum viewport height in pixels"),
|
|
223
|
+
maxHeight: external_exports.number().optional().describe("Maximum viewport height in pixels")
|
|
224
|
+
}).describe('Fires based on viewport (screen) size. Use for responsive behavior. Example: {"type": "viewport", "minWidth": 768} \u2014 fires on tablet and larger.');
|
|
225
|
+
var SessionMetricConditionZ = external_exports.object({
|
|
226
|
+
type: external_exports.literal("session_metric"),
|
|
227
|
+
key: SessionMetricKeyZ,
|
|
228
|
+
operator: external_exports.enum(["gte", "lte", "eq", "gt", "lt"]),
|
|
229
|
+
threshold: external_exports.number().describe("Numeric threshold to compare against")
|
|
230
|
+
}).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}');
|
|
231
|
+
var DismissedConditionZ = external_exports.object({
|
|
232
|
+
type: external_exports.literal("dismissed"),
|
|
233
|
+
key: external_exports.string().describe("Dismissal key (usually a tile or action ID)"),
|
|
234
|
+
inverted: external_exports.boolean().optional().describe("When true, fires if NOT dismissed (default behavior)")
|
|
235
|
+
}).describe("Checks if an item has been dismissed by the user. Use with inverted: true to show only if not dismissed.");
|
|
236
|
+
var CooldownActiveConditionZ = external_exports.object({
|
|
237
|
+
type: external_exports.literal("cooldown_active"),
|
|
238
|
+
key: external_exports.string().describe("Cooldown key"),
|
|
239
|
+
inverted: external_exports.boolean().optional().describe("When true, fires if cooldown is NOT active")
|
|
240
|
+
}).describe("Checks if a cooldown timer is currently active. Use to prevent showing the same intervention too frequently.");
|
|
241
|
+
var FrequencyLimitConditionZ = external_exports.object({
|
|
242
|
+
type: external_exports.literal("frequency_limit"),
|
|
243
|
+
key: external_exports.string().describe("Frequency counter key"),
|
|
244
|
+
limit: external_exports.number().describe("Maximum allowed count"),
|
|
245
|
+
inverted: external_exports.boolean().optional().describe("When true, fires if limit NOT reached")
|
|
246
|
+
}).describe("Checks if a frequency limit has been reached. Use to cap how many times an action fires per session.");
|
|
247
|
+
var MatchOpZ = external_exports.object({
|
|
248
|
+
equals: external_exports.union([external_exports.string(), external_exports.number(), external_exports.boolean()]).optional(),
|
|
249
|
+
contains: external_exports.string().optional()
|
|
250
|
+
}).refine((operator) => Number(operator.equals !== void 0) + Number(operator.contains !== void 0) === 1, {
|
|
251
|
+
message: "Exactly one of equals or contains must be specified."
|
|
252
|
+
}).describe("Match operator for counter filters. Exactly one of equals or contains must be specified.");
|
|
253
|
+
var CounterDefZ = external_exports.object({
|
|
254
|
+
events: external_exports.array(CountableEventZ).min(1).describe("Event names to count. Use values from the countable events enum."),
|
|
255
|
+
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.")
|
|
256
|
+
}).describe("Defines what events to count. Registered as an accumulator predicate at config-load time.");
|
|
257
|
+
var EventCountConditionZ = external_exports.object({
|
|
258
|
+
type: external_exports.literal("event_count"),
|
|
259
|
+
key: external_exports.string().describe("Unique key for this counter (used for accumulator registration)"),
|
|
260
|
+
operator: external_exports.enum(["gte", "lte", "eq", "gt", "lt"]),
|
|
261
|
+
count: external_exports.number().int().min(0).describe("Target count threshold"),
|
|
262
|
+
withinMs: external_exports.number().positive().optional().describe("Time window in ms. Omit = count across entire session."),
|
|
263
|
+
counter: CounterDefZ.optional().describe("Inline counter definition. Defines what events to count.")
|
|
264
|
+
}).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"}}}}');
|
|
265
|
+
var ConditionZ = external_exports.discriminatedUnion("type", [
|
|
266
|
+
PageUrlConditionZ,
|
|
267
|
+
RouteConditionZ,
|
|
268
|
+
AnchorVisibleConditionZ,
|
|
269
|
+
EventOccurredConditionZ,
|
|
270
|
+
StateEqualsConditionZ,
|
|
271
|
+
ViewportConditionZ,
|
|
272
|
+
SessionMetricConditionZ,
|
|
273
|
+
DismissedConditionZ,
|
|
274
|
+
CooldownActiveConditionZ,
|
|
275
|
+
FrequencyLimitConditionZ,
|
|
276
|
+
EventCountConditionZ
|
|
277
|
+
]);
|
|
278
|
+
var RuleZ = external_exports.object({
|
|
279
|
+
conditions: external_exports.array(ConditionZ).describe("Array of conditions \u2014 ALL must match (AND logic) for this rule to fire."),
|
|
280
|
+
value: external_exports.unknown().describe("Value returned when all conditions match. For triggerWhen: true = fire the action.")
|
|
281
|
+
}).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.");
|
|
282
|
+
var RuleStrategyZ = external_exports.object({
|
|
283
|
+
type: external_exports.literal("rules"),
|
|
284
|
+
rules: external_exports.array(RuleZ).describe("Ordered list of rules. Evaluated top-to-bottom \u2014 first match wins."),
|
|
285
|
+
default: external_exports.unknown().describe("Fallback value when no rule matches. For triggerWhen: false = do not fire by default.")
|
|
286
|
+
}).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.");
|
|
287
|
+
var ScoreStrategyZ = external_exports.object({
|
|
288
|
+
type: external_exports.literal("score"),
|
|
289
|
+
field: external_exports.string(),
|
|
290
|
+
threshold: external_exports.number(),
|
|
291
|
+
above: external_exports.unknown(),
|
|
292
|
+
below: external_exports.unknown()
|
|
293
|
+
}).describe("Score-based strategy. Compares a field value against a threshold.");
|
|
294
|
+
var ModelStrategyZ = external_exports.object({
|
|
295
|
+
type: external_exports.literal("model"),
|
|
296
|
+
modelId: external_exports.string(),
|
|
297
|
+
inputs: external_exports.array(external_exports.string()),
|
|
298
|
+
outputMapping: external_exports.record(external_exports.string(), external_exports.unknown()),
|
|
299
|
+
default: external_exports.unknown()
|
|
300
|
+
}).describe("ML model strategy. Sends inputs to a model and maps outputs.");
|
|
301
|
+
var ExternalStrategyZ = external_exports.object({
|
|
302
|
+
type: external_exports.literal("external"),
|
|
303
|
+
endpoint: external_exports.string(),
|
|
304
|
+
method: external_exports.enum(["GET", "POST"]).optional(),
|
|
305
|
+
default: external_exports.unknown(),
|
|
306
|
+
timeoutMs: external_exports.number().optional()
|
|
307
|
+
}).describe("External API strategy. Calls an endpoint to determine the value.");
|
|
308
|
+
var DecisionStrategyZ = external_exports.discriminatedUnion("type", [
|
|
309
|
+
RuleStrategyZ,
|
|
310
|
+
ScoreStrategyZ,
|
|
311
|
+
ModelStrategyZ,
|
|
312
|
+
ExternalStrategyZ
|
|
313
|
+
]);
|
|
314
|
+
var TriggerWhenZ = DecisionStrategyZ.nullable().optional();
|
|
315
|
+
var EventScopeZ = external_exports.object({
|
|
316
|
+
events: external_exports.array(external_exports.string()),
|
|
317
|
+
urlContains: external_exports.string().optional(),
|
|
318
|
+
props: external_exports.record(external_exports.union([external_exports.string(), external_exports.number(), external_exports.boolean()])).optional()
|
|
319
|
+
});
|
|
320
|
+
var NotifyZ = external_exports.object({
|
|
321
|
+
title: external_exports.string().optional().describe("Notification title"),
|
|
322
|
+
body: external_exports.string().optional().describe("Notification body text"),
|
|
323
|
+
icon: external_exports.string().optional().describe("Notification icon (emoji or URL)")
|
|
324
|
+
}).describe("Optional toast notification shown when this action triggers.").nullable().optional();
|
|
325
|
+
|
|
326
|
+
// ../../sdk-contracts/dist/telemetry-events.js
|
|
327
|
+
var entry = (e2) => e2;
|
|
328
|
+
var TELEMETRY_EVENTS = {
|
|
329
|
+
// --- intervention metrics (April 2026) — wire names unchanged, all plan-scope
|
|
330
|
+
syntro_config_served: entry({
|
|
331
|
+
subject: "config",
|
|
332
|
+
verb: "served",
|
|
333
|
+
scope: "plan",
|
|
334
|
+
props: ["tiles", "actions"]
|
|
335
|
+
}),
|
|
336
|
+
syntro_intervention_seen: entry({
|
|
337
|
+
subject: "intervention",
|
|
338
|
+
verb: "seen",
|
|
339
|
+
scope: "plan",
|
|
340
|
+
props: ["intervention_id", "intervention_kind"]
|
|
341
|
+
}),
|
|
342
|
+
syntro_intervention_triggered: entry({
|
|
343
|
+
subject: "intervention",
|
|
344
|
+
verb: "triggered",
|
|
345
|
+
scope: "plan",
|
|
346
|
+
props: ["intervention_id", "intervention_kind"]
|
|
347
|
+
}),
|
|
348
|
+
syntro_intervention_interacted: entry({
|
|
349
|
+
subject: "intervention",
|
|
350
|
+
verb: "interacted",
|
|
351
|
+
scope: "plan",
|
|
352
|
+
props: ["intervention_id", "intervention_kind", "interaction_type"]
|
|
353
|
+
}),
|
|
354
|
+
// --- concierge (FEAT-1789150184) ---------------------------------------
|
|
355
|
+
syntro_concierge_opened: entry({
|
|
356
|
+
subject: "concierge",
|
|
357
|
+
verb: "opened",
|
|
358
|
+
scope: "open",
|
|
359
|
+
props: ["trigger", "page_path"]
|
|
360
|
+
// trigger: ConciergeOpenTrigger
|
|
361
|
+
}),
|
|
362
|
+
syntro_concierge_closed: entry({
|
|
363
|
+
subject: "concierge",
|
|
364
|
+
verb: "closed",
|
|
365
|
+
scope: "open",
|
|
366
|
+
props: ["duration_ms", "reason", "messages_sent", "tiles_dismissed", "pages_viewed"]
|
|
367
|
+
// reason: ConciergeCloseReason
|
|
368
|
+
}),
|
|
369
|
+
syntro_deck_paged: entry({
|
|
370
|
+
subject: "deck",
|
|
371
|
+
verb: "paged",
|
|
372
|
+
scope: "open",
|
|
373
|
+
props: ["from_index", "to_index", "method"]
|
|
374
|
+
// method: DeckPageMethod
|
|
375
|
+
}),
|
|
376
|
+
syntro_tile_dismissed: entry({
|
|
377
|
+
subject: "tile",
|
|
378
|
+
verb: "dismissed",
|
|
379
|
+
scope: "open",
|
|
380
|
+
props: ["tile_id", "tile_kind", "visible_ms"]
|
|
381
|
+
}),
|
|
382
|
+
// The three chat atoms are OPEN-scope: they carry `surface` / `open_id` and
|
|
383
|
+
// only make sense inside a concierge open. An INLINE chat bar (no concierge)
|
|
384
|
+
// still dispatches them on every turn, and the core drops each one and
|
|
385
|
+
// increments the `telemetry_dropped` health counter. That is EXPECTED on an
|
|
386
|
+
// inline-chat host page, not a bug — `telemetry_dropped` there mixes "real
|
|
387
|
+
// defect" with "inline chat has no open scope", so do not alert on it alone.
|
|
388
|
+
syntro_chat_message_sent: entry({
|
|
389
|
+
subject: "chat_message",
|
|
390
|
+
verb: "sent",
|
|
391
|
+
scope: "open",
|
|
392
|
+
props: ["turn", "chars", "via"]
|
|
393
|
+
// via: ChatSendVia
|
|
394
|
+
}),
|
|
395
|
+
syntro_chat_reply_received: entry({
|
|
396
|
+
subject: "chat_reply",
|
|
397
|
+
verb: "received",
|
|
398
|
+
scope: "open",
|
|
399
|
+
props: ["turn", "ttft_ms", "total_ms", "mounts", "cards"]
|
|
400
|
+
}),
|
|
401
|
+
syntro_chat_interrupted: entry({
|
|
402
|
+
subject: "chat",
|
|
403
|
+
verb: "interrupted",
|
|
404
|
+
scope: "open",
|
|
405
|
+
props: ["turn", "after_ms"]
|
|
406
|
+
}),
|
|
407
|
+
syntro_chat_maximize_toggled: entry({
|
|
408
|
+
subject: "chat_maximize",
|
|
409
|
+
verb: "toggled",
|
|
410
|
+
scope: "open",
|
|
411
|
+
props: ["maximized"]
|
|
412
|
+
}),
|
|
413
|
+
syntro_chat_takeover_triggered: entry({
|
|
414
|
+
subject: "chat_takeover",
|
|
415
|
+
verb: "triggered",
|
|
416
|
+
scope: "open",
|
|
417
|
+
props: ["turn"]
|
|
418
|
+
}),
|
|
419
|
+
// --- search takeover (2026-09-17) ---------------------------------------
|
|
420
|
+
// All plan-scope: the search interceptor fires before any concierge open
|
|
421
|
+
// exists. No prop here ever carries query text (SEE the contract test's
|
|
422
|
+
// no-text-prop rule) — classification/signal_hits are the interceptor's
|
|
423
|
+
// own derived signals, never the string itself.
|
|
424
|
+
syntro_search_sent: entry({
|
|
425
|
+
subject: "search",
|
|
426
|
+
verb: "sent",
|
|
427
|
+
scope: "plan",
|
|
428
|
+
props: ["classification", "word_count", "signal_hits", "entry_point", "platform"]
|
|
429
|
+
// classification: SearchClassification, entry_point: SearchEntryPoint
|
|
430
|
+
}),
|
|
431
|
+
// `ms_to_skeleton` is intentionally the only timing prop: it is always
|
|
432
|
+
// knowable the instant the surface mounts. `ms_to_first_content` is NOT
|
|
433
|
+
// declared here (revision 1 defect) because a surface that never painted
|
|
434
|
+
// content still fires this event, and a required prop it cannot supply
|
|
435
|
+
// would make TelemetryCore.emit drop exactly the row worth measuring.
|
|
436
|
+
syntro_search_surface_seen: entry({
|
|
437
|
+
subject: "search_surface",
|
|
438
|
+
verb: "seen",
|
|
439
|
+
scope: "plan",
|
|
440
|
+
props: ["variant", "classification", "result_count", "ms_to_skeleton"]
|
|
441
|
+
// variant: SearchVariant, classification: SearchClassification
|
|
442
|
+
}),
|
|
443
|
+
syntro_search_result_tapped: entry({
|
|
444
|
+
subject: "search_result",
|
|
445
|
+
verb: "tapped",
|
|
446
|
+
scope: "plan",
|
|
447
|
+
props: ["variant", "region_id", "module", "position"]
|
|
448
|
+
// variant: SearchVariant
|
|
449
|
+
}),
|
|
450
|
+
syntro_search_followup_sent: entry({
|
|
451
|
+
subject: "search_followup",
|
|
452
|
+
verb: "sent",
|
|
453
|
+
scope: "plan",
|
|
454
|
+
props: ["delta_kind", "variant"]
|
|
455
|
+
// delta_kind: SearchDeltaKind, variant: SearchVariant
|
|
456
|
+
}),
|
|
457
|
+
// Declares `ms_since_boot` (not a "since surface mounted" timing prop) so a
|
|
458
|
+
// not-ready fallback the takeover never rendered is still measurable —
|
|
459
|
+
// revision 1 emitted nothing on this path.
|
|
460
|
+
syntro_search_native_served: entry({
|
|
461
|
+
subject: "search_native",
|
|
462
|
+
verb: "served",
|
|
463
|
+
scope: "plan",
|
|
464
|
+
props: ["reason", "platform", "ms_since_boot"]
|
|
465
|
+
// reason: SearchFallbackReason
|
|
466
|
+
}),
|
|
467
|
+
syntro_search_takeover_dismissed: entry({
|
|
468
|
+
subject: "search_takeover",
|
|
469
|
+
verb: "dismissed",
|
|
470
|
+
scope: "plan",
|
|
471
|
+
props: ["variant", "reason"]
|
|
472
|
+
// variant: SearchVariant, reason: SearchDismissReason
|
|
473
|
+
})
|
|
474
|
+
};
|
|
475
|
+
var TELEMETRY_EVENT_NAMES = Object.keys(TELEMETRY_EVENTS);
|
|
476
|
+
|
|
477
|
+
// src/SearchSurfaceElement.ts
|
|
478
|
+
import { css, html as html2, LitElement, nothing as nothing2 } from "lit";
|
|
479
|
+
import { repeat } from "lit/directives/repeat.js";
|
|
480
|
+
|
|
481
|
+
// src/regions.ts
|
|
482
|
+
import { html, nothing } from "lit";
|
|
483
|
+
|
|
484
|
+
// src/hrefSafety.ts
|
|
485
|
+
var ALLOWED_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:", "mailto:", "tel:"]);
|
|
486
|
+
var UNSAFE_HREF_FALLBACK = "#";
|
|
487
|
+
function isSafeProductHref(href) {
|
|
488
|
+
if (typeof href !== "string" || href.trim().length === 0) return false;
|
|
489
|
+
let parsed;
|
|
490
|
+
try {
|
|
491
|
+
parsed = new URL(href, "https://syntro.local/");
|
|
492
|
+
} catch {
|
|
493
|
+
return false;
|
|
494
|
+
}
|
|
495
|
+
return ALLOWED_PROTOCOLS.has(parsed.protocol);
|
|
496
|
+
}
|
|
497
|
+
function safeProductHref(href) {
|
|
498
|
+
return isSafeProductHref(href) ? href : UNSAFE_HREF_FALLBACK;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// src/regions.ts
|
|
502
|
+
var RESULT_TAP_EVENT = "syntro:search:result-tap";
|
|
503
|
+
var FOLLOWUP_EVENT = "syntro:search:followup";
|
|
504
|
+
var SEE_STORE_RESULTS_EVENT = "syntro:search:see-store-results";
|
|
505
|
+
function dispatchFrom(node, name, detail) {
|
|
506
|
+
node.dispatchEvent(new CustomEvent(name, { detail, bubbles: true, composed: false }));
|
|
507
|
+
}
|
|
508
|
+
function asRecord(value) {
|
|
509
|
+
return value !== null && typeof value === "object" ? value : {};
|
|
510
|
+
}
|
|
511
|
+
function asString(value) {
|
|
512
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
513
|
+
}
|
|
514
|
+
function readProduct(value) {
|
|
515
|
+
const raw = asRecord(value);
|
|
516
|
+
const handle = asString(raw.handle);
|
|
517
|
+
const title = asString(raw.title);
|
|
518
|
+
const url = asString(raw.url);
|
|
519
|
+
if (!handle || !title || !url) return null;
|
|
520
|
+
return { handle, title, url, price: asString(raw.price), imageUrl: asString(raw.imageUrl) };
|
|
521
|
+
}
|
|
522
|
+
function readProducts(value) {
|
|
523
|
+
if (!Array.isArray(value)) return [];
|
|
524
|
+
return value.map(readProduct).filter((product) => product !== null);
|
|
525
|
+
}
|
|
526
|
+
function readStrings(value) {
|
|
527
|
+
if (!Array.isArray(value)) return [];
|
|
528
|
+
return value.filter((entry2) => typeof entry2 === "string" && entry2.length > 0);
|
|
529
|
+
}
|
|
530
|
+
function readChips(value) {
|
|
531
|
+
if (!Array.isArray(value)) return [];
|
|
532
|
+
return value.map((entry2) => {
|
|
533
|
+
const raw = asRecord(entry2);
|
|
534
|
+
const key = asString(raw.key);
|
|
535
|
+
const label = asString(raw.label) ?? key;
|
|
536
|
+
return key && label ? { key, label } : null;
|
|
537
|
+
}).filter((chip) => chip !== null);
|
|
538
|
+
}
|
|
539
|
+
function readSuggestions(value) {
|
|
540
|
+
if (!Array.isArray(value)) return [];
|
|
541
|
+
return value.map((entry2) => {
|
|
542
|
+
const raw = asRecord(entry2);
|
|
543
|
+
const suggestion = asString(raw.value);
|
|
544
|
+
const label = asString(raw.label) ?? suggestion;
|
|
545
|
+
return suggestion && label ? { value: suggestion, label } : null;
|
|
546
|
+
}).filter((entry2) => entry2 !== null);
|
|
547
|
+
}
|
|
548
|
+
function readFollowupBarPayload(payload) {
|
|
549
|
+
const raw = asRecord(payload);
|
|
550
|
+
return {
|
|
551
|
+
query: asString(raw.query) ?? "",
|
|
552
|
+
chips: readChips(raw.chips),
|
|
553
|
+
suggestions: readSuggestions(raw.suggestions),
|
|
554
|
+
notice: asString(raw.notice) ?? ""
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
var NONE_DISMISSED = /* @__PURE__ */ new Set();
|
|
558
|
+
function visible(products, dismissed) {
|
|
559
|
+
return dismissed.size === 0 ? products : products.filter((product) => !dismissed.has(product.handle));
|
|
560
|
+
}
|
|
561
|
+
function visibleOne(product, dismissed) {
|
|
562
|
+
return product !== null && dismissed.has(product.handle) ? null : product;
|
|
563
|
+
}
|
|
564
|
+
function countRegionProducts(region, dismissed = NONE_DISMISSED) {
|
|
565
|
+
const raw = asRecord(region.payload);
|
|
566
|
+
switch (region.module) {
|
|
567
|
+
case "search:product-grid":
|
|
568
|
+
case "search:shortlist":
|
|
569
|
+
case "search:strip":
|
|
570
|
+
return visible(readProducts(raw.products), dismissed).length;
|
|
571
|
+
case "search:group":
|
|
572
|
+
return Array.isArray(raw.groups) ? raw.groups.reduce(
|
|
573
|
+
(total, group) => total + visible(readProducts(asRecord(group).products), dismissed).length,
|
|
574
|
+
0
|
|
575
|
+
) : 0;
|
|
576
|
+
case "search:hero":
|
|
577
|
+
case "search:compare":
|
|
578
|
+
return visibleOne(readProduct(raw.product), dismissed) === null ? 0 : 1;
|
|
579
|
+
default:
|
|
580
|
+
return 0;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
function regionHasAnswer(region, dismissed = NONE_DISMISSED) {
|
|
584
|
+
if (countRegionProducts(region, dismissed) > 0) return true;
|
|
585
|
+
if (region.module !== "search:content") return false;
|
|
586
|
+
const raw = asRecord(region.payload);
|
|
587
|
+
return readStrings(raw.blocks).length > 0 || asString(raw.body) !== void 0;
|
|
588
|
+
}
|
|
589
|
+
function productTile(product, region, position) {
|
|
590
|
+
const href = safeProductHref(product.url);
|
|
591
|
+
const imageHref = product.imageUrl ? safeProductHref(product.imageUrl) : void 0;
|
|
592
|
+
const detail = {
|
|
593
|
+
handle: product.handle,
|
|
594
|
+
regionId: region.id,
|
|
595
|
+
module: region.module,
|
|
596
|
+
position
|
|
597
|
+
};
|
|
598
|
+
return html`
|
|
599
|
+
<div class="tile">
|
|
600
|
+
<a
|
|
601
|
+
class="tile-link"
|
|
602
|
+
href=${href}
|
|
603
|
+
data-handle=${product.handle}
|
|
604
|
+
data-region-id=${region.id}
|
|
605
|
+
data-position=${position}
|
|
606
|
+
data-unsafe-url=${href === UNSAFE_HREF_FALLBACK ? "true" : nothing}
|
|
607
|
+
@click=${(event) => {
|
|
608
|
+
dispatchFrom(event.currentTarget, RESULT_TAP_EVENT, detail);
|
|
609
|
+
}}
|
|
610
|
+
>
|
|
611
|
+
${imageHref && imageHref !== UNSAFE_HREF_FALLBACK ? html`<img class="tile-image" src=${imageHref} alt="" loading="lazy" />` : nothing}
|
|
612
|
+
<span class="tile-title">${product.title}</span>
|
|
613
|
+
${product.price ? html`<span class="tile-price">${product.price}</span>` : nothing}
|
|
614
|
+
</a>
|
|
615
|
+
<button
|
|
616
|
+
class="tile-dismiss"
|
|
617
|
+
type="button"
|
|
618
|
+
data-dismiss=${product.handle}
|
|
619
|
+
aria-label=${`Hide ${product.title}`}
|
|
620
|
+
@click=${(event) => {
|
|
621
|
+
const detail2 = { kind: "item_dismissed", value: product.handle };
|
|
622
|
+
dispatchFrom(event.currentTarget, FOLLOWUP_EVENT, detail2);
|
|
623
|
+
}}
|
|
624
|
+
>
|
|
625
|
+
<span aria-hidden="true">✕</span>
|
|
626
|
+
</button>
|
|
627
|
+
</div>
|
|
628
|
+
`;
|
|
629
|
+
}
|
|
630
|
+
function productList(products, region, className, positionOffset = 0) {
|
|
631
|
+
return html`
|
|
632
|
+
<div class=${className}>
|
|
633
|
+
${products.map((product, index) => productTile(product, region, positionOffset + index))}
|
|
634
|
+
</div>
|
|
635
|
+
`;
|
|
636
|
+
}
|
|
637
|
+
function followupBar(region) {
|
|
638
|
+
const { query, chips, suggestions, notice } = readFollowupBarPayload(region.payload);
|
|
639
|
+
return html`
|
|
640
|
+
<div class="followup">
|
|
641
|
+
<input
|
|
642
|
+
class="followup-input"
|
|
643
|
+
type="search"
|
|
644
|
+
aria-label="Refine your search"
|
|
645
|
+
.value=${query}
|
|
646
|
+
@keydown=${(event) => {
|
|
647
|
+
if (event.key !== "Enter") return;
|
|
648
|
+
const input = event.currentTarget;
|
|
649
|
+
const detail = { kind: "refine", value: input.value };
|
|
650
|
+
dispatchFrom(input, FOLLOWUP_EVENT, detail);
|
|
651
|
+
}}
|
|
652
|
+
/>
|
|
653
|
+
<div class="chips" role="group" aria-label="What we understood">
|
|
654
|
+
${chips.map(
|
|
655
|
+
(chip) => html`
|
|
656
|
+
<button
|
|
657
|
+
class="chip"
|
|
658
|
+
type="button"
|
|
659
|
+
data-chip=${chip.key}
|
|
660
|
+
aria-label=${`Remove ${chip.label}`}
|
|
661
|
+
@click=${(event) => {
|
|
662
|
+
const detail = { kind: "chip_removed", value: chip.key };
|
|
663
|
+
dispatchFrom(event.currentTarget, FOLLOWUP_EVENT, detail);
|
|
664
|
+
}}
|
|
665
|
+
>
|
|
666
|
+
<span>${chip.label}</span><span aria-hidden="true">✕</span>
|
|
667
|
+
</button>
|
|
668
|
+
`
|
|
669
|
+
)}
|
|
670
|
+
</div>
|
|
671
|
+
${// Server prose, under the chips it explains: "Nothing under $5. Here
|
|
672
|
+
// are the closest matches." No node at all when the plan sent none,
|
|
673
|
+
// so an empty line never pushes the results down.
|
|
674
|
+
notice ? html`<p class="followup-notice">${notice}</p>` : nothing}
|
|
675
|
+
<div class="suggestions" role="group" aria-label="Suggested follow-ups">
|
|
676
|
+
${suggestions.map(
|
|
677
|
+
(suggestion) => html`
|
|
678
|
+
<button
|
|
679
|
+
class="suggestion"
|
|
680
|
+
type="button"
|
|
681
|
+
data-suggestion=${suggestion.value}
|
|
682
|
+
@click=${(event) => {
|
|
683
|
+
const detail = { kind: "refine", value: suggestion.value };
|
|
684
|
+
dispatchFrom(event.currentTarget, FOLLOWUP_EVENT, detail);
|
|
685
|
+
}}
|
|
686
|
+
>
|
|
687
|
+
${suggestion.label}
|
|
688
|
+
</button>
|
|
689
|
+
`
|
|
690
|
+
)}
|
|
691
|
+
</div>
|
|
692
|
+
</div>
|
|
693
|
+
`;
|
|
694
|
+
}
|
|
695
|
+
function groupedPicks(region, dismissed) {
|
|
696
|
+
const raw = asRecord(region.payload);
|
|
697
|
+
const groups = Array.isArray(raw.groups) ? raw.groups : [];
|
|
698
|
+
let position = 0;
|
|
699
|
+
return html`
|
|
700
|
+
${groups.map((group) => {
|
|
701
|
+
const entry2 = asRecord(group);
|
|
702
|
+
const title = asString(entry2.title);
|
|
703
|
+
const products = visible(readProducts(entry2.products), dismissed);
|
|
704
|
+
const offset = position;
|
|
705
|
+
position += products.length;
|
|
706
|
+
return html`
|
|
707
|
+
<section class="group">
|
|
708
|
+
${title ? html`<h3 class="group-title">${title}</h3>` : nothing}
|
|
709
|
+
${productList(products, region, "tiles", offset)}
|
|
710
|
+
</section>
|
|
711
|
+
`;
|
|
712
|
+
})}
|
|
713
|
+
`;
|
|
714
|
+
}
|
|
715
|
+
function heroProduct(region, dismissed) {
|
|
716
|
+
const raw = asRecord(region.payload);
|
|
717
|
+
const product = visibleOne(readProduct(raw.product), dismissed);
|
|
718
|
+
if (!product) return nothing;
|
|
719
|
+
const reason = asString(raw.reason);
|
|
720
|
+
return html`
|
|
721
|
+
<div class="hero">
|
|
722
|
+
${productTile(product, region, 0)}
|
|
723
|
+
${reason ? html`<p class="hero-reason">${reason}</p>` : nothing}
|
|
724
|
+
</div>
|
|
725
|
+
`;
|
|
726
|
+
}
|
|
727
|
+
function compareColumn(region, dismissed) {
|
|
728
|
+
const raw = asRecord(region.payload);
|
|
729
|
+
const product = visibleOne(readProduct(raw.product), dismissed);
|
|
730
|
+
if (!product) return nothing;
|
|
731
|
+
const attributes = readStrings(raw.attributes);
|
|
732
|
+
return html`
|
|
733
|
+
<div class="compare">
|
|
734
|
+
${productTile(product, region, 0)}
|
|
735
|
+
<ul class="compare-attributes">
|
|
736
|
+
${attributes.map((attribute) => html`<li>${attribute}</li>`)}
|
|
737
|
+
</ul>
|
|
738
|
+
</div>
|
|
739
|
+
`;
|
|
740
|
+
}
|
|
741
|
+
function compactList(region, dismissed) {
|
|
742
|
+
const raw = asRecord(region.payload);
|
|
743
|
+
const title = asString(raw.title);
|
|
744
|
+
return html`
|
|
745
|
+
${title ? html`<h3 class="group-title">${title}</h3>` : nothing}
|
|
746
|
+
${productList(visible(readProducts(raw.products), dismissed), region, "tiles tiles-compact")}
|
|
747
|
+
`;
|
|
748
|
+
}
|
|
749
|
+
function contentBlocks(region) {
|
|
750
|
+
const raw = asRecord(region.payload);
|
|
751
|
+
const title = asString(raw.title);
|
|
752
|
+
const blocks = readStrings(raw.blocks);
|
|
753
|
+
const body = asString(raw.body);
|
|
754
|
+
return html`
|
|
755
|
+
${title ? html`<h3 class="group-title">${title}</h3>` : nothing}
|
|
756
|
+
${blocks.map((block) => html`<p class="content-block">${block}</p>`)}
|
|
757
|
+
${body ? html`<p class="content-block">${body}</p>` : nothing}
|
|
758
|
+
`;
|
|
759
|
+
}
|
|
760
|
+
function chatContainer() {
|
|
761
|
+
return html`<div class="chat" data-mount data-module="search:chat"></div>`;
|
|
762
|
+
}
|
|
763
|
+
function renderRegionModule(region, dismissed = NONE_DISMISSED) {
|
|
764
|
+
switch (region.module) {
|
|
765
|
+
case "search:followup-bar":
|
|
766
|
+
return followupBar(region);
|
|
767
|
+
case "search:product-grid":
|
|
768
|
+
return productList(
|
|
769
|
+
visible(readProducts(asRecord(region.payload).products), dismissed),
|
|
770
|
+
region,
|
|
771
|
+
"tiles"
|
|
772
|
+
);
|
|
773
|
+
case "search:group":
|
|
774
|
+
return groupedPicks(region, dismissed);
|
|
775
|
+
case "search:hero":
|
|
776
|
+
return heroProduct(region, dismissed);
|
|
777
|
+
case "search:compare":
|
|
778
|
+
return compareColumn(region, dismissed);
|
|
779
|
+
case "search:shortlist":
|
|
780
|
+
case "search:strip":
|
|
781
|
+
return compactList(region, dismissed);
|
|
782
|
+
case "search:content":
|
|
783
|
+
return contentBlocks(region);
|
|
784
|
+
case "search:chat":
|
|
785
|
+
return chatContainer();
|
|
786
|
+
default:
|
|
787
|
+
return nothing;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// src/SearchSurfaceElement.ts
|
|
792
|
+
var SEARCH_SURFACE_TAG = "syntro-search-surface";
|
|
793
|
+
var SEE_STORE_RESULTS_LABEL = "See store results";
|
|
794
|
+
var SKELETON_BLOCKS = {
|
|
795
|
+
query: 0,
|
|
796
|
+
primary: 6,
|
|
797
|
+
secondary: 3,
|
|
798
|
+
support: 2,
|
|
799
|
+
converse: 1
|
|
800
|
+
};
|
|
801
|
+
var SearchSurfaceElement = class extends LitElement {
|
|
802
|
+
constructor() {
|
|
803
|
+
super();
|
|
804
|
+
this._onSeeStoreResults = () => {
|
|
805
|
+
this.renderRoot.dispatchEvent(new CustomEvent(SEE_STORE_RESULTS_EVENT));
|
|
806
|
+
};
|
|
807
|
+
this._variant = null;
|
|
808
|
+
this._query = "";
|
|
809
|
+
this._filled = {};
|
|
810
|
+
this._suppressed = [];
|
|
811
|
+
this._dismissed = /* @__PURE__ */ new Set();
|
|
812
|
+
this._frozen = false;
|
|
813
|
+
this._announcement = "";
|
|
814
|
+
}
|
|
815
|
+
/**
|
|
816
|
+
* Paint the variant's skeleton. Ignored once the surface is frozen, because
|
|
817
|
+
* a server plan that lands after the first content has painted would
|
|
818
|
+
* otherwise throw away what the shopper is already reading.
|
|
819
|
+
*/
|
|
820
|
+
showSkeleton(variant, query) {
|
|
821
|
+
if (this._frozen) return;
|
|
822
|
+
this._variant = variant;
|
|
823
|
+
this._query = query;
|
|
824
|
+
this._filled = {};
|
|
825
|
+
this._suppressed = [];
|
|
826
|
+
this._announcement = "";
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* Fill one region in place. The region keeps its position, so the page does
|
|
830
|
+
* not reflow around the shopper. A region id this variant does not own, or
|
|
831
|
+
* one already retired by `suppressRegion`, has nowhere to paint.
|
|
832
|
+
*
|
|
833
|
+
* Returns whether the region actually painted. The runtime needs that answer
|
|
834
|
+
* to tell "the plan filled a region" from "the plan addressed a variant this
|
|
835
|
+
* surface is not showing": a dropped fill counted as content would leave the
|
|
836
|
+
* shopper on a skeleton the SDK had already scored as a success.
|
|
837
|
+
*/
|
|
838
|
+
fillRegion(region) {
|
|
839
|
+
if (!this._variant) return false;
|
|
840
|
+
if (this._suppressed.includes(region.id)) return false;
|
|
841
|
+
if (!skeletonRegions(this._variant).some((slot) => slot.id === region.id)) return false;
|
|
842
|
+
this.freeze();
|
|
843
|
+
this._filled = { ...this._filled, [region.id]: region };
|
|
844
|
+
this._announce();
|
|
845
|
+
return true;
|
|
846
|
+
}
|
|
847
|
+
/** Content this renderer can display, including payload validation and dismissals. */
|
|
848
|
+
getRegionContent(id) {
|
|
849
|
+
const region = this._filled[id];
|
|
850
|
+
return region ? {
|
|
851
|
+
productCount: countRegionProducts(region, this._dismissed),
|
|
852
|
+
hasAnswer: regionHasAnswer(region, this._dismissed)
|
|
853
|
+
} : { productCount: 0, hasAnswer: false };
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* Retire a slot the plan decided not to fill. The slot leaves the grid
|
|
857
|
+
* entirely — no skeleton, no `aria-busy`, nothing for the live region to
|
|
858
|
+
* count — and every other slot keeps its order. Left in place, a region that
|
|
859
|
+
* is never going to fill pulses beside real results for as long as the
|
|
860
|
+
* shopper reads them.
|
|
861
|
+
*
|
|
862
|
+
* Idempotent, ignored for an id this variant does not own, and allowed after
|
|
863
|
+
* the surface has frozen: suppression is not a repaint, it only removes a
|
|
864
|
+
* slot that was never going to say anything.
|
|
865
|
+
*/
|
|
866
|
+
suppressRegion(id) {
|
|
867
|
+
if (this._suppressed.includes(id)) return;
|
|
868
|
+
this._suppressed = [...this._suppressed, id];
|
|
869
|
+
if (this._filled[id]) {
|
|
870
|
+
const { [id]: _removed, ...rest } = this._filled;
|
|
871
|
+
this._filled = rest;
|
|
872
|
+
}
|
|
873
|
+
if (this._frozen) this._announce();
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* Hide one product, everywhere, for the rest of this surface.
|
|
877
|
+
*
|
|
878
|
+
* Optimistic on purpose: the shopper tapped the tile's own dismiss button,
|
|
879
|
+
* so the tile goes now rather than after a round trip that may legitimately
|
|
880
|
+
* come back with nothing to patch. And it OUTLIVES the payload it was tapped
|
|
881
|
+
* in — a later repaint of the same region, or a whole new plan on a surface
|
|
882
|
+
* with no server session, must not hand back the product just rejected.
|
|
883
|
+
*/
|
|
884
|
+
dismissProduct(handle) {
|
|
885
|
+
if (!handle || this._dismissed.has(handle)) return;
|
|
886
|
+
this._dismissed = /* @__PURE__ */ new Set([...this._dismissed, handle]);
|
|
887
|
+
if (this._frozen) this._announce();
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* Drop one chip from the follow-up bar without waiting for a server repaint.
|
|
891
|
+
*
|
|
892
|
+
* Only reachable when the bar is showing server chips and the surface has no
|
|
893
|
+
* live session to patch — an expired one, in practice. With a session the
|
|
894
|
+
* server repaints the bar and this is never called.
|
|
895
|
+
*/
|
|
896
|
+
removeChip(key) {
|
|
897
|
+
const filled = this._filledQueryRegion();
|
|
898
|
+
if (!filled) return;
|
|
899
|
+
const bar = readFollowupBarPayload(filled.payload);
|
|
900
|
+
const chips = bar.chips.filter((chip) => chip.key !== key);
|
|
901
|
+
if (chips.length === bar.chips.length) return;
|
|
902
|
+
this._filled = { ...this._filled, [filled.id]: { ...filled, payload: { ...bar, chips } } };
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* The container a widget mounts into for this region, or `null` when the
|
|
906
|
+
* region paints no mountable module.
|
|
907
|
+
*
|
|
908
|
+
* The surface renders `search:chat` as an empty box and nothing else: the
|
|
909
|
+
* conversation here is the SDK's own chat widget, mounted by the runtime
|
|
910
|
+
* through its widget registry, never a second chat implementation living in
|
|
911
|
+
* this package. This is the whole affordance that makes that possible.
|
|
912
|
+
*/
|
|
913
|
+
mountTarget(regionId) {
|
|
914
|
+
const section = Array.from(this.shadowRoot?.querySelectorAll("[data-region-role]") ?? []).find(
|
|
915
|
+
(node) => node.getAttribute("data-region-id") === regionId
|
|
916
|
+
);
|
|
917
|
+
return section?.querySelector("[data-mount]") ?? null;
|
|
918
|
+
}
|
|
919
|
+
/** The query region as the plan filled it, if the plan filled it at all. */
|
|
920
|
+
_filledQueryRegion() {
|
|
921
|
+
if (!this._variant) return null;
|
|
922
|
+
for (const slot of skeletonRegions(this._variant)) {
|
|
923
|
+
if (slot.role !== "query") continue;
|
|
924
|
+
const filled = this._filled[slot.id];
|
|
925
|
+
if (filled) return filled;
|
|
926
|
+
}
|
|
927
|
+
return null;
|
|
928
|
+
}
|
|
929
|
+
/** Hold the current variant for the rest of this turn. */
|
|
930
|
+
freeze() {
|
|
931
|
+
this._frozen = true;
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* Only ever called after a region has filled, so "nothing here" is a
|
|
935
|
+
* finished answer, not a loading state. The skeleton is what says "still
|
|
936
|
+
* loading", and it says it visually while the live region stays silent.
|
|
937
|
+
*/
|
|
938
|
+
_announce() {
|
|
939
|
+
const total = Object.values(this._filled).reduce(
|
|
940
|
+
(sum, region) => sum + countRegionProducts(region, this._dismissed),
|
|
941
|
+
0
|
|
942
|
+
);
|
|
943
|
+
const count = total === 0 ? "No results found." : total === 1 ? "1 result" : `${total} results`;
|
|
944
|
+
const query = this._filledQueryRegion();
|
|
945
|
+
const notice = query ? readFollowupBarPayload(query.payload).notice : "";
|
|
946
|
+
this._announcement = notice ? `${notice} ${count}` : count;
|
|
947
|
+
}
|
|
948
|
+
_renderQueryRegion(slot) {
|
|
949
|
+
const filled = this._filled[slot.id];
|
|
950
|
+
const module = filled?.module ?? "search:followup-bar";
|
|
951
|
+
const payload = module === "search:followup-bar" ? (() => {
|
|
952
|
+
const bar = readFollowupBarPayload(filled?.payload);
|
|
953
|
+
return { ...bar, query: bar.query || this._query };
|
|
954
|
+
})() : filled?.payload;
|
|
955
|
+
const region = { id: slot.id, role: slot.role, module, payload };
|
|
956
|
+
return html2`
|
|
957
|
+
<section class="region region-query" data-region-id=${slot.id} data-region-role="query">
|
|
958
|
+
<div class="query-bar">
|
|
959
|
+
${renderRegionModule(region, this._dismissed)}
|
|
960
|
+
<button
|
|
961
|
+
class="store-results"
|
|
962
|
+
type="button"
|
|
963
|
+
data-action="see-store-results"
|
|
964
|
+
@click=${this._onSeeStoreResults}
|
|
965
|
+
>
|
|
966
|
+
${SEE_STORE_RESULTS_LABEL}
|
|
967
|
+
</button>
|
|
968
|
+
</div>
|
|
969
|
+
</section>
|
|
970
|
+
`;
|
|
971
|
+
}
|
|
972
|
+
_renderRegion(slot) {
|
|
973
|
+
if (slot.role === "query") return this._renderQueryRegion(slot);
|
|
974
|
+
const filled = this._filled[slot.id];
|
|
975
|
+
return html2`
|
|
976
|
+
<section
|
|
977
|
+
class="region"
|
|
978
|
+
data-region-id=${slot.id}
|
|
979
|
+
data-region-role=${slot.role}
|
|
980
|
+
data-skeleton=${filled ? nothing2 : "true"}
|
|
981
|
+
aria-busy=${filled ? nothing2 : "true"}
|
|
982
|
+
>
|
|
983
|
+
${filled ? renderRegionModule(filled, this._dismissed) : this._renderSkeleton(slot.role)}
|
|
984
|
+
</section>
|
|
985
|
+
`;
|
|
986
|
+
}
|
|
987
|
+
_renderSkeleton(role) {
|
|
988
|
+
const blocks = Array.from({ length: SKELETON_BLOCKS[role] });
|
|
989
|
+
return html2`
|
|
990
|
+
<div class="skeleton" aria-hidden="true">
|
|
991
|
+
${blocks.map(() => html2`<div class="skeleton-block"></div>`)}
|
|
992
|
+
</div>
|
|
993
|
+
`;
|
|
994
|
+
}
|
|
995
|
+
render() {
|
|
996
|
+
if (!this._variant) return nothing2;
|
|
997
|
+
const slots = skeletonRegions(this._variant).filter(
|
|
998
|
+
(slot) => !this._suppressed.includes(slot.id)
|
|
999
|
+
);
|
|
1000
|
+
return html2`
|
|
1001
|
+
<div class="surface" data-variant=${this._variant}>
|
|
1002
|
+
<h2 class="sr-only">Search results</h2>
|
|
1003
|
+
<p class="sr-only" role="status" aria-live="polite">${this._announcement}</p>
|
|
1004
|
+
${repeat(
|
|
1005
|
+
slots,
|
|
1006
|
+
(slot) => slot.id,
|
|
1007
|
+
(slot) => this._renderRegion(slot)
|
|
1008
|
+
)}
|
|
1009
|
+
</div>
|
|
1010
|
+
`;
|
|
1011
|
+
}
|
|
1012
|
+
};
|
|
1013
|
+
SearchSurfaceElement.properties = {
|
|
1014
|
+
_variant: { state: true },
|
|
1015
|
+
_query: { state: true },
|
|
1016
|
+
_filled: { state: true },
|
|
1017
|
+
_suppressed: { state: true },
|
|
1018
|
+
_dismissed: { state: true },
|
|
1019
|
+
_frozen: { state: true },
|
|
1020
|
+
_announcement: { state: true }
|
|
1021
|
+
};
|
|
1022
|
+
SearchSurfaceElement.styles = css`
|
|
1023
|
+
:host {
|
|
1024
|
+
display: block;
|
|
1025
|
+
font-family: var(--sc-font-family, system-ui, -apple-system, sans-serif);
|
|
1026
|
+
color: var(--sc-tile-text-color, var(--sc-content-text-color, #1a1a1a));
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
.surface {
|
|
1030
|
+
display: grid;
|
|
1031
|
+
gap: var(--sc-tile-stack-gap, 1rem);
|
|
1032
|
+
padding: 1rem;
|
|
1033
|
+
box-sizing: border-box;
|
|
1034
|
+
min-height: 100%;
|
|
1035
|
+
background: var(--sc-canvas-background, var(--sc-content-background, #ffffff));
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
.sr-only {
|
|
1039
|
+
position: absolute;
|
|
1040
|
+
width: 1px;
|
|
1041
|
+
height: 1px;
|
|
1042
|
+
margin: -1px;
|
|
1043
|
+
padding: 0;
|
|
1044
|
+
overflow: hidden;
|
|
1045
|
+
clip-path: inset(50%);
|
|
1046
|
+
white-space: nowrap;
|
|
1047
|
+
border: 0;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
/* Sticky, never fixed. A fixed bar measures itself against the visual
|
|
1051
|
+
viewport, so it jumps when the mobile URL bar animates and hides under
|
|
1052
|
+
the soft keyboard. Sticky inside our own scroll container does not. */
|
|
1053
|
+
.region-query {
|
|
1054
|
+
position: sticky;
|
|
1055
|
+
top: 0;
|
|
1056
|
+
z-index: 1;
|
|
1057
|
+
background: var(--sc-canvas-background, var(--sc-content-background, #ffffff));
|
|
1058
|
+
padding-block: 0.5rem;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
.region {
|
|
1062
|
+
min-width: 0;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
.query-bar {
|
|
1066
|
+
display: flex;
|
|
1067
|
+
flex-wrap: wrap;
|
|
1068
|
+
align-items: center;
|
|
1069
|
+
gap: 0.5rem;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
.followup {
|
|
1073
|
+
display: flex;
|
|
1074
|
+
flex-wrap: wrap;
|
|
1075
|
+
align-items: center;
|
|
1076
|
+
gap: 0.5rem;
|
|
1077
|
+
flex: 1 1 14rem;
|
|
1078
|
+
min-width: 0;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
.followup-input {
|
|
1082
|
+
flex: 1 1 12rem;
|
|
1083
|
+
min-width: 0;
|
|
1084
|
+
min-height: 2.5rem;
|
|
1085
|
+
padding: 0.5rem 0.75rem;
|
|
1086
|
+
font: inherit;
|
|
1087
|
+
color: inherit;
|
|
1088
|
+
border: 1px solid var(--sc-tile-border, rgba(0, 0, 0, 0.2));
|
|
1089
|
+
border-radius: var(--sc-border-radius, 0.5rem);
|
|
1090
|
+
background: var(--sc-content-search-background, transparent);
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
.followup-notice {
|
|
1094
|
+
flex: 1 1 100%;
|
|
1095
|
+
margin: 0;
|
|
1096
|
+
font-size: var(--sc-tile-subtitle-size, 0.875rem);
|
|
1097
|
+
color: var(--sc-content-text-secondary-color, inherit);
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
.chips,
|
|
1101
|
+
.suggestions {
|
|
1102
|
+
display: flex;
|
|
1103
|
+
flex-wrap: wrap;
|
|
1104
|
+
gap: 0.5rem;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
.chip,
|
|
1108
|
+
.suggestion,
|
|
1109
|
+
.store-results {
|
|
1110
|
+
display: inline-flex;
|
|
1111
|
+
align-items: center;
|
|
1112
|
+
gap: 0.25rem;
|
|
1113
|
+
min-height: 1.75rem;
|
|
1114
|
+
min-width: 1.75rem;
|
|
1115
|
+
padding: 0.25rem 0.75rem;
|
|
1116
|
+
font: inherit;
|
|
1117
|
+
cursor: pointer;
|
|
1118
|
+
border-radius: var(--sc-border-radius, 0.5rem);
|
|
1119
|
+
border: 1px solid var(--sc-chip-border, rgba(0, 0, 0, 0.2));
|
|
1120
|
+
background: var(--sc-chip-background, transparent);
|
|
1121
|
+
color: var(--sc-chip-foreground, inherit);
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
.store-results {
|
|
1125
|
+
margin-inline-start: auto;
|
|
1126
|
+
border-color: var(--sc-color-primary, currentColor);
|
|
1127
|
+
color: var(--sc-color-primary, inherit);
|
|
1128
|
+
background: transparent;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
.tiles {
|
|
1132
|
+
display: grid;
|
|
1133
|
+
gap: var(--sc-tile-gap, 0.75rem);
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
.tile {
|
|
1137
|
+
position: relative;
|
|
1138
|
+
min-width: 0;
|
|
1139
|
+
border: 1px solid var(--sc-tile-border, rgba(0, 0, 0, 0.12));
|
|
1140
|
+
border-radius: var(--sc-tile-border-radius, var(--sc-border-radius, 0.5rem));
|
|
1141
|
+
background: var(--sc-tile-background, transparent);
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
.tile-link {
|
|
1145
|
+
display: flex;
|
|
1146
|
+
flex-direction: column;
|
|
1147
|
+
gap: 0.25rem;
|
|
1148
|
+
padding: var(--sc-tile-body-padding, 0.75rem);
|
|
1149
|
+
color: inherit;
|
|
1150
|
+
text-decoration: none;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
.tile-image {
|
|
1154
|
+
width: 100%;
|
|
1155
|
+
max-width: 100%;
|
|
1156
|
+
aspect-ratio: 1 / 1;
|
|
1157
|
+
object-fit: cover;
|
|
1158
|
+
border-radius: inherit;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
.tile-title {
|
|
1162
|
+
font-size: var(--sc-tile-title-size, 0.9375rem);
|
|
1163
|
+
font-weight: var(--sc-tile-title-weight, 600);
|
|
1164
|
+
color: var(--sc-tile-title-color, inherit);
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
.tile-price {
|
|
1168
|
+
font-size: var(--sc-tile-subtitle-size, 0.875rem);
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
.tile-dismiss {
|
|
1172
|
+
position: absolute;
|
|
1173
|
+
inset-block-start: 0.25rem;
|
|
1174
|
+
inset-inline-end: 0.25rem;
|
|
1175
|
+
display: inline-flex;
|
|
1176
|
+
align-items: center;
|
|
1177
|
+
justify-content: center;
|
|
1178
|
+
min-width: 1.75rem;
|
|
1179
|
+
min-height: 1.75rem;
|
|
1180
|
+
padding: 0;
|
|
1181
|
+
font: inherit;
|
|
1182
|
+
cursor: pointer;
|
|
1183
|
+
border: 0;
|
|
1184
|
+
border-radius: 50%;
|
|
1185
|
+
color: var(--sc-tile-dismiss-color, inherit);
|
|
1186
|
+
background: var(--sc-tile-dismiss-background, transparent);
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
.group-title,
|
|
1190
|
+
.hero-reason,
|
|
1191
|
+
.content-block {
|
|
1192
|
+
margin: 0 0 0.5rem;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
.group-title {
|
|
1196
|
+
font-size: var(--sc-tile-title-size, 0.9375rem);
|
|
1197
|
+
font-weight: 600;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
.compare-attributes {
|
|
1201
|
+
margin: 0.5rem 0 0;
|
|
1202
|
+
padding-inline-start: 1.1em;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
.skeleton {
|
|
1206
|
+
display: grid;
|
|
1207
|
+
gap: var(--sc-tile-gap, 0.75rem);
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
.skeleton-block {
|
|
1211
|
+
height: 4rem;
|
|
1212
|
+
border-radius: var(--sc-tile-border-radius, var(--sc-border-radius, 0.5rem));
|
|
1213
|
+
background: var(--sc-tile-background, rgba(0, 0, 0, 0.06));
|
|
1214
|
+
opacity: 0.6;
|
|
1215
|
+
animation: syntro-search-pulse 1.4s ease-in-out infinite;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
@keyframes syntro-search-pulse {
|
|
1219
|
+
50% {
|
|
1220
|
+
opacity: 0.25;
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
:focus-visible {
|
|
1225
|
+
outline: 2px solid var(--sc-color-primary, currentColor);
|
|
1226
|
+
outline-offset: 2px;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1230
|
+
.skeleton-block {
|
|
1231
|
+
animation: none;
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
@media (forced-colors: active) {
|
|
1236
|
+
.tile,
|
|
1237
|
+
.chip,
|
|
1238
|
+
.suggestion,
|
|
1239
|
+
.store-results,
|
|
1240
|
+
.followup-input {
|
|
1241
|
+
border: 1px solid CanvasText;
|
|
1242
|
+
}
|
|
1243
|
+
.skeleton-block {
|
|
1244
|
+
border: 1px solid CanvasText;
|
|
1245
|
+
background: Canvas;
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
/* Every variant is a single column below this width. Above it, each one
|
|
1250
|
+
takes the shape the plan chose. */
|
|
1251
|
+
@media (min-width: 641px) {
|
|
1252
|
+
.tiles {
|
|
1253
|
+
grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
.tiles-compact {
|
|
1257
|
+
grid-template-columns: minmax(0, 1fr);
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
.surface[data-variant='rail'] {
|
|
1261
|
+
grid-template-columns: minmax(0, 16rem) minmax(0, 1fr);
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
.surface[data-variant='rail'] > [data-region-id='query-1'],
|
|
1265
|
+
.surface[data-variant='rail'] > [data-region-id='converse-1'] {
|
|
1266
|
+
grid-column: 1 / -1;
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
.surface[data-variant='rail'] > [data-region-id='secondary-1'] {
|
|
1270
|
+
grid-column: 1;
|
|
1271
|
+
grid-row: 2 / span 2;
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
.surface[data-variant='rail'] > [data-region-id='primary-1'] {
|
|
1275
|
+
grid-column: 2;
|
|
1276
|
+
grid-row: 2;
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
.surface[data-variant='rail'] > [data-region-id='support-1'] {
|
|
1280
|
+
grid-column: 2;
|
|
1281
|
+
grid-row: 3;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
.surface[data-variant='compare'] {
|
|
1285
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
.surface[data-variant='compare'] > [data-region-id='query-1'],
|
|
1289
|
+
.surface[data-variant='compare'] > [data-region-id='secondary-1'],
|
|
1290
|
+
.surface[data-variant='compare'] > [data-region-id='converse-1'] {
|
|
1291
|
+
grid-column: 1 / -1;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
.surface[data-variant='compare'] > [data-region-id='primary-1'] {
|
|
1295
|
+
grid-column: 1;
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
.surface[data-variant='compare'] > [data-region-id='primary-2'] {
|
|
1299
|
+
grid-column: 2;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
.surface[data-variant='hero'] > [data-region-id='secondary-1'] .tiles {
|
|
1303
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
`;
|
|
1307
|
+
function registerSearchSurfaceElement() {
|
|
1308
|
+
if (typeof customElements === "undefined") return;
|
|
1309
|
+
if (!customElements.get(SEARCH_SURFACE_TAG)) {
|
|
1310
|
+
customElements.define(SEARCH_SURFACE_TAG, SearchSurfaceElement);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
// src/runtime.ts
|
|
1315
|
+
registerSearchSurfaceElement();
|
|
1316
|
+
var SearchSurfaceMountable = {
|
|
1317
|
+
mount(container, config) {
|
|
1318
|
+
const surfaceConfig = stripMountPlumbing(config ?? null);
|
|
1319
|
+
const element = document.createElement("syntro-search-surface");
|
|
1320
|
+
container.appendChild(element);
|
|
1321
|
+
if (surfaceConfig?.variant) {
|
|
1322
|
+
element.showSkeleton(surfaceConfig.variant, surfaceConfig.query ?? "");
|
|
1323
|
+
}
|
|
1324
|
+
return () => element.remove();
|
|
1325
|
+
}
|
|
1326
|
+
};
|
|
1327
|
+
var runtime = {
|
|
1328
|
+
id: "adaptive-search",
|
|
1329
|
+
version: "1.0.0",
|
|
1330
|
+
name: "Adaptive Search",
|
|
1331
|
+
description: "Draws every search result as a Syntro surface.",
|
|
1332
|
+
/** No DOM-mutation executors: this surface renders only. */
|
|
1333
|
+
executors: [],
|
|
1334
|
+
widgets: [
|
|
1335
|
+
{
|
|
1336
|
+
id: "adaptive-search:surface",
|
|
1337
|
+
component: SearchSurfaceMountable,
|
|
1338
|
+
metadata: {
|
|
1339
|
+
name: "Search Surface",
|
|
1340
|
+
description: "The search results surface, painted per variant and filled region by region",
|
|
1341
|
+
icon: "\u{1F50E}",
|
|
1342
|
+
/**
|
|
1343
|
+
* The surface replaces a whole results page, so it needs the
|
|
1344
|
+
* full-viewport slot. It is not self-sufficient anywhere else: the
|
|
1345
|
+
* scrolling and scroll containment it relies on come from
|
|
1346
|
+
* `overlay_full`'s own slot styles (`overflow: auto` plus
|
|
1347
|
+
* `overscroll-behavior: contain`), not from this element.
|
|
1348
|
+
*/
|
|
1349
|
+
slots: ["overlay_full"]
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
]
|
|
1353
|
+
};
|
|
1354
|
+
var runtime_default = runtime;
|
|
1355
|
+
|
|
1356
|
+
export {
|
|
1357
|
+
SearchSurfaceMountable,
|
|
1358
|
+
runtime,
|
|
1359
|
+
runtime_default
|
|
1360
|
+
};
|
|
1361
|
+
/*! Bundled license information:
|
|
1362
|
+
|
|
1363
|
+
@lit/context/lib/context-request-event.js:
|
|
1364
|
+
@lit/context/lib/create-context.js:
|
|
1365
|
+
@lit/context/lib/controllers/context-consumer.js:
|
|
1366
|
+
@lit/context/lib/value-notifier.js:
|
|
1367
|
+
@lit/context/lib/controllers/context-provider.js:
|
|
1368
|
+
@lit/context/lib/context-root.js:
|
|
1369
|
+
(**
|
|
1370
|
+
* @license
|
|
1371
|
+
* Copyright 2021 Google LLC
|
|
1372
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
1373
|
+
*)
|
|
1374
|
+
|
|
1375
|
+
@lit/context/lib/decorators/provide.js:
|
|
1376
|
+
(**
|
|
1377
|
+
* @license
|
|
1378
|
+
* Copyright 2017 Google LLC
|
|
1379
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
1380
|
+
*)
|
|
1381
|
+
|
|
1382
|
+
@lit/context/lib/decorators/consume.js:
|
|
1383
|
+
(**
|
|
1384
|
+
* @license
|
|
1385
|
+
* Copyright 2022 Google LLC
|
|
1386
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
1387
|
+
*)
|
|
1388
|
+
*/
|
|
1389
|
+
//# sourceMappingURL=chunk-3KPQ5AOG.js.map
|