@runtypelabs/persona 4.4.2 → 4.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/types.ts CHANGED
@@ -847,26 +847,76 @@ export type AgentWidgetArtifactsFeature = {
847
847
  /**
848
848
  * How the transcript scrolls while an assistant response streams in.
849
849
  *
850
- * - `"follow"` (default): keep the newest content pinned to the bottom of the
851
- * viewport, pausing when the user scrolls up and resuming when they return
852
- * to the bottom.
853
- * - `"anchor-top"`: on send, scroll the user's message near the top of the
854
- * viewport and hold it there while the response streams in beneath it
855
- * (ChatGPT-style). The transcript never auto-scrolls during streaming.
850
+ * - `"anchor-top"` (default): on send, scroll the user's message near the top
851
+ * of the viewport and hold it there while the response streams in beneath it
852
+ * (ChatGPT-style). When a turn has no user send to anchor to (a proactive
853
+ * greeting, an injected assistant message, a resubmit, or first-load
854
+ * streaming), it falls back to `"follow"` for that turn so content never
855
+ * streams in off-screen.
856
+ * - `"follow"`: keep the newest content pinned to the bottom of the viewport,
857
+ * pausing when the user scrolls up and resuming when they return to the
858
+ * bottom. This was the default before 4.x.
856
859
  * - `"none"`: never auto-scroll; the scroll-to-bottom affordance is the only
857
860
  * way back to the latest content.
858
861
  */
859
862
  export type AgentWidgetScrollMode = "follow" | "anchor-top" | "none";
860
863
 
864
+ /**
865
+ * Where the transcript lands when a *saved* conversation is reopened (restored
866
+ * from the storage adapter / `initialMessages`), as opposed to a fresh send.
867
+ *
868
+ * - `"bottom"` (default): jump to the absolute end of the transcript, matching
869
+ * the historical behavior.
870
+ * - `"last-user-turn"`: pin the last user message near the top of the viewport
871
+ * (the last point the reader was actively driving the conversation) so a long
872
+ * restored thread opens at a readable place rather than at the very bottom.
873
+ */
874
+ export type AgentWidgetScrollRestorePosition = "bottom" | "last-user-turn";
875
+
861
876
  export type AgentWidgetScrollBehaviorFeature = {
862
- /** Scroll behavior during streamed responses. @default "follow" */
877
+ /** Scroll behavior during streamed responses. @default "anchor-top" */
863
878
  mode?: AgentWidgetScrollMode;
864
879
  /**
865
880
  * Gap (px) kept between the anchored user message and the top of the
866
- * viewport in `"anchor-top"` mode.
881
+ * viewport in `"anchor-top"` mode. Also used as the gap for
882
+ * `restorePosition: "last-user-turn"`.
867
883
  * @default 16
868
884
  */
869
885
  anchorTopOffset?: number;
886
+ /**
887
+ * Where to land when a saved conversation reopens. Additive and opt-in: the
888
+ * default preserves the historical "jump to the bottom" behavior.
889
+ * @default "bottom"
890
+ */
891
+ restorePosition?: AgentWidgetScrollRestorePosition;
892
+ /**
893
+ * When true, interactions beyond text selection — keyboard navigation
894
+ * (PageUp/PageDown/Home/End/arrows) inside the transcript and focusing a
895
+ * link or other interactive element within it — also pause auto-follow in
896
+ * `"follow"` mode. Treats "the reader is doing something here" as intent to
897
+ * stay put, not just "the reader dragged a selection". Opt-in; default keeps
898
+ * only the existing wheel/scroll/selection triggers.
899
+ * @default false
900
+ */
901
+ pauseOnInteraction?: boolean;
902
+ /**
903
+ * When true, the scroll-to-bottom affordance also surfaces new-content and
904
+ * still-streaming activity while the reader is pinned away from the latest
905
+ * content in `"anchor-top"` mode. Lets the reader know a response is arriving
906
+ * offscreen below. Defaults on alongside the `"anchor-top"` default; set
907
+ * `false` to keep the count + streaming hint silent while pinned.
908
+ * @default true
909
+ */
910
+ showActivityWhilePinned?: boolean;
911
+ /**
912
+ * When true, Persona maintains a visually-hidden `aria-live="polite"` region
913
+ * that announces important transcript events — a response starting, a
914
+ * response finishing, and "N new messages below" while the reader is scrolled
915
+ * away — at a comfortable cadence (never token-by-token). Opt-in so existing
916
+ * embeds don't suddenly gain screen-reader announcements.
917
+ * @default false
918
+ */
919
+ announce?: boolean;
870
920
  };
871
921
 
872
922
  export type AgentWidgetScrollToBottomFeature = {
@@ -0,0 +1,451 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
4
+
5
+ import { createAgentExperience } from "./ui";
6
+ import type { AgentWidgetConfig, AgentWidgetMessage } from "./types";
7
+
8
+ type RafCallback = (time: number) => void;
9
+
10
+ const CREATED_AT = "2026-03-29T00:00:00.000Z";
11
+
12
+ const createMount = () => {
13
+ const mount = document.createElement("div");
14
+ document.body.appendChild(mount);
15
+ return mount;
16
+ };
17
+
18
+ const getScrollContainer = (mount: HTMLElement) =>
19
+ mount.querySelector<HTMLElement>("#persona-scroll-container")!;
20
+
21
+ const getScrollToBottomButton = (mount: HTMLElement) =>
22
+ mount.querySelector<HTMLElement>("[data-persona-scroll-to-bottom]")!;
23
+
24
+ const getCountBadge = (mount: HTMLElement) =>
25
+ mount.querySelector<HTMLElement>("[data-persona-scroll-to-bottom-count]")!;
26
+
27
+ const getLiveRegion = (mount: HTMLElement) =>
28
+ mount.querySelector<HTMLElement>("[data-persona-live-region]");
29
+
30
+ const installRafMock = () => {
31
+ let nextId = 1;
32
+ const callbacks = new Map<number, RafCallback>();
33
+ let now = 0;
34
+ vi.stubGlobal("requestAnimationFrame", (cb: RafCallback) => {
35
+ const id = nextId++;
36
+ callbacks.set(id, cb);
37
+ return id;
38
+ });
39
+ vi.stubGlobal("cancelAnimationFrame", (id: number) => {
40
+ callbacks.delete(id);
41
+ });
42
+ return {
43
+ flush(maxFrames = 80) {
44
+ let frames = 0;
45
+ while (callbacks.size > 0 && frames < maxFrames) {
46
+ const pending = [...callbacks.values()];
47
+ callbacks.clear();
48
+ frames += 1;
49
+ now += 16;
50
+ pending.forEach((cb) => cb(now));
51
+ }
52
+ },
53
+ };
54
+ };
55
+
56
+ const installScrollMetrics = (
57
+ element: HTMLElement,
58
+ initial: { scrollHeight: number; clientHeight: number }
59
+ ) => {
60
+ let scrollTop = 0;
61
+ let scrollHeight = initial.scrollHeight;
62
+ const clientHeight = initial.clientHeight;
63
+ Object.defineProperties(element, {
64
+ scrollTop: {
65
+ configurable: true,
66
+ get: () => scrollTop,
67
+ set: (value: number) => {
68
+ scrollTop = Math.max(0, Math.min(value, Math.max(0, scrollHeight - clientHeight)));
69
+ },
70
+ },
71
+ scrollHeight: { configurable: true, get: () => scrollHeight },
72
+ clientHeight: { configurable: true, get: () => clientHeight },
73
+ });
74
+ return {
75
+ getScrollTop: () => scrollTop,
76
+ getBottom: () => Math.max(0, scrollHeight - clientHeight),
77
+ setScrollTop: (v: number) => {
78
+ element.scrollTop = v;
79
+ },
80
+ setScrollHeight: (v: number) => {
81
+ scrollHeight = v;
82
+ if (scrollTop > scrollHeight - clientHeight) {
83
+ scrollTop = Math.max(0, scrollHeight - clientHeight);
84
+ }
85
+ },
86
+ };
87
+ };
88
+
89
+ const emitStreamingMessage = (
90
+ controller: ReturnType<typeof createAgentExperience>,
91
+ id: string,
92
+ content: string
93
+ ) => {
94
+ controller.injectTestMessage({
95
+ type: "message",
96
+ message: { id, role: "assistant", content, createdAt: CREATED_AT, streaming: true },
97
+ });
98
+ };
99
+
100
+ const emitAssistantMessage = (
101
+ controller: ReturnType<typeof createAgentExperience>,
102
+ id: string,
103
+ content: string
104
+ ) => {
105
+ controller.injectTestMessage({
106
+ type: "message",
107
+ message: { id, role: "assistant", content, createdAt: CREATED_AT },
108
+ });
109
+ };
110
+
111
+ const emitUserMessage = (
112
+ controller: ReturnType<typeof createAgentExperience>,
113
+ id: string,
114
+ content: string
115
+ ) => {
116
+ controller.injectTestMessage({
117
+ type: "message",
118
+ message: { id, role: "user", content, createdAt: CREATED_AT },
119
+ });
120
+ };
121
+
122
+ const emitStreamingAssistant = (
123
+ controller: ReturnType<typeof createAgentExperience>,
124
+ id: string,
125
+ content: string
126
+ ) => {
127
+ controller.injectTestMessage({
128
+ type: "message",
129
+ message: { id, role: "assistant", content, createdAt: CREATED_AT, streaming: true },
130
+ });
131
+ };
132
+
133
+ // Establish a real anchor-top turn: a prior assistant message seeds
134
+ // send-detection, then a user send anchors. The next assistant message is the
135
+ // anchored response (so it does NOT hit the no-anchor follow fallback) — the
136
+ // scenario `showActivityWhilePinned` is about: the answer streaming in below a
137
+ // pinned question the reader is still reading from the top.
138
+ const anchorUserTurn = (
139
+ controller: ReturnType<typeof createAgentExperience>
140
+ ) => {
141
+ emitAssistantMessage(controller, "seed", "Earlier reply");
142
+ emitUserMessage(controller, "u1", "Question");
143
+ };
144
+
145
+ const baseConfig = (overrides: AgentWidgetConfig): AgentWidgetConfig => ({
146
+ apiUrl: "https://api.example.com/chat",
147
+ launcher: { enabled: false },
148
+ // Hermetic: never restore persisted history at construction. Otherwise a
149
+ // prior test's transcript (sharing message ids like "u1") leaks via
150
+ // localStorage and a user send reads as already-seen, so the anchor never
151
+ // takes and the assertion sees a follow-to-bottom instead.
152
+ persistState: false,
153
+ ...overrides,
154
+ });
155
+
156
+ afterEach(() => {
157
+ document.body.innerHTML = "";
158
+ localStorage.clear();
159
+ vi.restoreAllMocks();
160
+ vi.unstubAllGlobals();
161
+ vi.useRealTimers();
162
+ });
163
+
164
+ describe("scrollBehavior.pauseOnInteraction (Principle 3)", () => {
165
+ beforeEach(() => {
166
+ installRafMock();
167
+ });
168
+
169
+ it("pauses auto-follow on a transcript navigation keypress when enabled", () => {
170
+ const raf = installRafMock();
171
+ const mount = createMount();
172
+ const controller = createAgentExperience(
173
+ mount,
174
+ baseConfig({ features: { scrollBehavior: { mode: "follow", pauseOnInteraction: true } } })
175
+ );
176
+ const sc = getScrollContainer(mount);
177
+ const metrics = installScrollMetrics(sc, { scrollHeight: 1000, clientHeight: 400 });
178
+
179
+ emitStreamingMessage(controller, "a1", "First chunk");
180
+ raf.flush();
181
+ expect(metrics.getScrollTop()).toBe(metrics.getBottom());
182
+
183
+ sc.dispatchEvent(new KeyboardEvent("keydown", { key: "PageUp", bubbles: true }));
184
+
185
+ metrics.setScrollHeight(1080);
186
+ emitStreamingMessage(controller, "a1", "First chunk + more");
187
+ raf.flush();
188
+
189
+ // Paused: the stream no longer chases the bottom.
190
+ expect(metrics.getScrollTop()).toBe(600);
191
+ controller.destroy();
192
+ });
193
+
194
+ it("does NOT pause on keypress when the option is off (default)", () => {
195
+ const raf = installRafMock();
196
+ const mount = createMount();
197
+ const controller = createAgentExperience(
198
+ mount,
199
+ baseConfig({ features: { scrollBehavior: { mode: "follow" } } })
200
+ );
201
+ const sc = getScrollContainer(mount);
202
+ const metrics = installScrollMetrics(sc, { scrollHeight: 1000, clientHeight: 400 });
203
+
204
+ emitStreamingMessage(controller, "a1", "First chunk");
205
+ raf.flush();
206
+
207
+ sc.dispatchEvent(new KeyboardEvent("keydown", { key: "PageUp", bubbles: true }));
208
+
209
+ metrics.setScrollHeight(1080);
210
+ emitStreamingMessage(controller, "a1", "First chunk + more");
211
+ raf.flush();
212
+
213
+ // Still following: chases the new bottom.
214
+ expect(metrics.getScrollTop()).toBe(metrics.getBottom());
215
+ controller.destroy();
216
+ });
217
+ });
218
+
219
+ describe("scrollBehavior.showActivityWhilePinned (Principle 8)", () => {
220
+ beforeEach(() => {
221
+ installRafMock();
222
+ });
223
+
224
+ it("counts the anchored response arriving below the pinned turn by default", () => {
225
+ const mount = createMount();
226
+ // showActivityWhilePinned now defaults on (alongside the anchor-top default).
227
+ const controller = createAgentExperience(
228
+ mount,
229
+ baseConfig({ features: { scrollBehavior: { mode: "anchor-top" } } })
230
+ );
231
+ const sc = getScrollContainer(mount);
232
+ const metrics = installScrollMetrics(sc, { scrollHeight: 1000, clientHeight: 400 });
233
+ anchorUserTurn(controller);
234
+ metrics.setScrollTop(0); // reading the pinned question, away from the bottom
235
+
236
+ emitAssistantMessage(controller, "a1", "Arrived below");
237
+
238
+ expect(getCountBadge(mount).textContent).toBe("1");
239
+ expect(getScrollToBottomButton(mount).getAttribute("aria-label")).toContain("1 new");
240
+ controller.destroy();
241
+ });
242
+
243
+ it("stays silent when showActivityWhilePinned is disabled", () => {
244
+ const mount = createMount();
245
+ const controller = createAgentExperience(
246
+ mount,
247
+ baseConfig({
248
+ features: {
249
+ scrollBehavior: { mode: "anchor-top", showActivityWhilePinned: false },
250
+ },
251
+ })
252
+ );
253
+ const sc = getScrollContainer(mount);
254
+ const metrics = installScrollMetrics(sc, { scrollHeight: 1000, clientHeight: 400 });
255
+ anchorUserTurn(controller);
256
+ metrics.setScrollTop(0);
257
+
258
+ emitAssistantMessage(controller, "a1", "Arrived below");
259
+
260
+ expect(getCountBadge(mount).textContent).toBe("");
261
+ controller.destroy();
262
+ });
263
+ });
264
+
265
+ describe("scrollBehavior anchor-top no-anchor fallback", () => {
266
+ let raf: ReturnType<typeof installRafMock>;
267
+ beforeEach(() => {
268
+ raf = installRafMock();
269
+ });
270
+
271
+ const makeAnchorTop = (mount: HTMLElement) =>
272
+ createAgentExperience(
273
+ mount,
274
+ baseConfig({ features: { scrollBehavior: { mode: "anchor-top" } } })
275
+ );
276
+
277
+ it("follows to the bottom for an assistant turn with no user anchor", () => {
278
+ const mount = createMount();
279
+ const controller = makeAnchorTop(mount);
280
+ const sc = getScrollContainer(mount);
281
+ const metrics = installScrollMetrics(sc, { scrollHeight: 1000, clientHeight: 400 });
282
+
283
+ // A proactive/first-load assistant stream with no preceding user send: it
284
+ // has no anchor, so it falls back to follow-to-bottom rather than streaming
285
+ // in off-screen.
286
+ emitStreamingAssistant(controller, "a1", "Proactive reply");
287
+ raf.flush();
288
+
289
+ expect(metrics.getScrollTop()).toBe(metrics.getBottom());
290
+ controller.destroy();
291
+ });
292
+
293
+ it("anchors near the top (does not follow) when the turn follows a user send", () => {
294
+ const mount = createMount();
295
+ const controller = makeAnchorTop(mount);
296
+ const sc = getScrollContainer(mount);
297
+ const metrics = installScrollMetrics(sc, { scrollHeight: 1000, clientHeight: 400 });
298
+
299
+ anchorUserTurn(controller); // seed + user send → real anchor
300
+ raf.flush();
301
+ emitStreamingAssistant(controller, "a1", "Answer below the pinned question");
302
+ raf.flush();
303
+
304
+ // The anchored response is pinned near the top (jsdom offsetTop 0 → 0), not
305
+ // chased to the bottom.
306
+ expect(metrics.getScrollTop()).toBe(0);
307
+ controller.destroy();
308
+ });
309
+
310
+ it("keeps follow-on assistant content in an anchored turn pinned (no late-embed yank)", () => {
311
+ const mount = createMount();
312
+ const controller = makeAnchorTop(mount);
313
+ const sc = getScrollContainer(mount);
314
+ const metrics = installScrollMetrics(sc, { scrollHeight: 1000, clientHeight: 400 });
315
+
316
+ anchorUserTurn(controller); // user send → anchor
317
+ raf.flush();
318
+ emitAssistantMessage(controller, "a1", "Anchored answer");
319
+ raf.flush();
320
+ metrics.setScrollTop(0); // reading the pinned question from the top
321
+
322
+ // A second assistant message in the same anchored conversation — a
323
+ // multi-part reply or a late-injected embed (tweet/image/tool result) — must
324
+ // NOT re-arm the fallback or yank the viewport to the bottom.
325
+ emitStreamingAssistant(controller, "a2", "Late-injected embed content");
326
+ raf.flush();
327
+
328
+ expect(metrics.getScrollTop()).toBe(0);
329
+ controller.destroy();
330
+ });
331
+ });
332
+
333
+ describe("scrollBehavior.restorePosition (Principle 11)", () => {
334
+ beforeEach(() => {
335
+ installRafMock();
336
+ });
337
+
338
+ const history: AgentWidgetMessage[] = [
339
+ { id: "u1", role: "user", content: "First question", createdAt: CREATED_AT },
340
+ { id: "a1", role: "assistant", content: "First answer", createdAt: CREATED_AT },
341
+ { id: "u2", role: "user", content: "Second question", createdAt: CREATED_AT },
342
+ { id: "a2", role: "assistant", content: "Second answer", createdAt: CREATED_AT },
343
+ ];
344
+
345
+ it("pins the last user message near the top on open when set to last-user-turn", () => {
346
+ const raf = installRafMock();
347
+ const mount = createMount();
348
+ const controller = createAgentExperience(
349
+ mount,
350
+ baseConfig({
351
+ launcher: { enabled: true, autoExpand: false },
352
+ initialMessages: history,
353
+ features: { scrollBehavior: { mode: "follow", restorePosition: "last-user-turn" } },
354
+ })
355
+ );
356
+ const sc = getScrollContainer(mount);
357
+ const metrics = installScrollMetrics(sc, { scrollHeight: 1000, clientHeight: 400 });
358
+
359
+ // jsdom computes no layout; give the last user bubble a known offsetTop so
360
+ // the anchor geometry has something to target.
361
+ const lastUserBubble = sc.querySelector<HTMLElement>('[data-message-id="u2"]')!;
362
+ Object.defineProperty(lastUserBubble, "offsetTop", { configurable: true, get: () => 320 });
363
+
364
+ controller.open();
365
+ raf.flush();
366
+
367
+ // 320 - 16 (anchorTopOffset) = 304, above the bottom (600).
368
+ expect(metrics.getScrollTop()).toBe(304);
369
+ controller.destroy();
370
+ });
371
+
372
+ it("jumps to the bottom on open by default", () => {
373
+ const raf = installRafMock();
374
+ const mount = createMount();
375
+ const controller = createAgentExperience(
376
+ mount,
377
+ baseConfig({
378
+ launcher: { enabled: true, autoExpand: false },
379
+ initialMessages: history,
380
+ features: { scrollBehavior: { mode: "follow" } },
381
+ })
382
+ );
383
+ const sc = getScrollContainer(mount);
384
+ const metrics = installScrollMetrics(sc, { scrollHeight: 1000, clientHeight: 400 });
385
+
386
+ controller.open();
387
+ raf.flush();
388
+
389
+ expect(metrics.getScrollTop()).toBe(metrics.getBottom());
390
+ controller.destroy();
391
+ });
392
+ });
393
+
394
+ describe("scrollBehavior.announce (Principle 15)", () => {
395
+ it("always creates a polite live region", () => {
396
+ installRafMock();
397
+ const mount = createMount();
398
+ const controller = createAgentExperience(mount, baseConfig({}));
399
+ const region = getLiveRegion(mount);
400
+ expect(region).not.toBeNull();
401
+ expect(region!.getAttribute("aria-live")).toBe("polite");
402
+ controller.destroy();
403
+ });
404
+
405
+ it("announces new-content arrival at a debounced cadence when enabled", () => {
406
+ vi.useFakeTimers();
407
+ const raf = installRafMock();
408
+ const mount = createMount();
409
+ const controller = createAgentExperience(
410
+ mount,
411
+ baseConfig({ features: { scrollBehavior: { mode: "follow", announce: true } } })
412
+ );
413
+ const sc = getScrollContainer(mount);
414
+ const metrics = installScrollMetrics(sc, { scrollHeight: 1000, clientHeight: 400 });
415
+
416
+ emitStreamingMessage(controller, "seed", "hi");
417
+ raf.flush();
418
+ // Scroll up so the next message counts as "below".
419
+ metrics.setScrollTop(100);
420
+ sc.dispatchEvent(new Event("scroll"));
421
+
422
+ emitAssistantMessage(controller, "a1", "Arrived below");
423
+ vi.advanceTimersByTime(400);
424
+
425
+ expect(getLiveRegion(mount)!.textContent).toContain("new message");
426
+ controller.destroy();
427
+ });
428
+
429
+ it("stays silent when announce is off (default)", () => {
430
+ vi.useFakeTimers();
431
+ const raf = installRafMock();
432
+ const mount = createMount();
433
+ const controller = createAgentExperience(
434
+ mount,
435
+ baseConfig({ features: { scrollBehavior: { mode: "follow" } } })
436
+ );
437
+ const sc = getScrollContainer(mount);
438
+ const metrics = installScrollMetrics(sc, { scrollHeight: 1000, clientHeight: 400 });
439
+
440
+ emitStreamingMessage(controller, "seed", "hi");
441
+ raf.flush();
442
+ metrics.setScrollTop(100);
443
+ sc.dispatchEvent(new Event("scroll"));
444
+
445
+ emitAssistantMessage(controller, "a1", "Arrived below");
446
+ vi.advanceTimersByTime(400);
447
+
448
+ expect(getLiveRegion(mount)!.textContent).toBe("");
449
+ controller.destroy();
450
+ });
451
+ });
@@ -912,9 +912,12 @@ describe("createAgentExperience streaming scroll", () => {
912
912
  it("re-sticks to the bottom when the user sends a message after scrolling up", () => {
913
913
  const raf = installRafMock();
914
914
  const mount = createMount();
915
+ // Follow-specific: a user send re-sticks to the bottom (anchor-top, the
916
+ // default, would pin the sent message near the top instead).
915
917
  const controller = createAgentExperience(mount, {
916
918
  apiUrl: "https://api.example.com/chat",
917
- launcher: { enabled: false }
919
+ launcher: { enabled: false },
920
+ features: { scrollBehavior: { mode: "follow" } }
918
921
  });
919
922
 
920
923
  const scrollContainer = mount.querySelector<HTMLElement>("#persona-scroll-container");
@@ -941,9 +944,12 @@ describe("createAgentExperience streaming scroll", () => {
941
944
  it("shows a count badge for messages that arrive while paused", () => {
942
945
  const raf = installRafMock();
943
946
  const mount = createMount();
947
+ // Follow-specific paused-badge semantics (wheel-up pauses auto-follow, then
948
+ // a message arrives while paused and below the fold).
944
949
  const controller = createAgentExperience(mount, {
945
950
  apiUrl: "https://api.example.com/chat",
946
- launcher: { enabled: false }
951
+ launcher: { enabled: false },
952
+ features: { scrollBehavior: { mode: "follow" } }
947
953
  });
948
954
 
949
955
  const scrollContainer = mount.querySelector<HTMLElement>("#persona-scroll-container");