@salesforcedevs/dx-components 1.40.10 → 1.41.0

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,16 +1,16 @@
1
1
  {
2
2
  "name": "@salesforcedevs/dx-components",
3
- "version": "1.40.10",
3
+ "version": "1.41.0",
4
4
  "description": "DX Lightning web components",
5
5
  "license": "MIT",
6
6
  "engines": {
7
- "node": "24.22.1"
7
+ "node": "22.x"
8
8
  },
9
9
  "publishConfig": {
10
10
  "access": "public"
11
11
  },
12
12
  "dependencies": {
13
- "@coveo/headless": "3.58.1",
13
+ "@coveo/headless": "3.22.2",
14
14
  "@floating-ui/dom": "1.5.1",
15
15
  "@optimizely/optimizely-sdk": "5.3.0",
16
16
  "@shikijs/colorized-brackets": "^3.12.2",
@@ -44,5 +44,5 @@
44
44
  "luxon": "3.4.4",
45
45
  "msw": "^2.12.4"
46
46
  },
47
- "gitHead": "d83097b7ff12906e6296ee9031bd891e0b66e9e8"
47
+ "gitHead": "505fb7062378be9370bdb2dd44f66b5b327b3665"
48
48
  }
@@ -132,3 +132,22 @@ svg,
132
132
  display: inline-block;
133
133
  content: "(.\00a0\00a0)";
134
134
  }
135
+
136
+ .single-option {
137
+ display: block;
138
+ width: 100%;
139
+ padding: 6px 0;
140
+ text-align: left;
141
+ font-family: var(--dx-g-font-sans);
142
+ font-size: 14px;
143
+ color: rgb(24 24 24);
144
+ }
145
+
146
+ .single-option:hover,
147
+ .single-option-selected {
148
+ color: var(--dx-g-blue-vibrant-50);
149
+ }
150
+
151
+ .single-option-selected {
152
+ font-weight: bold;
153
+ }
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <form class="container">
3
3
  <span class="title-group-main title-group" onclick={dropdownToggle}>
4
- <p class="filter-menu-title">{header}</p>
4
+ <p class="filter-menu-title">{displayTitle}</p>
5
5
  <svg
6
6
  xmlns="http://www.w3.org/2000/svg"
7
7
  width="28"
@@ -18,7 +18,22 @@
18
18
  </span>
19
19
  <ul class="nested first-layer always-visible-scrollbar">
20
20
  <li>
21
- <template if:true={isBaseVariant}>
21
+ <template if:true={singleSelect}>
22
+ <template for:each={singleOptions} for:item="option">
23
+ <button
24
+ key={option.id}
25
+ type="button"
26
+ class={option.className}
27
+ role="option"
28
+ aria-selected={option.selected}
29
+ data-id={option.id}
30
+ onclick={onSingleSelect}
31
+ >
32
+ {option.name}
33
+ </button>
34
+ </template>
35
+ </template>
36
+ <template if:true={isCheckboxList}>
22
37
  <template for:each={filterMenuOptions} for:item="option">
23
38
  <dx-checkbox
24
39
  key={option.id}
@@ -11,6 +11,10 @@ export default class FilterMenu extends LightningElement {
11
11
  @api header: string = "Filter Menu";
12
12
  @api variant: string = "base";
13
13
  @api filtersDatesLoading?: boolean;
14
+ /** Render a single-choice list; the header shows the selected option. */
15
+ @api singleSelect: boolean = false;
16
+ /** Id of the selected option when singleSelect is set. */
17
+ @api value?: string;
14
18
 
15
19
  @api
16
20
  get options(): Option[] {
@@ -31,6 +35,31 @@ export default class FilterMenu extends LightningElement {
31
35
  return this.variant === "nested";
32
36
  }
33
37
 
38
+ get isCheckboxList() {
39
+ return this.isBaseVariant && !this.singleSelect;
40
+ }
41
+
42
+ get selectedOption() {
43
+ return this.options.find((o) => String(o.id) === this.value);
44
+ }
45
+
46
+ get displayTitle() {
47
+ return (this.singleSelect && this.selectedOption?.name) || this.header;
48
+ }
49
+
50
+ get singleOptions() {
51
+ return this.options.map((o) => {
52
+ const selected = String(o.id) === this.value;
53
+ return {
54
+ ...o,
55
+ selected,
56
+ className: selected
57
+ ? "single-option single-option-selected"
58
+ : "single-option"
59
+ };
60
+ });
61
+ }
62
+
34
63
  get showViewMoreBtn() {
35
64
  return this.options.length > 20;
36
65
  }
@@ -93,6 +122,20 @@ export default class FilterMenu extends LightningElement {
93
122
  this.showMore = !this.showMore;
94
123
  }
95
124
 
125
+ private onSingleSelect(e: any) {
126
+ const id = e.currentTarget.dataset.id;
127
+ const value = this.options.find((o) => String(o.id) === id);
128
+ this.template.querySelector(".nested")?.classList.remove("active");
129
+ this.template
130
+ .querySelector(".header-caret")
131
+ ?.classList.remove("caret-down");
132
+ this.dispatchEvent(
133
+ new CustomEvent("change", {
134
+ detail: { name: this.name, value, checked: true }
135
+ })
136
+ );
137
+ }
138
+
96
139
  private onChange(e: any) {
97
140
  const { checked, value } = e.detail;
98
141
  this.dispatchEvent(
@@ -1,5 +1,4 @@
1
1
  <template>
2
- <!-- linthtml-disable id-no-dup -->
3
2
  <label if:true={label} id="label" for="input" tabindex="-1">
4
3
  {label}
5
4
  <abbr if:true={required}>*</abbr>
@@ -1,3 +1,5 @@
1
+ @import "dxHelpers/text";
2
+
1
3
  :host {
2
4
  display: block;
3
5
  font-family: var(--dx-g-font-sans);
@@ -42,56 +44,68 @@
42
44
  margin-top: var(--dx-g-spacing-md);
43
45
  }
44
46
 
45
- /* Body + toolbar ------------------------------------------------------- */
47
+ /* Body --------------------------------------------------------------- */
46
48
  .dx-search-body {
47
49
  margin: 0 auto;
48
50
  max-width: 1200px;
49
51
  padding: var(--dx-g-spacing-xl);
50
52
  }
51
53
 
52
- .dx-search-toolbar {
54
+ .dx-search-layout {
53
55
  display: flex;
54
- flex-wrap: wrap;
55
- align-items: flex-end;
56
- justify-content: space-between;
56
+ align-items: flex-start;
57
+ gap: var(--dx-g-spacing-2xl);
58
+ }
59
+
60
+ .dx-search-filters {
61
+ flex: 0 0 280px;
62
+ display: flex;
63
+ flex-direction: column;
57
64
  gap: var(--dx-g-spacing-md);
58
- margin-bottom: var(--dx-g-spacing-lg);
59
65
  }
60
66
 
61
- .dx-facet-row {
67
+ .dx-filter-header {
62
68
  display: flex;
63
- flex-wrap: wrap;
64
- gap: var(--dx-g-spacing-sm);
69
+ justify-content: space-between;
70
+ align-items: baseline;
65
71
  }
66
72
 
67
- .dx-facet-chip {
68
- background-color: white;
69
- color: var(--dx-g-blue-vibrant-20);
70
- border: 1px solid var(--dx-g-blue-vibrant-50);
71
- border-radius: 500px;
72
- padding: 6px 14px;
73
+ .dx-filter-title {
74
+ margin: 0;
75
+ color: var(--dx-g-text-heading-color);
76
+ font-family: var(--dx-g-font-display);
77
+ font-size: var(--dx-g-text-lg);
78
+ }
79
+
80
+ .dx-filter-clear {
81
+ color: var(--dx-g-blue-vibrant-50);
82
+ font-family: var(--dx-g-font-display);
73
83
  font-size: var(--dx-g-text-sm);
74
- cursor: pointer;
75
84
  }
76
85
 
77
- .dx-facet-chip:hover {
78
- background-color: var(--dx-g-blue-vibrant-95);
86
+ .dx-filter-clear:disabled {
87
+ opacity: 0.4;
88
+ cursor: default;
79
89
  }
80
90
 
81
- .dx-facet-chip_active {
82
- background-color: var(--dx-g-blue-vibrant-50);
83
- color: white;
84
- font-weight: var(--dx-g-font-demi);
91
+ .dx-search-range {
92
+ margin: 0 0 var(--dx-g-spacing-lg);
85
93
  }
86
94
 
87
- .dx-facet-chip-count {
88
- margin-left: 6px;
89
- opacity: 0.75;
95
+ .dx-search-main {
96
+ flex: 1 1 0;
97
+ min-width: 0;
90
98
  }
91
99
 
92
- .dx-search-controls {
93
- display: flex;
94
- gap: var(--dx-g-spacing-md);
100
+ @media (max-width: 768px) {
101
+ .dx-search-layout {
102
+ flex-direction: column;
103
+ }
104
+
105
+ .dx-search-filters {
106
+ flex-basis: auto;
107
+ width: 100%;
108
+ }
95
109
  }
96
110
 
97
111
  /* Status states -------------------------------------------------------- */
@@ -113,49 +127,30 @@
113
127
  }
114
128
 
115
129
  .dx-result {
116
- background: white;
117
- border-radius: 16px;
118
- border: 1px solid rgb(24 24 24 / 4%);
119
- box-shadow: 0 0 2px 0 rgb(24 24 24 / 8%), 0 2px 4px 1px rgb(24 24 24 / 16%);
120
- padding: 20px;
121
- margin-bottom: var(--dx-g-spacing-lg);
122
- }
123
-
124
- .dx-result-info {
125
130
  display: flex;
126
- gap: 12px;
127
- margin-bottom: var(--dx-g-spacing-smd);
131
+ flex-direction: column;
132
+ align-items: flex-start;
133
+ gap: var(--dx-g-spacing-smd);
134
+ border-bottom: 1px solid var(--sds-g-gray-4);
135
+ padding-bottom: var(--dx-g-spacing-xl);
136
+ margin-bottom: var(--dx-g-spacing-xl);
128
137
  }
129
138
 
130
- .dx-badge {
131
- display: inline-flex;
132
- align-items: center;
133
- border-radius: 4px;
134
- padding: 2px 8px;
135
- font-size: var(--dx-g-text-xs);
136
- background-color: var(--dx-g-blue-vibrant-95);
137
- color: var(--dx-g-blue-vibrant-20);
138
- }
139
-
140
- .dx-result-title {
141
- color: var(--dx-g-blue-vibrant-20);
142
- display: block;
143
- font-family: var(--dx-g-font-display);
144
- font-size: var(--dx-g-text-lg);
145
- margin: 0 0 var(--dx-g-spacing-sm);
139
+ .dx-result-title:hover {
140
+ --dx-c-card-title-color: var(--dx-g-blue-vibrant-40);
146
141
  }
147
142
 
148
143
  .dx-result-date {
149
- color: #555;
150
- font-size: var(--dx-g-text-xs);
151
- margin: 0 0 var(--dx-g-spacing-xs);
144
+ margin: 0;
145
+ text-align: start;
152
146
  }
153
147
 
154
148
  .dx-result-excerpt {
155
- color: var(--dx-g-gray-10);
156
- font-size: 14px;
157
- line-height: 20px;
158
149
  margin: 0;
150
+ display: box;
151
+ -webkit-line-clamp: 3;
152
+ -webkit-box-orient: vertical;
153
+ overflow: hidden;
159
154
  }
160
155
 
161
156
  /* Pagination ----------------------------------------------------------- */
@@ -12,133 +12,154 @@
12
12
  </div>
13
13
 
14
14
  <div class="dx-search-body">
15
- <!-- Toolbar: content-type facets + sort + locale selector -->
16
- <div class="dx-search-toolbar">
17
- <div
18
- if:true={hasFacets}
19
- class="dx-facet-row"
20
- role="group"
21
- aria-label="Filter by content type"
22
- >
23
- <template for:each={facetChips} for:item="chip">
15
+ <div class="dx-search-layout">
16
+ <!-- Sidebar: content-type filter + language (like content archive) -->
17
+ <aside class="dx-search-filters" aria-label="Search filters">
18
+ <div class="dx-filter-header">
19
+ <p class="dx-filter-title">Filters</p>
24
20
  <button
25
- key={chip.value}
26
21
  type="button"
27
- class={chip.cssClass}
28
- data-value={chip.value}
29
- aria-pressed={chip.active}
30
- onclick={handleFacetClick}
22
+ class="dx-filter-clear"
23
+ disabled={noActiveFilters}
24
+ onclick={handleClearFilters}
31
25
  >
32
- {chip.label}
33
- <span class="dx-facet-chip-count">
34
- {chip.count}
35
- </span>
26
+ Clear all
36
27
  </button>
37
- </template>
38
- </div>
39
-
40
- <div class="dx-search-controls">
41
- <div if:true={showLocaleSelector} class="dx-search-locale">
42
- <dx-select
43
- label="Language"
44
- options={localeSelectOptions}
45
- value={localeValue}
46
- onchange={handleLocaleChange}
47
- ></dx-select>
48
28
  </div>
49
- <div class="dx-search-sort">
50
- <dx-select
51
- label="Sort by"
52
- options={sortOptions}
53
- value={sortValue}
54
- onchange={handleSortChange}
55
- ></dx-select>
56
- </div>
57
- </div>
58
- </div>
29
+ <dx-filter-menu
30
+ if:true={hasFacets}
31
+ class="dx-filter-type"
32
+ name="contentType"
33
+ header="Content Type"
34
+ single-select
35
+ options={typeFilterOptions}
36
+ value={selectedTypeValue}
37
+ onchange={handleTypeFilterChange}
38
+ ></dx-filter-menu>
39
+ <dx-filter-menu
40
+ if:true={showLocaleSelector}
41
+ class="dx-filter-locale"
42
+ name="language"
43
+ header="Language"
44
+ single-select
45
+ options={localeFilterOptions}
46
+ value={localeValue}
47
+ onchange={handleLocaleChange}
48
+ ></dx-filter-menu>
49
+ </aside>
59
50
 
60
- <!-- Loading -->
61
- <template if:true={isLoading}>
62
- <div class="dx-search-status" aria-live="polite">
63
- <dx-spinner size="medium" variant="base"></dx-spinner>
64
- </div>
65
- </template>
51
+ <div class="dx-search-main">
52
+ <p
53
+ if:true={showRange}
54
+ class="dx-search-range dx-text-body-2"
55
+ aria-live="polite"
56
+ >
57
+ Showing {rangeStart} - {rangeEnd} of {total}
58
+ </p>
59
+ <!-- Loading -->
60
+ <template if:true={isLoading}>
61
+ <div class="dx-search-status" aria-live="polite">
62
+ <dx-spinner
63
+ size="medium"
64
+ variant="base"
65
+ ></dx-spinner>
66
+ </div>
67
+ </template>
66
68
 
67
- <!-- Idle prompt (no query yet) -->
68
- <template if:true={isIdle}>
69
- <div class="dx-search-status" aria-live="polite">
70
- <dx-empty-state
71
- header="Search the developer site"
72
- subtitle="Enter a search term to get started."
73
- variant="alt"
74
- ></dx-empty-state>
75
- </div>
76
- </template>
69
+ <!-- Idle prompt (no query yet) -->
70
+ <template if:true={isIdle}>
71
+ <div class="dx-search-status" aria-live="polite">
72
+ <dx-empty-state
73
+ header="Search the developer site"
74
+ subtitle="Enter a search term to get started."
75
+ variant="alt"
76
+ ></dx-empty-state>
77
+ </div>
78
+ </template>
77
79
 
78
- <!-- Empty (query ran, no results) -->
79
- <template if:true={isEmpty}>
80
- <div class="dx-search-status" aria-live="polite">
81
- <dx-empty-state
82
- header="No results found"
83
- subtitle="Try different keywords or check your spelling."
84
- variant="alt"
85
- ></dx-empty-state>
86
- </div>
87
- </template>
80
+ <!-- Empty (query ran, no results) -->
81
+ <template if:true={isEmpty}>
82
+ <div class="dx-search-status" aria-live="polite">
83
+ <dx-empty-state
84
+ header="No results found"
85
+ subtitle="Try different keywords or check your spelling."
86
+ variant="alt"
87
+ ></dx-empty-state>
88
+ </div>
89
+ </template>
88
90
 
89
- <!-- Error -->
90
- <template if:true={isError}>
91
- <div class="dx-search-status dx-search-error" role="alert">
92
- <p>{errorMessage}</p>
93
- </div>
94
- </template>
91
+ <!-- Error -->
92
+ <template if:true={isError}>
93
+ <div
94
+ class="dx-search-status dx-search-error"
95
+ role="alert"
96
+ >
97
+ <p>{errorMessage}</p>
98
+ </div>
99
+ </template>
95
100
 
96
- <!-- Results -->
97
- <template if:true={hasResults}>
98
- <ul class="dx-result-list">
99
- <template for:each={displayResults} for:item="result">
100
- <li key={result.key} class="dx-result">
101
- <div
102
- if:true={result.hasBadge}
103
- class="dx-result-info"
101
+ <!-- Results -->
102
+ <template if:true={hasResults}>
103
+ <ul class="dx-result-list">
104
+ <template
105
+ for:each={displayResults}
106
+ for:item="result"
104
107
  >
105
- <span class="dx-badge">
106
- {result.contentTypeLabel}
107
- </span>
108
- </div>
109
- <a href={result.href} class="dx-result-title">
110
- {result.title}
111
- </a>
112
- <p if:true={result.hasDate} class="dx-result-date">
113
- {result.displayDate}
114
- </p>
115
- <!-- Highlighted snippet (P4.5): innerHTML set in
108
+ <li key={result.key} class="dx-result">
109
+ <dx-type-badge
110
+ if:true={result.hasBadge}
111
+ class="dx-result-badge"
112
+ variant={result.contentType}
113
+ value={result.contentTypeLabel}
114
+ size="small"
115
+ ></dx-type-badge>
116
+ <dx-card-title
117
+ class="dx-result-title"
118
+ href={result.href}
119
+ header={result.title}
120
+ ></dx-card-title>
121
+ <p
122
+ if:true={result.hasDate}
123
+ class="dx-result-date dx-text-label-1-dark"
124
+ >
125
+ {result.displayDate}
126
+ </p>
127
+ <!-- Highlighted snippet (P4.5): innerHTML set in
116
128
  renderedCallback, keyed by data-snippet-key. -->
117
- <template lwc:if={result.hasSnippetHtml}>
118
- <p
119
- class="dx-result-excerpt"
120
- data-snippet-key={result.key}
121
- lwc:dom="manual"
122
- ></p>
129
+ <template lwc:if={result.hasSnippetHtml}>
130
+ <p
131
+ class="dx-result-excerpt dx-text-body-2"
132
+ data-snippet-key={result.key}
133
+ lwc:dom="manual"
134
+ ></p>
135
+ </template>
136
+ <template
137
+ lwc:elseif={result.hasDescription}
138
+ >
139
+ <p
140
+ class="dx-result-excerpt dx-text-body-2"
141
+ >
142
+ {result.description}
143
+ </p>
144
+ </template>
145
+ </li>
123
146
  </template>
124
- <template lwc:elseif={result.hasDescription}>
125
- <p class="dx-result-excerpt">
126
- {result.description}
127
- </p>
128
- </template>
129
- </li>
130
- </template>
131
- </ul>
147
+ </ul>
132
148
 
133
- <div if:true={showPagination} class="dx-pagination-container">
134
- <dx-pagination
135
- current-page={currentPage}
136
- total-pages={totalPages}
137
- pages-to-show="5"
138
- onpagechange={handlePageChange}
139
- ></dx-pagination>
149
+ <div
150
+ if:true={showPagination}
151
+ class="dx-pagination-container"
152
+ >
153
+ <dx-pagination
154
+ current-page={currentPage}
155
+ total-pages={totalPages}
156
+ pages-to-show="5"
157
+ onpagechange={handlePageChange}
158
+ ></dx-pagination>
159
+ </div>
160
+ </template>
140
161
  </div>
141
- </template>
162
+ </div>
142
163
  </div>
143
164
  </div>
144
165
  </template>
@@ -0,0 +1,244 @@
1
+ /**
2
+ * Sample /api/search response for the "apex" query, used by the WithResults
3
+ * story. Shared fields are filled in by `row()`. The two blog rows are
4
+ * invented so the "blog" filter has something to show; the facet counts are
5
+ * from the real response.
6
+ */
7
+ const DEV = "https://developer.salesforce.com/";
8
+ const G = "Apex Developer Guide";
9
+
10
+ interface Row {
11
+ url: string;
12
+ title: string;
13
+ description?: string;
14
+ contentType?: string;
15
+ lastmod?: string | null;
16
+ matchType?: string;
17
+ sectionTitle?: string | null;
18
+ sectionHash?: string | null;
19
+ snippet: string;
20
+ snippetSource?: string;
21
+ }
22
+
23
+ const row = (r: Row, i: number) => ({
24
+ url: r.url,
25
+ title: r.title,
26
+ description: r.description ?? G,
27
+ locale: "en-us",
28
+ site: "developer",
29
+ contentType: r.contentType ?? "docs",
30
+ lastmod: r.lastmod ?? null,
31
+ score: 2 - i * 0.04,
32
+ lexScore: 18,
33
+ semScore: 0.45,
34
+ matchType: r.matchType ?? "hybrid",
35
+ sectionUrl: r.sectionHash ? `${r.url}#${r.sectionHash}` : null,
36
+ sectionTitle: r.sectionTitle ?? null,
37
+ snippet: r.snippet,
38
+ snippetSource: r.snippetSource ?? "headline"
39
+ });
40
+
41
+ const APEX = `${DEV}docs/platform/apex-dev/references/apex-dev/`;
42
+ const REF = `${DEV}docs/platform/apex-ref/references`;
43
+ const CLI = `${DEV}docs/platform/salesforce-cli-reference/guide/`;
44
+
45
+ const ROWS: Row[] = [
46
+ {
47
+ url: `${APEX}apex-intro.html`,
48
+ title: `Introducing Apex | ${G}`,
49
+ snippet:
50
+ "Introducing <b>Apex</b> | <b>Apex</b> Developer Guide\n\nof hosting server code, such as Java or .NET, in a secure and robust environment. What is <b>Apex</b>?"
51
+ },
52
+ {
53
+ url: `${APEX}apex-intro-what-is-apex.html`,
54
+ title: `What is Apex? | ${G}`,
55
+ snippet: "What is <b>Apex</b>?"
56
+ },
57
+ {
58
+ url: `${APEX}connectapi-overview.html`,
59
+ title: `Connect in Apex | ${G}`,
60
+ snippet:
61
+ "Connect in <b>Apex</b> | <b>Apex</b> Developer Guide\n\nConnect in <b>Apex</b> Use Connect in <b>Apex</b> to develop custom experiences in Salesforce."
62
+ },
63
+ {
64
+ url: `${CLI}cli_reference_apex_run.html`,
65
+ title: "apex run | apex Commands",
66
+ description:
67
+ "Execute anonymous Apex code entered on the command line or from a local file.",
68
+ matchType: "lexical",
69
+ snippet:
70
+ "<b>apex</b> run | <b>apex</b> Commands\n\n<b>apex</b> run Execute anonymous <b>Apex</b> code entered on the command line or from a local file."
71
+ },
72
+ {
73
+ url: `${DEV}docs/platform/salesforce-slack-sdk/guide/apex_ref.html`,
74
+ title: "Apex | Apex SDK for Slack (Beta)",
75
+ description: "Use Apex classes in the",
76
+ matchType: "lexical",
77
+ snippet:
78
+ "<b>Apex</b> | <b>Apex</b> SDK for Slack (Beta)\n\nk.ActionDispatcher Commands: Slack.SlashCommandDispatcher Events: Slack.EventDispatcher Shortcuts: Slack.ShortcutDispatcher To determine which classes and methods you"
79
+ },
80
+ {
81
+ url: `${DEV}developer-centers/apex`,
82
+ title: "Apex",
83
+ description:
84
+ "Explore the developer resources for Apex to write server-side logic with classes, triggers, and services.",
85
+ contentType: "page",
86
+ lastmod: "2026-10-09T20:57:17.000Z",
87
+ matchType: "lexical",
88
+ snippet:
89
+ "Read more Developer Guide Debug, Test, and Deploy <b>Apex</b> Develop your <b>Apex</b> code in a sandbox, debug it, unit-test it"
90
+ },
91
+ {
92
+ url: `${APEX}apex-dev-guide.html`,
93
+ title: `Apex Developer Guide | ${G}`,
94
+ snippet:
95
+ "<b>Apex</b> Developer Guide | <b>Apex</b> Developer Guide\n\nthe Salesforce Release Notes to learn about the most recent updates and changes to <b>Apex</b>."
96
+ },
97
+ {
98
+ url: APEX.slice(0, -1),
99
+ title: `Apex Developer Guide | ${G}`,
100
+ snippet:
101
+ "<b>Apex</b> Developer Guide | <b>Apex</b> Developer Guide\n\nthe Salesforce Release Notes to learn about the most recent updates and changes to <b>Apex</b>."
102
+ },
103
+ {
104
+ url: `${DEV}docs/platform/apex-dev`,
105
+ title: `Apex Developer Guide | ${G}`,
106
+ snippet:
107
+ "You can run <b>Apex</b> code using a variety of mechanisms. <b>Apex</b> code runs in atomic transactions."
108
+ },
109
+ {
110
+ url: `${CLI}cli_reference_apex_run_test.html`,
111
+ title: "apex run test | apex Commands",
112
+ description: "Invoke Apex tests in an org.",
113
+ matchType: "lexical",
114
+ sectionHash: "aliases-for-apex-run-test",
115
+ sectionTitle: "Aliases for apex run test",
116
+ snippet:
117
+ "<b>apex</b> run test | <b>apex</b> Commands — Aliases for <b>apex</b> run test\n\nd if the target-org configuration variable is already set."
118
+ },
119
+ {
120
+ url: `${APEX}apex-intro-learning-apex.html`,
121
+ title: `Learning Apex | ${G}`,
122
+ matchType: "lexical",
123
+ sectionHash: "in-this-guide-apex-developer-guide",
124
+ sectionTitle: "In This Guide (Apex Developer Guide)",
125
+ snippet:
126
+ "Learning <b>Apex</b> | <b>Apex</b> Developer Guide — In This Guide (<b>Apex</b> Developer Guide)\n\nIn This Guide (<b>Apex</b> Developer Guide) Beginning programmers can look at the following: Introducing <b>Apex</b> , and in particular:"
127
+ },
128
+ {
129
+ url: `${APEX}apex-running.html`,
130
+ title: `Running Apex | ${G}`,
131
+ matchType: "lexical",
132
+ snippet:
133
+ "Running <b>Apex</b> | <b>Apex</b> Developer Guide\n\nRunning <b>Apex</b> You can access many features of the Salesforce user interface programmatically in <b>Apex</b>, and you can integrate with external SOAP and REST Web services."
134
+ },
135
+ {
136
+ url: `${APEX}apex-testing.html`,
137
+ title: `Testing Apex | ${G}`,
138
+ matchType: "lexical",
139
+ snippet:
140
+ "Testing <b>Apex</b> | <b>Apex</b> Developer Guide\n\ne tests. You can use it to test classes in isolation, which is important for unit testing."
141
+ },
142
+ {
143
+ url: `${APEX}apex-scheduler.html`,
144
+ title: `Apex Scheduler | ${G}`,
145
+ matchType: "lexical",
146
+ sectionHash: "implementing-the-schedulable-interface",
147
+ sectionTitle: "Implementing the Schedulable Interface",
148
+ snippet:
149
+ "<b>Apex</b> Scheduler | <b>Apex</b> Developer Guide — Implementing the Schedulable Interface\n\nImplementing the Schedulable Interface To schedule an <b>Apex</b> class to run at regular intervals, first write an <b>Apex</b> class that"
150
+ },
151
+ {
152
+ url: `${DEV}docs/platform/apex-ref/references/apex-ref`,
153
+ title: "Apex Reference Guide",
154
+ description: "Apex Reference Guide",
155
+ lastmod: "2026-11-03T01:09:58.000Z",
156
+ matchType: "semantic",
157
+ snippet: "Apex Reference Guide",
158
+ snippetSource: "description"
159
+ },
160
+ {
161
+ url: `${REF}/apex-ref/apex-ref-guide.html`,
162
+ title: "Apex Reference Guide",
163
+ description: "Apex Reference Guide",
164
+ lastmod: "2026-11-03T01:10:03.000Z",
165
+ matchType: "semantic",
166
+ snippet: "Apex Reference Guide",
167
+ snippetSource: "description"
168
+ },
169
+ {
170
+ url: `${REF}/apex-ref/v68.0`,
171
+ title: "Apex Reference Guide",
172
+ description: "Apex Reference Guide",
173
+ lastmod: "2026-11-03T01:10:03.000Z",
174
+ matchType: "semantic",
175
+ snippet: "Apex Reference Guide",
176
+ snippetSource: "description"
177
+ },
178
+ {
179
+ url: REF,
180
+ title: "Apex Reference Guide",
181
+ description: "Apex Reference Guide",
182
+ lastmod: "2026-11-03T01:10:03.000Z",
183
+ matchType: "semantic",
184
+ snippet: "Apex Reference Guide",
185
+ snippetSource: "description"
186
+ },
187
+ {
188
+ url: `${DEV}docs/industries/industries-common-dev/guide/omnistudio-apex-reference.html`,
189
+ title: "Omnistudio Apex Reference | Industries Common Resources | Industries Common Resources Developer Guide",
190
+ description:
191
+ "Apex is a strongly typed, object-oriented programming language that allows developers to execute flow and transaction control statements on the Salesf",
192
+ matchType: "semantic",
193
+ snippet:
194
+ "Apex is a strongly typed, object-oriented programming language that allows developers to execute flow and transaction control statements on the Salesf",
195
+ snippetSource: "description"
196
+ },
197
+ // Invented sample rows so the "blog" filter has results to show.
198
+ {
199
+ url: `${DEV}blogs/2026/09/apex-cursors-in-practice`,
200
+ title: "Apex Cursors in Practice",
201
+ description: "How to process large result sets with Apex cursors.",
202
+ contentType: "blog",
203
+ lastmod: "2026-09-18T00:00:00.000Z",
204
+ snippet: "Process large result sets with <b>Apex</b> cursors."
205
+ },
206
+ {
207
+ url: `${DEV}blogs/2026/06/apex-testing-tips`,
208
+ title: "Five Apex Testing Tips",
209
+ description: "Write faster, more reliable Apex tests.",
210
+ contentType: "blog",
211
+ lastmod: "2026-06-02T00:00:00.000Z",
212
+ snippet: "Write faster, more reliable <b>Apex</b> tests."
213
+ }
214
+ ];
215
+
216
+ const ALL = ROWS.map(row);
217
+
218
+ /**
219
+ * Builds the response the stub returns, honouring the `type` filter and `lang`
220
+ * query params so the sidebar filters work in Storybook. Facet counts always
221
+ * reflect the full result set (disjunctive facets), as the real API does.
222
+ */
223
+ export function mockSearchResponse(params: URLSearchParams) {
224
+ const type = params.get("type");
225
+ const results = type ? ALL.filter((r) => r.contentType === type) : ALL;
226
+ return {
227
+ query: params.get("q") ?? "apex",
228
+ locale: params.get("lang") ?? "en-us",
229
+ site: null,
230
+ sort: params.get("sort") ?? "relevance",
231
+ limit: 20,
232
+ offset: 0,
233
+ total: type ? results.length : 59,
234
+ count: results.length,
235
+ facets: {
236
+ contentType: [
237
+ { value: "docs", count: 56 },
238
+ { value: "blog", count: 2 },
239
+ { value: "page", count: 1 }
240
+ ]
241
+ },
242
+ results
243
+ };
244
+ }
@@ -7,11 +7,8 @@ import type { LocaleOption } from "docUtils/searchLocale";
7
7
  import {
8
8
  decodeHtmlEntities,
9
9
  highlightedSnippetHtml,
10
- isSortOption,
11
10
  resultHref,
12
- SORT_OPTIONS,
13
- type SearchResult,
14
- type SortOption
11
+ type SearchResult
15
12
  } from "docUtils/searchClient";
16
13
  import { searchLocalePrefs } from "dxUtils/searchLocalePrefs";
17
14
  import { track as trackGTM } from "dxUtils/analytics";
@@ -23,11 +20,19 @@ import { parseSupportedLocales } from "docUtils/searchLocale";
23
20
  // are ingested under this site value).
24
21
  const DX_SITE = "developer";
25
22
 
26
- const SORT_LABELS: Record<SortOption, string> = {
27
- relevance: "Relevance",
28
- date: "Most recent"
23
+ // The search backend labels developer docs "docs"; the badge and label
24
+ // constants know them as "documentation". Filtering still sends the raw value.
25
+ const CONTENT_TYPE_ALIASES: Record<string, string> = {
26
+ docs: "documentation",
27
+ page: "website"
29
28
  };
30
29
 
30
+ function normalizeContentType(contentType: string | null): string | null {
31
+ return contentType != null
32
+ ? CONTENT_TYPE_ALIASES[contentType] ?? contentType
33
+ : null;
34
+ }
35
+
31
36
  interface DisplayResult {
32
37
  key: string;
33
38
  url: string;
@@ -42,6 +47,7 @@ interface DisplayResult {
42
47
  // text path renders exactly as before.
43
48
  snippetHtml: string | null;
44
49
  hasSnippetHtml: boolean;
50
+ contentType: string | null;
45
51
  contentTypeLabel: string | null;
46
52
  hasBadge: boolean;
47
53
  displayDate: string;
@@ -56,7 +62,7 @@ interface FacetChip {
56
62
  cssClass: string;
57
63
  }
58
64
 
59
- interface SortSelectOption {
65
+ interface SelectOption {
60
66
  value: string;
61
67
  label: string;
62
68
  }
@@ -162,7 +168,8 @@ export default class SearchHybrid extends LightningElement {
162
168
  r.description != null && r.description !== ""
163
169
  ? decodeHtmlEntities(r.description)
164
170
  : null;
165
- const label = this.contentTypeLabel(r.contentType);
171
+ const contentType = normalizeContentType(r.contentType);
172
+ const label = this.contentTypeLabel(contentType);
166
173
  const displayDate = this.formatDate(r.lastmod);
167
174
  // Prefer the highlighted snippet (headline source only); otherwise
168
175
  // null → the description text path below renders unchanged.
@@ -176,6 +183,7 @@ export default class SearchHybrid extends LightningElement {
176
183
  hasDescription: description !== null,
177
184
  snippetHtml,
178
185
  hasSnippetHtml: snippetHtml !== null,
186
+ contentType,
179
187
  contentTypeLabel: label,
180
188
  hasBadge: label !== null,
181
189
  displayDate,
@@ -240,8 +248,9 @@ export default class SearchHybrid extends LightningElement {
240
248
  if (contentType == null || contentType === "") {
241
249
  return null;
242
250
  }
251
+ const normalized = normalizeContentType(contentType) as string;
243
252
  const labels = CONTENT_TYPE_LABELS as Record<string, string>;
244
- return labels[contentType] ?? contentType;
253
+ return labels[normalized] ?? contentType;
245
254
  }
246
255
 
247
256
  // ---- content-type facet chips (disjunctive) ---------------------------
@@ -269,24 +278,51 @@ export default class SearchHybrid extends LightningElement {
269
278
  return this.facetChips.length > 0;
270
279
  }
271
280
 
272
- // ---- sort + locale select options -------------------------------------
273
-
274
- get sortOptions(): SortSelectOption[] {
275
- return SORT_OPTIONS.map((value) => ({
276
- value,
277
- label: SORT_LABELS[value]
281
+ /** Options for the sidebar dx-filter-menu (single-select by content type, sent to the API as `type`). */
282
+ get typeFilterOptions(): Array<{
283
+ id: string;
284
+ name: string;
285
+ value: string;
286
+ }> {
287
+ return this.facetChips.map((chip) => ({
288
+ id: chip.value,
289
+ name: `${chip.label} (${chip.count})`,
290
+ value: chip.value
278
291
  }));
279
292
  }
280
293
 
281
- get sortValue(): string {
282
- return this.vm?.sort ?? "relevance";
294
+ get selectedTypeValue(): string {
295
+ return this.vm?.selectedType ?? "";
296
+ }
297
+
298
+ get hasActiveType(): boolean {
299
+ return !!this.vm?.selectedType;
300
+ }
301
+
302
+ get noActiveFilters(): boolean {
303
+ return !this.hasActiveType && this.localeValue === this.defaultLanguage;
304
+ }
305
+
306
+ get showRange(): boolean {
307
+ return this.hasResults && this.total > 0;
308
+ }
309
+
310
+ get rangeStart(): number {
311
+ return ((this.vm?.page ?? 1) - 1) * (this.vm?.limit ?? 10) + 1;
312
+ }
313
+
314
+ get rangeEnd(): number {
315
+ const limit = this.vm?.limit ?? 10;
316
+ return Math.min(this.rangeStart + limit - 1, this.total);
283
317
  }
284
318
 
319
+ // ---- locale select options -------------------------------------
320
+
285
321
  get showLocaleSelector(): boolean {
286
322
  return this.controller?.showLocaleSelector ?? false;
287
323
  }
288
324
 
289
- get localeSelectOptions(): SortSelectOption[] {
325
+ get localeSelectOptions(): SelectOption[] {
290
326
  const options = this.controller?.localeOptions ?? [];
291
327
  return options.map((l: LocaleOption) => ({
292
328
  value: l.id,
@@ -294,6 +330,13 @@ export default class SearchHybrid extends LightningElement {
294
330
  }));
295
331
  }
296
332
 
333
+ get localeFilterOptions(): Array<{ id: string; name: string }> {
334
+ return this.localeSelectOptions.map((o) => ({
335
+ id: o.value,
336
+ name: o.label
337
+ }));
338
+ }
339
+
297
340
  get localeValue(): string {
298
341
  return this.vm?.locale ?? this.defaultLanguage;
299
342
  }
@@ -314,17 +357,29 @@ export default class SearchHybrid extends LightningElement {
314
357
 
315
358
  // ---- event handlers ---------------------------------------------------
316
359
 
317
- handleSortChange(event: CustomEvent<{ value: string }>): void {
318
- const value = event.detail?.value;
319
- if (isSortOption(value)) {
320
- this.controller?.setSort(value);
360
+ handleLocaleChange(event: CustomEvent<{ value?: { id?: string } }>): void {
361
+ const value = event.detail?.value?.id;
362
+ if (value) {
363
+ this.controller?.setLanguage(value);
321
364
  }
322
365
  }
323
366
 
324
- handleLocaleChange(event: CustomEvent<{ value: string }>): void {
325
- const value = event.detail?.value;
367
+ handleTypeFilterChange(
368
+ event: CustomEvent<{ value?: { value?: string } }>
369
+ ): void {
370
+ const value = event.detail?.value?.value;
326
371
  if (value) {
327
- this.controller?.setLanguage(value);
372
+ // Disjunctive toggle: the controller clears it if already active.
373
+ this.controller?.setType(value);
374
+ }
375
+ }
376
+
377
+ handleClearFilters(): void {
378
+ if (this.hasActiveType) {
379
+ this.controller?.setType(null);
380
+ }
381
+ if (this.localeValue !== this.defaultLanguage) {
382
+ this.controller?.setLanguage(this.defaultLanguage);
328
383
  }
329
384
  }
330
385
 
@@ -1,5 +1,4 @@
1
1
  <template>
2
- <!-- linthtml-disable id-no-dup -->
3
2
  <div
4
3
  id="search"
5
4
  class="CoveoSearchInterface"
@@ -1,5 +1,4 @@
1
1
  <template>
2
- <!-- linthtml-disable id-no-dup -->
3
2
  <div class={containerClass}>
4
3
  <label class="form-element_label" for="select">
5
4
  {label}