@zeesoft/i18n-verify 1.0.0 → 2.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.
@@ -40,14 +40,20 @@ class NightWatchDriver extends _baseDriver.default {
40
40
  async getValue(element) {
41
41
  return await element.getAttribute('value');
42
42
  }
43
+ async getElementID(element) {
44
+ return await element.getAttribute('id');
45
+ }
43
46
  async getElementDimension(element) {
44
47
  const eleSize = await element.getRect();
45
48
  if (![null, '', undefined].includes(eleSize)) {
46
49
  return [eleSize.x, eleSize.y, eleSize.height, eleSize.width];
47
50
  }
48
51
  }
49
- isElementDisplayed(element) {
50
- return this.$driver.isVisible(element);
52
+ async isElementDisplayed(element) {
53
+ return await this.$driver.isVisible(element);
54
+ }
55
+ async isElementPresent(element) {
56
+ return await this.$driver.isPresent(element);
51
57
  }
52
58
  async getElementStrings(element) {
53
59
  const elementStrings = [];
@@ -91,5 +97,38 @@ class NightWatchDriver extends _baseDriver.default {
91
97
  async getElementsCount(elements) {
92
98
  return elements.length;
93
99
  }
100
+ async switchFrame(frameID) {
101
+ if (frameID && frameID !== '') {
102
+ await this.$driver.frame(frameID, () => {
103
+ console.log(`Switching to iframe '${frameID}'`);
104
+ });
105
+ } else {
106
+ await this.$driver.frame(() => {
107
+ console.log('Switching to parentFrame');
108
+ });
109
+ }
110
+ }
111
+ pause(time) {
112
+ this.$driver.pause(time * 1000);
113
+ }
114
+ async onScreenStringHighlight(el, color) {
115
+ await this.$driver.execute((el, color) => {
116
+ el.style.border = `3px solid ${color}`;
117
+ setTimeout(() => {
118
+ el.style.border = 'none';
119
+ }, 50);
120
+ }, [el, color]);
121
+ }
122
+ async checkIfIframeIsPresent() {
123
+ return new Promise(res => {
124
+ this.$driver.isPresent({
125
+ selector: '//iframe',
126
+ locateStrategy: 'xpath',
127
+ suppressNotFoundErrors: true
128
+ }, result => {
129
+ res(result.value);
130
+ });
131
+ });
132
+ }
94
133
  }
95
134
  exports.default = NightWatchDriver;
@@ -47,9 +47,15 @@ class BaseDriver {
47
47
  getElementDimension() {
48
48
  throw new Error('implementation is missing');
49
49
  }
50
+ getElementID() {
51
+ throw new Error('implementation is missing');
52
+ }
50
53
  isElementDisplayed() {
51
54
  throw new Error('implementation is missing');
52
55
  }
56
+ isElementPresent() {
57
+ throw new Error('implementation is missing');
58
+ }
53
59
  getElementString() {
54
60
  throw new Error('implementation is missing');
55
61
  }
@@ -62,6 +68,18 @@ class BaseDriver {
62
68
  getElementsCount() {
63
69
  throw new Error('implementation is missing');
64
70
  }
71
+ switchFrame() {
72
+ throw new Error('implementation is missing');
73
+ }
74
+ pause() {
75
+ throw new Error('implementation is missing');
76
+ }
77
+ onScreenStringHighlight() {
78
+ throw new Error('implementation is missing');
79
+ }
80
+ checkIfIframeIsPresent() {
81
+ throw new Error('implementation is missing');
82
+ }
65
83
  printOnConsole(str) {
66
84
  console.log(str);
67
85
  }
@@ -40,6 +40,9 @@ class Config {
40
40
  static getGarbledChars() {
41
41
  return [...(this.#options.garbledChars || []), ..._constants.GARBLED_CHARACTERS];
42
42
  }
43
+ static getL10nReportsPath() {
44
+ return this.#options.l10nReportsPath;
45
+ }
43
46
  static setDriver(driver) {
44
47
  this.#driver = driver;
45
48
  }
package/dist/lib/i18n.js CHANGED
@@ -68,12 +68,18 @@ class I18nVerify {
68
68
  }
69
69
  async #getElement(verificationArea, locator, img_name, desc) {
70
70
  const ele = await this.#driver.getElement(locator);
71
- if (ele && (await this.#driver.isElementDisplayed(ele))) {
71
+ if (ele && (await this.#driver.isElementPresent(ele))) {
72
72
  let str;
73
73
  try {
74
74
  str = await this.#driver.getElementString(ele);
75
75
  try {
76
- await this.#generateReports(verificationArea, ele, img_name, str, desc);
76
+ await this.#generateReports({
77
+ verificationArea,
78
+ ele,
79
+ img_name,
80
+ str,
81
+ desc
82
+ });
77
83
  } catch (reportError) {
78
84
  console.error('Error generating report of element:', reportError);
79
85
  }
@@ -88,13 +94,19 @@ class I18nVerify {
88
94
  const eles = await this.#driver.getElements(xpaths);
89
95
  if (eles.length > 0) {
90
96
  for (const el of eles) {
91
- if (await this.#driver.isElementDisplayed(el)) {
97
+ if (await this.#driver.isElementPresent(el)) {
92
98
  let uiStrings;
93
99
  try {
94
100
  uiStrings = await this.#driver.getElementStrings(el);
95
101
  try {
96
102
  for (const str of uiStrings) {
97
- await this.#generateReports(verificationArea, el, img_name, str, desc);
103
+ await this.#generateReports({
104
+ verificationArea,
105
+ el,
106
+ img_name,
107
+ str,
108
+ desc
109
+ });
98
110
  }
99
111
  } catch (reportError) {
100
112
  console.error('Error generating report of element:', reportError);
@@ -104,11 +116,25 @@ class I18nVerify {
104
116
  }
105
117
  }
106
118
  }
107
- } else {
108
- console.error('No elements found:', xpaths);
119
+ }
120
+ if (await this.#driver.checkIfIframeIsPresent()) {
121
+ await this.#driver.switchFrame(null);
122
+ const iframes = await this.#driver.getElements('//iframe');
123
+ await this.#verifyIframeElements({
124
+ iframes,
125
+ img_name,
126
+ desc
127
+ });
109
128
  }
110
129
  }
111
- async #generateReports(verificationArea, el, image_name, str, desc) {
130
+ async #generateReports({
131
+ verificationArea,
132
+ el,
133
+ img_name,
134
+ str,
135
+ desc,
136
+ iframePresent = false
137
+ }) {
112
138
  //TODO: in some cases, the str is having 0
113
139
  //needs more inspection
114
140
  if (typeof str !== 'string' || !str || Number(str) === 0) {
@@ -116,45 +142,60 @@ class I18nVerify {
116
142
  }
117
143
  _reports.default.incrementCount(_constants.I18N_STATUS.REVIEWED);
118
144
  const i18nStatus = _index.default.getIssueType(str),
119
- src_img = (0, _file.joinPaths)(_reports.default.reviewedReports(str, image_name), image_name);
145
+ src_img = (0, _file.joinPaths)(_reports.default.reviewedReports(str, img_name), img_name);
120
146
  if (i18nStatus && src_img && typeof _reports.default[i18nStatus] === 'function') {
121
147
  const color = this.#defineColor(i18nStatus, str),
122
- dest_img = (0, _file.joinPaths)(_reports.default[i18nStatus](str, image_name), image_name);
123
- const eles = [];
124
- eles.push(el);
125
- browser.perform(async function () {
126
- for (const el of eles) {
127
- if (true) {
128
- // Highlight the element using custom JavaScript
129
- browser.execute(function (el, color) {
130
- el.style.border = `3px solid ${color}`;
131
- setTimeout(function () {
132
- el.style.border = 'none'; // Remove border
133
- }, 50);
134
- }, [el, color]);
135
- }
136
- }
148
+ dest_img = (0, _file.joinPaths)(_reports.default[i18nStatus](str, img_name), img_name);
149
+ this.#driver.onScreenStringHighlight(el, color);
150
+ await this.#highlightReports({
151
+ verificationArea,
152
+ el,
153
+ color,
154
+ src_img,
155
+ dest_img,
156
+ str,
157
+ iframePresent,
158
+ desc
137
159
  });
138
- await this.#highlightReports(verificationArea, el, color, src_img, dest_img, str);
139
160
  }
140
161
  }
141
- async #highlightReports(verificationArea, el, color, src_img, rep_img, str) {
162
+ async #highlightReports({
163
+ verificationArea,
164
+ el,
165
+ color,
166
+ src_img,
167
+ dest_img,
168
+ str,
169
+ iframePresent,
170
+ desc
171
+ }) {
142
172
  try {
143
- if (_fs.default.existsSync(src_img) && !_fs.default.existsSync(rep_img)) {
144
- _fs.default.copyFileSync(src_img, rep_img);
145
- } else if (!_fs.default.existsSync(rep_img) && !_fs.default.existsSync(src_img)) {
173
+ if (_fs.default.existsSync(src_img) && !_fs.default.existsSync(dest_img)) {
174
+ _fs.default.copyFileSync(src_img, dest_img);
175
+ } else if (!_fs.default.existsSync(dest_img) && !_fs.default.existsSync(src_img)) {
146
176
  await this.#captureCurrentScreen(src_img);
147
- _fs.default.copyFileSync(src_img, rep_img);
177
+ _fs.default.copyFileSync(src_img, dest_img);
148
178
  }
149
179
  } catch (error) {
150
180
  _logs.default.error(`zee-error#2 - ${src_img} is not exists and unable to capture screenshot`);
151
181
  }
152
- const area = await this.#driver.getElement(verificationArea);
182
+ let areaRec;
183
+ let eleRec = await this.#getRect(el);
153
184
  const areaColor = _constants.COLOR.GRB_BLUE;
154
- const areaRec = await this.#getRect(area);
155
- const eleRec = await this.#getRect(el);
156
- await _dGraphic.default.drawRectangle(areaRec, areaColor, rep_img);
157
- await _dGraphic.default.drawRectangle(eleRec, color, rep_img);
185
+ if (iframePresent) {
186
+ areaRec = verificationArea;
187
+ const keysToUpdate = ['x', 'y'];
188
+ for (const key of keysToUpdate) {
189
+ if (key in eleRec && key in areaRec) {
190
+ eleRec[key] = areaRec[key] + eleRec[key];
191
+ }
192
+ }
193
+ } else {
194
+ const area = await this.#driver.getElement(verificationArea);
195
+ areaRec = await this.#getRect(area);
196
+ }
197
+ await _dGraphic.default.drawRectangle(areaRec, areaColor, dest_img);
198
+ await _dGraphic.default.drawRectangle(eleRec, color, dest_img);
158
199
  }
159
200
  async #captureCurrentScreen(imagePath) {
160
201
  await this.#driver.captureScreenshot(imagePath);
@@ -173,5 +214,42 @@ class I18nVerify {
173
214
  _logs.default.debug(`[${str}] ${_constants.STATUS_COLOR[i18nStatus][1]} ${_constants.STATUS_INFO[i18nStatus]}${'\x1b[0m'}`);
174
215
  return _constants.STATUS_COLOR[i18nStatus][0];
175
216
  }
217
+ async #verifyIframeElements({
218
+ iframes,
219
+ img_name,
220
+ desc
221
+ }) {
222
+ for (const iframe of iframes) {
223
+ const frameID = await this.#driver.getElementID(iframe);
224
+ const verificationArea = await this.#getRect(iframe);
225
+ await this.#driver.switchFrame(frameID);
226
+ this.#driver.pause(10);
227
+ const iframeBody = await this.#driver.getElement('//body');
228
+ await this.#driver.isElementPresent(iframeBody);
229
+ const iframeElements = await this.#driver.getElements('//*[normalize-space(text())]');
230
+ if (iframeElements.length > 0) {
231
+ for (const el of iframeElements) {
232
+ if (await this.#driver.isElementPresent(el)) {
233
+ let uiStrings = await this.#driver.getElementStrings(el);
234
+ try {
235
+ for (const str of uiStrings) {
236
+ await this.#generateReports({
237
+ verificationArea,
238
+ el,
239
+ img_name,
240
+ str,
241
+ desc,
242
+ iframePresent: true
243
+ });
244
+ }
245
+ } catch (reportError) {
246
+ console.error('Error generating report of iframe elements:', reportError);
247
+ }
248
+ }
249
+ }
250
+ }
251
+ await this.#driver.switchFrame(null);
252
+ }
253
+ }
176
254
  }
177
255
  exports.default = I18nVerify;
@@ -12,6 +12,7 @@ const getConfigTemplate = (type = '') => `{
12
12
  "type": "${type}", // set automation framework type ${(0, _common.arr2Txt)(Object.values(_constants.SYSTEM_TYPES))}
13
13
  "qaLocale": "", // set the qa locale used in your project
14
14
  "l10nResourcePath": "./l10nResources", // set the dir path of l10n resources
15
+ "l10nReportsPath": "./l10nReports/", // set the dir path of l10n reports
15
16
  "supportedLocales": ["en_US", "fr_FR", "ja_JP"], // set the list of supported locales
16
17
  "defaultLocale": "en_US", // set default locale of the application
17
18
  "qaMock": { // set the prefix and suffix characters of qa locale
@@ -48,7 +49,7 @@ const validateConfig = config => {
48
49
 
49
50
  // add required property validations
50
51
  const missingKeys = [];
51
- if (!(0, _common.allPropertyExists)(['l10nResourcePath', 'supportedLocales', 'defaultLocale', 'type'], config, missingKeys)) {
52
+ if (!(0, _common.allPropertyExists)(['l10nResourcePath', 'l10nReportsPath', 'supportedLocales', 'defaultLocale', 'type'], config, missingKeys)) {
52
53
  throw new Error(`following properties are missing in ${_constants.CONFIG_FILE_NAME} file: ${(0, _common.arr2Txt)(missingKeys)}`);
53
54
  }
54
55
  if (!Object.values(_constants.SYSTEM_TYPES).includes(config?.type || '')) {
@@ -10,7 +10,7 @@ var _logs = _interopRequireDefault(require("../../logs.js"));
10
10
  var _base = _interopRequireDefault(require("../base.js"));
11
11
  var _css = require("./template/css.js");
12
12
  var _html = require("./template/html.js");
13
- var _javascript = require("./template/javascript.js");
13
+ var _index = _interopRequireDefault(require("./template/javascript/index.js"));
14
14
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
15
15
  class HtmlReport extends _base.default {
16
16
  constructor() {
@@ -18,9 +18,9 @@ class HtmlReport extends _base.default {
18
18
  }
19
19
  buildContent() {
20
20
  const metaInfo = this.getMetaInfo();
21
- const script = (0, _javascript.getJsData)(metaInfo);
21
+ const script = (0, _index.default)(metaInfo);
22
22
  const style = (0, _css.getCssData)();
23
- const body = (0, _html.getBodyData)(metaInfo);
23
+ const body = (0, _html.getBodyData)();
24
24
  return (0, _html.renderHtml)({
25
25
  appName: metaInfo.appName,
26
26
  reportTime: metaInfo.reportTime,
@@ -6,14 +6,18 @@ Object.defineProperty(exports, "__esModule", {
6
6
  exports.getCssData = void 0;
7
7
  const getCssData = () => `
8
8
  * {
9
- user-select: none;
10
- }
9
+ user-select: none;
10
+ }
11
11
 
12
12
  body {
13
13
  margin: 0;
14
14
  display: flex;
15
15
  flex-direction: column;
16
16
  font-family: Arial, sans-serif;
17
+ font-family: "Helvetica Neue", Arial, sans-serif;
18
+ -webkit-font-smoothing: antialiased; /* Webkit-based browsers */
19
+ -moz-osx-font-smoothing: grayscale; /* Firefox on OS X */
20
+ font-smooth: always; /* Non-standard, some browsers */
17
21
  }
18
22
 
19
23
  header {
@@ -29,7 +33,7 @@ const getCssData = () => `
29
33
  }
30
34
 
31
35
  .sidebar {
32
- width: 250px;
36
+ width: 16rem;
33
37
  height: 100%;
34
38
  background-color: #333;
35
39
  color: #fff;
@@ -77,7 +81,13 @@ const getCssData = () => `
77
81
  }
78
82
 
79
83
  .menu-item.has-submenu > a::after {
80
- content: '▼';
84
+ content: "⯆";
85
+ float: right;
86
+ margin-left: 5px;
87
+ }
88
+
89
+ .menu-item.has-submenu.selected > a::after {
90
+ content: "⯅";
81
91
  float: right;
82
92
  margin-left: 5px;
83
93
  }
@@ -86,6 +96,10 @@ const getCssData = () => `
86
96
  background-color: #555;
87
97
  }
88
98
 
99
+ .submenu .sub-menu-item {
100
+ margin: 3px auto;
101
+ }
102
+
89
103
  .submenu .sub-menu-item a:hover {
90
104
  background-color: #777;
91
105
  }
@@ -99,10 +113,11 @@ const getCssData = () => `
99
113
  }
100
114
 
101
115
  .content-wrapper {
102
- padding: 20px;
116
+ padding: 1rem;
103
117
  float: left;
104
- width: calc(100% - 250px);
118
+ width: calc(100% - 16rem);
105
119
  background-color: #f4f4f4;
120
+ overflow: hidden;
106
121
  }
107
122
 
108
123
  .content {
@@ -112,21 +127,287 @@ const getCssData = () => `
112
127
  .content .title {
113
128
  font-size: 1rem;
114
129
  color: #080404e2;
130
+ font-weight: 600;
115
131
  }
116
132
 
117
133
  .content .title::after {
118
- content: ':';
134
+ content: ":";
119
135
  }
120
136
 
121
137
  .content.active {
122
138
  display: flex !important;
123
139
  flex-direction: column;
140
+ gap: 0.9rem;
141
+ flex-wrap: wrap;
142
+ transition: all 2s ease-in-out;
143
+ }
144
+
145
+ .sidebar .footer {
146
+ bottom: 5px;
147
+ position: absolute;
148
+ font-size: 12.4px;
149
+ color: #14cf96;
150
+ font-family: system-ui;
151
+ }
152
+
153
+ #overview {
154
+ flex-direction: row;
155
+ justify-content: space-between;
156
+ }
157
+
158
+ #overview .card {
159
+ display: flex;
160
+ flex-direction: column;
161
+ width: 25rem;
162
+ height: 12.5rem;
163
+ border: 1px solid #ddd;
164
+ box-shadow: 0 22px 35px -16px rgba(0, 0, 0, 0.3);
165
+ border-radius: 5px;
166
+ background: white;
167
+ padding: 1rem;
168
+ }
169
+
170
+ #overview .overall-quality {
171
+ height: 20rem;
172
+ flex-basis: 100%;
173
+ line-height: 14px;
174
+ }
175
+
176
+ #overview .overall-quality .section {
177
+ display: flex;
178
+ flex-direction: row;
179
+ }
180
+
181
+ #overview .overall-quality .section .block {
182
+ width: 50%;
183
+ }
184
+
185
+ #overview .overall-quality .section .block article {
186
+ padding: 5px;
187
+ }
188
+
189
+ #overview .overall-quality .section .block article p.desc {
190
+ font-size: 0.9em;
191
+ }
192
+
193
+ #overview .overall-quality .section .block article .items li {
194
+ font-size: 0.87em;
195
+ }
196
+
197
+ #overview .overall-quality .section .block article .items li .detail {
198
+ margin-top: -10px;
199
+ }
200
+
201
+ #overview .overall-quality .section .block .graph {
202
+ margin-top: 1.5em;
203
+ }
204
+
205
+ #overview .string-quality,
206
+ #overview .number-quality,
207
+ #overview .datetime-quality,
208
+ #overview .skipped-items {
209
+ flex-basis: 45%;
210
+ }
211
+
212
+ #overview .string-quality .graph,
213
+ #overview .number-quality .graph,
214
+ #overview .datetime-quality .graph,
215
+ #overview .skipped-items .graph {
216
+ align-self: center;
217
+ }
218
+
219
+ #overview .string-quality .graph {
220
+ margin-left: 4rem;
221
+ }
222
+
223
+ #overview .number-quality.card,
224
+ #overview .datetime-quality.card {
225
+ height: 13rem;
226
+ }
227
+
228
+ #overview .number-quality .graph,
229
+ #overview .datetime-quality .graph {
230
+ margin-top: -0.9rem;
231
+ margin-left: 4.5rem;
232
+ }
233
+
234
+ #insights .cards {
235
+ display: flex;
236
+ flex-direction: column;
237
+ gap: 1.5rem;
238
+ }
239
+
240
+ #insights .cards .holder {
241
+ display: flex;
242
+ flex-direction: row;
243
+ gap: 1.5rem;
244
+ flex-wrap: wrap;
245
+ }
246
+
247
+ #insights .cards .card {
248
+ display: flex;
249
+ flex-direction: column;
250
+ width: 20rem;
251
+ min-height: 10rem;
252
+ border: 1px solid #ddd;
253
+ box-shadow: 0 22px 35px -16px rgba(0, 0, 0, 0.3);
254
+ border-radius: 5px;
255
+ background: white;
256
+ }
257
+
258
+ #insights .cards .card:hover {
259
+ transform: scale(1.015);
260
+ box-shadow: 0 22px 35px -16px #07ff7d33;
261
+ cursor: pointer;
262
+ transition: all 0.1s ease-in-out;
263
+ }
264
+
265
+ #insights .cards .card .title {
266
+ height: 2.5rem;
267
+ font-size: 1rem;
268
+ padding-top: 1rem;
269
+ font-weight: 500;
270
+ width: calc(100% - 1rem);
271
+ background: #0c5ad9d6;
272
+ border-top-left-radius: 5px;
273
+ border-top-right-radius: 5px;
274
+ padding-left: 1rem;
275
+ color: white;
276
+ }
277
+
278
+ #insights .cards .card .title::after {
279
+ content: "";
280
+ }
281
+
282
+ #insights .cards .card .card-content {
283
+ display: flex;
284
+ flex-direction: column;
124
285
  gap: 1rem;
286
+ padding: 1rem;
287
+ }
288
+
289
+ #insights .cards .card .status-bar {
290
+ display: flex;
291
+ flex-direction: column;
292
+ gap: 0.8rem;
293
+ }
294
+
295
+ #insights .cards .card .status-bar .bar-container .name {
296
+ font-size: 0.78rem;
297
+ -webkit-font-smoothing: antialiased;
298
+ -moz-osx-font-smoothing: grayscale;
299
+ }
300
+
301
+ #insights .cards .card .status-bar .bar-container .name::after {
302
+ content: ":";
303
+ }
304
+
305
+ #insights .cards .card .status-bar .bar-container .bar-holder {
306
+ width: 100%;
307
+ height: 10px;
308
+ border: 1px solid #00000038;
309
+ border-radius: 2px;
310
+ }
311
+
312
+ #insights .cards .card .status-bar .bar-container .bar-holder .bar {
313
+ background-color: #14cf96;
314
+ height: 100%;
315
+ font-size: 0.63rem;
316
+ text-align: right;
317
+ color: white;
318
+ }
319
+
320
+ #insights .cards .card .stat {
321
+ display: flex;
322
+ flex-direction: column;
323
+ gap: 0.3rem;
324
+ border: 0.5px solid #00000012;
325
+ border-radius: 6px;
326
+ }
327
+
328
+ #insights .cards .card .stat table td {
329
+ font-size: 0.78rem;
330
+ -webkit-font-smoothing: antialiased;
331
+ -moz-osx-font-smoothing: grayscale;
332
+ }
333
+
334
+ #insights .cards .card .stat table td.name {
335
+ width: 70%;
336
+ }
337
+
338
+ #insights .cards .card .stat table td.value::before {
339
+ content: ": ";
340
+ }
341
+
342
+ #locale {
343
+ background: white;
344
+ padding: 12px;
345
+ border-radius: 5px;
346
+ padding-top: 20px;
347
+ }
348
+
349
+ #locale .title {
350
+ content: ":";
351
+ }
352
+
353
+ #locale .coverage-info {
354
+ margin-top: 1rem;
355
+ width: 100%;
356
+ height: 80vh;
357
+ padding-right: 1rem;
358
+ padding-left: 0.1rem;
359
+ }
360
+
361
+ #locale table thead th select.filter {
362
+ margin-left: 1rem;
363
+ max-width: 8rem;
364
+ text-overflow: ellipsis;
365
+ }
366
+
367
+ #locale .image-viewer {
368
+ display: none;
369
+ position: fixed;
370
+ z-index: 9999;
371
+ top: 0;
372
+ left: 0;
373
+ width: 100%;
374
+ height: 100%;
375
+ background-color: rgba(0, 0, 0, 0.8);
376
+ text-align: center;
377
+ }
378
+
379
+ #locale .image-container {
380
+ position: relative;
381
+ top: 50%;
382
+ transform: translateY(-50%);
383
+ display: inline-block;
384
+ }
385
+
386
+ #locale .image-viewer img {
387
+ max-width: 90%;
388
+ max-height: 90%;
389
+ }
390
+
391
+ #locale .close-button {
392
+ position: absolute;
393
+ top: 0.5rem;
394
+ right: 1rem;
395
+ color: #ffffffbf;
396
+ font-size: 3.1rem;
397
+ cursor: pointer;
398
+ }
399
+
400
+ #locale .close-button:hover {
401
+ color: white;
402
+ }
403
+
404
+ #locale table td .view-screenshot-btn {
405
+ color: blue;
125
406
  }
126
407
 
127
408
  @media (max-width: 768px) {
128
409
  .sidebar {
129
- width: 100%;
410
+ width: 16rem;
130
411
  padding: 10px;
131
412
  }
132
413
  }`;