@salesforcedevs/docs-components 1.34.1-redoc-latest-banner-2 → 1.34.3-final-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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforcedevs/docs-components",
3
- "version": "1.34.1-redoc-latest-banner-2",
3
+ "version": "1.34.3-final-alpha",
4
4
  "description": "Docs Lightning web components for DSC",
5
5
  "license": "MIT",
6
6
  "main": "index.js",
@@ -25,6 +25,5 @@
25
25
  "@types/lodash.orderby": "4.6.9",
26
26
  "@types/lodash.uniqby": "4.7.9"
27
27
  },
28
- "gitHead": "4629fdd9ca18a13480044ad43515b91945d16aad",
29
- "stableVersion": "1.34.0"
28
+ "gitHead": "4629fdd9ca18a13480044ad43515b91945d16aad"
30
29
  }
@@ -0,0 +1,43 @@
1
+ export const DEFAULT_TOOLTIP_LABEL = "Click to copy";
2
+ export const COPIED_TOOLTIP_LABEL = "Copied!";
3
+ export const COPIED_RESET_MS = 1000;
4
+
5
+ export const ANALYTICS_CONTENT_CATEGORY = "content action toolbar";
6
+ export const MORE_ACTIONS_LABEL = "More actions";
7
+
8
+ export const COPY_MARKDOWN_LABEL = "Copy as Markdown";
9
+ export const VIEW_MARKDOWN_LABEL = "View as Markdown";
10
+ export const COPY_MARKDOWN_URL_LABEL = "Copy URL to Markdown";
11
+ export const VIEW_LLMS_LABEL = "View llms.txt";
12
+ export const COPY_LLMS_URL_LABEL = "Copy URL to llms.txt";
13
+ export const OPEN_CHATGPT_LABEL = "Open in ChatGPT";
14
+ export const OPEN_CLAUDE_LABEL = "Open in Claude";
15
+ export const OPEN_PERPLEXITY_LABEL = "Open in Perplexity";
16
+
17
+ export const LLMS_TXT_PATH = "/docs/llms.txt";
18
+ export const CHATGPT_URL = "https://chatgpt.com/";
19
+ export const CLAUDE_URL = "https://claude.ai/new";
20
+ export const PERPLEXITY_URL = "https://www.perplexity.ai/search";
21
+
22
+ export const ACTION_COPY_MARKDOWN = "copy-markdown";
23
+ export const ACTION_VIEW_MARKDOWN = "view-markdown";
24
+ export const ACTION_COPY_MARKDOWN_URL = "copy-markdown-url";
25
+ export const ACTION_VIEW_LLMS = "view-llms";
26
+ export const ACTION_COPY_LLMS_URL = "copy-llms-url";
27
+ export const ACTION_OPEN_CHATGPT = "open-chatgpt";
28
+ export const ACTION_OPEN_CLAUDE = "open-claude";
29
+ export const ACTION_OPEN_PERPLEXITY = "open-perplexity";
30
+
31
+ export const AI_ASSISTANTS = [
32
+ {
33
+ action: ACTION_OPEN_CHATGPT,
34
+ base: CHATGPT_URL,
35
+ label: OPEN_CHATGPT_LABEL
36
+ },
37
+ { action: ACTION_OPEN_CLAUDE, base: CLAUDE_URL, label: OPEN_CLAUDE_LABEL },
38
+ {
39
+ action: ACTION_OPEN_PERPLEXITY,
40
+ base: PERPLEXITY_URL,
41
+ label: OPEN_PERPLEXITY_LABEL
42
+ }
43
+ ] as const;
@@ -2,16 +2,27 @@
2
2
 
3
3
  :host {
4
4
  display: block;
5
+
6
+ /* 4px gap below the trigger, and the same 4px gap above when the menu
7
+ flips */
8
+ --popover-container-open-transform: translateY(var(--dx-g-spacing-xs));
9
+ --popover-container-open-transform-up: translateY(
10
+ calc(-1 * var(--dx-g-spacing-xs))
11
+ );
5
12
  }
6
13
 
7
14
  .toolbar {
8
15
  display: flex;
9
16
  align-items: center;
10
- flex-wrap: wrap;
17
+ flex-wrap: nowrap;
11
18
  gap: var(--dx-g-spacing-smd);
12
19
  margin-bottom: var(--dx-g-spacing-lg);
13
20
  }
14
21
 
22
+ .toolbar > * {
23
+ flex-shrink: 0;
24
+ }
25
+
15
26
  .toolbar-button {
16
27
  --dx-c-button-font-weight: var(--dx-g-font-demi);
17
28
  --dx-c-button-line-height: var(--dx-g-spacing-mlg);
@@ -24,9 +35,26 @@
24
35
  background-color: var(--dx-g-gray-70);
25
36
  }
26
37
 
27
- @media screen and (max-width: 480px) {
28
- .toolbar-button_copy-url,
29
- .divider_copy-url {
30
- display: none;
31
- }
38
+ .toolbar_no-markdown .divider_view-llms {
39
+ display: none;
40
+ }
41
+
42
+ .toolbar-menu {
43
+ --dx-c-popover-z-index: calc(var(--dx-g-z-index-100) + 6);
44
+
45
+ flex: 0 0 auto;
46
+ }
47
+
48
+ .menu-trigger {
49
+ white-space: nowrap;
50
+ flex-shrink: 0;
51
+ }
52
+
53
+ .toolbar > [data-overflow="true"] {
54
+ display: none;
55
+ }
56
+
57
+ .toolbar > .divider_view-markdown[data-overflow="true"] ~ .divider_menu,
58
+ .toolbar_no-markdown .divider_menu {
59
+ display: none;
32
60
  }
@@ -1,53 +1,88 @@
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
5
+ placement="top-right"
6
+ label={copyMarkdownTooltipLabel}
7
+ visible={copyMarkdownTooltipVisible}
8
+ >
9
+ <dx-button
10
+ class="toolbar-button"
11
+ variant="inline"
12
+ size="small"
13
+ icon-sprite="utility"
14
+ icon-symbol="copy"
15
+ icon-size="medium"
16
+ icon-position="right"
17
+ aria-label={copyMarkdownButtonText}
18
+ onclick={handleCopyMarkdown}
19
+ >
20
+ {copyMarkdownButtonText}
21
+ </dx-button>
22
+ </dx-tooltip>
23
+
24
+ <div class="divider divider_view-markdown"></div>
25
+
4
26
  <dx-button
5
- class="toolbar-button"
27
+ class="toolbar-button toolbar-button_view-markdown"
6
28
  variant="inline"
7
29
  size="small"
8
30
  icon-sprite="utility"
9
- icon-symbol="copy"
31
+ icon-symbol="new_window"
10
32
  icon-size="medium"
11
33
  icon-position="right"
12
- aria-label={copyMarkdownButtonText}
13
- onclick={handleCopyMarkdown}
34
+ aria-label={viewMarkdownButtonText}
35
+ onclick={handleViewMarkdown}
14
36
  >
15
- {copyMarkdownButtonText}
37
+ {viewMarkdownButtonText}
16
38
  </dx-button>
17
- </dx-tooltip>
39
+ </template>
18
40
 
19
- <div class="divider"></div>
41
+ <div class="divider divider_view-llms"></div>
20
42
 
21
43
  <dx-button
22
- class="toolbar-button"
44
+ class="toolbar-button toolbar-button_view-llms"
23
45
  variant="inline"
24
46
  size="small"
25
47
  icon-sprite="utility"
26
48
  icon-symbol="new_window"
27
49
  icon-size="medium"
28
50
  icon-position="right"
29
- aria-label={viewMarkdownButtonText}
30
- onclick={handleViewMarkdown}
51
+ aria-label={viewLlmsButtonText}
52
+ onclick={handleViewLlms}
31
53
  >
32
- {viewMarkdownButtonText}
54
+ {viewLlmsButtonText}
33
55
  </dx-button>
34
56
 
35
- <div class="divider divider_copy-url"></div>
57
+ <div class="divider divider_menu"></div>
36
58
 
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}
59
+ <dx-dropdown
60
+ class="toolbar-menu"
61
+ options={menuOptions}
62
+ placement="bottom-end"
63
+ width-mode="fit-content"
64
+ onchange={handleMenuSelect}
65
+ onopen={handleMenuOpen}
66
+ onclose={handleMenuClose}
67
+ >
68
+ <dx-tooltip
69
+ placement="top-right"
70
+ label={menuTooltipLabel}
71
+ visible={menuTooltipVisible}
72
+ suppressed={menuOpen}
48
73
  >
49
- {copyUrlButtonText}
50
- </dx-button>
51
- </dx-tooltip>
74
+ <dx-button
75
+ class="menu-trigger"
76
+ variant="secondary"
77
+ size="small"
78
+ icon-sprite="utility"
79
+ icon-symbol="threedots"
80
+ icon-size="medium"
81
+ icon-only
82
+ >
83
+ {moreActionsLabel}
84
+ </dx-button>
85
+ </dx-tooltip>
86
+ </dx-dropdown>
52
87
  </div>
53
88
  </template>
@@ -1,14 +1,26 @@
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
-
4
- const DEFAULT_COPY_TOOLTIP_LABEL = "Click to copy";
5
- const COPIED_TOOLTIP_LABEL = "Copied!";
6
- const COPIED_TOOLTIP_RESET_MS = 1000;
7
-
8
- const ANALYTICS_CONTENT_CATEGORY = "content action toolbar";
9
- const COPY_MARKDOWN_LABEL = "Copy as Markdown";
10
- const VIEW_MARKDOWN_LABEL = "View as Markdown";
11
- const COPY_URL_LABEL = "Copy URL to Markdown";
5
+ import {
6
+ DEFAULT_TOOLTIP_LABEL,
7
+ COPIED_TOOLTIP_LABEL,
8
+ COPIED_RESET_MS,
9
+ ANALYTICS_CONTENT_CATEGORY,
10
+ MORE_ACTIONS_LABEL,
11
+ COPY_MARKDOWN_LABEL,
12
+ VIEW_MARKDOWN_LABEL,
13
+ COPY_MARKDOWN_URL_LABEL,
14
+ VIEW_LLMS_LABEL,
15
+ COPY_LLMS_URL_LABEL,
16
+ LLMS_TXT_PATH,
17
+ ACTION_COPY_MARKDOWN,
18
+ ACTION_VIEW_MARKDOWN,
19
+ ACTION_COPY_MARKDOWN_URL,
20
+ ACTION_VIEW_LLMS,
21
+ ACTION_COPY_LLMS_URL,
22
+ AI_ASSISTANTS
23
+ } from "./constants";
12
24
 
13
25
  export default class ContentActionToolbar extends LightningElement {
14
26
  @api
@@ -23,17 +35,18 @@ export default class ContentActionToolbar extends LightningElement {
23
35
  }
24
36
  private _pageUrl?: string;
25
37
 
26
- copyMarkdownLabel: string = DEFAULT_COPY_TOOLTIP_LABEL;
27
- copyUrlLabel: string = DEFAULT_COPY_TOOLTIP_LABEL;
38
+ @api pageTitle?: string;
28
39
 
29
- private copyTooltipResetTimeout: number | null = null;
40
+ copyMarkdownTooltipLabel: string = DEFAULT_TOOLTIP_LABEL;
41
+ copyMarkdownTooltipVisible: boolean = false;
42
+ menuTooltipLabel: string = MORE_ACTIONS_LABEL;
43
+ menuTooltipVisible: boolean = false;
44
+ menuOpen: boolean = false;
30
45
 
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 {
46
+ private tooltipResetTimeout: number | null = null;
47
+ private resizeObserver: ResizeObserver | null = null;
48
+
49
+ private get absolutePageUrl(): string | null {
37
50
  if (!this.pageUrl) {
38
51
  return null;
39
52
  }
@@ -41,7 +54,15 @@ export default class ContentActionToolbar extends LightningElement {
41
54
  const url = new URL(this.pageUrl, window.location.href);
42
55
  url.hash = "";
43
56
  url.search = "";
57
+ return url.toString();
58
+ }
44
59
 
60
+ get markdownUrl(): string | null {
61
+ if (!this.absolutePageUrl) {
62
+ return null;
63
+ }
64
+
65
+ const url = new URL(this.absolutePageUrl);
45
66
  if (!url.pathname.endsWith(".html")) {
46
67
  return null;
47
68
  }
@@ -50,6 +71,39 @@ export default class ContentActionToolbar extends LightningElement {
50
71
  return url.toString();
51
72
  }
52
73
 
74
+ get llmsUrl(): string {
75
+ return new URL(LLMS_TXT_PATH, window.location.href).toString();
76
+ }
77
+
78
+ private buildAiAssistantUrl(base: string): string | null {
79
+ if (!this.absolutePageUrl) {
80
+ return null;
81
+ }
82
+
83
+ const title = this.pageTitle?.trim();
84
+ const intro = title
85
+ ? `I'm reading "${title}" at ${this.absolutePageUrl}.`
86
+ : `I'm reading ${this.absolutePageUrl}.`;
87
+
88
+ const prompt = `${intro} Read this page so I can ask questions about it. For broader context, see ${this.llmsUrl}`;
89
+
90
+ const url = new URL(base);
91
+ url.searchParams.set("q", prompt);
92
+ return url.toString();
93
+ }
94
+
95
+ get hasMarkdown(): boolean {
96
+ return Boolean(this.markdownUrl);
97
+ }
98
+
99
+ get toolbarClass(): string {
100
+ return cx("toolbar", !this.hasMarkdown && "toolbar_no-markdown");
101
+ }
102
+
103
+ get moreActionsLabel(): string {
104
+ return MORE_ACTIONS_LABEL;
105
+ }
106
+
53
107
  get copyMarkdownButtonText(): string {
54
108
  return COPY_MARKDOWN_LABEL;
55
109
  }
@@ -58,98 +112,355 @@ export default class ContentActionToolbar extends LightningElement {
58
112
  return VIEW_MARKDOWN_LABEL;
59
113
  }
60
114
 
61
- get copyUrlButtonText(): string {
62
- return COPY_URL_LABEL;
115
+ get viewLlmsButtonText(): string {
116
+ return VIEW_LLMS_LABEL;
117
+ }
118
+
119
+ get menuOptions(): OptionWithNested[] {
120
+ const groups: OptionWithNested[] = [];
121
+
122
+ if (this.hasMarkdown) {
123
+ groups.push({
124
+ id: "this-page",
125
+ label: "This Page",
126
+ options: [
127
+ {
128
+ id: ACTION_COPY_MARKDOWN,
129
+ label: COPY_MARKDOWN_LABEL,
130
+ iconSprite: "utility",
131
+ iconSymbol: "copy"
132
+ },
133
+ {
134
+ id: ACTION_VIEW_MARKDOWN,
135
+ label: VIEW_MARKDOWN_LABEL,
136
+ iconSprite: "utility",
137
+ iconSymbol: "new_window"
138
+ },
139
+ {
140
+ id: ACTION_COPY_MARKDOWN_URL,
141
+ label: COPY_MARKDOWN_URL_LABEL,
142
+ iconSprite: "utility",
143
+ iconSymbol: "link"
144
+ },
145
+ ...AI_ASSISTANTS.map(({ action, label }) => ({
146
+ id: action,
147
+ label,
148
+ iconSprite: "utility",
149
+ iconSymbol: "new_window"
150
+ }))
151
+ ]
152
+ });
153
+ }
154
+
155
+ groups.push({
156
+ id: "all-documentation",
157
+ label: "All Documentation",
158
+ options: [
159
+ {
160
+ id: ACTION_VIEW_LLMS,
161
+ label: VIEW_LLMS_LABEL,
162
+ iconSprite: "utility",
163
+ iconSymbol: "new_window"
164
+ },
165
+ {
166
+ id: ACTION_COPY_LLMS_URL,
167
+ label: COPY_LLMS_URL_LABEL,
168
+ iconSprite: "utility",
169
+ iconSymbol: "link"
170
+ }
171
+ ]
172
+ });
173
+
174
+ return groups;
63
175
  }
64
176
 
65
177
  async handleCopyMarkdown(event: Event) {
178
+ if (await this.copyMarkdown(event)) {
179
+ this.flashCopyMarkdownTooltip();
180
+ }
181
+ }
182
+
183
+ handleViewMarkdown(event: Event) {
66
184
  if (!this.markdownUrl) {
67
185
  return;
68
186
  }
69
187
 
70
- this.trackToolbarEvent(
71
- event,
72
- "custEv_linkClick",
73
- COPY_MARKDOWN_LABEL,
74
- this.markdownUrl
75
- );
188
+ this.trackToolbarEvent(event, VIEW_MARKDOWN_LABEL, this.markdownUrl);
189
+ window.open(this.markdownUrl, "_blank", "noopener,noreferrer");
190
+ }
191
+
192
+ handleViewLlms(event: Event) {
193
+ this.trackToolbarEvent(event, VIEW_LLMS_LABEL, this.llmsUrl);
194
+ window.open(this.llmsUrl, "_blank", "noopener,noreferrer");
195
+ }
196
+
197
+ private handleOpenAiAssistant(
198
+ event: Event,
199
+ label: string,
200
+ url: string | null
201
+ ) {
202
+ if (!url) {
203
+ return;
204
+ }
205
+
206
+ this.trackToolbarEvent(event, label, url);
207
+ window.open(url, "_blank", "noopener,noreferrer");
208
+ }
209
+
210
+ handleMenuOpen() {
211
+ this.menuOpen = true;
212
+ }
213
+
214
+ handleMenuClose() {
215
+ this.menuOpen = false;
216
+ }
217
+
218
+ async handleMenuSelect(event: CustomEvent) {
219
+ switch (event.detail) {
220
+ case ACTION_COPY_MARKDOWN:
221
+ if (await this.copyMarkdown(event)) {
222
+ this.flashMenuCopied();
223
+ }
224
+ return;
225
+ case ACTION_VIEW_MARKDOWN:
226
+ this.handleViewMarkdown(event);
227
+ return;
228
+ case ACTION_COPY_MARKDOWN_URL:
229
+ await this.handleCopyMarkdownUrl(event);
230
+ return;
231
+ case ACTION_VIEW_LLMS:
232
+ this.handleViewLlms(event);
233
+ return;
234
+ case ACTION_COPY_LLMS_URL:
235
+ await this.handleCopyLlmsUrl(event);
236
+ return;
237
+ default: {
238
+ const assistant = AI_ASSISTANTS.find(
239
+ ({ action }) => action === event.detail
240
+ );
241
+ if (assistant) {
242
+ this.handleOpenAiAssistant(
243
+ event,
244
+ assistant.label,
245
+ this.buildAiAssistantUrl(assistant.base)
246
+ );
247
+ }
248
+ }
249
+ }
250
+ }
251
+
252
+ private async copyMarkdown(event: Event): Promise<boolean> {
253
+ if (!this.markdownUrl) {
254
+ return false;
255
+ }
256
+
257
+ this.trackToolbarEvent(event, COPY_MARKDOWN_LABEL, this.markdownUrl);
76
258
 
77
259
  try {
78
260
  const response = await fetch(this.markdownUrl);
79
261
  if (!response.ok) {
80
- return;
262
+ return false;
81
263
  }
82
264
  const markdown = await response.text();
83
265
  await navigator.clipboard.writeText(markdown);
84
- this.flashCopied("copyMarkdownLabel");
266
+ return true;
85
267
  } catch (error) {
86
268
  console.error(error);
269
+ return false;
87
270
  }
88
271
  }
89
272
 
90
- handleViewMarkdown(event: Event) {
273
+ private async handleCopyMarkdownUrl(event: Event) {
91
274
  if (!this.markdownUrl) {
92
275
  return;
93
276
  }
94
277
 
95
278
  this.trackToolbarEvent(
96
279
  event,
97
- "custEv_linkClick",
98
- VIEW_MARKDOWN_LABEL,
280
+ COPY_MARKDOWN_URL_LABEL,
99
281
  this.markdownUrl
100
282
  );
101
283
 
102
- window.open(this.markdownUrl, "_blank", "noopener,noreferrer");
284
+ await this.copyToClipboard(this.markdownUrl);
103
285
  }
104
286
 
105
- async handleCopyUrl(event: Event) {
106
- if (!this.markdownUrl) {
107
- return;
108
- }
287
+ private async handleCopyLlmsUrl(event: Event) {
288
+ this.trackToolbarEvent(event, COPY_LLMS_URL_LABEL, this.llmsUrl);
109
289
 
110
- this.trackToolbarEvent(
111
- event,
112
- "custEv_linkClick",
113
- COPY_URL_LABEL,
114
- this.markdownUrl
115
- );
290
+ await this.copyToClipboard(this.llmsUrl);
291
+ }
116
292
 
293
+ private async copyToClipboard(text: string) {
117
294
  try {
118
- await navigator.clipboard.writeText(this.markdownUrl);
119
- this.flashCopied("copyUrlLabel");
295
+ await navigator.clipboard.writeText(text);
296
+ this.flashMenuCopied();
120
297
  } catch (error) {
121
298
  console.error(error);
122
299
  }
123
300
  }
124
301
 
125
- private trackToolbarEvent(
126
- event: Event,
127
- eventName: "custEv_linkClick",
128
- label: string,
129
- url: string
130
- ): void {
131
- track(event.currentTarget!, eventName, {
302
+ private trackToolbarEvent(event: Event, label: string, url: string): void {
303
+ track(event.currentTarget!, "custEv_linkClick", {
132
304
  click_text: label,
133
305
  click_url: url,
134
306
  element_type: "button_link",
135
307
  element_title: label,
136
308
  content_category: ANALYTICS_CONTENT_CATEGORY,
137
309
  // Source page the CTA was clicked from; `click_url` above is the
138
- // `.md` destination. Uses the same `page_path` expression as the
310
+ // destination. Uses the same `page_path` expression as the
139
311
  // amfReference doc_page_view events.
140
312
  page_path: window.location.pathname + window.location.search
141
313
  });
142
314
  }
143
315
 
144
- private flashCopied(labelKey: "copyMarkdownLabel" | "copyUrlLabel") {
145
- if (this.copyTooltipResetTimeout !== null) {
146
- window.clearTimeout(this.copyTooltipResetTimeout);
316
+ private flashCopyMarkdownTooltip() {
317
+ this.copyMarkdownTooltipLabel = COPIED_TOOLTIP_LABEL;
318
+ this.copyMarkdownTooltipVisible = true;
319
+ this.flashCopied();
320
+ }
321
+
322
+ private flashMenuCopied() {
323
+ this.menuTooltipLabel = COPIED_TOOLTIP_LABEL;
324
+ this.menuTooltipVisible = true;
325
+ this.flashCopied();
326
+ }
327
+
328
+ private flashCopied() {
329
+ if (this.tooltipResetTimeout !== null) {
330
+ window.clearTimeout(this.tooltipResetTimeout);
331
+ }
332
+ this.tooltipResetTimeout = window.setTimeout(() => {
333
+ this.copyMarkdownTooltipLabel = DEFAULT_TOOLTIP_LABEL;
334
+ this.copyMarkdownTooltipVisible = false;
335
+ this.menuTooltipVisible = false;
336
+ this.menuTooltipLabel = MORE_ACTIONS_LABEL;
337
+ this.tooltipResetTimeout = null;
338
+ }, COPIED_RESET_MS);
339
+ }
340
+
341
+ renderedCallback(): void {
342
+ this.layoutToolbar();
343
+ this.ensureResizeObserver();
344
+ }
345
+
346
+ disconnectedCallback(): void {
347
+ this.resizeObserver?.disconnect();
348
+ this.resizeObserver = null;
349
+ }
350
+
351
+ /* Keep every visible control on one row. The menu trigger stays at the
352
+ end; action buttons that no longer fit are hidden, last one first. */
353
+ private layoutToolbar(): void {
354
+ const toolbar = this.template.querySelector(
355
+ ".toolbar"
356
+ ) as HTMLElement | null;
357
+ if (!toolbar || toolbar.clientWidth <= 0) {
358
+ return;
359
+ }
360
+
361
+ const children = ([...toolbar.children] as HTMLElement[]).filter(
362
+ (node) => node instanceof HTMLElement
363
+ );
364
+ children.forEach((el) => {
365
+ delete el.dataset.overflow;
366
+ });
367
+ // Reading layout after clearing overflow flushes styles so the
368
+ // measurements below see the unhidden controls.
369
+ const availableWidth = toolbar.clientWidth;
370
+
371
+ const gap = this.readGap(toolbar);
372
+ const actionItems = children.filter(
373
+ (node) =>
374
+ !node.classList.contains("toolbar-menu") &&
375
+ !node.classList.contains("divider_menu") &&
376
+ this.isShown(node)
377
+ );
378
+
379
+ const menuDivider = toolbar.querySelector(
380
+ ".divider_menu"
381
+ ) as HTMLElement | null;
382
+ const menu = toolbar.querySelector(
383
+ ".toolbar-menu"
384
+ ) as HTMLElement | null;
385
+ const menuPieces = [menuDivider, menu].filter(
386
+ (el): el is HTMLElement => !!el && this.isShown(el)
387
+ );
388
+
389
+ let menuWidth = 0;
390
+ menuPieces.forEach((el, index) => {
391
+ menuWidth += el.offsetWidth + (index > 0 ? gap : 0);
392
+ });
393
+
394
+ const groups = this.groupActions(actionItems);
395
+ let used = 0;
396
+ let fittedCount = 0;
397
+
398
+ for (const group of groups) {
399
+ let groupWidth = 0;
400
+ group.forEach((el, index) => {
401
+ groupWidth += el.offsetWidth + (index > 0 ? gap : 0);
402
+ });
403
+
404
+ const gapBefore = used > 0 ? gap : 0;
405
+ const gapBeforeMenu = menuWidth > 0 ? gap : 0;
406
+ const next =
407
+ used + gapBefore + groupWidth + gapBeforeMenu + menuWidth;
408
+
409
+ if (next <= availableWidth + 1) {
410
+ used += gapBefore + groupWidth;
411
+ fittedCount += group.length;
412
+ } else {
413
+ break;
414
+ }
415
+ }
416
+
417
+ actionItems.slice(fittedCount).forEach((el) => {
418
+ el.dataset.overflow = "true";
419
+ });
420
+ }
421
+
422
+ private ensureResizeObserver(): void {
423
+ if (this.resizeObserver || typeof ResizeObserver === "undefined") {
424
+ return;
147
425
  }
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);
426
+
427
+ const toolbar = this.template.querySelector(".toolbar");
428
+ if (!toolbar) {
429
+ return;
430
+ }
431
+
432
+ this.resizeObserver = new ResizeObserver(() => {
433
+ this.layoutToolbar();
434
+ });
435
+ this.resizeObserver.observe(toolbar);
436
+ }
437
+
438
+ private groupActions(items: HTMLElement[]): HTMLElement[][] {
439
+ const groups: HTMLElement[][] = [];
440
+ let current: HTMLElement[] = [];
441
+
442
+ items.forEach((el) => {
443
+ if (el.classList.contains("divider") && current.length > 0) {
444
+ groups.push(current);
445
+ current = [el];
446
+ return;
447
+ }
448
+ current.push(el);
449
+ });
450
+
451
+ if (current.length > 0) {
452
+ groups.push(current);
453
+ }
454
+
455
+ return groups;
456
+ }
457
+
458
+ private isShown(el: HTMLElement): boolean {
459
+ return getComputedStyle(el).display !== "none";
460
+ }
461
+
462
+ private readGap(toolbar: HTMLElement): number {
463
+ const value = parseFloat(getComputedStyle(toolbar).columnGap);
464
+ return Number.isFinite(value) ? value : 0;
154
465
  }
155
466
  }
@@ -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
 
@@ -790,7 +790,8 @@ export default class RedocReference extends LightningElement {
790
790
  // "newer version" banner into a shared doc-phase-wrapper at container start
791
791
  private insertStatusItems(container: HTMLElement): void {
792
792
  const docPhaseInfo = this.getDocPhaseInfo();
793
- if (!docPhaseInfo && !this.oldVersionInfo) {
793
+ const oldVersionInfo = this.oldVersionInfo;
794
+ if (!docPhaseInfo && !oldVersionInfo) {
794
795
  return;
795
796
  }
796
797
 
@@ -806,10 +807,10 @@ export default class RedocReference extends LightningElement {
806
807
  wrapper.appendChild(docPhaseElement);
807
808
  }
808
809
 
809
- if (this.oldVersionInfo) {
810
+ if (oldVersionInfo) {
810
811
  const versionBanner = createElement("doc-phase", { is: DocPhase });
811
812
  Object.assign(versionBanner, {
812
- docPhaseInfo: this.oldVersionInfo,
813
+ docPhaseInfo: oldVersionInfo,
813
814
  dismissible: true,
814
815
  iconName: "warning"
815
816
  });
@@ -24,6 +24,14 @@
24
24
  lwc:if={docPhaseInfo}
25
25
  doc-phase-info={docPhaseInfo}
26
26
  ></doc-phase>
27
+ <doc-phase
28
+ slot="version-banner"
29
+ lwc:if={showVersionBanner}
30
+ doc-phase-info={oldVersionInfo}
31
+ icon-name="warning"
32
+ dismissible="true"
33
+ ondismissphase={handleDismissVersionBanner}
34
+ ></doc-phase>
27
35
  <slot></slot>
28
36
  </doc-content-layout>
29
37
  </template>
@@ -1,6 +1,12 @@
1
- import { LightningElement, api } from "lwc";
1
+ import { LightningElement, api, track } from "lwc";
2
2
  import { toJson } from "dxUtils/normalizers";
3
- import type { OptionWithLink, TreeNode } from "typings/custom";
3
+ import type { VersionedNode } from "dxUtils/topicVersion";
4
+ import {
5
+ findTopicVersionForCurrentPage,
6
+ latestHrefForCurrentPage
7
+ } from "dxUtils/topicVersion";
8
+ import { oldVersionDocInfo } from "docUtils/utils";
9
+ import type { DocPhaseInfo, OptionWithLink, TreeNode } from "typings/custom";
4
10
 
5
11
  /**
6
12
  * Per-topic type emitted by the docs content-type parser
@@ -10,9 +16,36 @@ import type { OptionWithLink, TreeNode } from "typings/custom";
10
16
  */
11
17
  const TOPIC_TYPE_SPEC = "spec";
12
18
 
19
+ /**
20
+ * Walks down the first-child chain and returns the first markdown content
21
+ * page found, descending through nested content-less parents. Returns
22
+ * undefined if the chain breaks on a spec or external link before reaching a
23
+ * content page.
24
+ */
25
+ function firstChildContentPage(node: TreeNode): TreeNode | undefined {
26
+ const firstChild = node.children?.[0];
27
+ if (!firstChild) {
28
+ return undefined;
29
+ }
30
+
31
+ const isSpec =
32
+ (firstChild as { topicType?: string }).topicType === TOPIC_TYPE_SPEC;
33
+ const isExternal = firstChild.link?.target === "_blank";
34
+ if (isSpec || isExternal) {
35
+ return undefined;
36
+ }
37
+
38
+ if (firstChild.link?.href) {
39
+ return firstChild;
40
+ }
41
+
42
+ return firstChildContentPage(firstChild);
43
+ }
44
+
13
45
  /**
14
46
  * Translates per-topic `topicType` into the generic `showForwardArrow` flag
15
- * that `dx-tree-tile` reads (spec topics get the forward arrow icon for Redoc).
47
+ * that `dx-tree-tile` reads (spec topics get the forward arrow icon for Redoc),
48
+ * and redirects content-less parent topics whose first child is a content page
16
49
  */
17
50
  function decorateTopicsWithForwardArrow(
18
51
  topics:
@@ -33,6 +66,21 @@ function decorateTopicsWithForwardArrow(
33
66
  decorated.children
34
67
  );
35
68
  }
69
+
70
+ /** Redirect a parent that has no content of its own to its first child
71
+ * but only when that first child is itself a content page
72
+ */
73
+ const isContentlessParent =
74
+ !!decorated.children?.length && !decorated.link?.href;
75
+
76
+ if (isContentlessParent) {
77
+ decorated.redirectToChild = true;
78
+ const firstChild = firstChildContentPage(topic);
79
+ if (firstChild?.link) {
80
+ decorated.link = { ...firstChild.link };
81
+ }
82
+ }
83
+
36
84
  return decorated;
37
85
  });
38
86
  }
@@ -67,6 +115,9 @@ export default class UnifiedContentLayout extends LightningElement {
67
115
 
68
116
  private _docPhaseInfo: string | null = null;
69
117
  private _sidebarContent: unknown = null;
118
+ private _oldVersionInfo: DocPhaseInfo | null = null;
119
+
120
+ @track showVersionBanner = false;
70
121
 
71
122
  @api
72
123
  get docPhaseInfo(): string | null {
@@ -83,9 +134,40 @@ export default class UnifiedContentLayout extends LightningElement {
83
134
  }
84
135
 
85
136
  set sidebarContent(value: string) {
86
- this._sidebarContent = decorateTopicsWithForwardArrow(
87
- toJson(value)?.topics
88
- );
137
+ const topics = decorateTopicsWithForwardArrow(toJson(value)?.topics);
138
+ this._sidebarContent = topics;
139
+ this.updateTopicVersionBanner(topics as VersionedNode[] | undefined);
140
+ }
141
+
142
+ get oldVersionInfo(): DocPhaseInfo | null {
143
+ return this._oldVersionInfo;
144
+ }
145
+
146
+ handleDismissVersionBanner(): void {
147
+ this.showVersionBanner = false;
148
+ }
149
+
150
+ /**
151
+ * Same banner references use when the reader is on an older version.
152
+ * Topic versions are not a separate page property: the sidebar tree
153
+ * carries each version's switching URL, and the current pathname says
154
+ * which one is open.
155
+ */
156
+ private updateTopicVersionBanner(
157
+ topics: VersionedNode[] | undefined
158
+ ): void {
159
+ const match = findTopicVersionForCurrentPage(topics);
160
+ const latest = match?.node.versions?.[0];
161
+ const latestHref = latestHrefForCurrentPage(match?.node.versions);
162
+
163
+ if (match && latest && match.version.id !== latest.id && latestHref) {
164
+ this._oldVersionInfo = oldVersionDocInfo(latestHref);
165
+ this.showVersionBanner = true;
166
+ return;
167
+ }
168
+
169
+ this._oldVersionInfo = null;
170
+ this.showVersionBanner = false;
89
171
  }
90
172
 
91
173
  private get enableFooter(): boolean {
@@ -120,6 +120,16 @@ dx-type-badge.not-latest-badge {
120
120
  var(--dx-g-spacing-sm);
121
121
  --dx-c-dropdown-option-border-radius: 0;
122
122
  --dx-c-popover-border: none;
123
+
124
+ /* Hug the labels (down to the shortest) but never wider than the ToC
125
+ content: 296px on desktop, and on the smallest breakpoint the sidebar
126
+ variable grows with the viewport up to 720px. Long labels wrap inside
127
+ that cap. */
128
+ --dx-c-dropdown-menu-max-width: var(--doc-version-picker-width, 296px);
129
+ --dx-c-dropdown-fit-white-space: normal;
130
+ --dx-c-dropdown-fit-overflow: hidden;
131
+ --dx-c-dropdown-fit-text-overflow: clip;
132
+ --dx-c-dropdown-option-overflow-wrap: anywhere;
123
133
  }
124
134
 
125
135
  .version-picker-button-small {
@@ -16,7 +16,7 @@
16
16
  analytics-event="custEv_docVersionSelect"
17
17
  analytics-payload={analyticsPayload}
18
18
  value={selectedVersion.id}
19
- full-width="true"
19
+ width-mode={dropdownWidthMode}
20
20
  onchange={onVersionChange}
21
21
  >
22
22
  <dx-button
@@ -124,6 +124,12 @@ export default class VersionPicker extends LightningElement {
124
124
  return this.small ? "version-picker-dropdown-small" : undefined;
125
125
  }
126
126
 
127
+ // "full" matches the trigger (ToC width). "fit-end" sizes to the labels,
128
+ // caps at the ToC width, and opens inward.
129
+ private get dropdownWidthMode(): "full" | "fit-end" {
130
+ return this.small ? "fit-end" : "full";
131
+ }
132
+
127
133
  private get triggerClass(): string {
128
134
  return cx(
129
135
  "version-picker-button",
@@ -148,10 +148,6 @@ dx-toc {
148
148
  .content-body {
149
149
  margin-top: var(--dx-c-content-vertical-spacing);
150
150
  }
151
-
152
- .content-body-doc-phase-container {
153
- border-left: 0;
154
- }
155
151
  }
156
152
 
157
153
  @media screen and (max-width: 768px) {
@@ -163,6 +159,10 @@ dx-toc {
163
159
  flex-direction: column;
164
160
  }
165
161
 
162
+ .content-body-doc-phase-container {
163
+ border-left: 0;
164
+ }
165
+
166
166
  .content-body-container {
167
167
  padding-right: 0;
168
168
  padding-left: 0;