umberto 3.2.1 → 4.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.
package/CHANGELOG.md CHANGED
@@ -1,6 +1,37 @@
1
1
  Changelog
2
2
  =========
3
3
 
4
+ ## [4.0.0](https://github.com/cksource/umberto/compare/v3.2.2...v4.0.0) (2023-10-10)
5
+
6
+ ### BREAKING CHANGES
7
+
8
+ * Upgraded the minimal versions of Node.js to `18.0.0` due to the end of LTS.
9
+
10
+ ### Bug fixes
11
+
12
+ * Added support for perm-link feature on API pages. Closes [#1167](https://github.com/cksource/umberto/issues/1167). ([commit](https://github.com/cksource/umberto/commit/20d167082e126541d3f2867ebebcf57b6c3173f8))
13
+ * Added the `[title]` attribute to the Table of Content and the navigation menu items. Closes [#1175](https://github.com/cksource/umberto/issues/1175). ([commit](https://github.com/cksource/umberto/commit/8f3c15f96864a4e8320d26fb2b0c00746529e06b))
14
+ * Fixed the "See source" button to be clickable when an item does not have a description. Closes [#1172](https://github.com/cksource/umberto/issues/1172). ([commit](https://github.com/cksource/umberto/commit/05b4e3c6893a8d4fdafd5e48c0293a1a50670a35))
15
+
16
+ ### Other changes
17
+
18
+ * Replaced the hyphen-minus (`-`) separator with a vertical pipe (`|`) in the generated title of pages. ([commit](https://github.com/cksource/umberto/commit/8bf6a3afb7337d7d816c3a5206eb3088bc1edf29))
19
+ * Updated the required version of Node.js to 18. See [ckeditor/ckeditor5#14924](https://github.com/ckeditor/ckeditor5/issues/14924). ([commit](https://github.com/cksource/umberto/commit/c7ee14ee127ae5b497569a687e731c40fa6378ef))
20
+
21
+
22
+ ## [3.2.2](https://github.com/cksource/umberto/compare/v3.2.1...v3.2.2) (2023-08-28)
23
+
24
+ ### Bug fixes
25
+
26
+ * Fixed jumping layout in the secondary (table of content) navigation in a case when a text is just barely fitting a single line. When it gets highlighted and the font geometry changes, and it gets to two lines. Closes [cksource/umberto#1152](https://github.com/cksource/umberto/issues/1152). ([commit](https://github.com/cksource/umberto/commit/102674435ea8f70c321d7c029bf984ddc16f7855))
27
+ * HTML tags will no longer render in ToC. Closes [#1157](https://github.com/cksource/umberto/issues/1157). ([commit](https://github.com/cksource/umberto/commit/cc44e61f496bef25f34d8e2d41eca8e849a54924))
28
+ * Skip search results from API on the root page in documentation. Closes [#1029](https://github.com/cksource/umberto/issues/1029). ([commit](https://github.com/cksource/umberto/commit/b4f9250a43ff887da6da7f042434aa1542582ea7))
29
+
30
+ ### Other changes
31
+
32
+ * The Google Analytics `<script>` tag produces output compatible with the latest version (v4). Closes [#1166](https://github.com/cksource/umberto/issues/1166). ([commit](https://github.com/cksource/umberto/commit/0cefecde3ccc12f171580dded361895bfb0b12cd))
33
+
34
+
4
35
  ## [3.2.1](https://github.com/cksource/umberto/compare/v3.2.0...v3.2.1) (2023-06-21)
5
36
 
6
37
  Internal changes only (updated dependencies, documentation, etc.).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "umberto",
3
- "version": "3.2.1",
3
+ "version": "4.0.0",
4
4
  "description": "CKSource Documentation builder",
5
5
  "main": "src/index.js",
6
6
  "files": [
@@ -57,8 +57,7 @@
57
57
  "webpack": "^5.74.0"
58
58
  },
59
59
  "engines": {
60
- "node": ">=16.0.0",
61
- "npm": ">=5.7.1"
60
+ "node": ">=18.0.0"
62
61
  },
63
62
  "author": "CKSource (http://cksource.com/)",
64
63
  "license": "MIT",
@@ -33,9 +33,13 @@ hexo.extend.helper.register( 'uToc', ( data, options = {} ) => {
33
33
 
34
34
  headings.each( function() {
35
35
  const hLevel = Number( this.name[ 1 ] );
36
- const text = $r( this ).find( '.headerlink' ).remove().end().text().trim();
37
36
  const id = $r( this ).attr( 'id' );
38
- const newItem = `<li><a href="#${ id }">${ text }</a></li>`;
37
+ const text = $r( this ).find( '.headerlink' ).remove().end().text().trim()
38
+ // Replace the chevrons with their HTML escape characters to avoid rendering HTML in ToC.
39
+ .replace( /</g, '&lt;' )
40
+ .replace( />/g, '&gt;' );
41
+
42
+ const newItem = `<li><a href="#${ id }" title="${ text }">${ text }</a></li>`;
39
43
 
40
44
  if ( tocLastLevels[ hLevel ] ) {
41
45
  // If there already is a parent node for current heading level, append the new toc item there.
@@ -5,8 +5,14 @@
5
5
 
6
6
  'use strict';
7
7
 
8
- const { stringify } = require( 'javascript-stringify' );
9
8
  const { cloneDeep } = require( 'lodash' );
9
+ const { stringify } = require( 'javascript-stringify' );
10
+ const fs = require( 'fs' );
11
+ const upath = require( 'upath' );
12
+
13
+ const defaultScriptTemplate = fs.readFileSync( upath.join( __dirname, 'templates', 'scripts', 'default.js' ), 'utf-8' );
14
+ const groupScriptTemplate = fs.readFileSync( upath.join( __dirname, 'templates', 'scripts', 'group.js' ), 'utf-8' );
15
+ const rootScriptTemplate = fs.readFileSync( upath.join( __dirname, 'templates', 'scripts', 'root.js' ), 'utf-8' );
10
16
 
11
17
  module.exports = ( searchConfig, {
12
18
  groups = [],
@@ -25,7 +31,7 @@ module.exports = ( searchConfig, {
25
31
  const options = {
26
32
  inputSelector: '#docsearch_input',
27
33
  algoliaOptions: {
28
- hitsPerPage: 10,
34
+ hitsPerPage: 250,
29
35
  attributesToRetrieve: '*'
30
36
  }
31
37
  };
@@ -35,7 +41,7 @@ module.exports = ( searchConfig, {
35
41
  dConfig,
36
42
  options,
37
43
  {
38
- isEnabled: dConfig.apiKey && dConfig.indexName,
44
+ isEnabled: Boolean( dConfig.apiKey && dConfig.indexName ),
39
45
  css: 'https://cdn.jsdelivr.net/docsearch.js/1/docsearch.min.css',
40
46
  js: 'https://cdn.jsdelivr.net/docsearch.js/2/docsearch.min.js'
41
47
  }
@@ -58,7 +64,6 @@ module.exports = ( searchConfig, {
58
64
 
59
65
  if ( transformDataFunctions ) {
60
66
  for ( const tag of Object.keys( transformDataFunctions ) ) {
61
- innerConfig.algoliaOptions.hitsPerPage = 250;
62
67
  const innerConfigStr = stringify( innerConfig ).replace( /^{|}$/g, '' );
63
68
 
64
69
  docSearchConfig.scripts[ tag ] = [
@@ -88,144 +93,35 @@ module.exports = ( searchConfig, {
88
93
  };
89
94
 
90
95
  function getTransformDataFns( groups, slug ) {
91
- if ( !Array.isArray( groups ) || groups.length === 0 ) {
92
- return null;
93
- }
96
+ const functions = {
97
+ rootScript: fillTemplate( rootScriptTemplate.toString() )
98
+ };
94
99
 
95
- const functions = {};
96
- const tags = `[ ${ groups.map( g => `'${ g.slug }'` ).join( ', ' ) } ]`;
97
-
98
- for ( const g of groups ) {
99
- functions[ g.slug ] = [
100
- 'function( hits ) {',
101
-
102
- 'var sortingFn = function( a, b ) {',
103
- 'a.custom_ranking = a.custom_ranking || 0;',
104
- 'b.custom_ranking = b.custom_ranking || 0;',
105
-
106
- 'if ( a.custom_ranking === b.custom_ranking ) return a.index - b.index;',
107
- 'if ( a.custom_ranking > b.custom_ranking ) return -1;',
108
- 'if ( a.custom_ranking < b.custom_ranking ) return 1;',
109
- '};',
110
-
111
- 'var result = [];',
112
- `var tags = ${ tags };`,
113
-
114
- 'var hitsGrouped = { api: [] };',
115
-
116
- `hitsGrouped[ '${ g.slug }' ] = hits`,
117
- '.filter( function( h ) {',
118
- `return h.tags.includes( '${ slug }' ) && h.tags.includes( '${ g.slug }' );`,
119
- '} )',
120
- '.slice( 0, 7 )',
121
- '.map( function( item, idx ) {',
122
- 'item.index = idx;',
123
- 'return item;',
124
- '} )',
125
- '.sort( sortingFn );',
126
-
127
- 'tags.forEach( function( tag ) {',
128
- `if ( tag !== '${ g.slug }' ) {`,
129
- 'hitsGrouped[ tag ] = hits',
130
- '.filter( function( h ) {',
131
- `return h.tags.includes( '${ slug }' ) && h.tags.includes( tag );`,
132
- '} )',
133
- '.slice( 0, 10 )',
134
- '.map( function( item, idx ) {',
135
- 'item.index = idx;',
136
- 'return item;',
137
- '} )',
138
- '.sort( sortingFn )',
139
- '.slice( 0, 3 );',
140
- '}',
141
- '} );',
142
-
143
- 'var apiSearchHits = [];',
144
-
145
- 'if ( window.apiSearch ) apiSearchHits = window.apiSearch();',
146
-
147
- 'hitsGrouped.api = hitsGrouped.api.filter( function( h ) {',
148
- 'var isDuplicated = false;',
149
-
150
- 'for ( var i = 0; i < apiSearchHits.length; i++ ) {',
151
- 'if ( h.url.includes( apiSearchHits[ i ].urlShort ) ) {',
152
- 'isDuplicated = true;',
153
- 'break;',
154
- '}',
155
- '}',
156
-
157
- 'return !isDuplicated;',
158
- '} );',
159
-
160
- 'hitsGrouped.api = ( apiSearchHits || [] ).concat( hitsGrouped.api );',
161
-
162
- `result = result.concat( hitsGrouped[ '${ g.slug }' ] );`,
163
-
164
- 'tags.forEach( function( tag ) {',
165
- `if ( tag !== '${ g.slug }' ) result = result.concat( hitsGrouped[ tag ].slice( 0, 4 ) );`,
166
- '} );',
167
-
168
- 'return result;',
169
- '}'
170
- ].join( '' );
100
+ if ( !Array.isArray( groups ) || groups.length === 0 ) {
101
+ return functions;
171
102
  }
172
103
 
173
- functions.defaultScript = [
174
- 'function( hits ) {',
175
- 'var sortingFn = function( a, b ) {',
176
- 'a.custom_ranking = a.custom_ranking || 0;',
177
- 'b.custom_ranking = b.custom_ranking || 0;',
178
-
179
- 'if ( a.custom_ranking === b.custom_ranking ) return a.index - b.index;',
180
- 'if ( a.custom_ranking > b.custom_ranking ) return -1;',
181
- 'if ( a.custom_ranking < b.custom_ranking ) return 1;',
182
- '};',
104
+ const tags = `${ groups.map( group => `${ group.slug }` ).join( '\', \'' ) }`;
183
105
 
184
- 'var result = [];',
185
- `var tags = ${ tags };`,
106
+ for ( const group of groups ) {
107
+ const groupSlug = group.slug;
186
108
 
187
- 'var hitsGrouped = { api: [] };',
188
-
189
- 'tags.forEach( function( tag ) {',
190
- 'hitsGrouped[ tag ] = hits',
191
- '.filter( function( h ) {',
192
- `return h.tags.includes( '${ slug }' ) && h.tags.includes( tag );`,
193
- '} )',
194
- '.slice( 0, 10 )',
195
- '.map( function( item, idx ) {',
196
- 'item.index = idx;',
197
- 'return item;',
198
- '} )',
199
- '.sort( sortingFn )',
200
- '.slice( 0, 3 );',
201
- '} );',
202
-
203
- 'var apiSearchHits = [];',
204
-
205
- 'if ( window.apiSearch ) apiSearchHits = window.apiSearch();',
206
-
207
- 'hitsGrouped.api = hitsGrouped.api.filter( function( h ) {',
208
- 'var isDuplicated = false;',
109
+ functions[ groupSlug ] = fillTemplate( groupScriptTemplate.toString(), { tags, slug, groupSlug } );
110
+ }
209
111
 
210
- 'for ( var i = 0; i < apiSearchHits.length; i++ ) {',
211
- 'if ( h.url.includes( apiSearchHits[ i ].urlShort ) ) {',
212
- 'isDuplicated = true;',
213
- 'break;',
214
- '}',
215
- '}',
112
+ functions.defaultScript = fillTemplate( defaultScriptTemplate.toString(), { tags, slug } );
216
113
 
217
- 'return !isDuplicated;',
218
- '} );',
114
+ return functions;
115
+ }
219
116
 
220
- 'hitsGrouped.api = ( apiSearchHits || [] ).concat( hitsGrouped.api );',
117
+ function fillTemplate( template, data ) {
118
+ let updatedTemplate = template.replace( /^function [\S]+\( hits \)/, 'function( hits )' );
221
119
 
222
- 'tags.forEach( function( tag ) {',
223
- 'result = result.concat( hitsGrouped[ tag ].slice( 0, 4 ) );',
224
- '} );',
120
+ for ( const key in data ) {
121
+ const pattern = new RegExp( `{{{ ${ key } }}}`, 'g' );
225
122
 
226
- 'return result;',
227
- '}'
228
- ].join( '' );
123
+ updatedTemplate = updatedTemplate.replace( pattern, data[ key ] );
124
+ }
229
125
 
230
- return functions;
126
+ return updatedTemplate.replace( /\n\s*/g, '' );
231
127
  }
@@ -0,0 +1,54 @@
1
+ function defaultScript( hits ) {
2
+ var sortingFn = function( a, b ) {
3
+ a.custom_ranking = a.custom_ranking || 0;
4
+ b.custom_ranking = b.custom_ranking || 0;
5
+
6
+ if ( a.custom_ranking === b.custom_ranking ) return a.index - b.index;
7
+ if ( a.custom_ranking > b.custom_ranking ) return -1;
8
+ if ( a.custom_ranking < b.custom_ranking ) return 1;
9
+ };
10
+
11
+ var result = [];
12
+ var tags = [ '{{{ tags }}}' ];
13
+
14
+ var hitsGrouped = { api: [] };
15
+
16
+ tags.forEach( function( tag ) {
17
+ hitsGrouped[ tag ] = hits
18
+ .filter( function( h ) {
19
+ return h.tags.includes( '{{{ slug }}}' ) && h.tags.includes( tag );
20
+ } )
21
+ .slice( 0, 10 )
22
+ .map( function( item, idx ) {
23
+ item.index = idx;
24
+ return item;
25
+ } )
26
+ .sort( sortingFn )
27
+ .slice( 0, 3 );
28
+ } );
29
+
30
+ var apiSearchHits = [];
31
+
32
+ if ( window.apiSearch ) apiSearchHits = window.apiSearch();
33
+
34
+ hitsGrouped.api = hitsGrouped.api.filter( function( h ) {
35
+ var isDuplicated = false;
36
+
37
+ for ( var i = 0; i < apiSearchHits.length; i++ ) {
38
+ if ( h.url.includes( apiSearchHits[ i ].urlShort ) ) {
39
+ isDuplicated = true;
40
+ break;
41
+ }
42
+ }
43
+
44
+ return !isDuplicated;
45
+ } );
46
+
47
+ hitsGrouped.api = ( apiSearchHits || [] ).concat( hitsGrouped.api );
48
+
49
+ tags.forEach( function( tag ) {
50
+ result = result.concat( hitsGrouped[ tag ].slice( 0, 4 ) );
51
+ } );
52
+
53
+ return result;
54
+ }
@@ -0,0 +1,69 @@
1
+ function groupScript( hits ) {
2
+ var sortingFn = function( a, b ) {
3
+ a.custom_ranking = a.custom_ranking || 0;
4
+ b.custom_ranking = b.custom_ranking || 0;
5
+
6
+ if ( a.custom_ranking === b.custom_ranking ) return a.index - b.index;
7
+ if ( a.custom_ranking > b.custom_ranking ) return -1;
8
+ if ( a.custom_ranking < b.custom_ranking ) return 1;
9
+ };
10
+
11
+ var result = [];
12
+ var tags = [ '{{{ tags }}}' ];
13
+
14
+ var hitsGrouped = { api: [] };
15
+
16
+ hitsGrouped[ '{{{ groupSlug }}}' ] = hits
17
+ .filter( function( h ) {
18
+ return h.tags.includes( '{{{ slug }}}' ) && h.tags.includes( '{{{ groupSlug }}}' );
19
+ } )
20
+ .slice( 0, 7 )
21
+ .map( function( item, idx ) {
22
+ item.index = idx;
23
+ return item;
24
+ } )
25
+ .sort( sortingFn );
26
+
27
+ tags.forEach( function( tag ) {
28
+ if ( tag !== '{{{ groupSlug }}}' ) {
29
+ hitsGrouped[ tag ] = hits
30
+ .filter( function( h ) {
31
+ return h.tags.includes( '{{{ slug }}}' ) && h.tags.includes( tag );
32
+ } )
33
+ .slice( 0, 10 )
34
+ .map( function( item, idx ) {
35
+ item.index = idx;
36
+ return item;
37
+ } )
38
+ .sort( sortingFn )
39
+ .slice( 0, 3 );
40
+ }
41
+ } );
42
+
43
+ var apiSearchHits = [];
44
+
45
+ if ( window.apiSearch ) apiSearchHits = window.apiSearch();
46
+
47
+ hitsGrouped.api = hitsGrouped.api.filter( function( h ) {
48
+ var isDuplicated = false;
49
+
50
+ for ( var i = 0; i < apiSearchHits.length; i++ ) {
51
+ if ( h.url.includes( apiSearchHits[ i ].urlShort ) ) {
52
+ isDuplicated = true;
53
+ break;
54
+ }
55
+ }
56
+
57
+ return !isDuplicated;
58
+ } );
59
+
60
+ hitsGrouped.api = ( apiSearchHits || [] ).concat( hitsGrouped.api );
61
+
62
+ result = result.concat( hitsGrouped[ '{{{ groupSlug }}}' ] );
63
+
64
+ tags.forEach( function( tag ) {
65
+ if ( tag !== '{{{ groupSlug }}}' ) result = result.concat( hitsGrouped[ tag ].slice( 0, 4 ) );
66
+ } );
67
+
68
+ return result;
69
+ }
@@ -0,0 +1,21 @@
1
+ function rootScript( hits ) {
2
+ var sortingFn = function( a, b ) {
3
+ a.custom_ranking = a.custom_ranking || 0;
4
+ b.custom_ranking = b.custom_ranking || 0;
5
+
6
+ if ( a.custom_ranking === b.custom_ranking ) return a.index - b.index;
7
+ if ( a.custom_ranking > b.custom_ranking ) return -1;
8
+ if ( a.custom_ranking < b.custom_ranking ) return 1;
9
+ };
10
+
11
+ return hits
12
+ .filter( function( h ) {
13
+ return !h.tags.includes( 'api' );
14
+ } )
15
+ .slice( 0, 10 )
16
+ .map( function( item, idx ) {
17
+ item.index = idx;
18
+ return item;
19
+ } )
20
+ .sort( sortingFn );
21
+ }
@@ -35,3 +35,8 @@ block content
35
35
 
36
36
  block navtree
37
37
  != navTree
38
+
39
+ block projectVersion
40
+ if projectLocals
41
+ meta( name= 'project-version' content= projectLocals.projectVersion )
42
+ meta( name= 'project-slug' content= projectLocals.projectSlug )
@@ -2,7 +2,7 @@ mixin navTreeItem( data )
2
2
  - const _class = page.path === data.path ? 'tree__item__wrapper tree__item__wrapper--active' : 'tree__item__wrapper';
3
3
  - const menuTitle = data[ 'menu-title' ] || uSplitToTitleAndContent( data.content ).title || data.title;
4
4
  li
5
- a( href=relative_url( page.path, data.path ) )
5
+ a( href=relative_url( page.path, data.path ) title=menuTitle )
6
6
  div( class=_class )
7
7
  span( class="tree__item--guide tree__item__text" ) #{ menuTitle }
8
8
  - if( data.shouldDisplayNewIndicator )
@@ -13,7 +13,9 @@ if ( docSearchConfig && docSearchConfig.isEnabled && !disableSearch && (!project
13
13
 
14
14
  script( src=config.js )
15
15
 
16
- if ( !page.groupSlug || !config.scripts[ page.groupSlug ] )
16
+ if ( page.path === 'index.html' )
17
+ != config.scripts.rootScript
18
+ else if ( !page.groupSlug || !config.scripts[ page.groupSlug ] )
17
19
  != config.scripts.defaultScript
18
20
  else
19
21
  != config.scripts[ page.groupSlug ]
@@ -1,25 +1,19 @@
1
1
  if googleanalytics && !googletagmanager
2
2
  script.
3
3
  if ( !{ JSON.stringify( googleanalytics.domains ) }.indexOf( location.hostname ) !== -1 ) {
4
- function pageView() {
5
- ga( 'send', 'pageview', location.pathname + location.search + location.hash );
6
- }
7
-
8
- (function( i, s, o, g, r, a, m ) {
9
- i[ 'GoogleAnalyticsObject' ] = r;
10
- i[ r ] = i[ r ] || function() {
11
- (i[ r ].q = i[ r ].q || []).push( arguments )
12
- }, i[ r ].l = 1 * new Date();
13
- a = s.createElement( o ),
14
- m = s.getElementsByTagName( o )[ 0 ];
4
+ ( function ( w, d, e, u, a, b ) {
5
+ a = d.createElement( e );
6
+ b = d.getElementsByTagName( e )[ 0 ];
15
7
  a.async = 1;
16
- a.src = g;
17
- m.parentNode.insertBefore( a, m )
18
- })( window, document, 'script', 'https://www.google-analytics.com/analytics.js', 'ga' );
8
+ a.src = u;
9
+ b.parentNode.insertBefore( a, b )
10
+ } )( window, document, 'script', 'https://www.googletagmanager.com/gtag/js?id=!{ googleanalytics.config.trackingId }' );
11
+ window.dataLayer = window.dataLayer || [];
19
12
 
20
- ga( 'create', !{ JSON.stringify( googleanalytics.config ) } );
21
-
22
- pageView();
13
+ function gtag() {
14
+ dataLayer.push( arguments );
15
+ }
23
16
 
24
- window.addEventListener( 'hashchange', pageView );
17
+ gtag( 'js', new Date() );
18
+ gtag( 'config', '!{ googleanalytics.config.trackingId }' );
25
19
  }
@@ -2,7 +2,7 @@ meta( charset='utf-8' )
2
2
  meta( name='viewport' content='width=device-width, initial-scale=1' )
3
3
 
4
4
  - const titleSuffix = page.groupId === 'api-reference' ? page.projectfullname + ' API docs' : page.projectfullname + ' Documentation';
5
- - const title = page[ 'meta-title' ] ? page[ 'meta-title' ] : page[ 'meta-title-short' ] ? page[ 'meta-title-short' ] + ' - ' + titleSuffix : page.title + ' - ' + titleSuffix;
5
+ - const title = page[ 'meta-title' ] ? page[ 'meta-title' ] : page[ 'meta-title-short' ] ? page[ 'meta-title-short' ] + ' | ' + titleSuffix : page.title + ' | ' + titleSuffix;
6
6
  title= title
7
7
 
8
8
  //- Used for meta tag and for og:*
@@ -18,6 +18,9 @@
18
18
  // To keep vertical align with paragraph.
19
19
  top: 0.2em;
20
20
 
21
+ // To keep "See source" link clickable.
22
+ z-index: 1;
23
+
21
24
  // If "See source" exists, we need to add spacing to first paragraph to prevent overlapping text.
22
25
  + .main-description p:first-child{
23
26
  padding-right: 100px;
@@ -68,7 +68,7 @@ nav.secondary-navigation {
68
68
 
69
69
  &.secondary-navigation__current-position {
70
70
  color: u-color( 'link' );
71
- font-weight: bold;
71
+ -webkit-text-stroke-width: thin;
72
72
 
73
73
  &::before {
74
74
  opacity: 1;