@runtypelabs/persona 4.7.0 → 4.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/animations/glyph-cycle.cjs +2 -2
- package/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/glyph-cycle.js +2 -2
- package/dist/animations/{types-CSmiKRVa.d.cts → types-4ROVJ1gA.d.cts} +85 -3
- package/dist/animations/{types-CSmiKRVa.d.ts → types-4ROVJ1gA.d.ts} +85 -3
- package/dist/animations/wipe.cjs +2 -2
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/chunk-5EIIHQLQ.js +1 -0
- package/dist/chunk-IO5VVUKP.js +3 -0
- package/dist/chunk-IPVK3KOM.js +1 -0
- package/dist/chunk-UPO4GUFC.js +1 -0
- package/dist/codegen.cjs +13 -13
- package/dist/codegen.js +14 -14
- package/dist/context-mentions-7S5KVUTG.js +169 -0
- package/dist/context-mentions-inline-TTCN7ZM2.js +4 -0
- package/dist/context-mentions-inline.cjs +4 -0
- package/dist/context-mentions-inline.d.cts +203 -0
- package/dist/context-mentions-inline.d.ts +203 -0
- package/dist/context-mentions-inline.js +4 -0
- package/dist/context-mentions.cjs +295 -0
- package/dist/context-mentions.d.cts +7025 -0
- package/dist/context-mentions.d.ts +7025 -0
- package/dist/context-mentions.js +295 -0
- package/dist/index.cjs +99 -68
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1507 -182
- package/dist/index.d.ts +1507 -182
- package/dist/index.global.js +88 -56
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +99 -68
- package/dist/index.js.map +1 -1
- package/dist/install.global.js +1 -1
- package/dist/install.global.js.map +1 -1
- package/dist/launcher.global.js +3 -2
- package/dist/launcher.global.js.map +1 -1
- package/dist/markdown-parsers.js +24 -24
- package/dist/plugin-kit.cjs +1 -1
- package/dist/plugin-kit.d.cts +17 -0
- package/dist/plugin-kit.d.ts +17 -0
- package/dist/plugin-kit.js +1 -1
- package/dist/runtype-tts-entry-UJAEF7NZ.js +1 -0
- package/dist/runtype-tts.js +1 -1
- package/dist/session-reconnect-JKIJBHS5.js +1 -0
- package/dist/smart-dom-reader.cjs +18 -17
- package/dist/smart-dom-reader.d.cts +1260 -16
- package/dist/smart-dom-reader.d.ts +1260 -16
- package/dist/smart-dom-reader.js +18 -17
- package/dist/testing.cjs +3 -3
- package/dist/testing.js +3 -3
- package/dist/theme-editor-preview.cjs +260 -58
- package/dist/theme-editor-preview.d.cts +1234 -15
- package/dist/theme-editor-preview.d.ts +1234 -15
- package/dist/theme-editor-preview.js +87 -58
- package/dist/theme-editor.cjs +10 -10
- package/dist/theme-editor.d.cts +1226 -15
- package/dist/theme-editor.d.ts +1226 -15
- package/dist/theme-editor.js +10 -10
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +76 -0
- package/dist/theme-reference.d.ts +76 -0
- package/dist/theme-reference.js +1 -1
- package/dist/voice-worklet-player.cjs +2 -2
- package/dist/voice-worklet-player.js +2 -2
- package/dist/webmcp-polyfill.js +2 -2
- package/dist/widget.css +1 -1
- package/package.json +16 -5
- package/src/artifacts-session.test.ts +178 -0
- package/src/client.test.ts +255 -1
- package/src/client.ts +157 -60
- package/src/components/artifact-card.test.ts +333 -0
- package/src/components/artifact-card.ts +75 -28
- package/src/components/artifact-inline.test.ts +1328 -0
- package/src/components/artifact-inline.ts +920 -0
- package/src/components/artifact-pane.test.ts +1042 -0
- package/src/components/artifact-pane.ts +440 -131
- package/src/components/artifact-preview.test.ts +1155 -0
- package/src/components/artifact-preview.ts +994 -0
- package/src/components/composer-parts.ts +3 -12
- package/src/components/context-mention-button.test.ts +70 -0
- package/src/components/context-mention-button.ts +82 -0
- package/src/components/context-mention-chip.ts +134 -0
- package/src/components/context-mention-menu.test.ts +508 -0
- package/src/components/context-mention-menu.ts +0 -0
- package/src/components/message-bubble.test.ts +175 -0
- package/src/components/message-bubble.ts +177 -19
- package/src/components/panel.ts +7 -10
- package/src/components/pill-composer-builder.test.ts +6 -2
- package/src/components/pill-composer-builder.ts +6 -6
- package/src/components/reasoning-bubble.ts +1 -13
- package/src/components/registry.ts +38 -3
- package/src/components/tool-bubble.ts +1 -13
- package/src/context-mentions-bundle.test.ts +163 -0
- package/src/context-mentions-entry.ts +185 -0
- package/src/context-mentions-inline-entry.test.ts +136 -0
- package/src/context-mentions-inline-entry.ts +226 -0
- package/src/context-mentions-inline-loader.test.ts +30 -0
- package/src/context-mentions-inline-loader.ts +36 -0
- package/src/context-mentions-inline.ts +15 -0
- package/src/context-mentions-loader.ts +32 -0
- package/src/context-mentions.ts +16 -0
- package/src/defaults.ts +1 -0
- package/src/generated/runtype-openapi-contract.ts +55 -3
- package/src/index-core.ts +46 -1
- package/src/index-global.ts +51 -0
- package/src/index.ts +8 -0
- package/src/markdown-parsers-loader.test.ts +158 -0
- package/src/markdown-parsers-loader.ts +83 -27
- package/src/plugin-kit.test.ts +40 -0
- package/src/plugin-kit.ts +39 -5
- package/src/runtime/host-layout.test.ts +163 -0
- package/src/runtime/host-layout.ts +110 -7
- package/src/runtime/init.ts +18 -61
- package/src/session.mentions.test.ts +175 -0
- package/src/session.test.ts +52 -4
- package/src/session.ts +197 -27
- package/src/smart-dom-reader.test.ts +129 -2
- package/src/smart-dom-reader.ts +127 -1
- package/src/styles/context-mention-menu-css.ts +176 -0
- package/src/styles/widget.css +1002 -152
- package/src/theme-editor/preview.ts +2 -0
- package/src/theme-editor/sections.test.ts +26 -1
- package/src/theme-editor/sections.ts +13 -5
- package/src/theme-reference.ts +2 -2
- package/src/tool-call-display-defaults.test.ts +1 -0
- package/src/types/theme.ts +79 -0
- package/src/types.ts +1041 -17
- package/src/ui.artifact-pane-gating.test.ts +636 -0
- package/src/ui.component-directive.test.ts +104 -0
- package/src/ui.composer-bar.test.ts +60 -2
- package/src/ui.detached-panel.test.ts +1049 -0
- package/src/ui.mention-submit.test.ts +235 -0
- package/src/ui.tool-display.test.ts +51 -0
- package/src/ui.ts +998 -159
- package/src/utils/artifact-custom-actions.ts +128 -0
- package/src/utils/artifact-display.test.ts +42 -0
- package/src/utils/artifact-display.ts +84 -0
- package/src/utils/artifact-file.test.ts +116 -0
- package/src/utils/artifact-file.ts +117 -0
- package/src/utils/artifact-gate.test.ts +112 -5
- package/src/utils/artifact-gate.ts +39 -14
- package/src/utils/artifact-loading-status.ts +55 -0
- package/src/utils/artifact-status-label.ts +190 -0
- package/src/utils/buttons.ts +7 -1
- package/src/utils/chunk-loader.test.ts +97 -0
- package/src/utils/chunk-loader.ts +88 -0
- package/src/utils/code-highlight.test.ts +186 -0
- package/src/utils/code-highlight.ts +400 -0
- package/src/utils/composer-contenteditable.test.ts +507 -0
- package/src/utils/composer-contenteditable.ts +626 -0
- package/src/utils/composer-document.test.ts +280 -0
- package/src/utils/composer-document.ts +293 -0
- package/src/utils/composer-history.test.ts +35 -7
- package/src/utils/composer-history.ts +30 -20
- package/src/utils/composer-input.ts +159 -0
- package/src/utils/context-mention-controller.test.ts +1215 -0
- package/src/utils/context-mention-controller.ts +1186 -0
- package/src/utils/context-mention-manager.test.ts +422 -0
- package/src/utils/context-mention-manager.ts +410 -0
- package/src/utils/context-mention-orchestrator.test.ts +538 -0
- package/src/utils/context-mention-orchestrator.ts +348 -0
- package/src/utils/icons.ts +2 -0
- package/src/utils/live-region.test.ts +108 -0
- package/src/utils/live-region.ts +94 -0
- package/src/utils/mention-channels.ts +63 -0
- package/src/utils/mention-llm-format.test.ts +91 -0
- package/src/utils/mention-llm-format.ts +79 -0
- package/src/utils/mention-matcher.test.ts +86 -0
- package/src/utils/mention-matcher.ts +221 -0
- package/src/utils/mention-token.ts +72 -0
- package/src/utils/mention-trigger.test.ts +155 -0
- package/src/utils/mention-trigger.ts +156 -0
- package/src/utils/roving-tablist.test.ts +152 -0
- package/src/utils/roving-tablist.ts +111 -0
- package/src/utils/spinner.ts +45 -0
- package/src/utils/theme.test.ts +76 -4
- package/src/utils/theme.ts +7 -0
- package/src/utils/tokens.ts +118 -11
- package/src/utils/tool-loading-animation.test.ts +32 -0
- package/src/utils/tool-loading-animation.ts +24 -0
- package/dist/chunk-DFBSCFYN.js +0 -1
- package/dist/runtype-tts-entry-HFUV2UF7.js +0 -1
- package/dist/session-reconnect-U77QFUR7.js +0 -1
package/src/defaults.ts
CHANGED
|
@@ -22,6 +22,7 @@ export type RuntypeExecutionStreamEvent = ({
|
|
|
22
22
|
totalSteps?: number;
|
|
23
23
|
type: "execution_start";
|
|
24
24
|
}) | ({
|
|
25
|
+
claudeManagedAgentId?: string;
|
|
25
26
|
completedAt?: string;
|
|
26
27
|
durationMs?: number;
|
|
27
28
|
executionId: string;
|
|
@@ -66,6 +67,15 @@ export type RuntypeExecutionStreamEvent = ({
|
|
|
66
67
|
completedAt?: string;
|
|
67
68
|
content?: string;
|
|
68
69
|
cost?: number;
|
|
70
|
+
estimatedContextBreakdown?: {
|
|
71
|
+
categories: Record<string, {
|
|
72
|
+
chars: number;
|
|
73
|
+
estimatedTokens: number;
|
|
74
|
+
}>;
|
|
75
|
+
contextWindowSize?: number;
|
|
76
|
+
estimatedTotalTokens: number;
|
|
77
|
+
estimatedUtilizationPercent?: number;
|
|
78
|
+
};
|
|
69
79
|
executionId: string;
|
|
70
80
|
fallback?: {
|
|
71
81
|
attempts: Array<{
|
|
@@ -103,6 +113,7 @@ export type RuntypeExecutionStreamEvent = ({
|
|
|
103
113
|
type: "step_start";
|
|
104
114
|
} | ({
|
|
105
115
|
completedAt?: string;
|
|
116
|
+
cost?: number;
|
|
106
117
|
durationMs?: number;
|
|
107
118
|
error?: string;
|
|
108
119
|
executionId: string;
|
|
@@ -208,6 +219,11 @@ export type RuntypeExecutionStreamEvent = ({
|
|
|
208
219
|
artifactType: "markdown" | "component";
|
|
209
220
|
component?: string;
|
|
210
221
|
executionId?: string;
|
|
222
|
+
file?: {
|
|
223
|
+
language?: string;
|
|
224
|
+
mimeType: string;
|
|
225
|
+
path: string;
|
|
226
|
+
};
|
|
211
227
|
id: string;
|
|
212
228
|
seq?: number;
|
|
213
229
|
title?: string;
|
|
@@ -316,6 +332,14 @@ export type RuntypeExecutionStreamEvent = ({
|
|
|
316
332
|
awaitReason?: string;
|
|
317
333
|
awaitedAt?: string;
|
|
318
334
|
crawlId?: string;
|
|
335
|
+
elicitation?: {
|
|
336
|
+
message: string;
|
|
337
|
+
mode: "form" | "url";
|
|
338
|
+
pauseCount?: number;
|
|
339
|
+
requestedSchema?: Record<string, unknown>;
|
|
340
|
+
serverName?: string;
|
|
341
|
+
url?: string;
|
|
342
|
+
};
|
|
319
343
|
executionId: string;
|
|
320
344
|
origin?: "webmcp" | "sdk";
|
|
321
345
|
pageOrigin?: string;
|
|
@@ -416,6 +440,14 @@ export type RuntypeFlowSSEEvent = {
|
|
|
416
440
|
awaitReason?: string;
|
|
417
441
|
awaitedAt: string;
|
|
418
442
|
crawlId?: string;
|
|
443
|
+
elicitation?: {
|
|
444
|
+
message: string;
|
|
445
|
+
mode: "form" | "url";
|
|
446
|
+
pauseCount?: number;
|
|
447
|
+
requestedSchema?: Record<string, unknown>;
|
|
448
|
+
serverName?: string;
|
|
449
|
+
url?: string;
|
|
450
|
+
};
|
|
419
451
|
executionId?: string;
|
|
420
452
|
flowId: string;
|
|
421
453
|
origin?: "webmcp" | "sdk";
|
|
@@ -466,7 +498,7 @@ export type RuntypeFlowSSEEvent = {
|
|
|
466
498
|
completedAt?: string;
|
|
467
499
|
duration?: number;
|
|
468
500
|
durationMs?: number;
|
|
469
|
-
error?: string;
|
|
501
|
+
error?: string | null;
|
|
470
502
|
executionId?: string;
|
|
471
503
|
executionTime?: number;
|
|
472
504
|
id?: string;
|
|
@@ -510,13 +542,21 @@ export type RuntypeFlowSSEEvent = {
|
|
|
510
542
|
totalSteps: number;
|
|
511
543
|
type: "step_skip";
|
|
512
544
|
when: string;
|
|
513
|
-
} | {
|
|
545
|
+
} | ({
|
|
546
|
+
elicitation?: {
|
|
547
|
+
message: string;
|
|
548
|
+
mode: "form" | "url";
|
|
549
|
+
pauseCount?: number;
|
|
550
|
+
requestedSchema?: Record<string, unknown>;
|
|
551
|
+
serverName?: string;
|
|
552
|
+
url?: string;
|
|
553
|
+
};
|
|
514
554
|
executionId?: string;
|
|
515
555
|
reason?: string;
|
|
516
556
|
seq?: number;
|
|
517
557
|
type: "step_await";
|
|
518
558
|
[key: string]: unknown;
|
|
519
|
-
} | ({
|
|
559
|
+
}) | ({
|
|
520
560
|
agentContext?: {
|
|
521
561
|
executionId: string;
|
|
522
562
|
iteration: number;
|
|
@@ -735,6 +775,18 @@ export type RuntypeClientChatRequest = {
|
|
|
735
775
|
export type RuntypeClientChatStreamEvent = RuntypeExecutionStreamEvent;
|
|
736
776
|
|
|
737
777
|
export type RuntypeClientResumeRequest = {
|
|
778
|
+
clientTools?: Array<{
|
|
779
|
+
description: string;
|
|
780
|
+
name: string;
|
|
781
|
+
origin?: "webmcp" | "sdk";
|
|
782
|
+
pageOrigin?: string;
|
|
783
|
+
parametersSchema: {
|
|
784
|
+
type: "object";
|
|
785
|
+
[key: string]: unknown;
|
|
786
|
+
};
|
|
787
|
+
untrustedContentHint?: boolean;
|
|
788
|
+
}>;
|
|
789
|
+
clientToolsFingerprint?: string;
|
|
738
790
|
executionId: string;
|
|
739
791
|
messages?: Array<{
|
|
740
792
|
content: string | (Array<{
|
package/src/index-core.ts
CHANGED
|
@@ -10,8 +10,12 @@ export type {
|
|
|
10
10
|
AgentWidgetFeatureFlags,
|
|
11
11
|
AgentWidgetArtifactsFeature,
|
|
12
12
|
AgentWidgetArtifactsLayoutConfig,
|
|
13
|
+
PersonaArtifactDisplayMode,
|
|
13
14
|
PersonaArtifactKind,
|
|
14
15
|
PersonaArtifactRecord,
|
|
16
|
+
PersonaArtifactActionContext,
|
|
17
|
+
PersonaArtifactStatusLabelContext,
|
|
18
|
+
PersonaArtifactCustomAction,
|
|
15
19
|
PersonaArtifactManualUpsert,
|
|
16
20
|
ArtifactConfigPayload,
|
|
17
21
|
AgentWidgetInitOptions,
|
|
@@ -25,6 +29,25 @@ export type {
|
|
|
25
29
|
// Context provider types (e.g. for config.contextProviders)
|
|
26
30
|
AgentWidgetContextProvider,
|
|
27
31
|
AgentWidgetContextProviderContext,
|
|
32
|
+
// Context mention types (e.g. for config.contextMentions)
|
|
33
|
+
AgentWidgetContextMentionConfig,
|
|
34
|
+
AgentWidgetContextMentionSource,
|
|
35
|
+
AgentWidgetContextMentionItem,
|
|
36
|
+
AgentWidgetContextMentionPayload,
|
|
37
|
+
AgentWidgetContextMentionRef,
|
|
38
|
+
AgentWidgetContextMentionSearchContext,
|
|
39
|
+
AgentWidgetContextMentionResolveContext,
|
|
40
|
+
AgentWidgetContextMentionMenuRenderContext,
|
|
41
|
+
AgentWidgetContextMentionItemRenderContext,
|
|
42
|
+
AgentWidgetContextMentionChipRenderContext,
|
|
43
|
+
AgentWidgetContextMentionTokenRenderContext,
|
|
44
|
+
// Per-mention LLM block formatter entry (config.contextMentions.llmFormat)
|
|
45
|
+
AgentWidgetMentionLlmEntry,
|
|
46
|
+
// Skills / slash-commands (config.contextMentions.triggers)
|
|
47
|
+
AgentWidgetMentionTriggerChannel,
|
|
48
|
+
AgentWidgetMentionTriggerPosition,
|
|
49
|
+
AgentWidgetContextMentionCommandContext,
|
|
50
|
+
AgentWidgetContextMentionComposerCapability,
|
|
28
51
|
AgentWidgetCustomFetch,
|
|
29
52
|
AgentWidgetSSEEventParser,
|
|
30
53
|
AgentWidgetSSEEventResult,
|
|
@@ -188,6 +211,17 @@ export {
|
|
|
188
211
|
createDirectivePostprocessor
|
|
189
212
|
} from "./postprocessors";
|
|
190
213
|
export type { MarkdownProcessorOptions } from "./postprocessors";
|
|
214
|
+
// Markdown parser-readiness escape hatch. On the IIFE/CDN build `marked` +
|
|
215
|
+
// `DOMPurify` load lazily, so content injected right after init can render as
|
|
216
|
+
// escaped plain text until the chunk resolves. Hosts can `await
|
|
217
|
+
// loadMarkdownParsers()` before injecting, or `onMarkdownParsersReady(cb)` to be
|
|
218
|
+
// told when they land (both resolve/no-op instantly on the npm build, where the
|
|
219
|
+
// parsers are bundled eagerly).
|
|
220
|
+
export {
|
|
221
|
+
loadMarkdownParsers,
|
|
222
|
+
onMarkdownParsersReady
|
|
223
|
+
} from "./markdown-parsers-loader";
|
|
224
|
+
export type { MarkdownParsersModule } from "./markdown-parsers-loader";
|
|
191
225
|
export {
|
|
192
226
|
createDefaultSanitizer,
|
|
193
227
|
resolveSanitizer
|
|
@@ -211,6 +245,13 @@ export {
|
|
|
211
245
|
fileToImagePart,
|
|
212
246
|
validateImageFile
|
|
213
247
|
} from "./utils/content";
|
|
248
|
+
// Context mention helpers (for building config.contextMentions.sources)
|
|
249
|
+
export {
|
|
250
|
+
defaultMentionFilter,
|
|
251
|
+
createStaticMentionSource,
|
|
252
|
+
createSlashCommandsSource,
|
|
253
|
+
} from "./utils/mention-matcher";
|
|
254
|
+
export type { SlashCommandDefinition } from "./utils/mention-matcher";
|
|
214
255
|
export {
|
|
215
256
|
collectEnrichedPageContext,
|
|
216
257
|
formatEnrichedContext,
|
|
@@ -361,7 +402,11 @@ export type {
|
|
|
361
402
|
|
|
362
403
|
// Component system exports
|
|
363
404
|
export { componentRegistry } from "./components/registry";
|
|
364
|
-
export type {
|
|
405
|
+
export type {
|
|
406
|
+
ComponentRenderer,
|
|
407
|
+
ComponentContext,
|
|
408
|
+
ComponentRegistrationOptions
|
|
409
|
+
} from "./components/registry";
|
|
365
410
|
export {
|
|
366
411
|
createComponentStreamParser,
|
|
367
412
|
isComponentDirectiveType
|
package/src/index-global.ts
CHANGED
|
@@ -137,3 +137,54 @@ setRuntypeTtsLoader(() => {
|
|
|
137
137
|
}
|
|
138
138
|
return import(/* @vite-ignore */ chunkUrl);
|
|
139
139
|
});
|
|
140
|
+
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
// Deferred Context Mentions loading.
|
|
143
|
+
//
|
|
144
|
+
// This bundle is built with `./context-mentions-entry` external (see
|
|
145
|
+
// `tsup.global.config.ts`): the mention controller/manager/menu runtime is kept
|
|
146
|
+
// out of the CDN payload. Register a loader that imports the self-contained
|
|
147
|
+
// `context-mentions.js` chunk from a sibling URL; the orchestrator calls it on
|
|
148
|
+
// the first `@`/affordance-button interaction (and can prefetch on composer
|
|
149
|
+
// focus) only when `contextMentions.enabled`. Same pattern as the chunks above.
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
import { setContextMentionsLoader } from "./context-mentions-loader";
|
|
153
|
+
import { setContextMentionsInlineLoader } from "./context-mentions-inline-loader";
|
|
154
|
+
|
|
155
|
+
setContextMentionsLoader(() => {
|
|
156
|
+
const chunkUrl = widgetScriptSrc?.replace(
|
|
157
|
+
/index\.global\.js($|\?)/,
|
|
158
|
+
"context-mentions.js$1",
|
|
159
|
+
);
|
|
160
|
+
if (!chunkUrl || chunkUrl === widgetScriptSrc) {
|
|
161
|
+
return Promise.reject(
|
|
162
|
+
new Error(
|
|
163
|
+
"Could not derive the context-mentions.js URL from the widget script URL " +
|
|
164
|
+
`(${widgetScriptSrc ?? "unavailable"}). Self-hosted deployments that ` +
|
|
165
|
+
"rename index.global.js should host context-mentions.js alongside it.",
|
|
166
|
+
),
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
return import(/* @vite-ignore */ chunkUrl);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Sibling loader for the inline-mention contenteditable chunk, loaded on composer
|
|
173
|
+
// mount when `contextMentions.display === "inline"`. Same sibling-URL scheme.
|
|
174
|
+
setContextMentionsInlineLoader(() => {
|
|
175
|
+
const chunkUrl = widgetScriptSrc?.replace(
|
|
176
|
+
/index\.global\.js($|\?)/,
|
|
177
|
+
"context-mentions-inline.js$1",
|
|
178
|
+
);
|
|
179
|
+
if (!chunkUrl || chunkUrl === widgetScriptSrc) {
|
|
180
|
+
return Promise.reject(
|
|
181
|
+
new Error(
|
|
182
|
+
"Could not derive the context-mentions-inline.js URL from the widget " +
|
|
183
|
+
`script URL (${widgetScriptSrc ?? "unavailable"}). Self-hosted ` +
|
|
184
|
+
"deployments that rename index.global.js should host " +
|
|
185
|
+
"context-mentions-inline.js alongside it.",
|
|
186
|
+
),
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
return import(/* @vite-ignore */ chunkUrl);
|
|
190
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -37,3 +37,11 @@ export type {
|
|
|
37
37
|
DemoCarouselOptions,
|
|
38
38
|
DemoCarouselHandle
|
|
39
39
|
} from "./components/demo-carousel";
|
|
40
|
+
|
|
41
|
+
// Accessible roving-tabindex tablist helper for custom artifact tab bars
|
|
42
|
+
// (features.artifacts.renderTabBar).
|
|
43
|
+
export { createRovingTablist } from "./utils/roving-tablist";
|
|
44
|
+
export type {
|
|
45
|
+
RovingTablistController,
|
|
46
|
+
RovingTablistOptions
|
|
47
|
+
} from "./utils/roving-tablist";
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { MarkdownParsersModule } from "./markdown-parsers-loader";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Tests for the parser-ready subscription registry.
|
|
6
|
+
*
|
|
7
|
+
* The loader holds module-level singleton state (`moduleCache`, subscribers), and
|
|
8
|
+
* `vitest.setup.ts` eager-provides parsers into the shared graph — which would make
|
|
9
|
+
* `onMarkdownParsersReady` no-op everywhere. So each test imports a FRESH copy of
|
|
10
|
+
* the loader via `vi.resetModules()` + dynamic import, starting from the unloaded
|
|
11
|
+
* (IIFE/CDN) state, and drives the transition itself.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// A stand-in parsers module; the registry never touches its fields.
|
|
15
|
+
const FAKE_PARSERS = {} as unknown as MarkdownParsersModule;
|
|
16
|
+
|
|
17
|
+
const freshLoader = async () => {
|
|
18
|
+
vi.resetModules();
|
|
19
|
+
return import("./markdown-parsers-loader");
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
vi.restoreAllMocks();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe("onMarkdownParsersReady", () => {
|
|
27
|
+
it("fires a pending subscriber once when the lazy chunk resolves", async () => {
|
|
28
|
+
const loader = await freshLoader();
|
|
29
|
+
loader.setMarkdownParsersLoader(() => Promise.resolve(FAKE_PARSERS));
|
|
30
|
+
|
|
31
|
+
const cb = vi.fn();
|
|
32
|
+
loader.onMarkdownParsersReady(cb);
|
|
33
|
+
expect(cb).not.toHaveBeenCalled(); // not yet: chunk not loaded
|
|
34
|
+
|
|
35
|
+
await loader.loadMarkdownParsers();
|
|
36
|
+
expect(cb).toHaveBeenCalledTimes(1);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("kicks the load itself so a lone surface still heals", async () => {
|
|
40
|
+
const loader = await freshLoader();
|
|
41
|
+
const load = vi.fn(() => Promise.resolve(FAKE_PARSERS));
|
|
42
|
+
loader.setMarkdownParsersLoader(load);
|
|
43
|
+
|
|
44
|
+
const cb = vi.fn();
|
|
45
|
+
// No explicit loadMarkdownParsers() call — subscribing must trigger it.
|
|
46
|
+
loader.onMarkdownParsersReady(cb);
|
|
47
|
+
await vi.waitFor(() => expect(cb).toHaveBeenCalledTimes(1));
|
|
48
|
+
expect(load).toHaveBeenCalledTimes(1);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("does not fire (and returns a no-op) when parsers are already loaded", async () => {
|
|
52
|
+
const loader = await freshLoader();
|
|
53
|
+
loader.provideMarkdownParsers(FAKE_PARSERS); // eager/ESM steady state
|
|
54
|
+
|
|
55
|
+
const cb = vi.fn();
|
|
56
|
+
const unsub = loader.onMarkdownParsersReady(cb);
|
|
57
|
+
await Promise.resolve();
|
|
58
|
+
expect(cb).not.toHaveBeenCalled();
|
|
59
|
+
expect(() => unsub()).not.toThrow();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("unsubscribe prevents the callback from firing", async () => {
|
|
63
|
+
const loader = await freshLoader();
|
|
64
|
+
loader.setMarkdownParsersLoader(() => Promise.resolve(FAKE_PARSERS));
|
|
65
|
+
|
|
66
|
+
const cb = vi.fn();
|
|
67
|
+
const unsub = loader.onMarkdownParsersReady(cb);
|
|
68
|
+
unsub();
|
|
69
|
+
|
|
70
|
+
await loader.loadMarkdownParsers();
|
|
71
|
+
expect(cb).not.toHaveBeenCalled();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("fires each subscriber exactly once (no re-fire on a later load call)", async () => {
|
|
75
|
+
const loader = await freshLoader();
|
|
76
|
+
loader.setMarkdownParsersLoader(() => Promise.resolve(FAKE_PARSERS));
|
|
77
|
+
|
|
78
|
+
const cb = vi.fn();
|
|
79
|
+
loader.onMarkdownParsersReady(cb);
|
|
80
|
+
|
|
81
|
+
await loader.loadMarkdownParsers();
|
|
82
|
+
await loader.loadMarkdownParsers(); // cached; must not re-notify
|
|
83
|
+
expect(cb).toHaveBeenCalledTimes(1);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("provideMarkdownParsers (eager) flushes pending subscribers", async () => {
|
|
87
|
+
const loader = await freshLoader();
|
|
88
|
+
// Subscribe while still unloaded, then have the eager path provide.
|
|
89
|
+
loader.setMarkdownParsersLoader(() => new Promise(() => {})); // never resolves
|
|
90
|
+
const cb = vi.fn();
|
|
91
|
+
loader.onMarkdownParsersReady(cb);
|
|
92
|
+
|
|
93
|
+
loader.provideMarkdownParsers(FAKE_PARSERS);
|
|
94
|
+
expect(cb).toHaveBeenCalledTimes(1);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("keeps pending subscribers across a failed chunk load so a successful retry heals them", async () => {
|
|
98
|
+
const loader = await freshLoader();
|
|
99
|
+
let attempt = 0;
|
|
100
|
+
loader.setMarkdownParsersLoader(() => {
|
|
101
|
+
attempt += 1;
|
|
102
|
+
return attempt === 1
|
|
103
|
+
? Promise.reject(new Error("chunk 404"))
|
|
104
|
+
: Promise.resolve(FAKE_PARSERS);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const cb = vi.fn();
|
|
108
|
+
loader.onMarkdownParsersReady(cb);
|
|
109
|
+
await expect(loader.loadMarkdownParsers()).rejects.toThrow("chunk 404");
|
|
110
|
+
// Failed attempt: not fired, but still registered.
|
|
111
|
+
expect(cb).not.toHaveBeenCalled();
|
|
112
|
+
|
|
113
|
+
// A later subscribe re-triggers the load (rejected promise was not cached)
|
|
114
|
+
// and the retry heals BOTH the new subscriber and the one that rendered
|
|
115
|
+
// escaped during the failed attempt.
|
|
116
|
+
const cb2 = vi.fn();
|
|
117
|
+
loader.onMarkdownParsersReady(cb2);
|
|
118
|
+
await vi.waitFor(() => expect(cb2).toHaveBeenCalledTimes(1));
|
|
119
|
+
expect(cb).toHaveBeenCalledTimes(1);
|
|
120
|
+
expect(attempt).toBe(2);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("unsubscribing after a failed load still prevents firing on the retry", async () => {
|
|
124
|
+
const loader = await freshLoader();
|
|
125
|
+
let attempt = 0;
|
|
126
|
+
loader.setMarkdownParsersLoader(() => {
|
|
127
|
+
attempt += 1;
|
|
128
|
+
return attempt === 1
|
|
129
|
+
? Promise.reject(new Error("chunk 404"))
|
|
130
|
+
: Promise.resolve(FAKE_PARSERS);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const cb = vi.fn();
|
|
134
|
+
const unsub = loader.onMarkdownParsersReady(cb);
|
|
135
|
+
await expect(loader.loadMarkdownParsers()).rejects.toThrow("chunk 404");
|
|
136
|
+
unsub();
|
|
137
|
+
|
|
138
|
+
await loader.loadMarkdownParsers();
|
|
139
|
+
expect(cb).not.toHaveBeenCalled();
|
|
140
|
+
expect(attempt).toBe(2);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("one throwing subscriber does not starve the others", async () => {
|
|
144
|
+
const loader = await freshLoader();
|
|
145
|
+
loader.setMarkdownParsersLoader(() => Promise.resolve(FAKE_PARSERS));
|
|
146
|
+
|
|
147
|
+
const boom = vi.fn(() => {
|
|
148
|
+
throw new Error("subscriber blew up");
|
|
149
|
+
});
|
|
150
|
+
const ok = vi.fn();
|
|
151
|
+
loader.onMarkdownParsersReady(boom);
|
|
152
|
+
loader.onMarkdownParsersReady(ok);
|
|
153
|
+
|
|
154
|
+
await loader.loadMarkdownParsers();
|
|
155
|
+
expect(boom).toHaveBeenCalledTimes(1);
|
|
156
|
+
expect(ok).toHaveBeenCalledTimes(1);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createChunkLoader } from "./utils/chunk-loader";
|
|
1
2
|
import type { Marked } from "marked";
|
|
2
3
|
import type DOMPurify from "dompurify";
|
|
3
4
|
|
|
@@ -6,12 +7,86 @@ export type MarkdownParsersModule = {
|
|
|
6
7
|
DOMPurify: typeof DOMPurify;
|
|
7
8
|
};
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
// Memoization + rejection-retry semantics live in `createChunkLoader`. The
|
|
11
|
+
// ESM/CJS fallback imports `./markdown-parsers-entry` directly; the IIFE/CDN
|
|
12
|
+
// build registers a loader that fetches the `markdown-parsers.js` chunk.
|
|
13
|
+
const { setLoader, load, provide, getSync } =
|
|
14
|
+
createChunkLoader<MarkdownParsersModule>({
|
|
15
|
+
fallbackImport: () => import("./markdown-parsers-entry"),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// Surfaces that want to self-heal when the lazy `markdown-parsers.js` chunk
|
|
19
|
+
// lands. See `onMarkdownParsersReady` below for why this is centralized.
|
|
20
|
+
const readySubscribers = new Set<() => void>();
|
|
21
|
+
|
|
22
|
+
// Fan out to everyone waiting to re-render, exactly once per subscriber.
|
|
23
|
+
// Called from BOTH the eager (`provideMarkdownParsers`) and lazy
|
|
24
|
+
// (`loadMarkdownParsers`) paths so a subscriber can't miss the transition.
|
|
25
|
+
const notifyParsersReady = (): void => {
|
|
26
|
+
// Snapshot + clear first: fire-once semantics, and a callback that re-subscribes
|
|
27
|
+
// (it won't — `onMarkdownParsersReady` no-ops once ready) can't loop.
|
|
28
|
+
const subs = [...readySubscribers];
|
|
29
|
+
readySubscribers.clear();
|
|
30
|
+
for (const cb of subs) {
|
|
31
|
+
// One bad subscriber must not starve the others (or leave messages escaped).
|
|
32
|
+
try {
|
|
33
|
+
cb();
|
|
34
|
+
} catch {
|
|
35
|
+
/* subscriber threw: swallow so the remaining re-renders still run */
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const setMarkdownParsersLoader = setLoader;
|
|
41
|
+
|
|
42
|
+
// On a failed chunk load (404, ad blocker, offline), `createChunkLoader`
|
|
43
|
+
// resets its cached promise so a later call retries. Pending subscribers are
|
|
44
|
+
// KEPT — a surface that rendered escaped during the failed attempt must still
|
|
45
|
+
// heal when a retry (kicked by a new subscriber or an explicit
|
|
46
|
+
// loadMarkdownParsers() call) succeeds; dropping them left that surface
|
|
47
|
+
// escaped forever after a transient failure. They're only retained while the
|
|
48
|
+
// parsers stay unloaded — success releases them (fire-once in
|
|
49
|
+
// `notifyParsersReady`), and callers can unsubscribe.
|
|
50
|
+
export const loadMarkdownParsers = (): Promise<MarkdownParsersModule> => {
|
|
51
|
+
const cached = getSync();
|
|
52
|
+
if (cached) return Promise.resolve(cached);
|
|
53
|
+
return load().then((mod) => {
|
|
54
|
+
notifyParsersReady();
|
|
55
|
+
return mod;
|
|
56
|
+
});
|
|
57
|
+
};
|
|
12
58
|
|
|
13
|
-
|
|
14
|
-
|
|
59
|
+
/**
|
|
60
|
+
* Register `cb` to run once the markdown parsers (marked + DOMPurify) become
|
|
61
|
+
* available, i.e. when the lazy `markdown-parsers.js` chunk resolves on the
|
|
62
|
+
* IIFE/CDN build. Returns an unsubscribe function.
|
|
63
|
+
*
|
|
64
|
+
* This exists so every markdown render surface (chat messages, artifact pane,
|
|
65
|
+
* and any future one) shares a SINGLE self-heal path instead of each wiring its
|
|
66
|
+
* own `loadMarkdownParsers().then(reRender)`. Before this, a new surface that
|
|
67
|
+
* forgot to do that rendered escaped plain text until a user interaction forced
|
|
68
|
+
* a re-render — the recurring first-render bug (chat messages, then the artifact
|
|
69
|
+
* pane) this centralizes away.
|
|
70
|
+
*
|
|
71
|
+
* If the parsers are ALREADY loaded, `cb` is not scheduled and a no-op
|
|
72
|
+
* unsubscribe is returned: the caller's first render already used real markdown,
|
|
73
|
+
* so there is nothing to heal (this is the ESM/CJS build's steady state, and the
|
|
74
|
+
* CDN build's state after the first chunk load). Registering also kicks the load
|
|
75
|
+
* so a surface that renders before anything else triggers it still heals.
|
|
76
|
+
* Fires at most once per subscription.
|
|
77
|
+
*/
|
|
78
|
+
export const onMarkdownParsersReady = (cb: () => void): (() => void) => {
|
|
79
|
+
if (getSync()) return () => {};
|
|
80
|
+
readySubscribers.add(cb);
|
|
81
|
+
// Ensure the chunk is actually being fetched; harmless if already in flight.
|
|
82
|
+
// Swallow rejection here — on failure the subscriber stays registered (see
|
|
83
|
+
// the note on `loadMarkdownParsers`) and the surface keeps its escaped
|
|
84
|
+
// fallback until a retry succeeds; this catch only avoids an unhandled
|
|
85
|
+
// rejection.
|
|
86
|
+
void loadMarkdownParsers().catch(() => {});
|
|
87
|
+
return () => {
|
|
88
|
+
readySubscribers.delete(cb);
|
|
89
|
+
};
|
|
15
90
|
};
|
|
16
91
|
|
|
17
92
|
/**
|
|
@@ -23,27 +98,8 @@ export const setMarkdownParsersLoader = (l: () => Promise<MarkdownParsersModule>
|
|
|
23
98
|
* it lazy-loads the `markdown-parsers.js` chunk instead.
|
|
24
99
|
*/
|
|
25
100
|
export const provideMarkdownParsers = (mod: MarkdownParsersModule): void => {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
export const loadMarkdownParsers = (): Promise<MarkdownParsersModule> => {
|
|
30
|
-
if (moduleCache) return Promise.resolve(moduleCache);
|
|
31
|
-
if (loadPromise) return loadPromise;
|
|
32
|
-
if (!loader) {
|
|
33
|
-
// Fallback for regular ESM/CJS consumers (they import directly)
|
|
34
|
-
loadPromise = import("./markdown-parsers-entry").then((mod) => {
|
|
35
|
-
moduleCache = mod;
|
|
36
|
-
return mod;
|
|
37
|
-
});
|
|
38
|
-
return loadPromise;
|
|
39
|
-
}
|
|
40
|
-
loadPromise = loader().then((mod) => {
|
|
41
|
-
moduleCache = mod;
|
|
42
|
-
return mod;
|
|
43
|
-
});
|
|
44
|
-
return loadPromise;
|
|
101
|
+
provide(mod);
|
|
102
|
+
notifyParsersReady();
|
|
45
103
|
};
|
|
46
104
|
|
|
47
|
-
export const getMarkdownParsersSync =
|
|
48
|
-
return moduleCache;
|
|
49
|
-
};
|
|
105
|
+
export const getMarkdownParsersSync = getSync;
|
package/src/plugin-kit.test.ts
CHANGED
|
@@ -188,6 +188,46 @@ describe("createPopover", () => {
|
|
|
188
188
|
expect(content.getRootNode()).toBe(shadow);
|
|
189
189
|
});
|
|
190
190
|
|
|
191
|
+
it("anchors vertically to a verticalOffset when placement is top-start", () => {
|
|
192
|
+
const { anchor, content } = setup();
|
|
193
|
+
anchor.getBoundingClientRect = () =>
|
|
194
|
+
({
|
|
195
|
+
left: 100,
|
|
196
|
+
right: 400,
|
|
197
|
+
top: 200,
|
|
198
|
+
bottom: 260,
|
|
199
|
+
width: 300,
|
|
200
|
+
height: 60,
|
|
201
|
+
x: 100,
|
|
202
|
+
y: 200,
|
|
203
|
+
toJSON: () => ({}),
|
|
204
|
+
}) as DOMRect;
|
|
205
|
+
content.getBoundingClientRect = () =>
|
|
206
|
+
({
|
|
207
|
+
width: 180,
|
|
208
|
+
height: 100,
|
|
209
|
+
left: 0,
|
|
210
|
+
right: 180,
|
|
211
|
+
top: 0,
|
|
212
|
+
bottom: 100,
|
|
213
|
+
x: 0,
|
|
214
|
+
y: 0,
|
|
215
|
+
toJSON: () => ({}),
|
|
216
|
+
}) as DOMRect;
|
|
217
|
+
|
|
218
|
+
const popover = createPopover({
|
|
219
|
+
anchor,
|
|
220
|
+
content,
|
|
221
|
+
placement: "top-start",
|
|
222
|
+
offset: 6,
|
|
223
|
+
verticalOffset: () => 40,
|
|
224
|
+
});
|
|
225
|
+
popover.open();
|
|
226
|
+
|
|
227
|
+
// anchorTop = 200 + 40 = 240 → top = 240 - 6 - 100 = 134
|
|
228
|
+
expect(content.style.top).toBe("134px");
|
|
229
|
+
});
|
|
230
|
+
|
|
191
231
|
it("destroy removes content and detaches listeners", () => {
|
|
192
232
|
vi.useFakeTimers();
|
|
193
233
|
const onDismiss = vi.fn();
|
package/src/plugin-kit.ts
CHANGED
|
@@ -117,6 +117,23 @@ export interface PopoverOptions {
|
|
|
117
117
|
offset?: number;
|
|
118
118
|
/** Set `content`'s `min-width` to the anchor's width. Default `false`. */
|
|
119
119
|
matchAnchorWidth?: boolean;
|
|
120
|
+
/**
|
|
121
|
+
* Horizontal anchoring override for `*-start` placements. Returns the desired
|
|
122
|
+
* left offset in px measured from the anchor's LEFT edge (e.g. the `@` trigger's
|
|
123
|
+
* x within the composer), or `null` to left-align with the anchor as usual. When
|
|
124
|
+
* this option is present the content is content-sized and capped to the anchor's
|
|
125
|
+
* width, and the resolved left is clamped so the content never overflows the
|
|
126
|
+
* anchor's left/right edges (Slack-style: a near-right trigger shifts the menu
|
|
127
|
+
* left to fit). Omit it entirely for plain anchor-left/right placement.
|
|
128
|
+
*/
|
|
129
|
+
horizontalOffset?: () => number | null;
|
|
130
|
+
/**
|
|
131
|
+
* Vertical anchoring override for `top-*` placements. Returns the desired
|
|
132
|
+
* anchor point's top offset in px measured from the anchor's TOP edge (e.g.
|
|
133
|
+
* the trigger line's top within the composer), or `null` to use the anchor's
|
|
134
|
+
* top edge as usual.
|
|
135
|
+
*/
|
|
136
|
+
verticalOffset?: () => number | null;
|
|
120
137
|
/**
|
|
121
138
|
* Inline `z-index` for `content`. Default `2147483000` so it overlays the rest
|
|
122
139
|
* of the widget. Pass `null` to leave z-index to your own CSS.
|
|
@@ -175,6 +192,8 @@ export function createPopover(options: PopoverOptions): PopoverHandle {
|
|
|
175
192
|
placement = "bottom-start",
|
|
176
193
|
offset = 6,
|
|
177
194
|
matchAnchorWidth = false,
|
|
195
|
+
horizontalOffset,
|
|
196
|
+
verticalOffset,
|
|
178
197
|
zIndex = 2147483000,
|
|
179
198
|
onOpen,
|
|
180
199
|
onDismiss,
|
|
@@ -189,16 +208,31 @@ export function createPopover(options: PopoverOptions): PopoverHandle {
|
|
|
189
208
|
const rect = anchor.getBoundingClientRect();
|
|
190
209
|
content.style.position = "fixed";
|
|
191
210
|
if (matchAnchorWidth) content.style.minWidth = `${rect.width}px`;
|
|
211
|
+
// Trigger-anchored mode: content is sized to its own contents but never wider
|
|
212
|
+
// than the composer, so it can shift horizontally within the composer's span.
|
|
213
|
+
if (horizontalOffset) content.style.maxWidth = `${rect.width}px`;
|
|
214
|
+
|
|
215
|
+
// Single content measurement per reposition (after width constraints apply).
|
|
216
|
+
const contentRect = content.getBoundingClientRect();
|
|
192
217
|
|
|
218
|
+
const offY = verticalOffset?.() ?? null;
|
|
219
|
+
const anchorTop = offY != null ? rect.top + offY : rect.top;
|
|
193
220
|
const top =
|
|
194
221
|
placement === "top-start" || placement === "top-end"
|
|
195
|
-
?
|
|
222
|
+
? anchorTop - offset - contentRect.height
|
|
196
223
|
: rect.bottom + offset;
|
|
197
224
|
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
225
|
+
const isEnd = placement === "bottom-end" || placement === "top-end";
|
|
226
|
+
// Default: anchor left (or anchor right for `*-end`). Trigger-anchored mode
|
|
227
|
+
// (offset present and non-null) overrides with the clamped trigger position so
|
|
228
|
+
// the menu never overflows the anchor's edges — a near-right trigger shifts it
|
|
229
|
+
// left to fit (Slack-style). A null offset falls back to the default.
|
|
230
|
+
let left = isEnd ? rect.right - contentRect.width : rect.left;
|
|
231
|
+
const off = horizontalOffset?.() ?? null;
|
|
232
|
+
if (off != null) {
|
|
233
|
+
const maxLeft = Math.max(rect.left, rect.right - contentRect.width);
|
|
234
|
+
left = Math.min(Math.max(rect.left + off, rect.left), maxLeft);
|
|
235
|
+
}
|
|
202
236
|
|
|
203
237
|
content.style.top = `${top}px`;
|
|
204
238
|
content.style.left = `${left}px`;
|