@salesforcedevs/docs-components 1.34.0 → 1.34.1-redoc-ver-picker-2

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-redoc-ver-picker-2",
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.0"
30
+ }
@@ -4,6 +4,7 @@ import DocPhase from "doc/phase";
4
4
  import DxFooter from "dx/footer";
5
5
  import DxIcon from "dx/icon";
6
6
  import SidebarFooterNav from "dx/sidebarFooterNav";
7
+ import VersionPicker from "doc/versionPicker";
7
8
  import SprigSurvey from "doc/sprigSurvey";
8
9
  import { throttle } from "throttle-debounce";
9
10
  import { pollUntil } from "dxUtils/async";
@@ -33,8 +34,22 @@ type ReferenceItem = {
33
34
  topic?: ReferenceTopic;
34
35
  };
35
36
 
37
+ /**
38
+ * A selectable spec version. Mirrors `ReferenceVersion` in `doc/amfReference`.
39
+ * Each version's `link.href` makes the picker's dropdown options navigate
40
+ * natively.
41
+ */
42
+ type ReferenceVersion = {
43
+ id: string;
44
+ label: string;
45
+ deprecated?: boolean;
46
+ selected?: boolean;
47
+ link: { href: string };
48
+ };
49
+
36
50
  type ReferenceConfig = {
37
51
  refList: ReferenceItem[];
52
+ versions?: ReferenceVersion[];
38
53
  };
39
54
 
40
55
  const SCROLL_THROTTLE_DELAY = 50;
@@ -42,6 +57,7 @@ const ELEMENT_TIMEOUT = 10000;
42
57
  const ELEMENT_CHECK_INTERVAL = 100;
43
58
  const DEFAULT_PROJECT_TITLE = "All Reference";
44
59
  const BACK_TARGET_STORAGE_KEY = "redoc-back-target";
60
+ const SELECTED_VIEW_STORAGE_KEY = "redoc-selected-view";
45
61
  const DOCS_PATH_SEGMENT = "docs";
46
62
  const DEFAULT_LOCALE = "en-us";
47
63
 
@@ -152,6 +168,32 @@ export default class RedocReference extends LightningElement {
152
168
  return refCount > 1;
153
169
  }
154
170
 
171
+ get versions(): ReferenceVersion[] {
172
+ return this._referenceConfig?.versions ?? [];
173
+ }
174
+
175
+ /** The version flagged `selected`, else the first (the latest/GA version). */
176
+ get selectedVersion(): ReferenceVersion | null {
177
+ const versions = this.versions;
178
+ return versions.find((v) => v.selected) ?? versions[0] ?? null;
179
+ }
180
+
181
+ /** True when the selected version is the latest/GA one (index 0). */
182
+ get latestVersion(): boolean {
183
+ return this.versions.length
184
+ ? this.selectedVersion?.id === this.versions[0].id
185
+ : false;
186
+ }
187
+
188
+ get hasVersionPicker(): boolean {
189
+ return this.versions.length > 0;
190
+ }
191
+
192
+ /** A single version has nothing to choose, so it renders read-only. */
193
+ get isVersionReadOnly(): boolean {
194
+ return this.versions.length === 1;
195
+ }
196
+
155
197
  // Reads stored back target
156
198
  private getBackTargetFromSession(): string | null {
157
199
  return sessionStorage.getItem(BACK_TARGET_STORAGE_KEY);
@@ -515,6 +557,9 @@ export default class RedocReference extends LightningElement {
515
557
  const apiContentDiv = await this.waitForApiContent(redocContainer);
516
558
  apiContentDiv.setAttribute("lwc:dom", "manual");
517
559
 
560
+ // White LNB background + divider between the sidebar and content.
561
+ this.injectRedocStyles(redocContainer);
562
+
518
563
  const docPhaseInfo = this.getDocPhaseInfo();
519
564
  if (docPhaseInfo) {
520
565
  this.insertDocPhase(apiContentDiv, docPhaseInfo);
@@ -525,6 +570,9 @@ export default class RedocReference extends LightningElement {
525
570
  // Inject the multi-spec project header into Redoc's left menu only.
526
571
  this.insertSidebarNav(redocContainer);
527
572
 
573
+ // Restore the view selected before a version switch, if any.
574
+ this.restoreSelectedView();
575
+
528
576
  // Wait for footer to be rendered before updating styles
529
577
  requestAnimationFrame(() => {
530
578
  this.updateRedocThirdColumnStyle(redocContainer);
@@ -553,6 +601,15 @@ export default class RedocReference extends LightningElement {
553
601
  });
554
602
  }
555
603
 
604
+ // Version picker, inserted directly above Redoc's built-in search box.
605
+ if (this.hasVersionPicker) {
606
+ const menuContent =
607
+ redocContainer.querySelector<HTMLElement>(".menu-content");
608
+ if (menuContent) {
609
+ this.insertVersionPicker(menuContent);
610
+ }
611
+ }
612
+
556
613
  // Locale picker
557
614
  if (this.hasLocalePicker) {
558
615
  const menuContent = redocContainer.querySelector(".menu-content");
@@ -560,6 +617,166 @@ export default class RedocReference extends LightningElement {
560
617
  }
561
618
  }
562
619
 
620
+ /**
621
+ * Inserts the version picker into Redoc's left menu, directly above its
622
+ * built-in search box (per the design). Redoc renders the menu
623
+ * asynchronously and uses hashed styled-component class names, so we poll
624
+ * for the search box via a set of stable, semantic selectors and fall back
625
+ * to prepending if it never appears.
626
+ */
627
+ private async insertVersionPicker(menuContent: HTMLElement): Promise<void> {
628
+ const pickerDom = this.buildVersionPickerDom();
629
+
630
+ const found = await pollUntil(
631
+ () => !!this.findSearchBox(menuContent),
632
+ ELEMENT_CHECK_INTERVAL,
633
+ ELEMENT_TIMEOUT
634
+ );
635
+
636
+ const searchBox = found ? this.findSearchBox(menuContent) : null;
637
+ if (searchBox) {
638
+ // Align the picker with the search box: the shared picker's default
639
+ // 40px inset / fixed 296px width are tuned for a wider sidebar and
640
+ // overflow Redoc's menu, so match the search box's inset and width.
641
+ this.alignPickerToSearchBox(pickerDom, menuContent, searchBox);
642
+
643
+ // Insert before the search box's top-level child of `.menu-content`.
644
+ let topLevelNode: HTMLElement = searchBox;
645
+ while (
646
+ topLevelNode.parentElement &&
647
+ topLevelNode.parentElement !== menuContent
648
+ ) {
649
+ topLevelNode = topLevelNode.parentElement;
650
+ }
651
+ menuContent.insertBefore(pickerDom, topLevelNode);
652
+ } else {
653
+ console.warn(
654
+ "Redoc search box not found; prepending version picker to menu."
655
+ );
656
+ menuContent.insertBefore(pickerDom, menuContent.firstChild);
657
+ }
658
+ }
659
+
660
+ /**
661
+ * Matches the picker's horizontal inset and width to Redoc's search box so
662
+ * the two align, regardless of Redoc's own menu padding. Sets the picker's
663
+ * padding/width custom properties, keeping the container's vertical rhythm.
664
+ */
665
+ private alignPickerToSearchBox(
666
+ pickerDom: HTMLElement,
667
+ menuContent: HTMLElement,
668
+ searchBox: HTMLElement
669
+ ): void {
670
+ const menuRect = menuContent.getBoundingClientRect();
671
+ const searchRect = searchBox.getBoundingClientRect();
672
+ const leftInset = Math.max(
673
+ 0,
674
+ Math.round(searchRect.left - menuRect.left)
675
+ );
676
+ const rightInset = Math.max(
677
+ 0,
678
+ Math.round(menuRect.right - searchRect.right)
679
+ );
680
+
681
+ pickerDom.style.setProperty(
682
+ "--doc-version-picker-padding",
683
+ `8px ${rightInset}px 8px ${leftInset}px`
684
+ );
685
+ pickerDom.style.setProperty("--doc-version-picker-width", "100%");
686
+ }
687
+
688
+ /** Finds Redoc's search box via stable, semantic selectors. */
689
+ private findSearchBox(menuContent: HTMLElement): HTMLElement | null {
690
+ return menuContent.querySelector<HTMLElement>(
691
+ '[role="search"], .search-input, input[type="search"], input[placeholder*="Search" i]'
692
+ );
693
+ }
694
+
695
+ /** Builds the version picker DOM by reusing `doc-version-picker`. */
696
+ private buildVersionPickerDom(): HTMLElement {
697
+ const wrapper = document.createElement("div");
698
+ wrapper.className = "redoc-version-picker";
699
+
700
+ const picker = createElement("doc-version-picker", {
701
+ is: VersionPicker
702
+ });
703
+
704
+ Object.assign(picker, {
705
+ versions: this.versions,
706
+ selectedVersion: this.selectedVersion,
707
+ latestVersion: this.latestVersion,
708
+ readOnly: this.isVersionReadOnly
709
+ });
710
+ picker.addEventListener("change", this.onVersionChange);
711
+ wrapper.appendChild(picker);
712
+
713
+ return wrapper;
714
+ }
715
+
716
+ /**
717
+ * Stashes the current view (Redoc's operation anchor is the URL hash) before
718
+ * the version link navigates, so `restoreSelectedView` can re-apply it once
719
+ * the target version has loaded.
720
+ */
721
+ private onVersionChange = (): void => {
722
+ sessionStorage.setItem(
723
+ SELECTED_VIEW_STORAGE_KEY,
724
+ window.location.hash || ""
725
+ );
726
+ };
727
+
728
+ /**
729
+ * Re-applies the view (operation anchor) that was active before a version
730
+ * switch. The version link navigates to the target version's root (no
731
+ * hash), so if a hash was stashed and its target exists in the new spec, we
732
+ * restore it; `handleInitialHashScrollFix` then corrects the scroll offset.
733
+ * A missing anchor is a no-op (lands on the spec root).
734
+ */
735
+ private restoreSelectedView(): void {
736
+ const storedHash = sessionStorage.getItem(SELECTED_VIEW_STORAGE_KEY);
737
+ sessionStorage.removeItem(SELECTED_VIEW_STORAGE_KEY);
738
+
739
+ if (!storedHash || window.location.hash) {
740
+ return;
741
+ }
742
+
743
+ const targetId = storedHash.replace(/^#/, "");
744
+ if (targetId && document.getElementById(targetId)) {
745
+ window.location.hash = storedHash;
746
+ }
747
+ }
748
+
749
+ /**
750
+ * Injects a scoped stylesheet into Redoc's light-DOM container so the LNB
751
+ * matches the design: white background with a divider between the sidebar
752
+ * and the main content. Component-scoped CSS can't reach Redoc's light DOM,
753
+ * hence the injected `<style>`. Guarded against duplicate insertion.
754
+ *
755
+ * Also suppresses `doc-version-picker`'s own top/bottom dividers for this
756
+ * placement — those are sidebar chrome for other consumers, but the design
757
+ * shows no line between the picker and Redoc's search box. Set via the
758
+ * picker's `--doc-version-picker-divider` custom property, which inherits
759
+ * through the shadow boundary.
760
+ */
761
+ private injectRedocStyles(redocContainer: HTMLElement): void {
762
+ if (redocContainer.querySelector("style[data-redoc-reference]")) {
763
+ return;
764
+ }
765
+
766
+ const style = document.createElement("style");
767
+ style.setAttribute("data-redoc-reference", "");
768
+ style.textContent = `
769
+ .redoc-container .menu-content {
770
+ background: white;
771
+ border-right: 1px solid var(--dx-g-gray-90);
772
+ }
773
+ .redoc-version-picker {
774
+ --doc-version-picker-divider: none;
775
+ }
776
+ `;
777
+ redocContainer.appendChild(style);
778
+ }
779
+
563
780
  /**
564
781
  * Builds the locale picker DOM by reusing `dx-sidebar-footer-nav`
565
782
  */
@@ -4,26 +4,45 @@
4
4
  :host {
5
5
  --dx-c-dropdown-option-font-weight: normal;
6
6
  --dx-c-dropdown-option-label-color: var(--dx-g-gray-10);
7
+ --dx-c-dropdown-option-padding: var(--dx-g-spacing-sm)
8
+ var(--dx-g-spacing-lg);
9
+ --dx-c-popover-padding: var(--dx-g-spacing-sm) 0;
7
10
  --popover-container-open-transform: translateY(4px);
8
11
  }
9
12
 
10
13
  .version-picker-container {
11
- padding: 8px var(--dx-g-spacing-lg) 8px
12
- var(--dx-g-global-header-padding-horizontal);
13
- border-top: 1px solid var(--dx-g-gray-90);
14
- border-bottom: 1px solid var(--dx-g-gray-90);
14
+ /* Consumers can override the padding via --doc-version-picker-padding;
15
+ defaults to the sidebar-header inset. */
16
+ padding: var(
17
+ --doc-version-picker-padding,
18
+ 8px var(--dx-g-spacing-lg) 8px
19
+ var(--dx-g-global-header-padding-horizontal)
20
+ );
21
+
22
+ /* Consumers can suppress or restyle the dividers via
23
+ --doc-version-picker-divider (e.g. `none`); defaults to the sidebar rule. */
24
+ border-top: var(
25
+ --doc-version-picker-divider,
26
+ 1px solid var(--dx-g-gray-90)
27
+ );
28
+ border-bottom: var(
29
+ --doc-version-picker-divider,
30
+ 1px solid var(--dx-g-gray-90)
31
+ );
15
32
  }
16
33
 
17
34
  .version-picker-button {
18
35
  display: flex;
19
36
  width: var(--doc-version-picker-width, 296px);
37
+
38
+ --dx-c-button-horizontal-spacing: var(--dx-g-spacing-sm);
39
+ --dx-g-button-icon-color: var(--dx-g-gray-50);
20
40
  }
21
41
 
22
42
  .version-picker-button:hover,
23
- .version-picker-button:active,
24
- .version-picker-button:focus {
25
- --dx-c-button-secondary-color-hover: var(--dx-g-cloud-blue-vibrant-95);
26
- --dx-c-button-primary-color: var(--dx-g-blue-vibrant-40);
43
+ .version-picker-button:focus-within,
44
+ .version-picker-button[aria-expanded="true"] {
45
+ --dx-g-button-icon-color: var(--dx-g-blue-vibrant-20);
27
46
  }
28
47
 
29
48
  /**
@@ -35,6 +54,27 @@ dx-button::part(content) {
35
54
  overflow: hidden;
36
55
  }
37
56
 
57
+ /* The border is an inset box-shadow so thickening it on hover/focus doesn't
58
+ resize the small variant's fit-content box. */
59
+ .version-picker-button::part(container) {
60
+ background: white;
61
+ box-shadow: inset 0 0 0 1px var(--dx-g-gray-50);
62
+ color: var(--dx-g-gray-10);
63
+ }
64
+
65
+ .version-picker-button::part(container):hover {
66
+ background: var(--dx-g-blue-vibrant-95);
67
+ box-shadow: inset 0 0 0 2px var(--dx-g-blue-vibrant-20);
68
+ }
69
+
70
+ /* aria-expanded keeps the focus ring while the menu is open. */
71
+ .version-picker-button:focus-within::part(container),
72
+ .version-picker-button[aria-expanded="true"]::part(container) {
73
+ background: var(--dx-g-blue-vibrant-95);
74
+ box-shadow: inset 0 0 0 2px var(--dx-g-blue-vibrant-20), 0 0 0 2px white,
75
+ 0 0 0 4px var(--dx-g-blue-vibrant-60);
76
+ }
77
+
38
78
  .selected-version {
39
79
  display: flex;
40
80
  flex-direction: row;
@@ -49,16 +89,100 @@ dx-button::part(content) {
49
89
  white-space: nowrap;
50
90
  }
51
91
 
52
- dx-type-badge.latest-badge {
53
- --dx-c-type-badge-color: var(--dx-g-green-vibrant-40);
54
- --dx-c-type-badge-background: var(--dx-g-green-vibrant-95);
55
-
92
+ dx-type-badge.latest-badge,
93
+ dx-type-badge.not-latest-badge {
56
94
  margin-left: var(--dx-g-spacing-sm);
57
95
  }
58
96
 
59
- dx-type-badge.not-latest-badge {
60
- --dx-c-type-badge-color: var(--dx-g-red-vibrant-40);
61
- --dx-c-type-badge-background: var(--dx-g-red-vibrant-95);
97
+ /* ------------------------------------------------------------------ *
98
+ * Small "dot" variant (opt-in via `small`). Additive
99
+ * ------------------------------------------------------------------ */
62
100
 
63
- margin-left: var(--dx-g-spacing-sm);
101
+ /* Option padding is sm both axes: the small menu tracks the ~104px trigger, so
102
+ the default 24px horizontal would crowd the labels. */
103
+ .version-picker-dropdown-small {
104
+ --dx-c-dropdown-option-font-size: var(--dx-g-text-xs);
105
+ --dx-c-dropdown-option-padding: var(--dx-g-spacing-sm)
106
+ var(--dx-g-spacing-sm);
107
+ --dx-c-dropdown-option-border-radius: 0;
108
+ --dx-c-popover-border: none;
109
+ }
110
+
111
+ .version-picker-button-small {
112
+ width: fit-content;
113
+ max-width: 104px;
114
+
115
+ --dx-c-button-font-size: var(--dx-g-text-xs);
116
+ --dx-c-button-font-weight: var(--dx-g-font-normal);
117
+ --dx-c-button-line-height: var(--dx-g-spacing-lg);
118
+ --dx-c-button-icon-gap: var(--dx-g-spacing-2xs);
119
+ }
120
+
121
+ /* min-width: 0 on each flex ancestor lets the label truncate: a flex item's
122
+ default min-width: auto refuses to shrink below its content and overrides the
123
+ host's max-width, so without this the trigger overflows instead. */
124
+ .version-picker-button-small::part(content) {
125
+ display: flex;
126
+ flex: 1;
127
+ min-width: 0;
128
+ width: auto;
129
+ overflow: hidden;
130
+ }
131
+
132
+ /* width:100% re-ties to the 104px-capped host (container is width:inherit,
133
+ which copies width but not max-width). Border/bg/states are shared above. */
134
+ .version-picker-button-small::part(container) {
135
+ width: 100%;
136
+ height: var(--dx-g-spacing-lg);
137
+ padding: 0 var(--dx-g-spacing-xs);
138
+ }
139
+
140
+ .selected-version-small {
141
+ flex: 1;
142
+ min-width: 0;
143
+ }
144
+
145
+ .version-picker-dot {
146
+ flex: 0 0 auto;
147
+ width: var(--dx-g-spacing-sm);
148
+ height: var(--dx-g-spacing-sm);
149
+ margin-right: var(--dx-g-spacing-xs);
150
+ border-radius: 50%;
151
+ }
152
+
153
+ .version-picker-dot-latest {
154
+ background: var(--dx-g-green-vibrant-60);
155
+ }
156
+
157
+ .version-picker-dot-not-latest {
158
+ background: var(--dx-g-yellow-vibrant-80);
159
+ }
160
+
161
+ /* inline-block (not flex) so the label can truncate; line-height centers it. */
162
+ .version-picker-readonly {
163
+ box-sizing: border-box;
164
+ display: inline-block;
165
+ width: var(--doc-version-picker-width, 296px);
166
+ height: var(--dx-g-spacing-xl);
167
+ margin: 0;
168
+ padding: 0 var(--dx-g-spacing-sm);
169
+ border: 1px solid var(--dx-g-gray-80);
170
+ border-radius: var(--dx-g-spacing-xs);
171
+ overflow: hidden;
172
+ color: var(--dx-g-gray-10);
173
+ text-overflow: ellipsis;
174
+ white-space: nowrap;
175
+ font: var(--dx-g-font-normal) var(--dx-g-text-sm) / var(--dx-g-spacing-xl)
176
+ var(--dx-g-font-sans),
177
+ sans-serif;
178
+ }
179
+
180
+ .version-picker-readonly-small {
181
+ width: fit-content;
182
+ max-width: 104px;
183
+ height: var(--dx-g-spacing-lg);
184
+ padding: 0 var(--dx-g-spacing-xs);
185
+ font: var(--dx-g-font-normal) var(--dx-g-text-xs) / var(--dx-g-spacing-lg)
186
+ var(--dx-g-font-sans),
187
+ sans-serif;
64
188
  }
@@ -1,37 +1,70 @@
1
1
  <template>
2
2
  <div lwc:if={showVersionPicker} class="version-picker-container">
3
+ <!-- Small read-only: plain value, no dropdown -->
4
+ <p
5
+ lwc:if={readOnly}
6
+ class={readOnlyClass}
7
+ title={selectedVersion.label}
8
+ >
9
+ {selectedVersion.label}
10
+ </p>
11
+
3
12
  <dx-dropdown
13
+ lwc:else
14
+ class={dropdownClass}
4
15
  options={versions}
5
16
  analytics-event="custEv_docVersionSelect"
6
17
  analytics-payload={analyticsPayload}
7
18
  value={selectedVersion.id}
8
- width="var(--doc-version-picker-width)"
19
+ full-width="true"
9
20
  onchange={onVersionChange}
10
21
  >
11
22
  <dx-button
12
- class="version-picker-button"
23
+ class={triggerClass}
13
24
  variant="tertiary"
14
25
  size="small"
26
+ font="sans"
15
27
  icon-symbol="chevrondown"
16
- icon-size="medium"
28
+ icon-size={triggerIconSize}
29
+ aria-label={triggerAriaLabel}
17
30
  >
18
- <div class="selected-version">
19
- <p class="selected-version-label">
20
- {selectedVersion.label}
21
- </p>
22
- <template lwc:if={showLatestTag}>
23
- <dx-type-badge
24
- class="latest-badge"
25
- lwc:if={latestVersion}
26
- value="Latest"
27
- size="small"
28
- ></dx-type-badge>
29
- <dx-type-badge
30
- class="not-latest-badge"
31
- lwc:else
32
- value="Not Latest"
33
- size="small"
34
- ></dx-type-badge>
31
+ <div class={selectedVersionClass}>
32
+ <!-- Small: leading colored dot -->
33
+ <template lwc:if={small}>
34
+ <template lwc:if={showLatestTag}>
35
+ <span class={dotClass} aria-hidden="true"></span>
36
+ </template>
37
+ <p
38
+ class="selected-version-label"
39
+ title={selectedVersion.label}
40
+ >
41
+ {selectedVersion.label}
42
+ </p>
43
+ </template>
44
+ <!-- Default: trailing Latest/Not-Latest badge -->
45
+ <template lwc:else>
46
+ <p
47
+ class="selected-version-label"
48
+ title={selectedVersion.label}
49
+ >
50
+ {selectedVersion.label}
51
+ </p>
52
+ <template lwc:if={showLatestTag}>
53
+ <dx-type-badge
54
+ class="latest-badge"
55
+ lwc:if={latestVersion}
56
+ variant="status-success"
57
+ value="Latest"
58
+ size="small"
59
+ ></dx-type-badge>
60
+ <dx-type-badge
61
+ class="not-latest-badge"
62
+ lwc:else
63
+ variant="status-warning"
64
+ value="Not Latest"
65
+ size="small"
66
+ ></dx-type-badge>
67
+ </template>
35
68
  </template>
36
69
  </div>
37
70
  </dx-button>
@@ -1,4 +1,5 @@
1
1
  import { LightningElement, api, track } from "lwc";
2
+ import cx from "classnames";
2
3
 
3
4
  import { AnalyticsPayload, OptionWithNested } from "typings/custom";
4
5
 
@@ -12,6 +13,8 @@ export default class VersionPicker extends LightningElement {
12
13
  private _selectedVersion?: OptionWithNested;
13
14
  private _latestVersion: boolean = false;
14
15
  private _hideBadge: boolean = false;
16
+ private _small: boolean = false;
17
+ private _readOnly: boolean = false;
15
18
 
16
19
  @api
17
20
  get versions() {
@@ -51,6 +54,27 @@ export default class VersionPicker extends LightningElement {
51
54
  this._hideBadge = normalizeBoolean(value);
52
55
  }
53
56
 
57
+ // Opt-in compact "dot" variant (24px, abbreviated label). Off by default so
58
+ // existing consumers render the default trigger unchanged.
59
+ @api
60
+ get small() {
61
+ return this._small;
62
+ }
63
+
64
+ set small(value) {
65
+ this._small = normalizeBoolean(value);
66
+ }
67
+
68
+ // Opt-in read-only state: plain value, no dropdown menu.
69
+ @api
70
+ get readOnly() {
71
+ return this._readOnly;
72
+ }
73
+
74
+ set readOnly(value) {
75
+ this._readOnly = normalizeBoolean(value);
76
+ }
77
+
54
78
  private get showVersionPicker() {
55
79
  return this._versions && this._versions.length !== 0;
56
80
  }
@@ -59,6 +83,62 @@ export default class VersionPicker extends LightningElement {
59
83
  return !this.hideBadge;
60
84
  }
61
85
 
86
+ private get readOnlyClass(): string {
87
+ return cx(
88
+ "version-picker-readonly",
89
+ this.small && "version-picker-readonly-small"
90
+ );
91
+ }
92
+
93
+ private get dotClass(): string {
94
+ return `version-picker-dot ${
95
+ this.latestVersion
96
+ ? "version-picker-dot-latest"
97
+ : "version-picker-dot-not-latest"
98
+ }`;
99
+ }
100
+
101
+ // Accessible name for the small trigger: includes the latest state so it
102
+ // isn't conveyed by the dot's color alone (WCAG 1.4.1). Omits the state
103
+ // when the indicator is hidden.
104
+ private get smallTriggerAriaLabel(): string {
105
+ const label = this.selectedVersion?.label ?? "";
106
+ if (!this.showLatestTag) {
107
+ return label;
108
+ }
109
+ return `${label}, ${this.latestVersion ? "Latest" : "Not Latest"}`;
110
+ }
111
+
112
+ // --- Per-variant values for the shared dropdown/button markup below. The
113
+ // small variant opts into extra classes and a compact chevron.
114
+
115
+ // Not cx(): the default must have NO class attribute (cx returns "", which
116
+ // would render class="" and break the frozen consumer snapshots).
117
+ private get dropdownClass(): string | undefined {
118
+ return this.small ? "version-picker-dropdown-small" : undefined;
119
+ }
120
+
121
+ private get triggerClass(): string {
122
+ return cx(
123
+ "version-picker-button",
124
+ this.small && "version-picker-button-small"
125
+ );
126
+ }
127
+
128
+ private get triggerIconSize(): string {
129
+ return this.small ? "xsmall" : "medium";
130
+ }
131
+
132
+ // Default returns "" (dx-button's own default) so the trigger's rendered
133
+ // aria-label is unchanged for the default variant.
134
+ private get triggerAriaLabel(): string {
135
+ return this.small ? this.smallTriggerAriaLabel : "";
136
+ }
137
+
138
+ private get selectedVersionClass(): string {
139
+ return cx("selected-version", this.small && "selected-version-small");
140
+ }
141
+
62
142
  private onVersionChange(e: CustomEvent) {
63
143
  this.dispatchEvent(new CustomEvent("change", { detail: e.detail }));
64
144
  }
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.