@energysage/es-ds-components 3.0.0-alpha.9 → 3.0.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.
@@ -1,5 +1,14 @@
1
+ <script setup lang="ts">
2
+ interface IProps {
3
+ fluid?: boolean;
4
+ }
5
+ const props = withDefaults(defineProps<IProps>(), {
6
+ fluid: false,
7
+ });
8
+ </script>
9
+
1
10
  <template>
2
- <div class="container">
11
+ <div :class="props.fluid ? 'container-fluid' : 'container'">
3
12
  <slot />
4
13
  </div>
5
14
  </template>
@@ -0,0 +1,20 @@
1
+ <script setup lang="ts">
2
+ import Column from 'primevue/column';
3
+ import type { ComponentOptionsMixin } from 'vue';
4
+
5
+ // magic solution for wrapping Column successfully
6
+ // https://www.reddit.com/r/vuejs/comments/1g17u7u/comment/lvjv91h/
7
+ defineOptions({
8
+ name: 'Column',
9
+ extends: (Column as unknown as ComponentOptionsMixin).extends,
10
+ inheritAttrs: false,
11
+ });
12
+ </script>
13
+
14
+ <!-- eslint-disable vue/valid-template-root -->
15
+ <template>
16
+ <!--
17
+ While this file needs to exist to keep Nuxt happy, it doesn't actually do anything.
18
+ Instances of es-data-table-column are placed inside es-data-table, and es-data-table handles their display.
19
+ -->
20
+ </template>
@@ -1,75 +1,38 @@
1
1
  <script setup lang="ts">
2
2
  import DataTable from 'primevue/datatable';
3
- import Column from 'primevue/column';
4
3
 
5
4
  const props = defineProps({
6
- fields: {
7
- type: Array,
8
- default: null,
9
- },
10
5
  items: {
11
6
  type: Array,
12
7
  required: true,
13
8
  },
14
- stickyFirstCol: {
9
+ scrollable: {
15
10
  type: Boolean,
16
11
  default: false,
17
12
  },
18
- stickyHeader: {
19
- type: [Boolean, String],
20
- default: false,
13
+ scrollHeight: {
14
+ type: String,
15
+ default: '',
21
16
  },
22
17
  });
23
18
 
24
- const checkFrozenRow = (index: any) => {
25
- if (index === 0 && props.stickyFirstCol) {
26
- return true;
27
- }
28
- return false;
29
- };
30
-
31
- // Code is taken from BootStrap Vue to implemnt <b-table> feature that automatically
32
- // samples the first row to extract field name and "humanize" by converting kebab-case,
33
- // snake_case, and camelCase to individual words and capitalizes each word.
34
- // Doc: https://bootstrap-vue.org/docs/components/table
35
- // Source Code: https://github.com/bootstrap-vue/bootstrap-vue/blob/5173dd19f6f46dc9d125cd7233fb59ccd2ef9296/src/utils/string.js#L30
36
- const startCase = (str: string) =>
37
- str
38
- .replace(/_/g, ' ')
39
- .replace(/([a-z])([A-Z])/g, (_, $1, $2) => `${$1} ${$2}`)
40
- .replace(/(\s|^)(\w)/g, (_, $1, $2) => $1 + $2.toUpperCase());
41
-
42
- const label = (col: any) => {
43
- if (typeof col === 'string') {
44
- return startCase(col);
19
+ // avoid passing in default scrollHeight value when scrollable is false
20
+ // otherwise it will limit the height when we don't want it to
21
+ const defaultScrollHeight = computed(() => {
22
+ if (props.scrollable) {
23
+ return props.scrollHeight || '305px';
45
24
  }
46
- return col?.label ? col.label : startCase(col?.key);
47
- };
48
25
 
49
- const columns = computed(() => {
50
- if (props.fields) {
51
- return props.fields;
52
- }
53
- // @ts-expect-error not sure
54
- return Object.keys(props.items[0]);
55
- });
56
-
57
- const scrollHeight = computed(() => {
58
- if (props.stickyHeader) {
59
- if (typeof props.stickyHeader === 'string') {
60
- return props.stickyHeader;
61
- }
62
- return '300px';
63
- }
64
26
  return '';
65
27
  });
66
28
  </script>
29
+
67
30
  <template>
68
31
  <data-table
69
- scrollable
32
+ class="border table__data-table table-spacing table"
70
33
  :pt="{
71
34
  table: {
72
- class: 'font-size-75 font-size-md-100',
35
+ class: 'font-size-75 font-size-md-100 w-100',
73
36
  style: 'border-collapse: separate; border-spacing: 0;',
74
37
  },
75
38
  wrapper: {
@@ -79,50 +42,34 @@ const scrollHeight = computed(() => {
79
42
  style: 'top: 0px; z-index: 2',
80
43
  },
81
44
  }"
82
- :value="props.items"
83
- class="border table__data-table table-spacing table"
84
- :scroll-height="scrollHeight">
85
- <!-- @vue-expect-error -->
86
- <column
87
- v-for="(col, index) of columns"
88
- :key="col?.key ? col.key : col"
89
- :field="col?.key ? col.key : col"
90
- :header="label(col)"
91
- :sortable="col?.sortable"
92
- :frozen="checkFrozenRow(index)"
93
- align-frozen="left"
94
- :pt="{
95
- sortIcon: {
96
- style: 'display: none',
97
- },
98
- headerCell: (options) => ({
99
- class: [
100
- {
101
- sortable: options.props.sortable,
102
- stickyFirstColHeader: options.props.frozen,
103
- },
104
- ],
105
- }),
106
- bodyCell: (options) => ({
107
- class: [
108
- {
109
- stickyFirstCol: options.props.frozen,
110
- },
111
- ],
112
- }),
113
- }">
114
- </column>
45
+ :scrollable="scrollable"
46
+ :scroll-height="defaultScrollHeight"
47
+ :value="items">
48
+ <slot />
115
49
  </data-table>
116
50
  </template>
51
+
117
52
  <style scoped lang="scss">
118
- :deep(.stickyFirstColHeader) {
119
- position: sticky !important;
53
+ :deep(th[data-p-sortable-column='true']) {
54
+ cursor: pointer;
55
+ user-select: none;
56
+
57
+ div[data-pc-section='headercontent'] {
58
+ align-items: center;
59
+ display: flex;
60
+ }
61
+ span[data-pc-section='sort'] {
62
+ display: inline-block;
63
+ margin-left: 0.5rem;
64
+ }
120
65
  }
121
- :deep(.stickyFirstCol) {
66
+
67
+ :deep(td[data-p-frozen-column='true']),
68
+ :deep(th[data-p-frozen-column='true']) {
122
69
  position: sticky !important;
123
- background: inherit;
124
70
  }
125
- :deep(.sortable) {
126
- cursor: pointer !important;
71
+
72
+ :deep(td[data-p-frozen-column='true']) {
73
+ background: inherit;
127
74
  }
128
75
  </style>
@@ -25,6 +25,25 @@ export default {
25
25
  return this.content.copyrightText.replace('{currentYear}', new Date().getFullYear());
26
26
  },
27
27
  },
28
+ mounted() {
29
+ // https://energysage.atlassian.net/wiki/spaces/FG/pages/1427865649/One-trust+Consent+Initialization+and+GTM
30
+ window.addEventListener('OneTrustLoadedCb', () => {
31
+ (window as any).OneTrust.OnConsentChanged(() => {
32
+ // OneTrust modal should modify cookie values, a hard-refresh will
33
+ // trigger re-loading GTM with updated cookie values, which in turn
34
+ // will only fire tags aligned with new preferences
35
+ window.location.reload();
36
+ return false;
37
+ });
38
+ document.querySelectorAll('.toggle-info-display').forEach((elem) => {
39
+ // Function closure to ensure event only fires on one elem
40
+ elem.addEventListener('click', (e) => {
41
+ e.stopImmediatePropagation();
42
+ (window as any).OneTrust.ToggleInfoDisplay();
43
+ });
44
+ });
45
+ });
46
+ },
28
47
  };
29
48
  </script>
30
49
 
@@ -120,16 +139,26 @@ export default {
120
139
  <!-- Trademark Info -->
121
140
  <!-- Legal -->
122
141
  <hr class="border-top border-blue-500 m-0" />
123
- <div class="row justify-content-center pt-100">
142
+ <div class="row pt-100">
124
143
  <div
125
144
  v-for="link in content.legalLinks"
126
145
  :key="link.text"
127
- class="col col-6 col-lg-auto font-size-75 font-weight-semibold font-size-md-50 font-weight-md-normal mt-25 mb-50 px-lg-300">
146
+ class="col col-6 col-lg-auto mt-25 mb-50 pr-lg-300">
128
147
  <es-nav-bar-link
148
+ v-if="link.url"
129
149
  :href="link.url"
130
- class="text-reset font-weight-normal">
150
+ class="text-reset font-weight-normal font-size-75 font-size-md-50 font-weight-md-normal">
131
151
  {{ link.text }}
132
152
  </es-nav-bar-link>
153
+ <div v-else>
154
+ <icon-ccpa-opt-out />
155
+ <es-button
156
+ inline
157
+ class="toggle-info-display text-reset font-weight-normal font-size-75 font-size-md-50 font-weight-md-normal ml-1"
158
+ variant="link">
159
+ {{ link.text }}
160
+ </es-button>
161
+ </div>
133
162
  </div>
134
163
  </div>
135
164
  <!-- Legal -->
@@ -0,0 +1,237 @@
1
+ <script setup lang="ts">
2
+ import SelectButton from 'primevue/selectbutton';
3
+
4
+ interface SegmentedControlItem {
5
+ label: string;
6
+ }
7
+
8
+ interface Props {
9
+ disabled?: boolean;
10
+ label: string;
11
+ options: SegmentedControlItem[];
12
+ }
13
+
14
+ interface ButtonDimensionsInterface {
15
+ options: Partial<CSSStyleDeclaration>[];
16
+ }
17
+
18
+ const props = withDefaults(defineProps<Props>(), {
19
+ disabled: false,
20
+ });
21
+ const model = defineModel<number>();
22
+ const segmentedControlRef = useTemplateRef('segmentedControl');
23
+ let resizeObserver: ResizeObserver | null = null;
24
+
25
+ // needed to avoid Content Layout Shift (CLS)
26
+ // as the inkbar is positioned via left and width attributes which are not set until mount
27
+ // meaning the active state would be initially missing and then pop in on mount, causing CLS
28
+ const isMounted = ref(false);
29
+
30
+ const buttonDimensions: ButtonDimensionsInterface = reactive({
31
+ options: props.options?.length ? props.options.map(() => ({})) : [],
32
+ });
33
+
34
+ const inkbarStyle: ComputedRef<Partial<CSSStyleDeclaration>> = computed(
35
+ () => buttonDimensions.options[model.value || 0],
36
+ );
37
+
38
+ // assume the provided label is unique and convert to an id
39
+ // by lowercasing and replacing all whitespace with dashes
40
+ const labelId = computed(() => {
41
+ return props.label ? props.label.toLowerCase().replace(/\s/g, '-') : '';
42
+ });
43
+
44
+ // return the left/width to use for the active state bubble, for each provided button
45
+ const getButtonDimensions = (buttons: HTMLCollection) => {
46
+ const result = [];
47
+ let runningLeftTotal = 0;
48
+ for (let i = 0; i < buttons.length; i += 1) {
49
+ const button = buttons[i] as unknown as HTMLElement;
50
+ result.push({
51
+ left: `${runningLeftTotal}px`,
52
+ width: `${button.offsetWidth}px`,
53
+ });
54
+
55
+ runningLeftTotal += button.offsetWidth;
56
+ }
57
+
58
+ return result;
59
+ };
60
+
61
+ onMounted(() => {
62
+ isMounted.value = true;
63
+
64
+ if (segmentedControlRef.value) {
65
+ // set the initial button dimensions
66
+ const buttons = segmentedControlRef.value.querySelectorAll(
67
+ '.es-segmented-control-button',
68
+ ) as unknown as HTMLCollection;
69
+ buttonDimensions.options = getButtonDimensions(buttons);
70
+
71
+ // set up a resize observer for whenever the segmented control's button group changes width
72
+ // this will most commonly happen when changing between desktop and mobile breakpoints
73
+ // but could also happen if the label text of any options changes
74
+ resizeObserver = new ResizeObserver((groups) => {
75
+ groups.forEach((group) => {
76
+ const updatedButtons = group.target.children;
77
+ buttonDimensions.options = getButtonDimensions(updatedButtons);
78
+ });
79
+ });
80
+
81
+ // start the resize observer listening to the segmented control's button group
82
+ const group = segmentedControlRef.value.querySelector('.es-segmented-control-group');
83
+ if (group) {
84
+ resizeObserver.observe(group);
85
+ }
86
+ }
87
+ });
88
+
89
+ onUnmounted(() => {
90
+ if (resizeObserver) {
91
+ resizeObserver.disconnect();
92
+ }
93
+ });
94
+ </script>
95
+
96
+ <template>
97
+ <div
98
+ v-if="options?.length"
99
+ ref="segmentedControl"
100
+ :aria-hidden="props.disabled || undefined"
101
+ class="es-segmented-control d-flex position-relative"
102
+ :class="{
103
+ disabled: props.disabled,
104
+ 'before-mount': !isMounted,
105
+ }">
106
+ <label
107
+ :id="labelId"
108
+ class="sr-only">
109
+ {{ label }}
110
+ </label>
111
+ <span
112
+ class="es-segmented-control-inkbar rounded-lg position-absolute"
113
+ :data-left="buttonDimensions.options[model || 0]?.left || 0"
114
+ :style="inkbarStyle as any" />
115
+ <select-button
116
+ v-model="model"
117
+ :allow-empty="false"
118
+ :aria-labelledby="labelId"
119
+ data-key="label"
120
+ :disabled="props.disabled"
121
+ :option-label="(option) => option.label"
122
+ :option-value="(option) => props.options.findIndex((innerOption) => option.label === innerOption.label)"
123
+ :options="props.options"
124
+ :pt="{
125
+ root: 'es-segmented-control-group bg-gray-100 d-flex rounded-lg',
126
+ button: 'es-segmented-control-button font-size-sm font-weight-bold position-relative px-200 px-lg-300 rounded-lg',
127
+ label: 'es-segmented-control-label position-relative',
128
+ }"
129
+ :unselectable="false" />
130
+ </div>
131
+ </template>
132
+
133
+ <style lang="scss" scoped>
134
+ @use '@energysage/es-ds-styles/scss/variables' as variables;
135
+
136
+ /* avoid Content Layout Shift (CLS) */
137
+ .es-segmented-control.before-mount {
138
+ .es-segmented-control-inkbar {
139
+ /* prevent inkbar with left and width of zero from showing up between first paint and mount */
140
+ opacity: 0;
141
+ }
142
+
143
+ :deep(.es-segmented-control-button) {
144
+ position: relative;
145
+
146
+ /* temporarily replicate the inkbar's active state until mount */
147
+ &[data-p-highlight='true'] {
148
+ &::before {
149
+ border-radius: variables.$border-radius-lg;
150
+ content: '';
151
+ left: 0;
152
+ position: absolute;
153
+ width: 100%;
154
+ z-index: 3;
155
+ }
156
+ }
157
+ }
158
+ }
159
+
160
+ /* active state bubble */
161
+ .es-segmented-control-inkbar,
162
+ .es-segmented-control.before-mount :deep(.es-segmented-control-button[data-p-highlight='true']::before) {
163
+ /* using style for bg instead of bg-white class so it can be overridden for disabled state */
164
+ background-color: variables.$white;
165
+ border: 2px solid variables.$warm-orange;
166
+ bottom: 2px;
167
+ top: 2px;
168
+ /* bring it ~2px away from each side to match the top/bottom */
169
+ transform: scaleX(0.98);
170
+
171
+ @media not (prefers-reduced-motion) {
172
+ transition:
173
+ left 250ms cubic-bezier(0.35, 0, 0.25, 1),
174
+ width 250ms cubic-bezier(0.35, 0, 0.25, 1);
175
+ }
176
+ }
177
+
178
+ /* each individual option */
179
+ :deep(.es-segmented-control-button) {
180
+ color: variables.$gray-700;
181
+ cursor: pointer;
182
+ /*
183
+ need a non-standard padding to cause a specific height that matches
184
+ better with our "lg" 14px border radius to get the rounded pill look
185
+ */
186
+ padding-bottom: 6px;
187
+ padding-top: 6px;
188
+ /* don't transition all properties, otherwise responsive bubble width updates will be delayed */
189
+ transition: color 250ms ease-in-out;
190
+ /* prevent text from highlighting when you click a bunch of times */
191
+ user-select: none;
192
+ white-space: nowrap;
193
+ z-index: 4;
194
+
195
+ &:hover {
196
+ color: variables.$gray-900;
197
+ }
198
+
199
+ &:focus-visible {
200
+ box-shadow: 0 0 0 3px variables.$warm-orange inset;
201
+ outline: none;
202
+ }
203
+ }
204
+
205
+ /* bring the label above the active state bubble */
206
+ :deep(.es-segmented-control-label) {
207
+ z-index: 4;
208
+ }
209
+
210
+ /* disabled state */
211
+ .es-segmented-control.disabled,
212
+ .es-segmented-control.before-mount.disabled {
213
+ cursor: default;
214
+ pointer-events: none;
215
+ user-select: none;
216
+
217
+ /* active bubble */
218
+ .es-segmented-control-inkbar,
219
+ :deep(.es-segmented-control-button[data-p-highlight='true']::before) {
220
+ background-color: variables.$gray-200;
221
+ border-color: variables.$gray-300;
222
+ }
223
+
224
+ /* text inside active bubble */
225
+ :deep(.es-segmented-control-button[data-p-highlight='true']) {
226
+ &,
227
+ &:hover {
228
+ color: variables.$gray-500;
229
+ }
230
+ }
231
+
232
+ /* all other items */
233
+ :deep(.es-segmented-control-button) {
234
+ color: variables.$gray-500;
235
+ }
236
+ }
237
+ </style>
@@ -6,7 +6,16 @@ import TabView from 'primevue/tabview';
6
6
  const model = defineModel<number>();
7
7
 
8
8
  // get the list of elements provided as children to the default slot
9
- const panels = useSlots().default?.() || [];
9
+ const initialChildren = useSlots().default?.() || [];
10
+ const panels: any[] = [];
11
+ initialChildren.forEach((child) => {
12
+ // unless this has a v-for element and we instead need to access its children
13
+ if (typeof child.type === 'symbol' && Array.isArray(child.children)) {
14
+ panels.push(...child.children);
15
+ } else {
16
+ panels.push(child);
17
+ }
18
+ });
10
19
 
11
20
  // keep track of the active tab index
12
21
  // - from above, if v-model is used
@@ -36,7 +45,6 @@ const updateActiveIndex = (index: number) => {
36
45
  panelContainer: 'tab-content',
37
46
  }"
38
47
  @update:active-index="updateActiveIndex">
39
- <!-- @vue-expect-error -->
40
48
  <tab-panel
41
49
  v-for="(panel, index) in panels"
42
50
  :key="index"
@@ -60,7 +68,6 @@ const updateActiveIndex = (index: number) => {
60
68
  ],
61
69
  }),
62
70
  }">
63
- <!-- @vue-expect-error -->
64
71
  <component
65
72
  :is="item"
66
73
  v-for="(item, idx) in panel.children.default()"
@@ -0,0 +1,61 @@
1
+ <script lang="ts">
2
+ export default {
3
+ name: 'IconCcpaOptOut',
4
+ props: {
5
+ /**
6
+ * Width
7
+ */
8
+ width: {
9
+ type: String,
10
+ default: '30px',
11
+ required: false,
12
+ },
13
+ /**
14
+ * Height
15
+ */
16
+ height: {
17
+ type: String,
18
+ default: '14px',
19
+ required: false,
20
+ },
21
+ },
22
+ };
23
+ </script>
24
+
25
+ <template>
26
+ <svg
27
+ xmlns="http://www.w3.org/2000/svg"
28
+ xml:space="preserve"
29
+ id="Layer_1"
30
+ :style="{
31
+ height: height,
32
+ width: width,
33
+ }"
34
+ aria-label="California Consumer Privacy Act (CCPA) Opt-Out Icon"
35
+ role="img"
36
+ x="0"
37
+ y="0"
38
+ version="1.1"
39
+ viewBox="0 0 30 14">
40
+ <g
41
+ id="Final-Copy-2"
42
+ style="display: inline">
43
+ <path
44
+ class="st0"
45
+ d="M7.4,12.8h6.8l3.1-11.6H7.4C4.2,1.2,1.6,3.8,1.6,7S4.2,12.8,7.4,12.8z"
46
+ style="fill: #fff" />
47
+ <path
48
+ id="path2"
49
+ d="M22.6 0H7.4c-3.9 0-7 3.1-7 7s3.1 7 7 7h15.2c3.9 0 7-3.1 7-7s-3.2-7-7-7zm-21 7c0-3.2 2.6-5.8 5.8-5.8h9.9l-3.1 11.6H7.4c-3.2 0-5.8-2.6-5.8-5.8Z"
50
+ style="clip-rule: evenodd; fill: #06f; fill-rule: evenodd" />
51
+ <path
52
+ id="x"
53
+ d="M24.6 4c.2.2.2.6 0 .8L22.5 7l2.2 2.2c.2.2.2.6 0 .8-.2.2-.6.2-.8 0l-2.2-2.2-2.2 2.2c-.2.2-.6.2-.8 0-.2-.2-.2-.6 0-.8L20.8 7l-2.2-2.2c-.2-.2-.2-.6 0-.8.2-.2.6-.2.8 0l2.2 2.2L23.8 4c.2-.2.6-.2.8 0Z"
54
+ style="fill: #fff" />
55
+ <path
56
+ id="y"
57
+ d="M12.7 4.1c.2.2.3.6.1.8L8.6 9.8c-.1.1-.2.2-.3.2-.2.1-.5.1-.7-.1L5.4 7.7c-.2-.2-.2-.6 0-.8.2-.2.6-.2.8 0L8 8.6l3.8-4.5c.2-.2.6-.2.9 0z"
58
+ style="fill: #06f" />
59
+ </g>
60
+ </svg>
61
+ </template>
package/nuxt.config.ts CHANGED
@@ -18,6 +18,11 @@ export default defineNuxtConfig({
18
18
  },
19
19
  },
20
20
 
21
+ // Configure how Nuxt auto-imports composables into your application.
22
+ imports: {
23
+ dirs: ['composables', 'types'],
24
+ },
25
+
21
26
  modules: [
22
27
  // https://google-fonts.nuxtjs.org/getting-started/setup
23
28
  '@nuxtjs/google-fonts',
@@ -66,6 +71,7 @@ export default defineNuxtConfig({
66
71
  'primevue/progressbar',
67
72
  'primevue/radiobutton',
68
73
  'primevue/rating',
74
+ 'primevue/selectbutton',
69
75
  'primevue/skeleton',
70
76
  'primevue/slider',
71
77
  'primevue/tabpanel',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@energysage/es-ds-components",
3
- "version": "3.0.0-alpha.9",
3
+ "version": "3.0.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "An EnergySage Vue component library",
@@ -17,7 +17,7 @@
17
17
  "author": "EnergySage",
18
18
  "license": "MIT",
19
19
  "dependencies": {
20
- "@energysage/es-ds-styles": "^3.0.0-alpha.8",
20
+ "@energysage/es-ds-styles": "^3.0.0",
21
21
  "@nuxtjs/google-fonts": "^3.2.0",
22
22
  "@vuelidate/core": "^2.0.3",
23
23
  "@vuelidate/validators": "^2.0.4",
@@ -0,0 +1,12 @@
1
+ export interface EsCarouselBreakpointInterface {
2
+ numScroll?: number;
3
+ numVisible: number;
4
+ }
5
+
6
+ export interface EsCarouselBreakpointsInterface {
7
+ sm?: EsCarouselBreakpointInterface;
8
+ md?: EsCarouselBreakpointInterface;
9
+ lg?: EsCarouselBreakpointInterface;
10
+ xl?: EsCarouselBreakpointInterface;
11
+ xxl?: EsCarouselBreakpointInterface;
12
+ }
package/types/index.ts ADDED
@@ -0,0 +1 @@
1
+ export type * from './es-carousel';
@@ -76,6 +76,7 @@ export default (
76
76
  { text: 'Privacy policy', url: `${ES_DOMAIN}/privacy-policy/` },
77
77
  { text: 'Mobile terms of use', url: `${ES_DOMAIN}/mobile-terms-of-use/` },
78
78
  { text: 'Non-discrimination policy', url: `${ES_DOMAIN}/nondiscrimination/` },
79
+ { text: 'Your privacy choices' },
79
80
  ],
80
81
  copyrightText: '© Copyright 2009-{currentYear} EnergySage, Inc. All rights reserved.',
81
82
  trademarkText: `ENERGYSAGE is a registered trademark and the EnergySage logo is a trademark of EnergySage, Inc.