@salesforcedevs/docs-components 1.34.0 → 1.34.1-ai-toolbar-action

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/package.json CHANGED
@@ -1,29 +1,30 @@
1
1
  {
2
- "name": "@salesforcedevs/docs-components",
3
- "version": "1.34.0",
4
- "description": "Docs Lightning web components for DSC",
5
- "license": "MIT",
6
- "main": "index.js",
7
- "engines": {
8
- "node": "22.x"
9
- },
10
- "publishConfig": {
11
- "access": "public"
12
- },
13
- "dependencies": {
14
- "@api-components/amf-helper-mixin": "4.5.29",
15
- "classnames": "2.5.1",
16
- "dompurify": "3.2.4",
17
- "kagekiri": "1.4.2",
18
- "lodash.orderby": "4.6.0",
19
- "lodash.uniqby": "4.7.0",
20
- "query-string": "7.1.3",
21
- "sentence-case": "3.0.4"
22
- },
23
- "devDependencies": {
24
- "@types/classnames": "2.3.1",
25
- "@types/lodash.orderby": "4.6.9",
26
- "@types/lodash.uniqby": "4.7.9"
27
- },
28
- "gitHead": "db75cd19dd7a77fe4e09856439c4905c32e64d1f"
29
- }
2
+ "name": "@salesforcedevs/docs-components",
3
+ "version": "1.34.1-ai-toolbar-action",
4
+ "description": "Docs Lightning web components for DSC",
5
+ "license": "MIT",
6
+ "main": "index.js",
7
+ "engines": {
8
+ "node": "22.x"
9
+ },
10
+ "publishConfig": {
11
+ "access": "public"
12
+ },
13
+ "dependencies": {
14
+ "@api-components/amf-helper-mixin": "4.5.29",
15
+ "classnames": "2.5.1",
16
+ "dompurify": "3.2.4",
17
+ "kagekiri": "1.4.2",
18
+ "lodash.orderby": "4.6.0",
19
+ "lodash.uniqby": "4.7.0",
20
+ "query-string": "7.1.3",
21
+ "sentence-case": "3.0.4"
22
+ },
23
+ "devDependencies": {
24
+ "@types/classnames": "2.3.1",
25
+ "@types/lodash.orderby": "4.6.9",
26
+ "@types/lodash.uniqby": "4.7.9"
27
+ },
28
+ "gitHead": "4629fdd9ca18a13480044ad43515b91945d16aad",
29
+ "stableVersion": "1.34.1"
30
+ }
@@ -24,9 +24,63 @@
24
24
  background-color: var(--dx-g-gray-70);
25
25
  }
26
26
 
27
- @media screen and (max-width: 480px) {
28
- .toolbar-button_copy-url,
29
- .divider_copy-url {
30
- display: none;
27
+ .toolbar-button_view-markdown,
28
+ .divider_view-markdown,
29
+ .toolbar-button_view-llms,
30
+ .divider_view-llms {
31
+ display: none;
32
+ }
33
+
34
+ .toolbar_no-markdown .toolbar-button_view-llms {
35
+ display: inline-flex;
36
+ }
37
+
38
+ .toolbar:not(.toolbar_no-markdown) .divider_menu {
39
+ display: none;
40
+ }
41
+
42
+ .menu-trigger-wrap {
43
+ position: relative;
44
+ display: inline-flex;
45
+ }
46
+
47
+ .menu-trigger {
48
+ --dx-c-button-custom-color: var(--dx-g-gray-50);
49
+ --dx-c-button-custom-color-hover: var(--dx-g-gray-10);
50
+ }
51
+
52
+ .menu-trigger-copied {
53
+ position: absolute;
54
+ bottom: 100%;
55
+ right: calc(-1 * var(--dx-g-spacing-xs));
56
+ margin-bottom: var(--dx-g-spacing-xs);
57
+ padding: var(--dx-g-spacing-2xs) var(--dx-g-spacing-sm);
58
+ background: var(--dx-g-gray-50);
59
+ color: white;
60
+ font-family: var(--dx-g-font-sans);
61
+ font-size: var(--dx-g-text-xs);
62
+ white-space: nowrap;
63
+ border-radius: 4px;
64
+ pointer-events: none;
65
+ }
66
+
67
+ @media screen and (min-width: 480px) {
68
+ .toolbar-button_view-markdown,
69
+ .divider_view-markdown {
70
+ display: inline-flex;
71
+ }
72
+
73
+ .toolbar:not(.toolbar_no-markdown) .divider_menu {
74
+ display: block;
75
+ }
76
+ }
77
+
78
+ @media screen and (min-width: 768px) {
79
+ .toolbar-button_view-llms {
80
+ display: inline-flex;
81
+ }
82
+
83
+ .toolbar:not(.toolbar_no-markdown) .divider_view-llms {
84
+ display: inline-flex;
31
85
  }
32
86
  }
@@ -1,53 +1,85 @@
1
1
  <template>
2
- <div class="toolbar" lwc:if={markdownUrl}>
3
- <dx-tooltip placement="top-right" label={copyMarkdownLabel}>
2
+ <div class={toolbarClass}>
3
+ <template lwc:if={hasMarkdown}>
4
+ <dx-tooltip placement="top-right" label={copyMarkdownTooltipLabel}>
5
+ <dx-button
6
+ class="toolbar-button"
7
+ variant="inline"
8
+ size="small"
9
+ icon-sprite="utility"
10
+ icon-symbol="copy"
11
+ icon-size="medium"
12
+ icon-position="right"
13
+ aria-label={copyMarkdownButtonText}
14
+ onclick={handleCopyMarkdown}
15
+ >
16
+ {copyMarkdownButtonText}
17
+ </dx-button>
18
+ </dx-tooltip>
19
+
20
+ <div class="divider divider_view-markdown"></div>
21
+
4
22
  <dx-button
5
- class="toolbar-button"
23
+ class="toolbar-button toolbar-button_view-markdown"
6
24
  variant="inline"
7
25
  size="small"
8
26
  icon-sprite="utility"
9
- icon-symbol="copy"
27
+ icon-symbol="new_window"
10
28
  icon-size="medium"
11
29
  icon-position="right"
12
- aria-label={copyMarkdownButtonText}
13
- onclick={handleCopyMarkdown}
30
+ aria-label={viewMarkdownButtonText}
31
+ onclick={handleViewMarkdown}
14
32
  >
15
- {copyMarkdownButtonText}
33
+ {viewMarkdownButtonText}
16
34
  </dx-button>
17
- </dx-tooltip>
35
+ </template>
18
36
 
19
- <div class="divider"></div>
37
+ <div class="divider divider_view-llms"></div>
20
38
 
21
39
  <dx-button
22
- class="toolbar-button"
40
+ class="toolbar-button toolbar-button_view-llms"
23
41
  variant="inline"
24
42
  size="small"
25
43
  icon-sprite="utility"
26
44
  icon-symbol="new_window"
27
45
  icon-size="medium"
28
46
  icon-position="right"
29
- aria-label={viewMarkdownButtonText}
30
- onclick={handleViewMarkdown}
47
+ aria-label={viewLlmsButtonText}
48
+ onclick={handleViewLlms}
31
49
  >
32
- {viewMarkdownButtonText}
50
+ {viewLlmsButtonText}
33
51
  </dx-button>
34
52
 
35
- <div class="divider divider_copy-url"></div>
53
+ <div class="divider divider_menu"></div>
36
54
 
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={copyUrlButtonText}
47
- onclick={handleCopyUrl}
55
+ <span class="menu-trigger-wrap">
56
+ <dx-tooltip placement="top-right" label={moreActionsLabel}>
57
+ <dx-dropdown
58
+ class="toolbar-menu"
59
+ options={menuOptions}
60
+ placement="bottom-end"
61
+ width-mode="fit-content"
62
+ aria-label={moreActionsLabel}
63
+ onchange={handleMenuSelect}
64
+ >
65
+ <dx-button
66
+ class="menu-trigger"
67
+ variant="icon-only"
68
+ size="small"
69
+ icon-sprite="utility"
70
+ icon-symbol="threedots"
71
+ icon-size="medium"
72
+ aria-label={moreActionsLabel}
73
+ ></dx-button>
74
+ </dx-dropdown>
75
+ </dx-tooltip>
76
+ <span
77
+ class="menu-trigger-copied"
78
+ if:true={showCopiedFlash}
79
+ role="status"
48
80
  >
49
- {copyUrlButtonText}
50
- </dx-button>
51
- </dx-tooltip>
81
+ {copiedLabel}
82
+ </span>
83
+ </span>
52
84
  </div>
53
85
  </template>
@@ -1,14 +1,39 @@
1
1
  import { LightningElement, api } from "lwc";
2
+ import cx from "classnames";
3
+ import { OptionWithNested } from "typings/custom";
2
4
  import { track } from "dxUtils/analytics";
3
5
 
4
- const DEFAULT_COPY_TOOLTIP_LABEL = "Click to copy";
6
+ const DEFAULT_TOOLTIP_LABEL = "Click to copy";
5
7
  const COPIED_TOOLTIP_LABEL = "Copied!";
6
- const COPIED_TOOLTIP_RESET_MS = 1000;
8
+ const COPIED_RESET_MS = 1000;
7
9
 
8
10
  const ANALYTICS_CONTENT_CATEGORY = "content action toolbar";
11
+ const MORE_ACTIONS_LABEL = "More actions";
12
+
9
13
  const COPY_MARKDOWN_LABEL = "Copy as Markdown";
10
14
  const VIEW_MARKDOWN_LABEL = "View as Markdown";
11
- const COPY_URL_LABEL = "Copy URL to Markdown";
15
+ const COPY_MARKDOWN_URL_LABEL = "Copy URL to Markdown";
16
+ const VIEW_LLMS_LABEL = "View llms.txt";
17
+ const COPY_LLMS_URL_LABEL = "Copy URL to llms.txt";
18
+ const OPEN_CHATGPT_LABEL = "Open in ChatGPT";
19
+ const OPEN_CLAUDE_LABEL = "Open in Claude";
20
+ const OPEN_PERPLEXITY_LABEL = "Open in Perplexity";
21
+
22
+ const LLMS_TXT_PATH = "/docs/llms.txt";
23
+ const CHATGPT_URL = "https://chatgpt.com/";
24
+ const CLAUDE_URL = "https://claude.ai/new";
25
+ const PERPLEXITY_URL = "https://www.perplexity.ai/search";
26
+
27
+ const LLMS_TXT_CONTEXT_URL = "https://developer.salesforce.com/docs/llms.txt";
28
+
29
+ const ACTION_COPY_MARKDOWN = "copy-markdown";
30
+ const ACTION_VIEW_MARKDOWN = "view-markdown";
31
+ const ACTION_COPY_MARKDOWN_URL = "copy-markdown-url";
32
+ const ACTION_VIEW_LLMS = "view-llms";
33
+ const ACTION_COPY_LLMS_URL = "copy-llms-url";
34
+ const ACTION_OPEN_CHATGPT = "open-chatgpt";
35
+ const ACTION_OPEN_CLAUDE = "open-claude";
36
+ const ACTION_OPEN_PERPLEXITY = "open-perplexity";
12
37
 
13
38
  export default class ContentActionToolbar extends LightningElement {
14
39
  @api
@@ -23,17 +48,15 @@ export default class ContentActionToolbar extends LightningElement {
23
48
  }
24
49
  private _pageUrl?: string;
25
50
 
26
- copyMarkdownLabel: string = DEFAULT_COPY_TOOLTIP_LABEL;
27
- copyUrlLabel: string = DEFAULT_COPY_TOOLTIP_LABEL;
51
+ @api pageTitle?: string;
28
52
 
29
- private copyTooltipResetTimeout: number | null = null;
53
+ copyMarkdownTooltipLabel: string = DEFAULT_TOOLTIP_LABEL;
54
+ copiedLabel: string | null = null;
30
55
 
31
- /**
32
- * Returns the `.md` equivalent of `pageUrl` with any hash and query
33
- * string stripped, or `null` when `pageUrl` is not set or does not end
34
- * with `.html`.
35
- */
36
- get markdownUrl(): string | null {
56
+ private copyMarkdownResetTimeout: number | null = null;
57
+ private menuCopiedResetTimeout: number | null = null;
58
+
59
+ private get absolutePageUrl(): string | null {
37
60
  if (!this.pageUrl) {
38
61
  return null;
39
62
  }
@@ -41,7 +64,15 @@ export default class ContentActionToolbar extends LightningElement {
41
64
  const url = new URL(this.pageUrl, window.location.href);
42
65
  url.hash = "";
43
66
  url.search = "";
67
+ return url.toString();
68
+ }
69
+
70
+ get markdownUrl(): string | null {
71
+ if (!this.absolutePageUrl) {
72
+ return null;
73
+ }
44
74
 
75
+ const url = new URL(this.absolutePageUrl);
45
76
  if (!url.pathname.endsWith(".html")) {
46
77
  return null;
47
78
  }
@@ -50,6 +81,57 @@ export default class ContentActionToolbar extends LightningElement {
50
81
  return url.toString();
51
82
  }
52
83
 
84
+ get llmsUrl(): string {
85
+ return new URL(LLMS_TXT_PATH, window.location.href).toString();
86
+ }
87
+
88
+ get chatGptUrl(): string | null {
89
+ return this.buildAiAssistantUrl(CHATGPT_URL);
90
+ }
91
+
92
+ get claudeUrl(): string | null {
93
+ return this.buildAiAssistantUrl(CLAUDE_URL);
94
+ }
95
+
96
+ get perplexityUrl(): string | null {
97
+ return this.buildAiAssistantUrl(PERPLEXITY_URL);
98
+ }
99
+
100
+ private buildAiAssistantUrl(base: string): string | null {
101
+ if (!this.absolutePageUrl) {
102
+ return null;
103
+ }
104
+
105
+ const title = this.pageTitle?.trim();
106
+ const intro = title
107
+ ? `I'm reading "${title}" at ${this.absolutePageUrl}.`
108
+ : `I'm reading ${this.absolutePageUrl}.`;
109
+
110
+ const url = new URL(base);
111
+ url.searchParams.set(
112
+ "q",
113
+ `${intro} Read this page so I can ask questions about it. ` +
114
+ `For broader context, see ${LLMS_TXT_CONTEXT_URL}`
115
+ );
116
+ return url.toString();
117
+ }
118
+
119
+ get hasMarkdown(): boolean {
120
+ return Boolean(this.markdownUrl);
121
+ }
122
+
123
+ get toolbarClass(): string {
124
+ return cx("toolbar", !this.hasMarkdown && "toolbar_no-markdown");
125
+ }
126
+
127
+ get moreActionsLabel(): string {
128
+ return MORE_ACTIONS_LABEL;
129
+ }
130
+
131
+ get showCopiedFlash(): boolean {
132
+ return this.copiedLabel !== null;
133
+ }
134
+
53
135
  get copyMarkdownButtonText(): string {
54
136
  return COPY_MARKDOWN_LABEL;
55
137
  }
@@ -58,98 +140,241 @@ export default class ContentActionToolbar extends LightningElement {
58
140
  return VIEW_MARKDOWN_LABEL;
59
141
  }
60
142
 
61
- get copyUrlButtonText(): string {
62
- return COPY_URL_LABEL;
143
+ get viewLlmsButtonText(): string {
144
+ return VIEW_LLMS_LABEL;
145
+ }
146
+
147
+ get menuOptions(): OptionWithNested[] {
148
+ const groups: OptionWithNested[] = [];
149
+
150
+ if (this.hasMarkdown) {
151
+ groups.push({
152
+ id: "this-page",
153
+ label: "This Page",
154
+ options: [
155
+ {
156
+ id: ACTION_COPY_MARKDOWN,
157
+ label: COPY_MARKDOWN_LABEL,
158
+ iconSprite: "utility",
159
+ iconSymbol: "copy"
160
+ },
161
+ {
162
+ id: ACTION_VIEW_MARKDOWN,
163
+ label: VIEW_MARKDOWN_LABEL,
164
+ iconSprite: "utility",
165
+ iconSymbol: "new_window"
166
+ },
167
+ {
168
+ id: ACTION_COPY_MARKDOWN_URL,
169
+ label: COPY_MARKDOWN_URL_LABEL,
170
+ iconSprite: "utility",
171
+ iconSymbol: "link"
172
+ },
173
+ {
174
+ id: ACTION_OPEN_CHATGPT,
175
+ label: OPEN_CHATGPT_LABEL,
176
+ iconSprite: "utility",
177
+ iconSymbol: "new_window"
178
+ },
179
+ {
180
+ id: ACTION_OPEN_CLAUDE,
181
+ label: OPEN_CLAUDE_LABEL,
182
+ iconSprite: "utility",
183
+ iconSymbol: "new_window"
184
+ },
185
+ {
186
+ id: ACTION_OPEN_PERPLEXITY,
187
+ label: OPEN_PERPLEXITY_LABEL,
188
+ iconSprite: "utility",
189
+ iconSymbol: "new_window"
190
+ }
191
+ ]
192
+ });
193
+ }
194
+
195
+ groups.push({
196
+ id: "all-documentation",
197
+ label: "All Documentation",
198
+ options: [
199
+ {
200
+ id: ACTION_VIEW_LLMS,
201
+ label: VIEW_LLMS_LABEL,
202
+ iconSprite: "utility",
203
+ iconSymbol: "new_window"
204
+ },
205
+ {
206
+ id: ACTION_COPY_LLMS_URL,
207
+ label: COPY_LLMS_URL_LABEL,
208
+ iconSprite: "utility",
209
+ iconSymbol: "link"
210
+ }
211
+ ]
212
+ });
213
+
214
+ return groups;
63
215
  }
64
216
 
65
217
  async handleCopyMarkdown(event: Event) {
218
+ if (await this.copyMarkdown(event)) {
219
+ this.flashCopyMarkdownTooltip();
220
+ }
221
+ }
222
+
223
+ handleViewMarkdown(event: Event) {
66
224
  if (!this.markdownUrl) {
67
225
  return;
68
226
  }
69
227
 
70
- this.trackToolbarEvent(
71
- event,
72
- "custEv_linkClick",
73
- COPY_MARKDOWN_LABEL,
74
- this.markdownUrl
75
- );
228
+ this.trackToolbarEvent(event, VIEW_MARKDOWN_LABEL, this.markdownUrl);
229
+ window.open(this.markdownUrl, "_blank", "noopener,noreferrer");
230
+ }
231
+
232
+ handleViewLlms(event: Event) {
233
+ this.trackToolbarEvent(event, VIEW_LLMS_LABEL, this.llmsUrl);
234
+ window.open(this.llmsUrl, "_blank", "noopener,noreferrer");
235
+ }
236
+
237
+ private handleOpenAiAssistant(
238
+ event: Event,
239
+ label: string,
240
+ url: string | null
241
+ ) {
242
+ if (!url) {
243
+ return;
244
+ }
245
+
246
+ this.trackToolbarEvent(event, label, url);
247
+ window.open(url, "_blank", "noopener,noreferrer");
248
+ }
249
+
250
+ async handleMenuSelect(event: CustomEvent) {
251
+ switch (event.detail) {
252
+ case ACTION_COPY_MARKDOWN:
253
+ if (await this.copyMarkdown(event)) {
254
+ this.flashMenuCopied();
255
+ }
256
+ return;
257
+ case ACTION_VIEW_MARKDOWN:
258
+ this.handleViewMarkdown(event);
259
+ return;
260
+ case ACTION_COPY_MARKDOWN_URL:
261
+ await this.handleCopyMarkdownUrl(event);
262
+ return;
263
+ case ACTION_VIEW_LLMS:
264
+ this.handleViewLlms(event);
265
+ return;
266
+ case ACTION_COPY_LLMS_URL:
267
+ await this.handleCopyLlmsUrl(event);
268
+ return;
269
+ case ACTION_OPEN_CHATGPT:
270
+ this.handleOpenAiAssistant(
271
+ event,
272
+ OPEN_CHATGPT_LABEL,
273
+ this.chatGptUrl
274
+ );
275
+ return;
276
+ case ACTION_OPEN_CLAUDE:
277
+ this.handleOpenAiAssistant(
278
+ event,
279
+ OPEN_CLAUDE_LABEL,
280
+ this.claudeUrl
281
+ );
282
+ return;
283
+ case ACTION_OPEN_PERPLEXITY:
284
+ this.handleOpenAiAssistant(
285
+ event,
286
+ OPEN_PERPLEXITY_LABEL,
287
+ this.perplexityUrl
288
+ );
289
+ break;
290
+ default:
291
+ }
292
+ }
293
+
294
+ private async copyMarkdown(event: Event): Promise<boolean> {
295
+ if (!this.markdownUrl) {
296
+ return false;
297
+ }
298
+
299
+ this.trackToolbarEvent(event, COPY_MARKDOWN_LABEL, this.markdownUrl);
76
300
 
77
301
  try {
78
302
  const response = await fetch(this.markdownUrl);
79
303
  if (!response.ok) {
80
- return;
304
+ return false;
81
305
  }
82
306
  const markdown = await response.text();
83
307
  await navigator.clipboard.writeText(markdown);
84
- this.flashCopied("copyMarkdownLabel");
308
+ return true;
85
309
  } catch (error) {
86
310
  console.error(error);
311
+ return false;
87
312
  }
88
313
  }
89
314
 
90
- handleViewMarkdown(event: Event) {
315
+ private async handleCopyMarkdownUrl(event: Event) {
91
316
  if (!this.markdownUrl) {
92
317
  return;
93
318
  }
94
319
 
95
320
  this.trackToolbarEvent(
96
321
  event,
97
- "custEv_linkClick",
98
- VIEW_MARKDOWN_LABEL,
322
+ COPY_MARKDOWN_URL_LABEL,
99
323
  this.markdownUrl
100
324
  );
101
325
 
102
- window.open(this.markdownUrl, "_blank", "noopener,noreferrer");
103
- }
104
-
105
- async handleCopyUrl(event: Event) {
106
- if (!this.markdownUrl) {
107
- return;
326
+ try {
327
+ await navigator.clipboard.writeText(this.markdownUrl);
328
+ this.flashMenuCopied();
329
+ } catch (error) {
330
+ console.error(error);
108
331
  }
332
+ }
109
333
 
110
- this.trackToolbarEvent(
111
- event,
112
- "custEv_linkClick",
113
- COPY_URL_LABEL,
114
- this.markdownUrl
115
- );
334
+ private async handleCopyLlmsUrl(event: Event) {
335
+ this.trackToolbarEvent(event, COPY_LLMS_URL_LABEL, this.llmsUrl);
116
336
 
117
337
  try {
118
- await navigator.clipboard.writeText(this.markdownUrl);
119
- this.flashCopied("copyUrlLabel");
338
+ await navigator.clipboard.writeText(this.llmsUrl);
339
+ this.flashMenuCopied();
120
340
  } catch (error) {
121
341
  console.error(error);
122
342
  }
123
343
  }
124
344
 
125
- private trackToolbarEvent(
126
- event: Event,
127
- eventName: "custEv_linkClick",
128
- label: string,
129
- url: string
130
- ): void {
131
- track(event.currentTarget!, eventName, {
345
+ private trackToolbarEvent(event: Event, label: string, url: string): void {
346
+ track(event.currentTarget!, "custEv_linkClick", {
132
347
  click_text: label,
133
348
  click_url: url,
134
349
  element_type: "button_link",
135
350
  element_title: label,
136
351
  content_category: ANALYTICS_CONTENT_CATEGORY,
137
352
  // Source page the CTA was clicked from; `click_url` above is the
138
- // `.md` destination. Uses the same `page_path` expression as the
353
+ // destination. Uses the same `page_path` expression as the
139
354
  // amfReference doc_page_view events.
140
355
  page_path: window.location.pathname + window.location.search
141
356
  });
142
357
  }
143
358
 
144
- private flashCopied(labelKey: "copyMarkdownLabel" | "copyUrlLabel") {
145
- if (this.copyTooltipResetTimeout !== null) {
146
- window.clearTimeout(this.copyTooltipResetTimeout);
359
+ private flashCopyMarkdownTooltip() {
360
+ if (this.copyMarkdownResetTimeout !== null) {
361
+ window.clearTimeout(this.copyMarkdownResetTimeout);
362
+ }
363
+ this.copyMarkdownTooltipLabel = COPIED_TOOLTIP_LABEL;
364
+ this.copyMarkdownResetTimeout = window.setTimeout(() => {
365
+ this.copyMarkdownTooltipLabel = DEFAULT_TOOLTIP_LABEL;
366
+ this.copyMarkdownResetTimeout = null;
367
+ }, COPIED_RESET_MS);
368
+ }
369
+
370
+ private flashMenuCopied() {
371
+ if (this.menuCopiedResetTimeout !== null) {
372
+ window.clearTimeout(this.menuCopiedResetTimeout);
147
373
  }
148
- this[labelKey] = COPIED_TOOLTIP_LABEL;
149
- this.copyTooltipResetTimeout = window.setTimeout(() => {
150
- this.copyMarkdownLabel = DEFAULT_COPY_TOOLTIP_LABEL;
151
- this.copyUrlLabel = DEFAULT_COPY_TOOLTIP_LABEL;
152
- this.copyTooltipResetTimeout = null;
153
- }, COPIED_TOOLTIP_RESET_MS);
374
+ this.copiedLabel = COPIED_TOOLTIP_LABEL;
375
+ this.menuCopiedResetTimeout = window.setTimeout(() => {
376
+ this.copiedLabel = null;
377
+ this.menuCopiedResetTimeout = null;
378
+ }, COPIED_RESET_MS);
154
379
  }
155
380
  }
@@ -40,7 +40,7 @@ export function interceptWindowOpenForContentActionToolbar() {
40
40
  const originalOpen = window.open.bind(window);
41
41
  window.open = ((url?: string | URL, target?: string, features?: string) => {
42
42
  const stringUrl = url?.toString() ?? "";
43
- if (stringUrl.endsWith(".md")) {
43
+ if (stringUrl.endsWith(".md") || stringUrl.endsWith("/llms.txt")) {
44
44
  return originalOpen(DUMMY_MARKDOWN_DATA_URL, target, features);
45
45
  }
46
46
  return originalOpen(url ?? "", target, features);
@@ -307,9 +307,10 @@ export default class ContentLayout extends LightningElement {
307
307
  const toolbar = (this.contentActionToolbarElement ??
308
308
  createElement(CONTENT_ACTION_TOOLBAR_TAG, {
309
309
  is: ContentActionToolbar
310
- })) as HTMLElement & { pageUrl?: string };
310
+ })) as HTMLElement & { pageUrl?: string; pageTitle?: string };
311
311
 
312
312
  toolbar.pageUrl = this.sidebarValue;
313
+ toolbar.pageTitle = heading.textContent?.trim() || undefined;
313
314
 
314
315
  if (toolbar.previousElementSibling !== heading) {
315
316
  heading.parentNode?.insertBefore(toolbar, heading.nextSibling);
@@ -74,6 +74,13 @@ export default class Header extends HeaderBase {
74
74
  return (this.hideDocHeader && !this.mobile) || this.showDocDivider;
75
75
  }
76
76
 
77
+ private get isDocsPath(): boolean {
78
+ return (
79
+ window.location.pathname.includes("/docs/") &&
80
+ window.location.pathname !== "/docs/apis"
81
+ );
82
+ }
83
+
77
84
  connectedCallback(): void {
78
85
  super.connectedCallback();
79
86
  this.tabletMatchMedia = window.matchMedia(
@@ -83,9 +90,7 @@ export default class Header extends HeaderBase {
83
90
  this.tabletMatchMedia.addEventListener("change", this.onTabletChange);
84
91
 
85
92
  if (
86
- (!this.shouldHideHeader &&
87
- window.location.pathname.includes("/docs/") &&
88
- window.location.pathname !== "/docs/apis") ||
93
+ (!this.shouldHideHeader && this.isDocsPath) ||
89
94
  window.location.pathname ===
90
95
  "/tableau/embedding-playground/overview" ||
91
96
  isStorybook()
@@ -109,6 +114,12 @@ export default class Header extends HeaderBase {
109
114
  );
110
115
  }
111
116
 
117
+ renderedCallback(): void {
118
+ if (this.hideDocHeader) {
119
+ this.shouldRender = !this.shouldHideHeader && this.isDocsPath;
120
+ }
121
+ }
122
+
112
123
  private onTabletChange = (e: MediaQueryListEvent | MediaQueryList) =>
113
124
  (this.tablet = e.matches);
114
125
 
@@ -10,11 +10,38 @@ import type { OptionWithLink, TreeNode } from "typings/custom";
10
10
  */
11
11
  const TOPIC_TYPE_SPEC = "spec";
12
12
 
13
+ /**
14
+ * Walks down the first-child chain and returns the first markdown content
15
+ * page found, descending through nested content-less parents. Returns
16
+ * undefined if the chain breaks on a spec or external link before reaching a
17
+ * content page.
18
+ */
19
+ function firstChildContentPage(node: TreeNode): TreeNode | undefined {
20
+ const firstChild = node.children?.[0];
21
+ if (!firstChild) {
22
+ return undefined;
23
+ }
24
+
25
+ const isSpec =
26
+ (firstChild as { topicType?: string }).topicType === TOPIC_TYPE_SPEC;
27
+ const isExternal = firstChild.link?.target === "_blank";
28
+ if (isSpec || isExternal) {
29
+ return undefined;
30
+ }
31
+
32
+ if (firstChild.link?.href) {
33
+ return firstChild;
34
+ }
35
+
36
+ return firstChildContentPage(firstChild);
37
+ }
38
+
13
39
  /**
14
40
  * Translates per-topic `topicType` into the generic `showForwardArrow` flag
15
- * that `dx-tree-tile` reads (spec topics get the forward arrow icon for Redoc).
41
+ * that `dx-tree-tile` reads (spec topics get the forward arrow icon for Redoc),
42
+ * and redirects content-less parent topics whose first child is a content page
16
43
  */
17
- function decorateTopicsWithForwardArrow(
44
+ function decorateTopics(
18
45
  topics: Array<TreeNode & { topicType?: string }> | undefined
19
46
  ): TreeNode[] | undefined {
20
47
  return topics?.map((topic) => {
@@ -23,10 +50,23 @@ function decorateTopicsWithForwardArrow(
23
50
  decorated.showForwardArrow = true;
24
51
  }
25
52
  if (decorated.children) {
26
- decorated.children = decorateTopicsWithForwardArrow(
27
- decorated.children
28
- );
53
+ decorated.children = decorateTopics(decorated.children);
29
54
  }
55
+
56
+ /** Redirect a parent that has no content of its own to its first child
57
+ * but only when that first child is itself a content page
58
+ */
59
+ const isContentlessParent =
60
+ !!decorated.children?.length && !decorated.link?.href;
61
+
62
+ if (isContentlessParent) {
63
+ decorated.redirectToChild = true;
64
+ const firstChild = firstChildContentPage(topic);
65
+ if (firstChild?.link) {
66
+ decorated.link = { ...firstChild.link };
67
+ }
68
+ }
69
+
30
70
  return decorated;
31
71
  });
32
72
  }
@@ -77,9 +117,7 @@ export default class UnifiedContentLayout extends LightningElement {
77
117
  }
78
118
 
79
119
  set sidebarContent(value: string) {
80
- this._sidebarContent = decorateTopicsWithForwardArrow(
81
- toJson(value)?.topics
82
- );
120
+ this._sidebarContent = decorateTopics(toJson(value)?.topics);
83
121
  }
84
122
 
85
123
  private get enableFooter(): boolean {
package/LICENSE DELETED
@@ -1,12 +0,0 @@
1
- Copyright (c) 2020, Salesforce.com, Inc.
2
- All rights reserved.
3
-
4
- Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
5
-
6
- * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
7
-
8
- * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
9
-
10
- * Neither the name of Salesforce.com nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
11
-
12
- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.