@internetarchive/collection-browser 4.4.0 → 4.5.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.
Files changed (68) hide show
  1. package/dist/index.d.ts +1 -0
  2. package/dist/index.js.map +1 -1
  3. package/dist/src/app-root.d.ts +8 -0
  4. package/dist/src/app-root.js +26 -0
  5. package/dist/src/app-root.js.map +1 -1
  6. package/dist/src/collection-browser.d.ts +8 -0
  7. package/dist/src/collection-browser.js +19 -1
  8. package/dist/src/collection-browser.js.map +1 -1
  9. package/dist/src/collection-facets/facet-row.d.ts +6 -0
  10. package/dist/src/collection-facets/facet-row.js +158 -140
  11. package/dist/src/collection-facets/facet-row.js.map +1 -1
  12. package/dist/src/collection-facets/facets-template.js +25 -23
  13. package/dist/src/collection-facets/facets-template.js.map +1 -1
  14. package/dist/src/data-source/collection-browser-data-source.js.map +1 -1
  15. package/dist/src/styles/tile-action-styles.d.ts +14 -0
  16. package/dist/src/styles/tile-action-styles.js +59 -0
  17. package/dist/src/styles/tile-action-styles.js.map +1 -0
  18. package/dist/src/tiles/base-tile-component.d.ts +17 -1
  19. package/dist/src/tiles/base-tile-component.js +50 -1
  20. package/dist/src/tiles/base-tile-component.js.map +1 -1
  21. package/dist/src/tiles/grid/item-tile.js +139 -138
  22. package/dist/src/tiles/grid/item-tile.js.map +1 -1
  23. package/dist/src/tiles/item-image.js +28 -28
  24. package/dist/src/tiles/item-image.js.map +1 -1
  25. package/dist/src/tiles/list/tile-list-compact-header.js +71 -46
  26. package/dist/src/tiles/list/tile-list-compact-header.js.map +1 -1
  27. package/dist/src/tiles/list/tile-list-compact.d.ts +1 -1
  28. package/dist/src/tiles/list/tile-list-compact.js +138 -100
  29. package/dist/src/tiles/list/tile-list-compact.js.map +1 -1
  30. package/dist/src/tiles/list/tile-list.d.ts +1 -1
  31. package/dist/src/tiles/list/tile-list.js +316 -298
  32. package/dist/src/tiles/list/tile-list.js.map +1 -1
  33. package/dist/src/tiles/models.d.ts +11 -0
  34. package/dist/src/tiles/models.js.map +1 -1
  35. package/dist/src/tiles/tile-dispatcher.d.ts +14 -0
  36. package/dist/src/tiles/tile-dispatcher.js +319 -216
  37. package/dist/src/tiles/tile-dispatcher.js.map +1 -1
  38. package/dist/src/tiles/tile-display-value-provider.js +2 -1
  39. package/dist/src/tiles/tile-display-value-provider.js.map +1 -1
  40. package/dist/test/collection-facets/facet-row.test.js +55 -23
  41. package/dist/test/collection-facets/facet-row.test.js.map +1 -1
  42. package/dist/test/tiles/grid/item-tile.test.js +79 -79
  43. package/dist/test/tiles/grid/item-tile.test.js.map +1 -1
  44. package/dist/test/tiles/list/tile-list.test.js +136 -136
  45. package/dist/test/tiles/list/tile-list.test.js.map +1 -1
  46. package/dist/test/tiles/tile-dispatcher.test.js +101 -87
  47. package/dist/test/tiles/tile-dispatcher.test.js.map +1 -1
  48. package/index.ts +29 -28
  49. package/package.json +2 -2
  50. package/src/app-root.ts +30 -0
  51. package/src/collection-browser.ts +16 -1
  52. package/src/collection-facets/facet-row.ts +309 -299
  53. package/src/collection-facets/facets-template.ts +85 -83
  54. package/src/data-source/collection-browser-data-source.ts +1465 -1465
  55. package/src/styles/tile-action-styles.ts +59 -0
  56. package/src/tiles/base-tile-component.ts +124 -65
  57. package/src/tiles/grid/item-tile.ts +347 -346
  58. package/src/tiles/item-image.ts +214 -214
  59. package/src/tiles/list/tile-list-compact-header.ts +112 -86
  60. package/src/tiles/list/tile-list-compact.ts +278 -239
  61. package/src/tiles/list/tile-list.ts +718 -700
  62. package/src/tiles/models.ts +21 -8
  63. package/src/tiles/tile-dispatcher.ts +637 -527
  64. package/src/tiles/tile-display-value-provider.ts +133 -134
  65. package/test/collection-facets/facet-row.test.ts +421 -375
  66. package/test/tiles/grid/item-tile.test.ts +520 -520
  67. package/test/tiles/list/tile-list.test.ts +576 -576
  68. package/test/tiles/tile-dispatcher.test.ts +320 -300
@@ -1,214 +1,214 @@
1
- import {
2
- css,
3
- CSSResultGroup,
4
- html,
5
- LitElement,
6
- nothing,
7
- PropertyValues,
8
- } from 'lit';
9
- import { customElement, property, query, state } from 'lit/decorators.js';
10
- import { ClassInfo, classMap } from 'lit/directives/class-map.js';
11
-
12
- import type { TileModel } from '../models';
13
-
14
- import {
15
- baseItemImageStyles,
16
- waveformGradientStyles,
17
- } from '../styles/item-image-styles';
18
- import { searchIcon } from '../assets/img/icons/mediatype/search';
19
-
20
- @customElement('item-image')
21
- export class ItemImage extends LitElement {
22
- /**
23
- * Map to cache which identifiers have waveform-style thumbnails, so that
24
- * they can have their waveform styling applied immediately, rather than
25
- * waiting for the image content to load before applying it (which can
26
- * cause noticeable flicker when such tiles refresh).
27
- */
28
- private static readonly waveformByIdentifier = new Map<string, boolean>();
29
-
30
- @property({ type: Object }) model?: TileModel;
31
-
32
- @property({ type: String }) baseImageUrl?: string;
33
-
34
- @property({ type: Boolean }) isListTile = false;
35
-
36
- @property({ type: Boolean }) isCompactTile = false;
37
-
38
- @property({ type: Boolean }) loggedIn = false;
39
-
40
- @property({ type: Boolean }) suppressBlurring = false;
41
-
42
- @state() private isWaveform = false;
43
-
44
- @state() private isNotFound = false;
45
-
46
- @query('img') private baseImage!: HTMLImageElement;
47
-
48
- protected willUpdate(changed: PropertyValues): void {
49
- if (changed.has('model')) {
50
- // If this identifier is known to have a waveform image, then set isWaveform upfront
51
- const identifier = this.model?.identifier;
52
- this.isWaveform =
53
- ItemImage.waveformByIdentifier.get(identifier as string) === true;
54
- }
55
- }
56
-
57
- render() {
58
- return html`
59
- <div class=${classMap(this.itemBaseClass)}>${this.imageTemplate}</div>
60
- `;
61
- }
62
-
63
- private get imageTemplate() {
64
- if (this.model?.mediatype === 'search') {
65
- return html`${searchIcon}`;
66
- }
67
-
68
- return html`
69
- <img
70
- class=${classMap(this.itemImageClass)}
71
- src="${this.imageSrc}"
72
- alt=""
73
- @load=${this.onLoad}
74
- @error=${this.onError}
75
- />
76
- `;
77
- }
78
-
79
- /**
80
- * Helpers
81
- */
82
- private get imageSrc() {
83
- if (this.isNotFound) return this.notFoundSrc;
84
-
85
- // Use the correct image for web capture tiles, if possible
86
- if (this.model?.captureDates && this.model.identifier) {
87
- try {
88
- const url = new URL(this.model.identifier);
89
- const domain = encodeURIComponent(url.hostname);
90
- return this.baseImageUrl
91
- ? `https://web.archive.org/thumb/${domain}?generate=1`
92
- : nothing;
93
- } catch {
94
- return `${this.baseImageUrl}/images/notfound.png`;
95
- }
96
- }
97
-
98
- // Use the thumbnail URL specified in the model if it exists
99
- if (this.model?.thumbnailUrl) return this.model.thumbnailUrl;
100
-
101
- // Don't try to load invalid image URLs
102
- return this.baseImageUrl && this.model?.identifier
103
- ? `${this.baseImageUrl}/services/img/${this.model.identifier}`
104
- : nothing;
105
- }
106
-
107
- private get notFoundSrc() {
108
- return this.baseImageUrl
109
- ? `${this.baseImageUrl}/images/notfound.png`
110
- : nothing;
111
- }
112
-
113
- private get hashBasedGradient() {
114
- if (!this.model?.identifier) {
115
- return 'waveform-grad0';
116
- }
117
- const gradient = this.hashStrToInt(this.model.identifier) % 6; // returns 0-5
118
- return `waveform-grad${gradient}`;
119
- }
120
-
121
- private hashStrToInt(str: string): number {
122
- return str
123
- .split('')
124
- .reduce((acc: number, char: string) => acc + char.charCodeAt(0), 0);
125
- }
126
-
127
- /**
128
- * Classes
129
- */
130
- private get itemBaseClass(): ClassInfo {
131
- return {
132
- 'drop-shadow': true,
133
- 'list-box': this.isListTile,
134
- 'search-image': this.model?.mediatype === 'search',
135
- [this.hashBasedGradient]: this.isWaveform,
136
- };
137
- }
138
-
139
- private get itemImageClass(): ClassInfo {
140
- const hasSensitiveContent = !!(
141
- this.model?.contentWarning || this.model?.loginRequired
142
- );
143
- const shouldBlur = hasSensitiveContent && !this.suppressBlurring;
144
-
145
- return {
146
- contain: !this.isCompactTile && !this.isWaveform,
147
- cover: this.isCompactTile,
148
- blur: shouldBlur,
149
- waveform: this.isWaveform,
150
- 'account-image': this.isAccountImage, // for account tile image
151
- 'collection-image': this.model?.mediatype === 'collection', // for collection tile image
152
- };
153
- }
154
-
155
- /**
156
- * Helper function to determine if account tile image
157
- */
158
- private get isAccountImage() {
159
- return (
160
- this.model?.mediatype === 'account' &&
161
- !this.isCompactTile &&
162
- !this.isListTile
163
- );
164
- }
165
-
166
- /**
167
- * Event listener sets isWaveform true if image is waveform
168
- */
169
- private onLoad() {
170
- if (
171
- (this.model?.mediatype === 'audio' ||
172
- this.model?.mediatype === 'etree') &&
173
- this.baseImage.naturalWidth / this.baseImage.naturalHeight === 4
174
- ) {
175
- this.isWaveform = true;
176
- if (this.model?.identifier) {
177
- ItemImage.waveformByIdentifier.set(this.model.identifier, true);
178
- }
179
- }
180
- }
181
-
182
- private onError() {
183
- this.isNotFound = true;
184
- }
185
-
186
- /**
187
- * CSS
188
- */
189
- static get styles(): CSSResultGroup {
190
- return [
191
- baseItemImageStyles,
192
- waveformGradientStyles,
193
- css`
194
- img {
195
- height: var(--imgHeight, 16rem);
196
- width: var(--imgWidth, 16rem);
197
- }
198
-
199
- .search-image {
200
- display: flex;
201
- align-items: center;
202
- justify-content: center;
203
- background: rgb(245, 245, 247);
204
- border-radius: 4px;
205
- }
206
-
207
- svg {
208
- height: 10rem;
209
- width: 10rem;
210
- }
211
- `,
212
- ];
213
- }
214
- }
1
+ import {
2
+ css,
3
+ CSSResultGroup,
4
+ html,
5
+ LitElement,
6
+ nothing,
7
+ PropertyValues,
8
+ } from 'lit';
9
+ import { customElement, property, query, state } from 'lit/decorators.js';
10
+ import { ClassInfo, classMap } from 'lit/directives/class-map.js';
11
+
12
+ import type { TileModel } from '../models';
13
+
14
+ import {
15
+ baseItemImageStyles,
16
+ waveformGradientStyles,
17
+ } from '../styles/item-image-styles';
18
+ import { searchIcon } from '../assets/img/icons/mediatype/search';
19
+
20
+ @customElement('item-image')
21
+ export class ItemImage extends LitElement {
22
+ /**
23
+ * Map to cache which identifiers have waveform-style thumbnails, so that
24
+ * they can have their waveform styling applied immediately, rather than
25
+ * waiting for the image content to load before applying it (which can
26
+ * cause noticeable flicker when such tiles refresh).
27
+ */
28
+ private static readonly waveformByIdentifier = new Map<string, boolean>();
29
+
30
+ @property({ type: Object }) model?: TileModel;
31
+
32
+ @property({ type: String }) baseImageUrl?: string;
33
+
34
+ @property({ type: Boolean }) isListTile = false;
35
+
36
+ @property({ type: Boolean }) isCompactTile = false;
37
+
38
+ @property({ type: Boolean }) loggedIn = false;
39
+
40
+ @property({ type: Boolean }) suppressBlurring = false;
41
+
42
+ @state() private isWaveform = false;
43
+
44
+ @state() private isNotFound = false;
45
+
46
+ @query('img') private baseImage!: HTMLImageElement;
47
+
48
+ protected willUpdate(changed: PropertyValues): void {
49
+ if (changed.has('model')) {
50
+ // If this identifier is known to have a waveform image, then set isWaveform upfront
51
+ const identifier = this.model?.identifier;
52
+ this.isWaveform =
53
+ ItemImage.waveformByIdentifier.get(identifier as string) === true;
54
+ }
55
+ }
56
+
57
+ render() {
58
+ return html`
59
+ <div class=${classMap(this.itemBaseClass)}>${this.imageTemplate}</div>
60
+ `;
61
+ }
62
+
63
+ private get imageTemplate() {
64
+ if (this.model?.mediatype === 'search') {
65
+ return html`${searchIcon}`;
66
+ }
67
+
68
+ return html`
69
+ <img
70
+ class=${classMap(this.itemImageClass)}
71
+ src="${this.imageSrc}"
72
+ alt=""
73
+ @load=${this.onLoad}
74
+ @error=${this.onError}
75
+ />
76
+ `;
77
+ }
78
+
79
+ /**
80
+ * Helpers
81
+ */
82
+ private get imageSrc() {
83
+ if (this.isNotFound) return this.notFoundSrc;
84
+
85
+ // Use the correct image for web capture tiles, if possible
86
+ if (this.model?.captureDates && this.model.identifier) {
87
+ try {
88
+ const url = new URL(this.model.identifier);
89
+ const domain = encodeURIComponent(url.hostname);
90
+ return this.baseImageUrl
91
+ ? `https://web.archive.org/thumb/${domain}?generate=1`
92
+ : nothing;
93
+ } catch {
94
+ return `${this.baseImageUrl}/images/notfound.png`;
95
+ }
96
+ }
97
+
98
+ // Use the thumbnail URL specified in the model if it exists
99
+ if (this.model?.thumbnailUrl) return this.model.thumbnailUrl;
100
+
101
+ // Don't try to load invalid image URLs
102
+ return this.baseImageUrl && this.model?.identifier
103
+ ? `${this.baseImageUrl}/services/img/${this.model.identifier}`
104
+ : nothing;
105
+ }
106
+
107
+ private get notFoundSrc() {
108
+ return this.baseImageUrl
109
+ ? `${this.baseImageUrl}/images/notfound.png`
110
+ : nothing;
111
+ }
112
+
113
+ private get hashBasedGradient() {
114
+ if (!this.model?.identifier) {
115
+ return 'waveform-grad0';
116
+ }
117
+ const gradient = this.hashStrToInt(this.model.identifier) % 6; // returns 0-5
118
+ return `waveform-grad${gradient}`;
119
+ }
120
+
121
+ private hashStrToInt(str: string): number {
122
+ return str
123
+ .split('')
124
+ .reduce((acc: number, char: string) => acc + char.charCodeAt(0), 0);
125
+ }
126
+
127
+ /**
128
+ * Classes
129
+ */
130
+ private get itemBaseClass(): ClassInfo {
131
+ return {
132
+ 'drop-shadow': true,
133
+ 'list-box': this.isListTile,
134
+ 'search-image': this.model?.mediatype === 'search',
135
+ [this.hashBasedGradient]: this.isWaveform,
136
+ };
137
+ }
138
+
139
+ private get itemImageClass(): ClassInfo {
140
+ const hasSensitiveContent = !!(
141
+ this.model?.contentWarning || this.model?.loginRequired
142
+ );
143
+ const shouldBlur = hasSensitiveContent && !this.suppressBlurring;
144
+
145
+ return {
146
+ contain: !this.isCompactTile && !this.isWaveform,
147
+ cover: this.isCompactTile,
148
+ blur: shouldBlur,
149
+ waveform: this.isWaveform,
150
+ 'account-image': this.isAccountImage, // for account tile image
151
+ 'collection-image': this.model?.mediatype === 'collection', // for collection tile image
152
+ };
153
+ }
154
+
155
+ /**
156
+ * Helper function to determine if account tile image
157
+ */
158
+ private get isAccountImage() {
159
+ return (
160
+ this.model?.mediatype === 'account' &&
161
+ !this.isCompactTile &&
162
+ !this.isListTile
163
+ );
164
+ }
165
+
166
+ /**
167
+ * Event listener sets isWaveform true if image is waveform
168
+ */
169
+ private onLoad() {
170
+ if (
171
+ (this.model?.mediatype === 'audio' ||
172
+ this.model?.mediatype === 'etree') &&
173
+ this.baseImage.naturalWidth / this.baseImage.naturalHeight === 4
174
+ ) {
175
+ this.isWaveform = true;
176
+ if (this.model?.identifier) {
177
+ ItemImage.waveformByIdentifier.set(this.model.identifier, true);
178
+ }
179
+ }
180
+ }
181
+
182
+ private onError() {
183
+ this.isNotFound = true;
184
+ }
185
+
186
+ /**
187
+ * CSS
188
+ */
189
+ static get styles(): CSSResultGroup {
190
+ return [
191
+ baseItemImageStyles,
192
+ waveformGradientStyles,
193
+ css`
194
+ img {
195
+ height: var(--imgHeight, 16rem);
196
+ width: var(--imgWidth, 16rem);
197
+ }
198
+
199
+ .search-image {
200
+ display: flex;
201
+ align-items: center;
202
+ justify-content: center;
203
+ background: rgb(245, 245, 247);
204
+ border-radius: 4px;
205
+ }
206
+
207
+ svg {
208
+ height: 10rem;
209
+ width: 10rem;
210
+ }
211
+ `,
212
+ ];
213
+ }
214
+ }
@@ -1,86 +1,112 @@
1
- import { css, html } from 'lit';
2
- import { customElement } from 'lit/decorators.js';
3
- import { msg } from '@lit/localize';
4
- import { BaseTileComponent } from '../base-tile-component';
5
-
6
- @customElement('tile-list-compact-header')
7
- export class TileListCompactHeader extends BaseTileComponent {
8
- /*
9
- * Reactive properties inherited from BaseTileComponent:
10
- * - model?: TileModel;
11
- * - currentWidth?: number;
12
- * - currentHeight?: number;
13
- * - baseNavigationUrl?: string;
14
- * - baseImageUrl?: string;
15
- * - collectionPagePath?: string;
16
- * - sortParam: SortParam | null = null;
17
- * - creatorFilter?: string;
18
- * - mobileBreakpoint?: number;
19
- * - loggedIn = false;
20
- * - suppressBlurring = false;
21
- */
22
-
23
- render() {
24
- return html`
25
- <div id="list-line-header" class="${this.classSize}">
26
- <div id="thumb"></div>
27
- <div id="title">${msg('Title')}</div>
28
- <div id="creator">${msg('Creator')}</div>
29
- <div id="date">
30
- ${this.displayValueProvider.dateLabel || msg('Published')}
31
- </div>
32
- <div id="icon">${msg('Type')}</div>
33
- <div id="views">${this.displayValueProvider.viewsLabel}</div>
34
- </div>
35
- `;
36
- }
37
-
38
- private get classSize(): string {
39
- if (
40
- this.mobileBreakpoint &&
41
- this.currentWidth &&
42
- this.currentWidth < this.mobileBreakpoint
43
- ) {
44
- return 'mobile';
45
- }
46
- return 'desktop';
47
- }
48
-
49
- static get styles() {
50
- return css`
51
- html {
52
- font-size: unset;
53
- }
54
-
55
- div {
56
- font-size: 14px;
57
- font-weight: bold;
58
- line-height: 20px;
59
- }
60
-
61
- .mobile #views {
62
- display: none;
63
- }
64
-
65
- #views {
66
- text-align: right;
67
- padding-right: 8px;
68
- }
69
-
70
- #list-line-header {
71
- display: grid;
72
- column-gap: 10px;
73
- align-items: flex-end;
74
- padding-bottom: 2px;
75
- }
76
-
77
- #list-line-header.mobile {
78
- grid-template-columns: 36px 3fr 2fr 68px 35px;
79
- }
80
-
81
- #list-line-header.desktop {
82
- grid-template-columns: 51px 3fr 2fr 95px 30px 115px;
83
- }
84
- `;
85
- }
86
- }
1
+ import { css, html, nothing } from 'lit';
2
+ import { customElement } from 'lit/decorators.js';
3
+ import { classMap } from 'lit/directives/class-map.js';
4
+ import { msg } from '@lit/localize';
5
+ import { BaseTileComponent } from '../base-tile-component';
6
+
7
+ @customElement('tile-list-compact-header')
8
+ export class TileListCompactHeader extends BaseTileComponent {
9
+ /*
10
+ * Reactive properties inherited from BaseTileComponent:
11
+ * - model?: TileModel;
12
+ * - tileActions: TileAction[] = [];
13
+ * - currentWidth?: number;
14
+ * - currentHeight?: number;
15
+ * - baseNavigationUrl?: string;
16
+ * - baseImageUrl?: string;
17
+ * - collectionPagePath?: string;
18
+ * - sortParam: SortParam | null = null;
19
+ * - creatorFilter?: string;
20
+ * - mobileBreakpoint?: number;
21
+ * - loggedIn = false;
22
+ * - suppressBlurring = false;
23
+ */
24
+
25
+ render() {
26
+ const hasActions = this.tileActions.length > 0;
27
+ const headerClasses = classMap({
28
+ mobile: this.classSize === 'mobile',
29
+ desktop: this.classSize === 'desktop',
30
+ 'has-actions': hasActions,
31
+ });
32
+
33
+ return html`
34
+ <div id="list-line-header" class=${headerClasses}>
35
+ <div id="thumb"></div>
36
+ ${hasActions ? html`<div id="actions-header"></div>` : nothing}
37
+ <div id="title">${msg('Title')}</div>
38
+ <div id="creator">${msg('Creator')}</div>
39
+ <div id="date">
40
+ ${this.displayValueProvider.dateLabel || msg('Published')}
41
+ </div>
42
+ <div id="icon">${msg('Type')}</div>
43
+ <div id="views">${this.displayValueProvider.viewsLabel}</div>
44
+ </div>
45
+ `;
46
+ }
47
+
48
+ private get classSize(): string {
49
+ if (
50
+ this.mobileBreakpoint &&
51
+ this.currentWidth &&
52
+ this.currentWidth < this.mobileBreakpoint
53
+ ) {
54
+ return 'mobile';
55
+ }
56
+ return 'desktop';
57
+ }
58
+
59
+ static get styles() {
60
+ return css`
61
+ html {
62
+ font-size: unset;
63
+ }
64
+
65
+ div {
66
+ font-size: 14px;
67
+ font-weight: bold;
68
+ line-height: 20px;
69
+ }
70
+
71
+ .mobile #views {
72
+ display: none;
73
+ }
74
+
75
+ #views {
76
+ text-align: right;
77
+ padding-right: 8px;
78
+ }
79
+
80
+ #list-line-header {
81
+ display: grid;
82
+ column-gap: 10px;
83
+ align-items: flex-end;
84
+ padding-bottom: 2px;
85
+ }
86
+
87
+ #list-line-header.mobile {
88
+ grid-template-columns: 36px 3fr 2fr 68px 35px;
89
+ }
90
+
91
+ #list-line-header.desktop {
92
+ grid-template-columns: 51px 3fr 2fr 95px 30px 115px;
93
+ }
94
+
95
+ /*
96
+ * When tile actions are present in the rows below, reserve a matching
97
+ * column here so the columns stay aligned with each row.
98
+ */
99
+ #list-line-header.mobile.has-actions {
100
+ grid-template-columns:
101
+ 36px var(--tileActionColumnWidth, 90px) 3fr 2fr
102
+ 68px 35px;
103
+ }
104
+
105
+ #list-line-header.desktop.has-actions {
106
+ grid-template-columns:
107
+ 51px var(--tileActionColumnWidth, 100px) 3fr 2fr
108
+ 95px 30px 115px;
109
+ }
110
+ `;
111
+ }
112
+ }