@nyaruka/temba-components 0.29.3 → 0.31.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/{a439f561.js → 0f5edc46.js} +257 -72
  3. package/dist/index.js +257 -72
  4. package/dist/static/icons/symbol-defs.svg +10 -20
  5. package/dist/sw.js +1 -1
  6. package/dist/sw.js.map +1 -1
  7. package/dist/templates/components-body.html +1 -1
  8. package/dist/templates/components-head.html +1 -1
  9. package/out-tsc/src/checkbox/Checkbox.js +1 -0
  10. package/out-tsc/src/checkbox/Checkbox.js.map +1 -1
  11. package/out-tsc/src/contacts/ContactName.js +19 -16
  12. package/out-tsc/src/contacts/ContactName.js.map +1 -1
  13. package/out-tsc/src/contacts/ContactNameFetch.js +36 -0
  14. package/out-tsc/src/contacts/ContactNameFetch.js.map +1 -0
  15. package/out-tsc/src/contacts/ContactUrn.js +12 -1
  16. package/out-tsc/src/contacts/ContactUrn.js.map +1 -1
  17. package/out-tsc/src/flow/FlowStoreElement.js +43 -0
  18. package/out-tsc/src/flow/FlowStoreElement.js.map +1 -0
  19. package/out-tsc/src/interfaces.js.map +1 -1
  20. package/out-tsc/src/list/RunList.js +317 -0
  21. package/out-tsc/src/list/RunList.js.map +1 -0
  22. package/out-tsc/src/list/TembaList.js +38 -14
  23. package/out-tsc/src/list/TembaList.js.map +1 -1
  24. package/out-tsc/src/options/Options.js +18 -2
  25. package/out-tsc/src/options/Options.js.map +1 -1
  26. package/out-tsc/src/store/Store.js +13 -3
  27. package/out-tsc/src/store/Store.js.map +1 -1
  28. package/out-tsc/src/tabpane/TabPane.js +3 -1
  29. package/out-tsc/src/tabpane/TabPane.js.map +1 -1
  30. package/out-tsc/src/utils/index.js +1 -0
  31. package/out-tsc/src/utils/index.js.map +1 -1
  32. package/out-tsc/src/vectoricon/VectorIcon.js +6 -6
  33. package/out-tsc/src/vectoricon/VectorIcon.js.map +1 -1
  34. package/out-tsc/temba-modules.js +6 -0
  35. package/out-tsc/temba-modules.js.map +1 -1
  36. package/out-tsc/test/temba-checkbox.test.js +33 -0
  37. package/out-tsc/test/temba-checkbox.test.js.map +1 -1
  38. package/out-tsc/test/utils.test.js +1 -1
  39. package/out-tsc/test/utils.test.js.map +1 -1
  40. package/package.json +1 -1
  41. package/screenshots/truth/checkbox/checkbox-label-background-hover.png +0 -0
  42. package/screenshots/truth/checkbox/checkbox-no-label-no-background-hover.png +0 -0
  43. package/screenshots/truth/checkbox/checkbox-whitespace-label-no-background-hover.png +0 -0
  44. package/src/checkbox/Checkbox.ts +2 -0
  45. package/src/contacts/ContactName.ts +19 -17
  46. package/src/contacts/ContactNameFetch.ts +32 -0
  47. package/src/contacts/ContactUrn.ts +12 -1
  48. package/src/flow/FlowStoreElement.ts +42 -0
  49. package/src/interfaces.ts +19 -0
  50. package/src/list/RunList.ts +353 -0
  51. package/src/list/TembaList.ts +50 -14
  52. package/src/options/Options.ts +17 -2
  53. package/src/store/Store.ts +20 -3
  54. package/src/tabpane/TabPane.ts +3 -1
  55. package/src/utils/index.ts +3 -0
  56. package/src/vectoricon/VectorIcon.ts +5 -5
  57. package/static/css/temba-components.css +1 -1
  58. package/static/icons/Read Me.txt +15 -15
  59. package/static/icons/SVG/hourglass.svg +5 -0
  60. package/static/icons/demo-external-svg.html +142 -157
  61. package/static/icons/demo-files/demo.css +4 -4
  62. package/static/icons/demo.html +152 -177
  63. package/static/icons/selection.json +396 -339
  64. package/static/icons/style.css +0 -4
  65. package/static/icons/symbol-defs.svg +10 -20
  66. package/temba-modules.ts +6 -0
  67. package/test/temba-checkbox.test.ts +51 -0
  68. package/test/utils.test.ts +1 -1
@@ -0,0 +1,353 @@
1
+ import { html, TemplateResult } from 'lit';
2
+ import { property } from 'lit/decorators';
3
+ import { Checkbox } from '../checkbox/Checkbox';
4
+ import { Select } from '../select/Select';
5
+ import { capitalize } from '../utils';
6
+ import { TembaList } from './TembaList';
7
+
8
+ const FLOW_COLOR = 'rgb(223, 65, 159)';
9
+
10
+ export class RunList extends TembaList {
11
+ @property({ type: String })
12
+ flow: string;
13
+
14
+ @property({ type: Object, attribute: false })
15
+ results: any[];
16
+
17
+ @property({ type: Boolean })
18
+ responses = true;
19
+
20
+ @property({ type: Object })
21
+ resultPreview: any;
22
+
23
+ @property({ type: Object })
24
+ selectedRun: any;
25
+
26
+ private resultKeys = {};
27
+
28
+ public firstUpdated(changedProperties: Map<string, any>) {
29
+ super.firstUpdated(changedProperties);
30
+ }
31
+
32
+ public updated(changedProperties: Map<string, any>): void {
33
+ super.updated(changedProperties);
34
+ if (changedProperties.has('responses') || changedProperties.has('flow')) {
35
+ if (this.flow) {
36
+ this.endpoint = `/api/v2/runs.json?flow=${this.flow}${
37
+ this.responses ? '&responded=1' : ''
38
+ }`;
39
+ }
40
+ }
41
+
42
+ if (changedProperties.has('resultPreview')) {
43
+ this.createRenderOption();
44
+ }
45
+
46
+ if (changedProperties.has('results')) {
47
+ if (this.results) {
48
+ const select = this.shadowRoot.querySelector('temba-select') as Select;
49
+ select.setOptions(this.results);
50
+ this.resultKeys = this.results.reduce(
51
+ (current, result) => ({ ...current, [result.key]: result }),
52
+ {}
53
+ );
54
+ }
55
+ }
56
+ }
57
+
58
+ public renderResultPreview(run: any) {
59
+ if (this.resultPreview) {
60
+ const runResult = run.values[this.resultPreview.key];
61
+ if (runResult) {
62
+ if (this.resultPreview.categories.length > 1) {
63
+ if (runResult.category) {
64
+ return runResult.category;
65
+ }
66
+ } else {
67
+ return runResult.value;
68
+ }
69
+ }
70
+ }
71
+ return null;
72
+ }
73
+
74
+ public removeRun(id: number) {
75
+ this.items = this.items.filter(run => run.id !== id);
76
+ this.cursorIndex = Math.min(this.cursorIndex, this.items.length);
77
+ this.requestUpdate('cursorIndex');
78
+ }
79
+
80
+ public getIcon(run: any): TemplateResult {
81
+ let icon = null;
82
+ if (run.exit_type == 'completed') {
83
+ icon = html`<temba-icon
84
+ name="check"
85
+ style="--icon-color:#666;margin-left:0.5em"
86
+ />`;
87
+ } else if (run.exit_type == 'interrupted') {
88
+ icon = html`<temba-icon
89
+ name="x-octagon"
90
+ style="--icon-color:${FLOW_COLOR};margin-left:0.5em"
91
+ />`;
92
+ } else if (run.exit_type == 'expired') {
93
+ icon = html`<temba-icon
94
+ name="clock"
95
+ style="--icon-color:${FLOW_COLOR};margin-left:0.5em"
96
+ />`;
97
+ } else if (!run.exit_type) {
98
+ if (run.responded) {
99
+ icon = html`<temba-icon
100
+ name="activity"
101
+ style="--icon-color:var(--color-primary-dark);margin-left:0.5em"
102
+ />`;
103
+ } else {
104
+ icon = html`<temba-icon
105
+ name="hourglass"
106
+ style="--icon-color:var(--color-primary-dark);margin-left:0.5em"
107
+ />`;
108
+ }
109
+ }
110
+ return icon;
111
+ }
112
+
113
+ public createRenderOption() {
114
+ this.renderOption = (run: any): TemplateResult => {
115
+ let statusStyle = '';
116
+
117
+ if (!run.exited_on) {
118
+ statusStyle = 'font-weight:400;';
119
+ }
120
+
121
+ if (!run.responded) {
122
+ statusStyle += '';
123
+ }
124
+
125
+ return html`
126
+ <div class="row" style="${statusStyle}display:flex;align-items:center">
127
+ <div
128
+ style="width: 12em;white-space:nowrap;overflow: hidden; text-overflow: ellipsis;"
129
+ >
130
+ <temba-contact-name
131
+ name=${run.contact.name}
132
+ urn=${run.contact.urn}
133
+ icon-size="15"
134
+ />
135
+ </div>
136
+
137
+ <div
138
+ style="margin: 0em 1em;flex:1;white-space:nowrap; overflow:hidden; text-overflow: ellipsis;"
139
+ >
140
+ ${this.renderResultPreview(run)}
141
+ </div>
142
+
143
+ <div style="flex-shrink:1">
144
+ ${this.store.getShortDuration(run.modified_on)}
145
+ </div>
146
+ ${this.getIcon(run)}
147
+ </div>
148
+ `;
149
+ };
150
+ }
151
+
152
+ public getRefreshEndpoint() {
153
+ if (this.items.length > 0) {
154
+ const modifiedOn = this.items[0].modified_on;
155
+ return this.endpoint + '&after=' + modifiedOn;
156
+ }
157
+ return this.endpoint;
158
+ }
159
+
160
+ public toggleResponded() {
161
+ this.responses = (
162
+ this.shadowRoot.querySelector('#responded') as Checkbox
163
+ ).checked;
164
+ }
165
+
166
+ public handleColumnChanged(event: any) {
167
+ if (event.target.values.length > 0) {
168
+ this.resultPreview = event.target.values[0];
169
+ } else {
170
+ this.resultPreview = null;
171
+ }
172
+ }
173
+
174
+ public handleSelected(selected: any) {
175
+ this.selectedRun = selected;
176
+ }
177
+
178
+ public getListStyle(): string {
179
+ return '';
180
+ }
181
+
182
+ public renderHeader(): TemplateResult {
183
+ return html`
184
+ <div style="display:flex;width:100%;margin-bottom: 1em;">
185
+ <div style="flex-grow:1">
186
+ ${this.results
187
+ ? html`
188
+ <temba-select
189
+ clearable
190
+ placeholder="Result Preview"
191
+ @change=${this.handleColumnChanged}
192
+ />
193
+ `
194
+ : null}
195
+ </div>
196
+ <div style="margin-left:1em;">
197
+ <temba-checkbox
198
+ id="responded"
199
+ label="Responses Only"
200
+ checked="true"
201
+ @click=${this.toggleResponded}
202
+ />
203
+ </div>
204
+ </div>
205
+ <div
206
+ style="
207
+ font-size:0.8em;
208
+ color:rgba(0,0,0,.4);
209
+ text-align:right;
210
+ background:#f9f9f9;
211
+ border: 1px solid var(--color-widget-border);
212
+ margin-bottom:-0.5em;
213
+ padding-bottom: 0.6em;
214
+ padding-top: 0.3em;
215
+ padding-right: 4.5em;
216
+ border-top-right-radius: var(--curvature);
217
+ border-top-left-radius: var(--curvature)
218
+ "
219
+ >
220
+ Last Updated
221
+ </div>
222
+ `;
223
+ }
224
+
225
+ public renderFooter(): TemplateResult {
226
+ if (!this.selectedRun || !this.resultKeys) {
227
+ return null;
228
+ }
229
+
230
+ const exitType = this.selectedRun.exit_type;
231
+ const resultKeys = Object.keys(this.selectedRun.values);
232
+
233
+ return html` <div
234
+ style="margin-top: 1.5em; margin-bottom:0.5em;flex-grow:1;border-radius:var(--curvature); border: 1px solid var(--color-widget-border);"
235
+ >
236
+ <div style="display:flex;flex-direction:column;">
237
+ <div
238
+ style="font-size:1.5em;background:#f9f9f9;padding:.75em;padding-top:.35em;display:flex;align-items:center;border-top-right-radius:var(--curvature);border-top-left-radius:var(--curvature)"
239
+ >
240
+ <div>
241
+ <temba-contact-name
242
+ style="cursor:pointer"
243
+ name=${this.selectedRun.contact.name}
244
+ urn=${this.selectedRun.contact.urn}
245
+ onclick="goto(event, this)"
246
+ href="/contact/read/${this.selectedRun.contact.uuid}/"
247
+ ></temba-contact-name>
248
+ <div
249
+ style="display:flex;margin-left:-0.2em;margin-top:0.25em;font-size: 0.65em"
250
+ >
251
+ ${this.selectedRun.exit_type
252
+ ? html`
253
+ ${this.getIcon(this.selectedRun)}
254
+ <div style="margin-left:0.5em;flex-grow:1">
255
+ ${capitalize(this.selectedRun.exit_type)}
256
+ ${exitType == 'completed'
257
+ ? html` in
258
+ ${this.store.getShortDuration(
259
+ this.selectedRun.created_on,
260
+ this.selectedRun.exited_on,
261
+ true
262
+ )}`
263
+ : null}
264
+ ${exitType == 'interrupted' || exitType == 'expired'
265
+ ? html` after
266
+ ${this.store.getShortDuration(
267
+ this.selectedRun.created_on,
268
+ this.selectedRun.exited_on,
269
+ true
270
+ )}`
271
+ : null}
272
+ </div>
273
+ `
274
+ : html`${this.getIcon(this.selectedRun)}
275
+ <div style="margin-left:0.5em;flex-grow:1">
276
+ Active for
277
+ ${this.store.getShortDuration(
278
+ this.selectedRun.created_on,
279
+ null,
280
+ true
281
+ )}
282
+ </div>`}
283
+ </div>
284
+ </div>
285
+ <div style="flex-grow:1"></div>
286
+ <div style="display:flex;flex-direction: column">
287
+ <div style="font-size:0.75em">
288
+ ${new Date(this.selectedRun.created_on).toLocaleString()}
289
+ </div>
290
+ <div
291
+ style="font-size:0.6em;align-self:flex-end;color:#888;line-height:0.75em"
292
+ >
293
+ Started
294
+ </div>
295
+ </div>
296
+ <temba-icon
297
+ clickable
298
+ style="margin-left:0.75em;"
299
+ name="trash"
300
+ onclick="deleteRun(${this.selectedRun.id});"
301
+ ></temba-icon>
302
+ </div>
303
+
304
+ ${resultKeys.length > 0
305
+ ? html`
306
+ <div
307
+ style="padding:1em;overflow-y:auto;overflow-x:hidden;max-height:15vh;"
308
+ >
309
+ <div
310
+ style="display:flex;font-size:1.2em;position:relative;right:0px"
311
+ >
312
+ <div style="flex-grow:1"></div>
313
+ </div>
314
+
315
+ <table width="100%">
316
+ <tr>
317
+ <th style="text-align:left" width="25%">Result</th>
318
+ <th style="text-align:left" width="25%">Category</th>
319
+ <th style="text-align:left">Value</th>
320
+ </tr>
321
+
322
+ ${Object.keys(this.selectedRun.values).map((key: string) => {
323
+ const result = this.selectedRun.values[key];
324
+ const meta = this.resultKeys[key];
325
+
326
+ // if our result is no longer represented in the flow, skip it
327
+ if (meta) {
328
+ return html`<tr>
329
+ <td>${result.name}</td>
330
+ <td>
331
+ ${meta.categories.length > 1 ? result.category : '--'}
332
+ </td>
333
+ <td>${result.value}</td>
334
+ </tr>`;
335
+ }
336
+ return null;
337
+ })}
338
+ </table>
339
+ </div>
340
+ `
341
+ : null}
342
+ </div>
343
+ </div>`;
344
+ }
345
+
346
+ constructor() {
347
+ super();
348
+ this.reverseRefresh = false;
349
+ this.valueKey = 'uuid';
350
+ this.hideShadow = true;
351
+ this.createRenderOption();
352
+ }
353
+ }
@@ -2,6 +2,7 @@ import { css, html, TemplateResult } from 'lit';
2
2
  import { property } from 'lit/decorators';
3
3
  import { CustomEventType } from '../interfaces';
4
4
  import { RapidElement } from '../RapidElement';
5
+ import { Store } from '../store/Store';
5
6
  import { fetchResultsPage, ResultsPage } from '../utils';
6
7
 
7
8
  const DEFAULT_REFRESH = 10000;
@@ -37,6 +38,9 @@ export class TembaList extends RapidElement {
37
38
  @property({ type: Boolean })
38
39
  collapsed: boolean;
39
40
 
41
+ @property({ type: Boolean })
42
+ hideShadow: boolean;
43
+
40
44
  @property({ attribute: false })
41
45
  getNextRefresh: (firstOption: any) => any;
42
46
 
@@ -56,6 +60,8 @@ export class TembaList extends RapidElement {
56
60
  @property({ type: String })
57
61
  refreshKey = '0';
58
62
 
63
+ reverseRefresh = true;
64
+
59
65
  // our next page from our endpoint
60
66
  nextPage: string = null;
61
67
 
@@ -63,6 +69,8 @@ export class TembaList extends RapidElement {
63
69
  clearRefreshTimeout: any;
64
70
  pending: AbortController[] = [];
65
71
 
72
+ store: Store;
73
+
66
74
  // used for testing only
67
75
  preserve: boolean;
68
76
 
@@ -72,9 +80,6 @@ export class TembaList extends RapidElement {
72
80
  static get styles() {
73
81
  return css`
74
82
  :host {
75
- display: block;
76
- height: 100%;
77
- width: 100%;
78
83
  }
79
84
 
80
85
  temba-options {
@@ -82,18 +87,12 @@ export class TembaList extends RapidElement {
82
87
  width: 100%;
83
88
  flex-grow: 1;
84
89
  }
85
-
86
- .wrapper {
87
- display: flex;
88
- flex-direction: column;
89
- height: 100%;
90
- align-items: center;
91
- }
92
90
  `;
93
91
  }
94
92
 
95
93
  constructor() {
96
94
  super();
95
+ this.store = document.querySelector('temba-store') as Store;
97
96
  this.handleSelection.bind(this);
98
97
  }
99
98
 
@@ -139,7 +138,9 @@ export class TembaList extends RapidElement {
139
138
  }
140
139
 
141
140
  if (changedProperties.has('mostRecentItem')) {
142
- this.fireCustomEvent(CustomEventType.Refreshed);
141
+ if (this.mostRecentItem) {
142
+ this.fireCustomEvent(CustomEventType.Refreshed);
143
+ }
143
144
  }
144
145
 
145
146
  if (changedProperties.has('cursorIndex')) {
@@ -221,6 +222,11 @@ export class TembaList extends RapidElement {
221
222
  * Refreshes the first page, updating any found items in our list
222
223
  */
223
224
  private async refreshTop(): Promise<void> {
225
+ const refreshEndpoint = this.getRefreshEndpoint();
226
+ if (!refreshEndpoint) {
227
+ return;
228
+ }
229
+
224
230
  // cancel any outstanding requests
225
231
  while (this.pending.length > 0) {
226
232
  const pending = this.pending.pop();
@@ -256,7 +262,19 @@ export class TembaList extends RapidElement {
256
262
  });
257
263
 
258
264
  // insert our new items at the front
259
- const newItems = [...page.results.reverse(), ...items];
265
+ let results = page.results;
266
+ if (this.reverseRefresh) {
267
+ results = page.results.reverse();
268
+ }
269
+ const newItems = [...results, ...items];
270
+
271
+ const topItem = newItems[0];
272
+ if (
273
+ !this.mostRecentItem ||
274
+ JSON.stringify(this.mostRecentItem) !== JSON.stringify(topItem)
275
+ ) {
276
+ this.mostRecentItem = topItem;
277
+ }
260
278
 
261
279
  if (prevItem) {
262
280
  const newItem = newItems[this.cursorIndex];
@@ -324,6 +342,8 @@ export class TembaList extends RapidElement {
324
342
  } catch (error) {
325
343
  // aborted
326
344
  this.reset();
345
+
346
+ console.log('error, resetting');
327
347
  return;
328
348
  }
329
349
 
@@ -410,6 +430,18 @@ export class TembaList extends RapidElement {
410
430
  }
411
431
  }
412
432
 
433
+ public renderHeader(): TemplateResult {
434
+ return null;
435
+ }
436
+
437
+ public renderFooter(): TemplateResult {
438
+ return null;
439
+ }
440
+
441
+ public getListStyle() {
442
+ return '';
443
+ }
444
+
413
445
  private handleSelection(event: CustomEvent) {
414
446
  const { selected, index } = event.detail;
415
447
 
@@ -421,10 +453,13 @@ export class TembaList extends RapidElement {
421
453
  }
422
454
 
423
455
  public render(): TemplateResult {
424
- return html`<div class="wrapper">
456
+ return html`
457
+ ${this.renderHeader()}
425
458
  <temba-options
459
+ style="${this.getListStyle()}"
426
460
  ?visible=${true}
427
461
  ?block=${true}
462
+ ?hideShadow=${this.hideShadow}
428
463
  ?collapsed=${this.collapsed}
429
464
  ?loading=${this.loading}
430
465
  .renderOption=${this.renderOption}
@@ -436,6 +471,7 @@ export class TembaList extends RapidElement {
436
471
  >
437
472
  <slot></slot>
438
473
  </temba-options>
439
- </div>`;
474
+ ${this.renderFooter()}
475
+ `;
440
476
  }
441
477
  }
@@ -16,7 +16,6 @@ export class Options extends RapidElement {
16
16
  .options-container {
17
17
  background: var(--color-widget-bg-focused);
18
18
  user-select: none;
19
- box-shadow: var(--options-shadow);
20
19
  border-radius: var(--curvature-widget);
21
20
  overflow: hidden;
22
21
  margin-top: var(--options-margin-top);
@@ -32,6 +31,10 @@ export class Options extends RapidElement {
32
31
  border: 1px transparent;
33
32
  }
34
33
 
34
+ .shadow {
35
+ box-shadow: var(--options-shadow);
36
+ }
37
+
35
38
  .anchored {
36
39
  position: fixed;
37
40
  }
@@ -52,12 +55,19 @@ export class Options extends RapidElement {
52
55
  }
53
56
 
54
57
  :host([block]) {
55
- box-shadow: var(--options-block-shadow);
56
58
  border-radius: var(--curvature);
57
59
  display: block;
58
60
  height: 100%;
59
61
  }
60
62
 
63
+ :host([block]) .shadow {
64
+ box-shadow: var(--options-block-shadow);
65
+ }
66
+
67
+ .bordered {
68
+ border: 1px solid var(--color-widget-border) !important;
69
+ }
70
+
61
71
  :host([block]) .options {
62
72
  margin-bottom: 1.5em;
63
73
  }
@@ -218,6 +228,9 @@ export class Options extends RapidElement {
218
228
  @property({ type: Boolean })
219
229
  collapsed: boolean;
220
230
 
231
+ @property({ type: Boolean })
232
+ hideShadow = false;
233
+
221
234
  @property({ attribute: false })
222
235
  getName: { (option: any): string } = function (option: any) {
223
236
  return option[this.nameKey || 'name'];
@@ -589,6 +602,8 @@ export class Options extends RapidElement {
589
602
  top: this.poppedTop,
590
603
  anchored: !this.block,
591
604
  loading: this.loading,
605
+ shadow: !this.hideShadow,
606
+ bordered: this.hideShadow,
592
607
  });
593
608
 
594
609
  const classesInner = getClasses({
@@ -179,11 +179,28 @@ export class Store extends RapidElement {
179
179
  return 'en';
180
180
  }
181
181
 
182
- public getShortDuration(isoDate: string) {
183
- const scheduled = DateTime.fromISO(isoDate);
184
- const now = DateTime.now();
182
+ public getShortDuration(
183
+ isoDateA: string,
184
+ isoDateB: string = null,
185
+ showSeconds = false
186
+ ) {
187
+ const scheduled = DateTime.fromISO(isoDateA);
188
+ const now = isoDateB ? DateTime.fromISO(isoDateB) : DateTime.now();
185
189
 
186
190
  const duration = scheduled.diff(now).valueOf();
191
+
192
+ if (showSeconds) {
193
+ return this.humanizer.humanize(duration, {
194
+ language: this.getLanguageCode(),
195
+ largest: 1,
196
+ round: true,
197
+ });
198
+ }
199
+
200
+ if (Math.abs(duration) < 60000) {
201
+ return 'just now';
202
+ }
203
+
187
204
  return this.humanizer.humanize(duration, {
188
205
  language: this.getLanguageCode(),
189
206
  largest: 1,
@@ -177,7 +177,9 @@ export class TabPane extends RapidElement {
177
177
  ? html`
178
178
  <div class="badge">
179
179
  ${tab.count > 0
180
- ? html`<div class="count">${tab.count}</div>`
180
+ ? html`<div class="count">
181
+ ${tab.count.toLocaleString()}
182
+ </div>`
181
183
  : null}
182
184
  </div>
183
185
  `
@@ -599,3 +599,6 @@ export enum COOKIE_KEYS {
599
599
  MENU_COLLAPSED = 'menu-collapsed',
600
600
  TICKET_SHOW_DETAILS = 'tickets.show-details',
601
601
  }
602
+
603
+ export const capitalize = ([first, ...rest], locale = navigator.language) =>
604
+ first === undefined ? '' : first.toLocaleUpperCase(locale) + rest.join('');
@@ -4,7 +4,7 @@ import { property } from 'lit/decorators';
4
4
  import { getClasses } from '../utils';
5
5
 
6
6
  // for cache busting, increase whenever the icon set changes
7
- const ICON_VERSION = 12;
7
+ const ICON_VERSION = 13;
8
8
 
9
9
  export class VectorIcon extends LitElement {
10
10
  @property({ type: String })
@@ -36,7 +36,7 @@ export class VectorIcon extends LitElement {
36
36
  animationDuration = 200;
37
37
 
38
38
  @property({ type: String })
39
- href = '';
39
+ src = '';
40
40
 
41
41
  @property({ type: Number, attribute: false })
42
42
  steps = 2;
@@ -200,7 +200,7 @@ export class VectorIcon extends LitElement {
200
200
  this.steps}ms
201
201
  ${this.easing}"
202
202
  class="${getClasses({
203
- sheet: this.href === '',
203
+ sheet: this.src === '',
204
204
  [this.animateChange]: !!this.animateChange,
205
205
  [this.animateChange + '-' + this.animationStep]:
206
206
  this.animationStep > 0,
@@ -210,8 +210,8 @@ export class VectorIcon extends LitElement {
210
210
  })}"
211
211
  >
212
212
  <use
213
- href="${this.href
214
- ? this.href
213
+ href="${this.src
214
+ ? this.src
215
215
  : `${
216
216
  this.prefix || (window as any).static_url || '/static/'
217
217
  }icons/symbol-defs.svg?v=${ICON_VERSION}#icon-${
@@ -104,7 +104,7 @@
104
104
 
105
105
  --icon-color: var(--text-color);
106
106
  --icon-color-hover: var(--icon-color);
107
- --icon-color-circle-hover: rgb(245, 245, 245);
107
+ --icon-color-circle-hover: rgba(245, 245, 245, .8);
108
108
 
109
109
  --transition-speed: 250ms;
110
110
  --event-padding: 0.5em 1em;