@internetarchive/elements 1.1.3 → 1.1.4-webdev-9274.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.
@@ -0,0 +1,93 @@
1
+ import { LitElement, type CSSResultGroup, type TemplateResult } from 'lit';
2
+ /** What the text beside the bar says: the goal amount, a message, or nothing. */
3
+ export type GoalMessageMode = 'off' | 'amount' | 'message';
4
+ /** Whether the current amount is printed on the bar. */
5
+ export type CurrentAmountMode = 'on' | 'off';
6
+ /**
7
+ * A horizontal progress bar for a fundraising goal, as shown in the
8
+ * archive.org donation banner. The fill grows with `currentAmount`, the amount
9
+ * raised is printed on the fill when it fits and beside it when it doesn't,
10
+ * and the goal (or a goal message) sits to the right of the bar.
11
+ */
12
+ export declare class IADonationThermometer extends LitElement {
13
+ goalMessageMode: GoalMessageMode;
14
+ goalNearMessage: string;
15
+ goalReachedMessage: string;
16
+ /** The accessible name of the progress bar, for screen readers. */
17
+ label: string;
18
+ goalAmount: number;
19
+ currentAmountMode: CurrentAmountMode;
20
+ currentAmount: number;
21
+ private thermometerValue?;
22
+ private thermometerFill?;
23
+ private thermometerValueWidth;
24
+ private thermometerFillWidth;
25
+ /**
26
+ * Watches the fill and the value label so the label can move to whichever
27
+ * side of the fill has room for it.
28
+ */
29
+ private resizeObserver?;
30
+ /**
31
+ * The observed nodes are tracked so each is observed exactly once. Calling
32
+ * `observe` on an already-observed target re-reports its size, which costs a
33
+ * notification on every render. Lit also discards the value label when
34
+ * `currentAmountMode` turns it off, so a replacement has to be picked up.
35
+ */
36
+ private observedFillElement;
37
+ private observedValueElement;
38
+ render(): TemplateResult;
39
+ private get thermometerValueTemplate();
40
+ /**
41
+ * Which side of the fill's end the value label sits on, as a class name for
42
+ * the CSS that places it. The label sits over the fill when the fill is
43
+ * wider than the label plus a little breathing room, and just past the
44
+ * fill's right edge otherwise.
45
+ */
46
+ private get thermometerValuePosition();
47
+ connectedCallback(): void;
48
+ disconnectedCallback(): void;
49
+ updated(): void;
50
+ /** Observes the fill and the value label, picking up either if it is replaced. */
51
+ private observeParts;
52
+ /**
53
+ * Swaps `observed` for `current` on the observer and returns whichever is
54
+ * now being watched. A target that hasn't changed is left alone.
55
+ */
56
+ private observeInPlaceOf;
57
+ private handleResize;
58
+ private get goalMessage();
59
+ private get currentAmountDisplayValue();
60
+ private get goalAmountDisplayValue();
61
+ /**
62
+ * Formats a dollar amount in millions, e.g. `$0.5MM` or `$35MM`. Amounts
63
+ * under ten million keep one decimal place, larger ones round to a whole
64
+ * number.
65
+ */
66
+ private formatNumber;
67
+ private get currentGoalMessage();
68
+ /**
69
+ * The goal the bar measures against. A goal that isn't a positive, finite
70
+ * number leaves the bar empty, since there is no range to report progress
71
+ * against. Named clear of `ariaValueMax`, which `HTMLElement` declares as a
72
+ * public string.
73
+ */
74
+ private get progressMax();
75
+ /**
76
+ * The progress reported to screen readers, clamped into the
77
+ * `aria-valuemin`/`aria-valuemax` range the spec requires.
78
+ * `aria-valuetext` carries the amount as it is displayed.
79
+ */
80
+ private get progressValue();
81
+ /**
82
+ * How much of the bar is filled, as a percentage, clamped to `0`-`100`. An
83
+ * amount or goal that can't produce a usable percentage empties the bar.
84
+ */
85
+ private get percentComplete();
86
+ static get styles(): CSSResultGroup;
87
+ }
88
+ declare global {
89
+ interface HTMLElementTagNameMap {
90
+ 'ia-donation-thermometer': IADonationThermometer;
91
+ }
92
+ }
93
+ //# sourceMappingURL=ia-donation-thermometer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ia-donation-thermometer.d.ts","sourceRoot":"","sources":["../../../../src/elements/ia-donation-thermometer/ia-donation-thermometer.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,UAAU,EAEV,KAAK,cAAc,EACnB,KAAK,cAAc,EACpB,MAAM,KAAK,CAAC;AAOb,iFAAiF;AACjF,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE3D,wDAAwD;AACxD,MAAM,MAAM,iBAAiB,GAAG,IAAI,GAAG,KAAK,CAAC;AAE7C;;;;;GAKG;AACH,qBACa,qBAAsB,SAAQ,UAAU;IACvB,eAAe,EAAE,eAAe,CAAY;IAE5C,eAAe,SAEzC;IAE0B,kBAAkB,SAE5C;IAEF,mEAAmE;IACvC,KAAK,SAA4B;IAEjC,UAAU,SAAa;IAEvB,iBAAiB,EAAE,iBAAiB,CAAQ;IAE5C,aAAa,SAAK;IAEjB,OAAO,CAAC,gBAAgB,CAAC,CAAiB;IAE3C,OAAO,CAAC,eAAe,CAAC,CAAiB;IAE5D,OAAO,CAAC,qBAAqB,CAAK;IAElC,OAAO,CAAC,oBAAoB,CAAK;IAE1C;;;OAGG;IACH,OAAO,CAAC,cAAc,CAAC,CAAiB;IAExC;;;;;OAKG;IACH,OAAO,CAAC,mBAAmB,CAAwB;IAEnD,OAAO,CAAC,oBAAoB,CAAwB;IAEpD,MAAM,IAAI,cAAc;IA2BxB,OAAO,KAAK,wBAAwB,GAMnC;IAED;;;;;OAKG;IACH,OAAO,KAAK,wBAAwB,GAKnC;IAED,iBAAiB,IAAI,IAAI;IAUzB,oBAAoB,IAAI,IAAI;IAQ5B,OAAO,IAAI,IAAI;IAIf,kFAAkF;IAClF,OAAO,CAAC,YAAY;IAgBpB;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IAWxB,OAAO,CAAC,YAAY;IAYpB,OAAO,KAAK,WAAW,GAItB;IAED,OAAO,KAAK,yBAAyB,GAEpC;IAED,OAAO,KAAK,sBAAsB,GAEjC;IAED;;;;OAIG;IACH,OAAO,CAAC,YAAY;IAYpB,OAAO,KAAK,kBAAkB,GAS7B;IAED;;;;;OAKG;IACH,OAAO,KAAK,WAAW,GAItB;IAED;;;;OAIG;IACH,OAAO,KAAK,aAAa,GAGxB;IAED;;;OAGG;IACH,OAAO,KAAK,eAAe,GAK1B;IAED,MAAM,KAAK,MAAM,IAAI,cAAc,CAmIlC;CACF;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,yBAAyB,EAAE,qBAAqB,CAAC;KAClD;CACF"}
@@ -0,0 +1,369 @@
1
+ import { __decorate } from "tslib";
2
+ import { css, html, LitElement, nothing, } from 'lit';
3
+ import { property, query, state } from 'lit/decorators.js';
4
+ import { customElement } from '../../util/custom-element.js';
5
+ import { msg, str } from '@lit/localize';
6
+ import themeStyles from '../../themes/theme-styles.js';
7
+ /**
8
+ * A horizontal progress bar for a fundraising goal, as shown in the
9
+ * archive.org donation banner. The fill grows with `currentAmount`, the amount
10
+ * raised is printed on the fill when it fits and beside it when it doesn't,
11
+ * and the goal (or a goal message) sits to the right of the bar.
12
+ */
13
+ let IADonationThermometer = class IADonationThermometer extends LitElement {
14
+ constructor() {
15
+ super(...arguments);
16
+ this.goalMessageMode = 'amount';
17
+ this.goalNearMessage = msg('We’ve almost reached our goal!');
18
+ this.goalReachedMessage = msg('We’ve reached our goal!');
19
+ /** The accessible name of the progress bar, for screen readers. */
20
+ this.label = msg('Donation progress');
21
+ this.goalAmount = 7_500_000;
22
+ this.currentAmountMode = 'on';
23
+ this.currentAmount = 0;
24
+ this.thermometerValueWidth = 0;
25
+ this.thermometerFillWidth = 0;
26
+ /**
27
+ * The observed nodes are tracked so each is observed exactly once. Calling
28
+ * `observe` on an already-observed target re-reports its size, which costs a
29
+ * notification on every render. Lit also discards the value label when
30
+ * `currentAmountMode` turns it off, so a replacement has to be picked up.
31
+ */
32
+ this.observedFillElement = null;
33
+ this.observedValueElement = null;
34
+ }
35
+ render() {
36
+ return html `
37
+ <div class="thermometer-message-container">
38
+ <div class="thermometer-container">
39
+ <div
40
+ class="thermometer-background ${this.thermometerValuePosition}"
41
+ style="--fill-end--: ${this.percentComplete}%"
42
+ role="progressbar"
43
+ aria-label="${this.label}"
44
+ aria-valuemin="0"
45
+ aria-valuemax="${this.progressMax}"
46
+ aria-valuenow="${this.progressValue}"
47
+ aria-valuetext="${this.currentAmountDisplayValue}"
48
+ >
49
+ <div class="thermometer-clip">
50
+ <div class="thermometer-fill"></div>
51
+ </div>
52
+ ${this.thermometerValueTemplate}
53
+ </div>
54
+ </div>
55
+ ${this.goalMessageMode !== 'off'
56
+ ? html `<div class="donate-goal">${this.currentGoalMessage}</div>`
57
+ : nothing}
58
+ </div>
59
+ `;
60
+ }
61
+ get thermometerValueTemplate() {
62
+ return this.currentAmountMode === 'off'
63
+ ? nothing
64
+ : html `
65
+ <div class="thermometer-value">${this.currentAmountDisplayValue}</div>
66
+ `;
67
+ }
68
+ /**
69
+ * Which side of the fill's end the value label sits on, as a class name for
70
+ * the CSS that places it. The label sits over the fill when the fill is
71
+ * wider than the label plus a little breathing room, and just past the
72
+ * fill's right edge otherwise.
73
+ */
74
+ get thermometerValuePosition() {
75
+ const buffer = 10;
76
+ return this.thermometerValueWidth + buffer < this.thermometerFillWidth
77
+ ? 'value-left'
78
+ : 'value-right';
79
+ }
80
+ connectedCallback() {
81
+ super.connectedCallback();
82
+ this.resizeObserver = new ResizeObserver((entries) => this.handleResize(entries));
83
+ // On a re-connect the fill already exists; on first connect it doesn't
84
+ // until the first render, and `updated` picks it up then.
85
+ this.observeParts();
86
+ }
87
+ disconnectedCallback() {
88
+ super.disconnectedCallback();
89
+ this.resizeObserver?.disconnect();
90
+ this.resizeObserver = undefined;
91
+ this.observedFillElement = null;
92
+ this.observedValueElement = null;
93
+ }
94
+ updated() {
95
+ this.observeParts();
96
+ }
97
+ /** Observes the fill and the value label, picking up either if it is replaced. */
98
+ observeParts() {
99
+ const observer = this.resizeObserver;
100
+ if (!observer)
101
+ return;
102
+ this.observedFillElement = this.observeInPlaceOf(observer, this.observedFillElement, this.thermometerFill ?? null);
103
+ this.observedValueElement = this.observeInPlaceOf(observer, this.observedValueElement, this.thermometerValue ?? null);
104
+ }
105
+ /**
106
+ * Swaps `observed` for `current` on the observer and returns whichever is
107
+ * now being watched. A target that hasn't changed is left alone.
108
+ */
109
+ observeInPlaceOf(observer, observed, current) {
110
+ if (current === observed)
111
+ return observed;
112
+ if (observed)
113
+ observer.unobserve(observed);
114
+ if (current)
115
+ observer.observe(current);
116
+ return current;
117
+ }
118
+ handleResize(entries) {
119
+ for (const entry of entries) {
120
+ const width = entry.borderBoxSize?.[0]?.inlineSize ?? entry.contentRect.width;
121
+ if (entry.target === this.observedFillElement) {
122
+ this.thermometerFillWidth = width;
123
+ }
124
+ else if (entry.target === this.observedValueElement) {
125
+ this.thermometerValueWidth = width;
126
+ }
127
+ }
128
+ }
129
+ get goalMessage() {
130
+ return this.currentAmount >= this.goalAmount
131
+ ? this.goalReachedMessage
132
+ : this.goalNearMessage;
133
+ }
134
+ get currentAmountDisplayValue() {
135
+ return this.formatNumber(this.currentAmount);
136
+ }
137
+ get goalAmountDisplayValue() {
138
+ return this.formatNumber(this.goalAmount);
139
+ }
140
+ /**
141
+ * Formats a dollar amount in millions, e.g. `$0.5MM` or `$35MM`. Amounts
142
+ * under ten million keep one decimal place, larger ones round to a whole
143
+ * number.
144
+ */
145
+ formatNumber(number) {
146
+ if (!Number.isFinite(number) || number === 0)
147
+ return '$0';
148
+ const suffix = 'MM';
149
+ const divisor = 1_000_000;
150
+ const result = number / divisor;
151
+ const roundToOne = result < 10;
152
+ const rounded = roundToOne
153
+ ? Math.round((result + Number.EPSILON) * 10) / 10
154
+ : Math.round(result);
155
+ return `$${rounded}${suffix}`;
156
+ }
157
+ get currentGoalMessage() {
158
+ switch (this.goalMessageMode) {
159
+ case 'amount':
160
+ return msg(str `${this.goalAmountDisplayValue} goal`);
161
+ case 'message':
162
+ return this.goalMessage;
163
+ case 'off':
164
+ return '';
165
+ }
166
+ }
167
+ /**
168
+ * The goal the bar measures against. A goal that isn't a positive, finite
169
+ * number leaves the bar empty, since there is no range to report progress
170
+ * against. Named clear of `ariaValueMax`, which `HTMLElement` declares as a
171
+ * public string.
172
+ */
173
+ get progressMax() {
174
+ return Number.isFinite(this.goalAmount) && this.goalAmount > 0
175
+ ? this.goalAmount
176
+ : 0;
177
+ }
178
+ /**
179
+ * The progress reported to screen readers, clamped into the
180
+ * `aria-valuemin`/`aria-valuemax` range the spec requires.
181
+ * `aria-valuetext` carries the amount as it is displayed.
182
+ */
183
+ get progressValue() {
184
+ if (!Number.isFinite(this.currentAmount))
185
+ return 0;
186
+ return Math.min(Math.max(this.currentAmount, 0), this.progressMax);
187
+ }
188
+ /**
189
+ * How much of the bar is filled, as a percentage, clamped to `0`-`100`. An
190
+ * amount or goal that can't produce a usable percentage empties the bar.
191
+ */
192
+ get percentComplete() {
193
+ const goal = this.progressMax;
194
+ if (goal === 0 || !Number.isFinite(this.currentAmount))
195
+ return 0;
196
+ const percent = (this.currentAmount / goal) * 100;
197
+ return Math.min(Math.max(percent, 0), 100);
198
+ }
199
+ static get styles() {
200
+ return [
201
+ themeStyles,
202
+ css `
203
+ :host {
204
+ --donation-thermometer-height--: var(
205
+ --ia-donation-thermometer-height,
206
+ 20px
207
+ );
208
+ --donation-thermometer-fill-color--: var(
209
+ --ia-donation-thermometer-fill-color,
210
+ #23765d
211
+ );
212
+ --donation-thermometer-track-color--: var(
213
+ --ia-donation-thermometer-track-color,
214
+ #b8f5e2
215
+ );
216
+ --donation-thermometer-border--: var(
217
+ --ia-donation-thermometer-border,
218
+ 1px solid var(--donation-thermometer-fill-color--)
219
+ );
220
+ /* Large enough to round any height into a pill */
221
+ --donation-thermometer-border-radius--: var(
222
+ --ia-donation-thermometer-border-radius,
223
+ 9999px
224
+ );
225
+ --donation-thermometer-value-on-fill-color--: var(
226
+ --ia-donation-thermometer-value-on-fill-color,
227
+ var(--true-white)
228
+ );
229
+ --donation-thermometer-value-on-track-color--: var(
230
+ --ia-donation-thermometer-value-on-track-color,
231
+ var(--donation-thermometer-fill-color--)
232
+ );
233
+ --donation-thermometer-goal-color--: var(--primary-text-color);
234
+ --donation-thermometer-goal-padding--: var(
235
+ --ia-donation-thermometer-goal-padding,
236
+ 0 10px
237
+ );
238
+
239
+ display: block;
240
+ height: var(--donation-thermometer-height--);
241
+ }
242
+
243
+ .thermometer-message-container {
244
+ height: 100%;
245
+ display: flex;
246
+ align-items: center;
247
+ }
248
+
249
+ .thermometer-container {
250
+ height: 100%;
251
+ flex: 1;
252
+ }
253
+
254
+ .thermometer-background {
255
+ box-sizing: border-box;
256
+ position: relative;
257
+ background-color: var(--donation-thermometer-track-color--);
258
+ height: 100%;
259
+ border-radius: var(--donation-thermometer-border-radius--);
260
+ border: var(--donation-thermometer-border--);
261
+ }
262
+
263
+ /*
264
+ * Clips the square-ended fill to the rounded track. It covers only the
265
+ * fill, so the label is never cut off by it.
266
+ */
267
+ .thermometer-clip {
268
+ position: absolute;
269
+ inset: 0;
270
+ overflow: hidden;
271
+ border-radius: var(--donation-thermometer-border-radius--);
272
+ }
273
+
274
+ .thermometer-fill {
275
+ background-color: var(--donation-thermometer-fill-color--);
276
+ height: 100%;
277
+ /*
278
+ * The fallback matters: without it a missing --fill-end-- makes the
279
+ * declaration invalid, width resolves to auto, and the bar paints as
280
+ * a fully funded fundraiser.
281
+ */
282
+ width: var(--fill-end--, 0%);
283
+ }
284
+
285
+ /*
286
+ * The label is placed by offset from the end of the fill, so it is one
287
+ * node in one position in the DOM whichever side it shows on, and it
288
+ * never counts towards the width of the fill it is measured against.
289
+ */
290
+ .thermometer-value {
291
+ position: absolute;
292
+ top: 0;
293
+ bottom: 0;
294
+ left: var(--fill-end--, 0%);
295
+ /*
296
+ * Without this, the left offset caps the shrink-to-fit width of a
297
+ * translated label and it collapses to its padding.
298
+ */
299
+ width: max-content;
300
+ display: flex;
301
+ align-items: center;
302
+ font-weight: bold;
303
+ white-space: nowrap;
304
+ }
305
+
306
+ /*
307
+ * Both paddings total 1.5rem, so changing sides leaves the label the
308
+ * same width and cannot feed back into the side it is placed on.
309
+ */
310
+ .value-left .thermometer-value {
311
+ transform: translateX(-100%);
312
+ color: var(--donation-thermometer-value-on-fill-color--);
313
+ padding: 0 0.5rem 0 1rem;
314
+ }
315
+
316
+ .value-right .thermometer-value {
317
+ color: var(--donation-thermometer-value-on-track-color--);
318
+ padding: 0 1rem 0 0.5rem;
319
+ }
320
+
321
+ .donate-goal {
322
+ text-align: left;
323
+ padding: var(--donation-thermometer-goal-padding--);
324
+ text-transform: uppercase;
325
+ font-weight: bold;
326
+ color: var(--donation-thermometer-goal-color--);
327
+ }
328
+ `,
329
+ ];
330
+ }
331
+ };
332
+ __decorate([
333
+ property({ type: String })
334
+ ], IADonationThermometer.prototype, "goalMessageMode", void 0);
335
+ __decorate([
336
+ property({ type: String })
337
+ ], IADonationThermometer.prototype, "goalNearMessage", void 0);
338
+ __decorate([
339
+ property({ type: String })
340
+ ], IADonationThermometer.prototype, "goalReachedMessage", void 0);
341
+ __decorate([
342
+ property({ type: String })
343
+ ], IADonationThermometer.prototype, "label", void 0);
344
+ __decorate([
345
+ property({ type: Number })
346
+ ], IADonationThermometer.prototype, "goalAmount", void 0);
347
+ __decorate([
348
+ property({ type: String })
349
+ ], IADonationThermometer.prototype, "currentAmountMode", void 0);
350
+ __decorate([
351
+ property({ type: Number })
352
+ ], IADonationThermometer.prototype, "currentAmount", void 0);
353
+ __decorate([
354
+ query('.thermometer-value')
355
+ ], IADonationThermometer.prototype, "thermometerValue", void 0);
356
+ __decorate([
357
+ query('.thermometer-fill')
358
+ ], IADonationThermometer.prototype, "thermometerFill", void 0);
359
+ __decorate([
360
+ state()
361
+ ], IADonationThermometer.prototype, "thermometerValueWidth", void 0);
362
+ __decorate([
363
+ state()
364
+ ], IADonationThermometer.prototype, "thermometerFillWidth", void 0);
365
+ IADonationThermometer = __decorate([
366
+ customElement('ia-donation-thermometer')
367
+ ], IADonationThermometer);
368
+ export { IADonationThermometer };
369
+ //# sourceMappingURL=ia-donation-thermometer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ia-donation-thermometer.js","sourceRoot":"","sources":["../../../../src/elements/ia-donation-thermometer/ia-donation-thermometer.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n css,\n html,\n LitElement,\n nothing,\n type CSSResultGroup,\n type TemplateResult,\n} from 'lit';\nimport { property, query, state } from 'lit/decorators.js';\nimport { customElement } from '@src/util/custom-element';\nimport { msg, str } from '@lit/localize';\n\nimport themeStyles from '@src/themes/theme-styles';\n\n/** What the text beside the bar says: the goal amount, a message, or nothing. */\nexport type GoalMessageMode = 'off' | 'amount' | 'message';\n\n/** Whether the current amount is printed on the bar. */\nexport type CurrentAmountMode = 'on' | 'off';\n\n/**\n * A horizontal progress bar for a fundraising goal, as shown in the\n * archive.org donation banner. The fill grows with `currentAmount`, the amount\n * raised is printed on the fill when it fits and beside it when it doesn't,\n * and the goal (or a goal message) sits to the right of the bar.\n */\n@customElement('ia-donation-thermometer')\nexport class IADonationThermometer extends LitElement {\n @property({ type: String }) goalMessageMode: GoalMessageMode = 'amount';\n\n @property({ type: String }) goalNearMessage = msg(\n 'We’ve almost reached our goal!',\n );\n\n @property({ type: String }) goalReachedMessage = msg(\n 'We’ve reached our goal!',\n );\n\n /** The accessible name of the progress bar, for screen readers. */\n @property({ type: String }) label = msg('Donation progress');\n\n @property({ type: Number }) goalAmount = 7_500_000;\n\n @property({ type: String }) currentAmountMode: CurrentAmountMode = 'on';\n\n @property({ type: Number }) currentAmount = 0;\n\n @query('.thermometer-value') private thermometerValue?: HTMLDivElement;\n\n @query('.thermometer-fill') private thermometerFill?: HTMLDivElement;\n\n @state() private thermometerValueWidth = 0;\n\n @state() private thermometerFillWidth = 0;\n\n /**\n * Watches the fill and the value label so the label can move to whichever\n * side of the fill has room for it.\n */\n private resizeObserver?: ResizeObserver;\n\n /**\n * The observed nodes are tracked so each is observed exactly once. Calling\n * `observe` on an already-observed target re-reports its size, which costs a\n * notification on every render. Lit also discards the value label when\n * `currentAmountMode` turns it off, so a replacement has to be picked up.\n */\n private observedFillElement: Element | null = null;\n\n private observedValueElement: Element | null = null;\n\n render(): TemplateResult {\n return html`\n <div class=\"thermometer-message-container\">\n <div class=\"thermometer-container\">\n <div\n class=\"thermometer-background ${this.thermometerValuePosition}\"\n style=\"--fill-end--: ${this.percentComplete}%\"\n role=\"progressbar\"\n aria-label=\"${this.label}\"\n aria-valuemin=\"0\"\n aria-valuemax=\"${this.progressMax}\"\n aria-valuenow=\"${this.progressValue}\"\n aria-valuetext=\"${this.currentAmountDisplayValue}\"\n >\n <div class=\"thermometer-clip\">\n <div class=\"thermometer-fill\"></div>\n </div>\n ${this.thermometerValueTemplate}\n </div>\n </div>\n ${this.goalMessageMode !== 'off'\n ? html`<div class=\"donate-goal\">${this.currentGoalMessage}</div>`\n : nothing}\n </div>\n `;\n }\n\n private get thermometerValueTemplate(): TemplateResult | typeof nothing {\n return this.currentAmountMode === 'off'\n ? nothing\n : html`\n <div class=\"thermometer-value\">${this.currentAmountDisplayValue}</div>\n `;\n }\n\n /**\n * Which side of the fill's end the value label sits on, as a class name for\n * the CSS that places it. The label sits over the fill when the fill is\n * wider than the label plus a little breathing room, and just past the\n * fill's right edge otherwise.\n */\n private get thermometerValuePosition(): 'value-left' | 'value-right' {\n const buffer = 10;\n return this.thermometerValueWidth + buffer < this.thermometerFillWidth\n ? 'value-left'\n : 'value-right';\n }\n\n connectedCallback(): void {\n super.connectedCallback();\n this.resizeObserver = new ResizeObserver((entries) =>\n this.handleResize(entries),\n );\n // On a re-connect the fill already exists; on first connect it doesn't\n // until the first render, and `updated` picks it up then.\n this.observeParts();\n }\n\n disconnectedCallback(): void {\n super.disconnectedCallback();\n this.resizeObserver?.disconnect();\n this.resizeObserver = undefined;\n this.observedFillElement = null;\n this.observedValueElement = null;\n }\n\n updated(): void {\n this.observeParts();\n }\n\n /** Observes the fill and the value label, picking up either if it is replaced. */\n private observeParts(): void {\n const observer = this.resizeObserver;\n if (!observer) return;\n\n this.observedFillElement = this.observeInPlaceOf(\n observer,\n this.observedFillElement,\n this.thermometerFill ?? null,\n );\n this.observedValueElement = this.observeInPlaceOf(\n observer,\n this.observedValueElement,\n this.thermometerValue ?? null,\n );\n }\n\n /**\n * Swaps `observed` for `current` on the observer and returns whichever is\n * now being watched. A target that hasn't changed is left alone.\n */\n private observeInPlaceOf(\n observer: ResizeObserver,\n observed: Element | null,\n current: Element | null,\n ): Element | null {\n if (current === observed) return observed;\n if (observed) observer.unobserve(observed);\n if (current) observer.observe(current);\n return current;\n }\n\n private handleResize(entries: ResizeObserverEntry[]): void {\n for (const entry of entries) {\n const width =\n entry.borderBoxSize?.[0]?.inlineSize ?? entry.contentRect.width;\n if (entry.target === this.observedFillElement) {\n this.thermometerFillWidth = width;\n } else if (entry.target === this.observedValueElement) {\n this.thermometerValueWidth = width;\n }\n }\n }\n\n private get goalMessage(): string {\n return this.currentAmount >= this.goalAmount\n ? this.goalReachedMessage\n : this.goalNearMessage;\n }\n\n private get currentAmountDisplayValue(): string {\n return this.formatNumber(this.currentAmount);\n }\n\n private get goalAmountDisplayValue(): string {\n return this.formatNumber(this.goalAmount);\n }\n\n /**\n * Formats a dollar amount in millions, e.g. `$0.5MM` or `$35MM`. Amounts\n * under ten million keep one decimal place, larger ones round to a whole\n * number.\n */\n private formatNumber(number: number): string {\n if (!Number.isFinite(number) || number === 0) return '$0';\n const suffix = 'MM';\n const divisor = 1_000_000;\n const result = number / divisor;\n const roundToOne = result < 10;\n const rounded = roundToOne\n ? Math.round((result + Number.EPSILON) * 10) / 10\n : Math.round(result);\n return `$${rounded}${suffix}`;\n }\n\n private get currentGoalMessage(): string {\n switch (this.goalMessageMode) {\n case 'amount':\n return msg(str`${this.goalAmountDisplayValue} goal`);\n case 'message':\n return this.goalMessage;\n case 'off':\n return '';\n }\n }\n\n /**\n * The goal the bar measures against. A goal that isn't a positive, finite\n * number leaves the bar empty, since there is no range to report progress\n * against. Named clear of `ariaValueMax`, which `HTMLElement` declares as a\n * public string.\n */\n private get progressMax(): number {\n return Number.isFinite(this.goalAmount) && this.goalAmount > 0\n ? this.goalAmount\n : 0;\n }\n\n /**\n * The progress reported to screen readers, clamped into the\n * `aria-valuemin`/`aria-valuemax` range the spec requires.\n * `aria-valuetext` carries the amount as it is displayed.\n */\n private get progressValue(): number {\n if (!Number.isFinite(this.currentAmount)) return 0;\n return Math.min(Math.max(this.currentAmount, 0), this.progressMax);\n }\n\n /**\n * How much of the bar is filled, as a percentage, clamped to `0`-`100`. An\n * amount or goal that can't produce a usable percentage empties the bar.\n */\n private get percentComplete(): number {\n const goal = this.progressMax;\n if (goal === 0 || !Number.isFinite(this.currentAmount)) return 0;\n const percent = (this.currentAmount / goal) * 100;\n return Math.min(Math.max(percent, 0), 100);\n }\n\n static get styles(): CSSResultGroup {\n return [\n themeStyles,\n css`\n :host {\n --donation-thermometer-height--: var(\n --ia-donation-thermometer-height,\n 20px\n );\n --donation-thermometer-fill-color--: var(\n --ia-donation-thermometer-fill-color,\n #23765d\n );\n --donation-thermometer-track-color--: var(\n --ia-donation-thermometer-track-color,\n #b8f5e2\n );\n --donation-thermometer-border--: var(\n --ia-donation-thermometer-border,\n 1px solid var(--donation-thermometer-fill-color--)\n );\n /* Large enough to round any height into a pill */\n --donation-thermometer-border-radius--: var(\n --ia-donation-thermometer-border-radius,\n 9999px\n );\n --donation-thermometer-value-on-fill-color--: var(\n --ia-donation-thermometer-value-on-fill-color,\n var(--true-white)\n );\n --donation-thermometer-value-on-track-color--: var(\n --ia-donation-thermometer-value-on-track-color,\n var(--donation-thermometer-fill-color--)\n );\n --donation-thermometer-goal-color--: var(--primary-text-color);\n --donation-thermometer-goal-padding--: var(\n --ia-donation-thermometer-goal-padding,\n 0 10px\n );\n\n display: block;\n height: var(--donation-thermometer-height--);\n }\n\n .thermometer-message-container {\n height: 100%;\n display: flex;\n align-items: center;\n }\n\n .thermometer-container {\n height: 100%;\n flex: 1;\n }\n\n .thermometer-background {\n box-sizing: border-box;\n position: relative;\n background-color: var(--donation-thermometer-track-color--);\n height: 100%;\n border-radius: var(--donation-thermometer-border-radius--);\n border: var(--donation-thermometer-border--);\n }\n\n /*\n * Clips the square-ended fill to the rounded track. It covers only the\n * fill, so the label is never cut off by it.\n */\n .thermometer-clip {\n position: absolute;\n inset: 0;\n overflow: hidden;\n border-radius: var(--donation-thermometer-border-radius--);\n }\n\n .thermometer-fill {\n background-color: var(--donation-thermometer-fill-color--);\n height: 100%;\n /*\n * The fallback matters: without it a missing --fill-end-- makes the\n * declaration invalid, width resolves to auto, and the bar paints as\n * a fully funded fundraiser.\n */\n width: var(--fill-end--, 0%);\n }\n\n /*\n * The label is placed by offset from the end of the fill, so it is one\n * node in one position in the DOM whichever side it shows on, and it\n * never counts towards the width of the fill it is measured against.\n */\n .thermometer-value {\n position: absolute;\n top: 0;\n bottom: 0;\n left: var(--fill-end--, 0%);\n /*\n * Without this, the left offset caps the shrink-to-fit width of a\n * translated label and it collapses to its padding.\n */\n width: max-content;\n display: flex;\n align-items: center;\n font-weight: bold;\n white-space: nowrap;\n }\n\n /*\n * Both paddings total 1.5rem, so changing sides leaves the label the\n * same width and cannot feed back into the side it is placed on.\n */\n .value-left .thermometer-value {\n transform: translateX(-100%);\n color: var(--donation-thermometer-value-on-fill-color--);\n padding: 0 0.5rem 0 1rem;\n }\n\n .value-right .thermometer-value {\n color: var(--donation-thermometer-value-on-track-color--);\n padding: 0 1rem 0 0.5rem;\n }\n\n .donate-goal {\n text-align: left;\n padding: var(--donation-thermometer-goal-padding--);\n text-transform: uppercase;\n font-weight: bold;\n color: var(--donation-thermometer-goal-color--);\n }\n `,\n ];\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'ia-donation-thermometer': IADonationThermometer;\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@internetarchive/elements",
3
- "version": "1.1.3",
3
+ "version": "1.1.4-webdev-9274.0",
4
4
  "description": "A web component library from the Internet Archive.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "type": "module",