@salesforcedevs/docs-components 1.29.0-llm-alpha1 → 1.29.0-newct-alpha
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/lwc.config.json +1 -1
- package/package.json +1 -1
- package/src/modules/doc/contentLayout/contentLayout.ts +2 -64
- package/src/modules/doc/unifiedContentLayout/unifiedContentLayout.css +9 -0
- package/src/modules/doc/unifiedContentLayout/unifiedContentLayout.html +31 -0
- package/src/modules/doc/unifiedContentLayout/unifiedContentLayout.ts +98 -0
- package/src/modules/doc/aiToolbar/aiToolbar.css +0 -40
- package/src/modules/doc/aiToolbar/aiToolbar.html +0 -53
- package/src/modules/doc/aiToolbar/aiToolbar.ts +0 -83
- package/src/modules/doc/aiToolbar/aiToolbarMocks.ts +0 -48
package/lwc.config.json
CHANGED
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
{ "npm": "@salesforcedevs/dw-components" }
|
|
6
6
|
],
|
|
7
7
|
"expose": [
|
|
8
|
-
"doc/aiToolbar",
|
|
9
8
|
"doc/amfReference",
|
|
10
9
|
"doc/banner",
|
|
11
10
|
"doc/localeBanner",
|
|
@@ -18,6 +17,7 @@
|
|
|
18
17
|
"doc/contentMedia",
|
|
19
18
|
"doc/docXmlContent",
|
|
20
19
|
"doc/lwcContentLayout",
|
|
20
|
+
"doc/unifiedContentLayout",
|
|
21
21
|
"doc/header",
|
|
22
22
|
"doc/heading",
|
|
23
23
|
"doc/headingAnchor",
|
package/package.json
CHANGED
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
/* eslint-disable @lwc/lwc/no-document-query */
|
|
2
|
-
import { LightningElement, api,
|
|
3
|
-
import { closest
|
|
2
|
+
import { LightningElement, api, track } from "lwc";
|
|
3
|
+
import { closest } from "kagekiri";
|
|
4
4
|
import { toJson, normalizeBoolean } from "dxUtils/normalizers";
|
|
5
5
|
import { highlightTerms } from "dxUtils/highlight";
|
|
6
6
|
import { SearchSyncer } from "docUtils/searchSyncer";
|
|
7
7
|
import type { OptionWithLink } from "typings/custom";
|
|
8
8
|
import { buildDocLinkClickHandler } from "dxUtils/analytics";
|
|
9
|
-
import AiToolbar from "doc/aiToolbar";
|
|
10
|
-
|
|
11
|
-
const AI_TOOLBAR_TAG = "doc-ai-toolbar";
|
|
12
|
-
const PAGE_HEADING_SELECTOR = "h1";
|
|
13
9
|
|
|
14
10
|
type AnchorMap = { [key: string]: { intersect: boolean; id: string } };
|
|
15
11
|
|
|
@@ -99,17 +95,6 @@ export default class ContentLayout extends LightningElement {
|
|
|
99
95
|
/** Optional origin URL for the footer MFE (e.g. wp-json endpoint). Passed through to dx-footer. */
|
|
100
96
|
@api origin: string | null = null;
|
|
101
97
|
|
|
102
|
-
/** Controls whether the AI toolbar is displayed. */
|
|
103
|
-
@api
|
|
104
|
-
get showAiToolbar() {
|
|
105
|
-
return this._showAiToolbar;
|
|
106
|
-
}
|
|
107
|
-
set showAiToolbar(value) {
|
|
108
|
-
this._showAiToolbar = normalizeBoolean(value);
|
|
109
|
-
this.updateAiToolbar();
|
|
110
|
-
}
|
|
111
|
-
private _showAiToolbar = false;
|
|
112
|
-
|
|
113
98
|
@api
|
|
114
99
|
get breadcrumbs() {
|
|
115
100
|
return this._breadcrumbs;
|
|
@@ -167,7 +152,6 @@ export default class ContentLayout extends LightningElement {
|
|
|
167
152
|
protected hasRendered: boolean = false;
|
|
168
153
|
protected contentLoaded: boolean = false;
|
|
169
154
|
protected sidebarOpen: boolean = false;
|
|
170
|
-
protected aiToolbarElement: HTMLElement | null = null;
|
|
171
155
|
|
|
172
156
|
get shouldDisplayFeedback() {
|
|
173
157
|
return this.contentLoaded && typeof Sprig !== "undefined";
|
|
@@ -273,8 +257,6 @@ export default class ContentLayout extends LightningElement {
|
|
|
273
257
|
window.addEventListener("scroll", this.adjustNavPosition);
|
|
274
258
|
window.addEventListener("resize", this.adjustNavPosition);
|
|
275
259
|
|
|
276
|
-
this.updateAiToolbar();
|
|
277
|
-
|
|
278
260
|
if (!this.hasRendered) {
|
|
279
261
|
this.hasRendered = true;
|
|
280
262
|
this.restoreScroll();
|
|
@@ -284,47 +266,6 @@ export default class ContentLayout extends LightningElement {
|
|
|
284
266
|
}
|
|
285
267
|
}
|
|
286
268
|
|
|
287
|
-
/**
|
|
288
|
-
* Inserts the AI toolbar into the slotted content immediately after the
|
|
289
|
-
* first H1 found inside this layout. The H1 typically lives deep in
|
|
290
|
-
* `doc-content`'s shadow (rendered via `lwc:dom="manual"`), so we use
|
|
291
|
-
* kagekiri's shadow-piercing query to locate it from here.
|
|
292
|
-
*/
|
|
293
|
-
protected updateAiToolbar(): void {
|
|
294
|
-
if (!this.showAiToolbar) {
|
|
295
|
-
this.removeAiToolbar();
|
|
296
|
-
return;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
const heading = querySelector(
|
|
300
|
-
PAGE_HEADING_SELECTOR,
|
|
301
|
-
this.template.host
|
|
302
|
-
) as HTMLElement | null;
|
|
303
|
-
|
|
304
|
-
if (!heading) {
|
|
305
|
-
return;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
if (this.aiToolbarElement?.previousElementSibling === heading) {
|
|
309
|
-
return;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
this.removeAiToolbar();
|
|
313
|
-
|
|
314
|
-
const toolbar = createElement(AI_TOOLBAR_TAG, {
|
|
315
|
-
is: AiToolbar
|
|
316
|
-
}) as unknown as HTMLElement;
|
|
317
|
-
heading.parentNode?.insertBefore(toolbar, heading.nextSibling);
|
|
318
|
-
this.aiToolbarElement = toolbar;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
protected removeAiToolbar(): void {
|
|
322
|
-
if (this.aiToolbarElement) {
|
|
323
|
-
this.aiToolbarElement.remove();
|
|
324
|
-
this.aiToolbarElement = null;
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
|
|
328
269
|
disconnectedCallback(): void {
|
|
329
270
|
this.disconnectObserver();
|
|
330
271
|
window.removeEventListener(
|
|
@@ -340,8 +281,6 @@ export default class ContentLayout extends LightningElement {
|
|
|
340
281
|
|
|
341
282
|
// Remove link click handler
|
|
342
283
|
this.template.removeEventListener("click", this.handleLinkClick);
|
|
343
|
-
|
|
344
|
-
this.removeAiToolbar();
|
|
345
284
|
}
|
|
346
285
|
|
|
347
286
|
restoreScroll() {
|
|
@@ -584,7 +523,6 @@ export default class ContentLayout extends LightningElement {
|
|
|
584
523
|
|
|
585
524
|
onSlotChange(): void {
|
|
586
525
|
this.updateRNB();
|
|
587
|
-
this.updateAiToolbar();
|
|
588
526
|
this.contentLoaded = true;
|
|
589
527
|
}
|
|
590
528
|
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Keep the host transparent to layout: no positioning context, no new
|
|
3
|
+
* stacking/containment block, no margin/padding. This is critical so the
|
|
4
|
+
* inner <doc-content-layout>'s sticky math for the sidebar / right-nav / doc-
|
|
5
|
+
* phase wrapper continues to use the document viewport as its reference.
|
|
6
|
+
*/
|
|
7
|
+
:host {
|
|
8
|
+
display: block;
|
|
9
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<doc-content-layout
|
|
3
|
+
breadcrumbs={breadcrumbs}
|
|
4
|
+
sidebar-content={sidebarContent}
|
|
5
|
+
sidebar-value={sidebarValue}
|
|
6
|
+
sidebar-header={sidebarHeader}
|
|
7
|
+
toc-title={tocTitle}
|
|
8
|
+
toc-options={tocOptions}
|
|
9
|
+
toc-aria-level={tocAriaLevel}
|
|
10
|
+
enable-slot-change={enableSlotChange}
|
|
11
|
+
use-old-sidebar={useOldSidebar}
|
|
12
|
+
languages={languages}
|
|
13
|
+
language={language}
|
|
14
|
+
bail-href={bailHref}
|
|
15
|
+
bail-label={bailLabel}
|
|
16
|
+
dev-center={devCenter}
|
|
17
|
+
brand={brand}
|
|
18
|
+
empty-state-message={emptyStateMessage}
|
|
19
|
+
show-footer={showFooter}
|
|
20
|
+
reading-time={readingTime}
|
|
21
|
+
share-title={shareTitle}
|
|
22
|
+
share-url={shareUrl}
|
|
23
|
+
share-twitter-via={shareTwitterVia}
|
|
24
|
+
origin={origin}
|
|
25
|
+
>
|
|
26
|
+
<slot></slot>
|
|
27
|
+
<slot name="doc-phase" slot="doc-phase"></slot>
|
|
28
|
+
<slot name="version-banner" slot="version-banner"></slot>
|
|
29
|
+
<slot name="sidebar-header" slot="sidebar-header"></slot>
|
|
30
|
+
</doc-content-layout>
|
|
31
|
+
</template>
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { LightningElement, api } from "lwc";
|
|
2
|
+
import type { OptionWithLink } from "typings/custom";
|
|
3
|
+
import "doc/contentLayout";
|
|
4
|
+
import "doc/phase";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Orchestrator LWC for the unified `docs` content-type.
|
|
8
|
+
*
|
|
9
|
+
* Today (Phase 2): thin pass-through over `<doc-content-layout>` for markdown
|
|
10
|
+
* topics. The data contract matches what `DocsContentTypeParser.compile()`
|
|
11
|
+
* already emits (`dxSideBarContents`, `routeurl`, `breadcrumbs`,
|
|
12
|
+
* `docPhaseInfo`, etc.), so the parser does not need to fabricate a
|
|
13
|
+
* `reference-set-config` shape the way `<doc-amf-reference>` requires.
|
|
14
|
+
*
|
|
15
|
+
* Navigation: LNB clicks go through the browser's default `<a href>`
|
|
16
|
+
* navigation (the same behavior as references and every other SFDocs
|
|
17
|
+
* content-type). SFDocs is a server-rendered (Nunjucks) framework; LWR's
|
|
18
|
+
* Outlet only swaps LWC view components, not server-rendered HTML, so there
|
|
19
|
+
* is no soft-nav mechanism here today. A previous experiment to fake soft
|
|
20
|
+
* nav with `history.pushState` + a synthetic `popstate` was removed because
|
|
21
|
+
* it updated the URL bar but did NOT re-fetch the new topic's content (the
|
|
22
|
+
* content body stayed as the originally-loaded topic until a manual
|
|
23
|
+
* refresh). Real client-side soft nav for server-rendered routes would
|
|
24
|
+
* require a Turbo-style fetch + DOM surgery; if that becomes a requirement
|
|
25
|
+
* it should live in @salesforcedocs/doc-framework so every content-type
|
|
26
|
+
* benefits, not be patched into this wrapper.
|
|
27
|
+
*
|
|
28
|
+
* The two side-effect imports above (`doc/contentLayout`, `doc/phase`) exist
|
|
29
|
+
* so LWR's static-analysis-based modulepreload emitter discovers them on
|
|
30
|
+
* the route. Without them the docs route's first paint shows a visible
|
|
31
|
+
* flash on local dev because the inner LWC + phase badge load only after
|
|
32
|
+
* this wrapper's bundle parses.
|
|
33
|
+
*
|
|
34
|
+
* Future:
|
|
35
|
+
* - Phase 3: version-picker wiring in the `sidebar-header` named slot
|
|
36
|
+
* (parser emits `versions` / `selectedVersion`; wrapper renders
|
|
37
|
+
* `<doc-version-picker>` here).
|
|
38
|
+
* - Phase 4: markdown-vs-spec branching in the default slot (use the
|
|
39
|
+
* existing `<doc-amf-topic>` + `AmfModelParser` primitives, mirroring
|
|
40
|
+
* `<doc-amf-reference>`'s template pattern but without inheriting its
|
|
41
|
+
* `/references/` URL conventions or per-reference sidebar tiles).
|
|
42
|
+
*
|
|
43
|
+
* Every property is a plain pass-through. `contentLayout`'s own setters run
|
|
44
|
+
* `toJson` / `normalizeBoolean` on the values, so re-normalizing here would
|
|
45
|
+
* only risk diverging from upstream as the inner component evolves.
|
|
46
|
+
*/
|
|
47
|
+
export default class UnifiedContentLayout extends LightningElement {
|
|
48
|
+
/* ---------- Sidebar / breadcrumb / TOC inputs ---------- */
|
|
49
|
+
@api breadcrumbs: string | null = null;
|
|
50
|
+
@api sidebarContent: string | object | null = null;
|
|
51
|
+
@api sidebarValue: string | null = null;
|
|
52
|
+
@api sidebarHeader: string | null = null;
|
|
53
|
+
|
|
54
|
+
@api tocTitle?: string;
|
|
55
|
+
@api tocOptions?: string;
|
|
56
|
+
@api tocAriaLevel?: string;
|
|
57
|
+
|
|
58
|
+
/* ---------- contentLayout feature toggles ---------- */
|
|
59
|
+
@api enableSlotChange: string | boolean = false;
|
|
60
|
+
@api useOldSidebar: string | boolean = false;
|
|
61
|
+
@api showFooter: string | boolean = false;
|
|
62
|
+
|
|
63
|
+
/* ---------- Localization ---------- */
|
|
64
|
+
@api languages: OptionWithLink[] | null = null;
|
|
65
|
+
@api language: string | null = null;
|
|
66
|
+
|
|
67
|
+
/* ---------- Empty-state / chrome ---------- */
|
|
68
|
+
@api bailHref: string | null = null;
|
|
69
|
+
@api bailLabel: string | null = null;
|
|
70
|
+
@api emptyStateMessage: string | null = null;
|
|
71
|
+
@api readingTime: string | null = null;
|
|
72
|
+
|
|
73
|
+
@api devCenter: string | null = null;
|
|
74
|
+
@api brand: string | null = null;
|
|
75
|
+
|
|
76
|
+
/* ---------- Social share ---------- */
|
|
77
|
+
@api shareTitle: string | null = null;
|
|
78
|
+
@api shareUrl: string | null = null;
|
|
79
|
+
@api shareTwitterVia: string | null = null;
|
|
80
|
+
|
|
81
|
+
/* ---------- Footer integration ---------- */
|
|
82
|
+
@api origin: string | null = null;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Forwarded imperative method so any caller that holds a reference to the
|
|
86
|
+
* wrapper (and previously called `contentLayout.setSidebarInputValue`)
|
|
87
|
+
* keeps working without changes.
|
|
88
|
+
*/
|
|
89
|
+
@api
|
|
90
|
+
setSidebarInputValue(searchTerm: string): void {
|
|
91
|
+
const inner = this.template.querySelector("doc-content-layout") as
|
|
92
|
+
| (HTMLElement & {
|
|
93
|
+
setSidebarInputValue?: (term: string) => void;
|
|
94
|
+
})
|
|
95
|
+
| null;
|
|
96
|
+
inner?.setSidebarInputValue?.(searchTerm);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
@import "dxHelpers/reset";
|
|
2
|
-
|
|
3
|
-
:host {
|
|
4
|
-
display: inline-flex;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
.ai-toolbar {
|
|
8
|
-
display: flex;
|
|
9
|
-
align-items: center;
|
|
10
|
-
gap: var(--dx-g-spacing-sm);
|
|
11
|
-
padding-bottom: var(--dx-g-spacing-sm);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
.toolbar-button {
|
|
15
|
-
--dx-g-button-inline-color: var(--dx-g-blue-vibrant-50);
|
|
16
|
-
--dx-g-button-inline-color-hover: var(--dx-g-blue-vibrant-30);
|
|
17
|
-
--dx-c-button-font-size: var(--dx-g-text-sm);
|
|
18
|
-
--dx-c-button-horizontal-spacing: 0;
|
|
19
|
-
--dx-c-button-icon-vertical-align: middle;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
.toolbar-button::part(content) {
|
|
23
|
-
font-family: var(--dx-g-font-display);
|
|
24
|
-
font-weight: var(--dx-g-font-demi);
|
|
25
|
-
line-height: var(--dx-g-spacing-mlg);
|
|
26
|
-
letter-spacing: 0.07px;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
.divider {
|
|
30
|
-
width: 1px;
|
|
31
|
-
height: var(--dx-g-spacing-md);
|
|
32
|
-
background-color: var(--dx-g-gray-70);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
@media screen and (max-width: 480px) {
|
|
36
|
-
.toolbar-button_copy-url,
|
|
37
|
-
.divider_copy-url {
|
|
38
|
-
display: none;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div class="ai-toolbar">
|
|
3
|
-
<dx-tooltip placement="top-right" label={copyMarkdownLabel}>
|
|
4
|
-
<dx-button
|
|
5
|
-
class="toolbar-button"
|
|
6
|
-
variant="inline"
|
|
7
|
-
size="small"
|
|
8
|
-
icon-sprite="utility"
|
|
9
|
-
icon-symbol="copy"
|
|
10
|
-
icon-size="medium"
|
|
11
|
-
icon-position="right"
|
|
12
|
-
aria-label="Copy as Markdown"
|
|
13
|
-
onclick={handleCopyMarkdown}
|
|
14
|
-
>
|
|
15
|
-
Copy as Markdown
|
|
16
|
-
</dx-button>
|
|
17
|
-
</dx-tooltip>
|
|
18
|
-
|
|
19
|
-
<div class="divider"></div>
|
|
20
|
-
|
|
21
|
-
<dx-button
|
|
22
|
-
class="toolbar-button"
|
|
23
|
-
variant="inline"
|
|
24
|
-
size="small"
|
|
25
|
-
icon-sprite="utility"
|
|
26
|
-
icon-symbol="new_window"
|
|
27
|
-
icon-size="medium"
|
|
28
|
-
icon-position="right"
|
|
29
|
-
aria-label="View as Markdown"
|
|
30
|
-
onclick={handleViewMarkdown}
|
|
31
|
-
>
|
|
32
|
-
View as Markdown
|
|
33
|
-
</dx-button>
|
|
34
|
-
|
|
35
|
-
<div class="divider divider_copy-url"></div>
|
|
36
|
-
|
|
37
|
-
<dx-tooltip placement="top-right" label={copyUrlLabel}>
|
|
38
|
-
<dx-button
|
|
39
|
-
class="toolbar-button toolbar-button_copy-url"
|
|
40
|
-
variant="inline"
|
|
41
|
-
size="small"
|
|
42
|
-
icon-sprite="utility"
|
|
43
|
-
icon-symbol="link"
|
|
44
|
-
icon-size="medium"
|
|
45
|
-
icon-position="right"
|
|
46
|
-
aria-label="Copy URL to Markdown"
|
|
47
|
-
onclick={handleCopyUrl}
|
|
48
|
-
>
|
|
49
|
-
Copy URL to Markdown
|
|
50
|
-
</dx-button>
|
|
51
|
-
</dx-tooltip>
|
|
52
|
-
</div>
|
|
53
|
-
</template>
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import { LightningElement } from "lwc";
|
|
2
|
-
|
|
3
|
-
const DEFAULT_COPY_TOOLTIP_LABEL = "Click to copy";
|
|
4
|
-
const COPIED_TOOLTIP_LABEL = "Copied!";
|
|
5
|
-
const COPIED_TOOLTIP_RESET_MS = 2000;
|
|
6
|
-
|
|
7
|
-
export default class AiToolbar extends LightningElement {
|
|
8
|
-
copyMarkdownLabel: string = DEFAULT_COPY_TOOLTIP_LABEL;
|
|
9
|
-
copyUrlLabel: string = DEFAULT_COPY_TOOLTIP_LABEL;
|
|
10
|
-
|
|
11
|
-
private copyTooltipResetTimeout: number | null = null;
|
|
12
|
-
|
|
13
|
-
async handleCopyMarkdown() {
|
|
14
|
-
const markdownUrl = this.getMarkdownUrl();
|
|
15
|
-
if (!markdownUrl) {
|
|
16
|
-
return;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
try {
|
|
20
|
-
const response = await fetch(markdownUrl);
|
|
21
|
-
if (!response.ok) {
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
const markdown = await response.text();
|
|
25
|
-
await navigator.clipboard.writeText(markdown);
|
|
26
|
-
this.flashCopied("copyMarkdownLabel");
|
|
27
|
-
} catch (error) {
|
|
28
|
-
console.error(error);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
handleViewMarkdown() {
|
|
33
|
-
const markdownUrl = this.getMarkdownUrl();
|
|
34
|
-
if (!markdownUrl) {
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
window.open(markdownUrl, "_blank", "noopener,noreferrer");
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
async handleCopyUrl() {
|
|
41
|
-
const markdownUrl = this.getMarkdownUrl();
|
|
42
|
-
if (!markdownUrl) {
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
try {
|
|
47
|
-
await navigator.clipboard.writeText(markdownUrl);
|
|
48
|
-
this.flashCopied("copyUrlLabel");
|
|
49
|
-
} catch (error) {
|
|
50
|
-
console.error(error);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Returns the `.md` equivalent of the current page URL with any hash and
|
|
56
|
-
* query string stripped, or `null` when the current page does not end
|
|
57
|
-
* with `.html`.
|
|
58
|
-
*/
|
|
59
|
-
private getMarkdownUrl(): string | null {
|
|
60
|
-
const url = new URL(window.location.href);
|
|
61
|
-
url.hash = "";
|
|
62
|
-
url.search = "";
|
|
63
|
-
|
|
64
|
-
if (!url.pathname.endsWith(".html")) {
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
url.pathname = url.pathname.replace(/\.html$/, ".md");
|
|
69
|
-
return url.toString();
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
private flashCopied(labelKey: "copyMarkdownLabel" | "copyUrlLabel") {
|
|
73
|
-
if (this.copyTooltipResetTimeout !== null) {
|
|
74
|
-
window.clearTimeout(this.copyTooltipResetTimeout);
|
|
75
|
-
}
|
|
76
|
-
this[labelKey] = COPIED_TOOLTIP_LABEL;
|
|
77
|
-
this.copyTooltipResetTimeout = window.setTimeout(() => {
|
|
78
|
-
this.copyMarkdownLabel = DEFAULT_COPY_TOOLTIP_LABEL;
|
|
79
|
-
this.copyUrlLabel = DEFAULT_COPY_TOOLTIP_LABEL;
|
|
80
|
-
this.copyTooltipResetTimeout = null;
|
|
81
|
-
}, COPIED_TOOLTIP_RESET_MS);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { http, HttpResponse } from "msw";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Mocks for the AI toolbar so stories never hit the real
|
|
5
|
-
* docs backend. Any story rendering `doc-ai-toolbar` (directly or via
|
|
6
|
-
* `doc-content-layout`) must register `aiToolbarMswHandlers` and call
|
|
7
|
-
* `interceptWindowOpenForAiToolbar()`.
|
|
8
|
-
*/
|
|
9
|
-
export const DUMMY_MARKDOWN_CONTENT = `# Dummy Markdown
|
|
10
|
-
|
|
11
|
-
Storybook serves this placeholder content in place of the real markdown
|
|
12
|
-
that the docs backend would return for the current page. It exists so
|
|
13
|
-
the "Copied" tooltip, the "View as Markdown" new tab, and the
|
|
14
|
-
"Copy URL to Markdown" clipboard behavior are all exercisable in
|
|
15
|
-
storybook without hitting the live backend.
|
|
16
|
-
|
|
17
|
-
- Item 1
|
|
18
|
-
- Item 2
|
|
19
|
-
- Item 3
|
|
20
|
-
`;
|
|
21
|
-
|
|
22
|
-
export const DUMMY_MARKDOWN_DATA_URL = `data:text/markdown;charset=utf-8,${encodeURIComponent(
|
|
23
|
-
DUMMY_MARKDOWN_CONTENT
|
|
24
|
-
)}`;
|
|
25
|
-
|
|
26
|
-
/** Intercepts any `.md` GET request and returns the dummy markdown. */
|
|
27
|
-
export const aiToolbarMswHandlers = [
|
|
28
|
-
http.get(/\.md(\?.*)?$/, () => HttpResponse.text(DUMMY_MARKDOWN_CONTENT))
|
|
29
|
-
];
|
|
30
|
-
|
|
31
|
-
let windowOpenIntercepted = false;
|
|
32
|
-
|
|
33
|
-
/** Redirects `window.open` calls for `.md` URLs to the dummy markdown data URL (MSW does not cover new tabs). */
|
|
34
|
-
export function interceptWindowOpenForAiToolbar() {
|
|
35
|
-
if (windowOpenIntercepted) {
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
windowOpenIntercepted = true;
|
|
39
|
-
|
|
40
|
-
const originalOpen = window.open.bind(window);
|
|
41
|
-
window.open = ((url?: string | URL, target?: string, features?: string) => {
|
|
42
|
-
const stringUrl = url?.toString() ?? "";
|
|
43
|
-
if (stringUrl.endsWith(".md")) {
|
|
44
|
-
return originalOpen(DUMMY_MARKDOWN_DATA_URL, target, features);
|
|
45
|
-
}
|
|
46
|
-
return originalOpen(url ?? "", target, features);
|
|
47
|
-
}) as typeof window.open;
|
|
48
|
-
}
|