@salesforcedevs/docs-components 1.34.1 → 1.34.4-topic-alpha.1

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.
@@ -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);
@@ -3,4 +3,5 @@
3
3
  --doc-c-redoc-sidebar-top: calc(
4
4
  var(--dx-g-global-header-height) + var(--dx-g-doc-header-height)
5
5
  );
6
+ --doc-c-redoc-sidebar-bg: white;
6
7
  }