on master help sign in
Merge pull request #133 from urbit/search-bar-release
e409c2fMorgan Sutherland
content/posts/2016-5-16-roadmap.md+1
@@ -3,6 +3,7 @@ title = "~2016.5.16 Urbit Roadmap 2016"
3date = 2016-05-16
3date = 2016-05-16
4author = "Curtis Yarvin"
4author = "Curtis Yarvin"
5description = "Our near-term goals for the Urbit community, documentation, maturity, utility and governance."
5description = "Our near-term goals for the Urbit community, documentation, maturity, utility and governance."
6+search = false
6[taxonomies]
7[taxonomies]
7posts = ["Updates"]
8posts = ["Updates"]
8+++
9+++
content/posts/_index.md+1
@@ -1,4 +1,5 @@
1+++
1+++
2render = false
2render = false
3sort_by = "date"
3sort_by = "date"
4+in_search_index = false
4+++
5+++
sass/_main.scss+138−6
@@ -42,6 +42,13 @@ iframe {
42 padding-bottom: (32rem / 4);
42 padding-bottom: (32rem / 4);
43}
43}
44
44
45+.pop-up {
46+ width: 100%;
47+ max-width: 600px;
48+ margin: (28rem / 4) auto;
49+ background: #fff;
50+}
51+
45.container--fixed {
52.container--fixed {
46 position: fixed;
53 position: fixed;
47 margin-top: -(24rem / 4);
54 margin-top: -(24rem / 4);
@@ -91,7 +98,19 @@ iframe {
91 height: 100%;
98 height: 100%;
92 z-index: 0;
99 z-index: 0;
93 .content-nav {
100 .content-nav {
101+ &__search-bar {
102+ width: 100%;
103+ padding-left: 0rem;
104+ }
105+ &__index {
106+ height: calc(100% - 48px);
94− overflow-y: scroll;
107+ overflow-y: scroll;
108+ &__item {
109+ &:last-child{
110+ padding-bottom: 8rem;
111+ }
112+ }
113+ }
95 .list:first-child {
114 .list:first-child {
96 padding-bottom: (14rem / 4);
115 padding-bottom: (14rem / 4);
97 }
116 }
@@ -229,6 +248,10 @@ iframe {
229 transform: translate(0, 7px);
248 transform: translate(0, 7px);
230 }
249 }
231 }
250 }
251+ &__search-icon {
252+ -webkit-transform: rotate(-45deg);
253+ vertical-align: bottom;
254+ }
232}
255}
233
256
234.has-active-main-nav {
257.has-active-main-nav {
@@ -278,6 +301,55 @@ iframe {
278 max-width: 320px;
301 max-width: 320px;
279}
302}
280
303
304+.padding-none {
305+ padding: 0 !important;
306+}
307+
308+.border-none {
309+ border: none !important;
310+}
311+
312+.b-transparent {
313+ background: rgba(0,0,0,0.4);
314+}
315+
316+.top0 {
317+ top: 0;
318+}
319+
320+.float-right {
321+ float: right;
322+}
323+
324+.search-overlay {
325+ .search-bar {
326+ &__input {
327+ padding-left: 40px;
328+ }
329+ }
330+}
331+
332+.search-results {
333+ max-height: 400px;
334+ &__item a:hover {
335+ background: #F9F9F9;
336+ span {
337+ display:inline-block;
338+ }
339+ }
340+}
341+
342+.has-active-search-window {
343+ .search-overlay {
344+ display: block;
345+ }
346+}
347+
348+#search-reset {
349+ cursor: pointer;
350+ user-select: none;
351+}
352+
281.form--big {
353.form--big {
282 max-width: 390px;
354 max-width: 390px;
283 .btn {
355 .btn {
@@ -286,21 +358,47 @@ iframe {
286}
358}
287
359
288.hoonschool-application {
360.hoonschool-application {
289− &__experience {
361+ &__experience {
290− resize: vertical;
362+ resize: vertical;
291− }
363+ }
292}
364}
293
365
294/*
366/*
295 * styling
367 * styling
296 */
368 */
297
369
370+// remove default x icon
371+ input[type="search"]::-webkit-search-decoration,
372+ input[type="search"]::-webkit-search-cancel-button,
373+ input[type="search"]::-webkit-search-results-button,
374+ input[type="search"]::-webkit-search-results-decoration {
375+ display: none;
376+}
377+
378+input[type="search"] {
379+ border: 1px solid #B1B2B3;
380+ font-size: (4rem / 4);
381+ line-height: (6rem / 4);
382+ transition: all 0.1s linear;
383+ padding-right: 40px;
384+ position: relative;
385+ border-radius: 0 !important;
386+}
387+
298−textarea, input[type="text"], input[type="email"], input[type="number"], select {
388+textarea, input[type="text"], input[type="email"], input[type="number"], input[type="search"], select {
299 border: 1px solid #000;
389 border: 1px solid #000;
300 padding: 12px;
390 padding: 12px;
301 width: 100%;
391 width: 100%;
302 outline: none;
392 outline: none;
303 box-sizing: border-box;
393 box-sizing: border-box;
394+ &:active, &:focus {
395+ border: 1px solid #000000;
396+ }
397+ &:required, &:invalid {
398+ box-shadow: none !important;
399+ border: 1px solid #EE892B;
400+ color: #EE892B;
401+ }
304}
402}
305
403
306textarea {
404textarea {
@@ -319,6 +417,38 @@ button, .btn {
319 cursor: pointer;
417 cursor: pointer;
320}
418}
321
419
420+.search-bar {
421+ border: 1px solid #B1B2B3;
422+ background: #fff;
423+ font-size: (4rem / 4);
424+ transition: all 0.1s linear;
425+ padding: 12px 12px 12px 40px;
426+ position: relative;
427+ font-weight: normal;
428+ text-align: left;
429+ box-shadow: none !important;
430+ color: #B1B2B3;
431+}
432+
433+.has-valid-input {
434+ position: relative;
435+ &:after {
436+ content: '✓';
437+ color: #2AA779;
438+ position: absolute;
439+ right: 10px;
440+ top: 50%;
441+ transform: translate(0,-50%);
442+ width: 15px;
443+ height: 24px;
444+ margin-top: -4px;
445+ }
446+ input[type="text"], input[type="email"], input[type="number"] {
447+ border: 1px solid #2AA779;
448+ }
449+}
450+
451+
322/*
452/*
323 * typography
453 * typography
324 */
454 */
@@ -493,9 +623,11 @@ summary {
493 user-select: none;
623 user-select: none;
494 -webkit-user-select: none;
624 -webkit-user-select: none;
495}
625}
626+
496summary:focus {
627summary:focus {
497 outline: none;
628 outline: none;
498}
629}
630+
499details {
631details {
500 padding-bottom: (1rem / 4);
632 padding-bottom: (1rem / 4);
501 padding-top: (1rem / 4);
633 padding-top: (1rem / 4);
@@ -503,4 +635,4 @@ details {
503 padding-left: 0;
635 padding-left: 0;
504 list-style: none;
636 list-style: none;
505 }
637 }
506−}
638+}
sass/_responsive.scss+14
@@ -4,6 +4,9 @@
4 }
4 }
5}
5}
6@media only screen and (max-width: 800px) {
6@media only screen and (max-width: 800px) {
7+ .has-active-search-window .search-overlay, .search-bar {
8+ display: none;
9+ }
7 .container--fixed, .content-nav {
10 .container--fixed, .content-nav {
8 margin: 0;
11 margin: 0;
9 }
12 }
@@ -69,6 +72,13 @@
69 display: block;
72 display: block;
70 margin: (7rem) / 4;
73 margin: (7rem) / 4;
71 overflow: initial;
74 overflow: initial;
75+ width: 100%;
76+ margin: 0;
77+ margin-top: (-7rem / 4);
78+ &__index {
79+ margin: (7rem /4) 0 0 (7rem / 4);
80+ padding: (7rem /4) (7rem / 4) 0 0;
81+ }
72 }
82 }
73 .content-nav-icon {
83 .content-nav-icon {
74 &:before {
84 &:before {
@@ -78,6 +88,10 @@
78 border-right: 10px solid transparent;
88 border-right: 10px solid transparent;
79 }
89 }
80 }
90 }
91+ .content, .content-spacer {
92+ flex-basis: auto;
93+ max-width: none;
94+ }
81 }
95 }
82}
96}
83
97
static/scripts.js+303−29
@@ -1,7 +1,4 @@
1−//
2− // toggling of navigation menus
1+// toggling of navigation menus
3−//
4−
5var toggleMainNav = document.getElementById('js-main-nav-toggle');
2var toggleMainNav = document.getElementById('js-main-nav-toggle');
6if (toggleMainNav !== null) {
3if (toggleMainNav !== null) {
7 toggleMainNav.onclick = function() {
4 toggleMainNav.onclick = function() {
@@ -16,34 +13,311 @@ if (toggleContentNav !== null) {
16 }
13 }
17}
14}
18
15
19−//
16+function initHotKeys() {
20− // styling of input fields
17+
21−//
18+ var bodyEl = document.body;
22−
19+ var searchInput = document.getElementById('search');
23−var inputElements = document.querySelectorAll('input.tlon-input');
20+
24−for (var i = 0; i < inputElements.length; i++) {
25− //listen for changes in input
26− inputElements[i].addEventListener('input', function() {
21+ document.addEventListener('keydown', function (event) {
27− // if input has a value do this
22+ if (event.defaultPrevented) {
28− if (this.value) {
23+ return;
29− var inputContainer = this.parentNode;
24+ }
30− var validInputs = inputContainer.querySelectorAll('input.tlon-input:valid');
25+
31− var inputs = inputContainer.querySelectorAll('input.tlon-input');
26+ var key = event.key || event.keyCode;
32− if (validInputs.length !== 0) {
27+
33− // has valid input
28+ // ESC to close search window
34− for (var i = 0; i < validInputs.length; i++) {
29+ if (key === 'Escape' || key === 'Esc' || key === 27) {
30+ document.body.classList.remove('has-active-search-window');
31+ console.log('esc');
32+ }
33+
34+ // / to close toggle window
35+ if (key === '/' || key === 'Period' || key === 58) {
36+ event.preventDefault();
37+ document.body.classList.toggle('has-active-search-window');
35− if (this.classList.contains('is-valid') == false ) {
38+ if (bodyEl.classList.contains('has-active-search-window')) {
36− validInputs[i].parentNode.classList.add('has-valid-input');
39+ searchInput.focus();
37− }
40+ }
41+ }
42+
43+ //when search is active TAB puts search input into focus
44+ if (bodyEl.classList.contains('has-active-search-window')) {
45+ if (key === 'Tab' || key === 'Tab' || key === 9) {
46+ searchInput.focus();
38 }
47 }
39− } else {
48+ }
40− // doesn't have valid input
49+ });
41− for (var i = 0; i < inputs.length; i++) {
50+};
42− inputs[i].parentNode.classList.remove('has-valid-input');
51+
52+
53+// search functionality
54+function debounce(func, wait) {
55+ var timeout;
56+
57+ return function () {
58+ var context = this;
59+ var args = arguments;
60+ clearTimeout(timeout);
61+
62+ timeout = setTimeout(function () {
63+ timeout = null;
64+ func.apply(context, args);
65+ }, wait);
66+ };
67+}
68+
69+// The strategy is as follows:
70+// First, assign a value to each word in the document:
71+// Words that correspond to search terms (stemmer aware): 40
72+// Normal words: 2
73+// First word in a sentence: 8
74+// Then use a sliding window with a constant number of words and count the
75+// sum of the values of the words within the window. Then use the window that got the
76+// maximum sum. If there are multiple maximas, then get the last one.
77+// Enclose the terms in <b>.
78+function makeTeaser(body, terms) {
79+ var TERM_WEIGHT = 40;
80+ var NORMAL_WORD_WEIGHT = 2;
81+ var FIRST_WORD_WEIGHT = 8;
82+ //var TEASER_MAX_WORDS = 17;
83+ var TEASER_MAX_CHARS = 63;
84+
85+ var stemmedTerms = terms.map(function (w) {
86+ return elasticlunr.stemmer(w.toLowerCase());
87+ });
88+
89+ var termFound = false;
90+ var index = 0;
91+ var weighted = []; // contains elements of ["word", weight, index_in_document]
92+
93+ // split in sentences, then words
94+ var sentences = body.toLowerCase().split(". ");
95+
96+ for (var i in sentences) {
97+ var words = sentences[i].split(" ");
98+ var value = FIRST_WORD_WEIGHT;
99+
100+ for (var j in words) {
101+ var word = words[j];
102+
103+ if (word.length > 0) {
104+ for (var k in stemmedTerms) {
105+ if (elasticlunr.stemmer(word).startsWith(stemmedTerms[k])) {
106+ value = TERM_WEIGHT;
107+ termFound = true;
108+ }
43 }
109 }
110+ weighted.push([word, value, index]);
111+ value = NORMAL_WORD_WEIGHT;
112+ }
113+
114+ index += word.length;
115+ index += 1; // ' ' or '.' if last word in sentence
116+ }
117+
118+ index += 1; // because we split at a two-char boundary '. '
119+ }
120+
121+ if (weighted.length === 0) {
122+ return body;
123+ }
124+
125+ var windowWeights = [];
126+ var windowSize = 0;
127+
128+ // loop over words (weighted) till max chars is reached adding a counter to windowSize every time a new word has passed
129+ var sumChars = 0;
130+ for (var i = 0; i < weighted.length; i++) {
131+ sumChars += weighted[i][0].length;
132+ if (sumChars <= TEASER_MAX_CHARS) {
133+ windowSize++;
134+ }
135+ }
136+
137+ // We add a window with all the weights first
138+ var curSum = 0;
139+ for (var i = 0; i < windowSize; i++) {
140+ curSum += weighted[i][1];
141+ }
142+ windowWeights.push(curSum);
143+
144+ for (var i = 0; i < weighted.length - windowSize; i++) {
145+ curSum -= weighted[i][1];
146+ curSum += weighted[i + windowSize][1];
147+ windowWeights.push(curSum);
148+ }
149+
150+ // If we didn't find the term, just pick the first window
151+ var maxSumIndex = 0;
152+ if (termFound) {
153+ var maxFound = 0;
154+ // backwards
155+ for (var i = windowWeights.length - 1; i >= 0; i--) {
156+ if (windowWeights[i] > maxFound) {
157+ maxFound = windowWeights[i];
158+ maxSumIndex = i;
44 }
159 }
45− } else {
46− this.parentNode.classList.remove('has-valid-input');
47 }
160 }
161+ }
162+
163+ var teaser = [];
164+ var startIndex = weighted[maxSumIndex][2];
165+ for (var i = maxSumIndex; i < maxSumIndex + windowSize; i++) {
166+ var word = weighted[i];
167+ if (startIndex < word[2]) {
168+ // missing text from index to start of `word`
169+ teaser.push(body.substring(startIndex, word[2]));
170+ startIndex = word[2];
171+ }
172+
173+ startIndex = word[2] + word[0].length;
174+ teaser.push(body.substring(word[2], startIndex));
175+
176+ }
177+ teaser.push("…");
178+ return teaser.join("");
179+}
180+
181+function formatSearchResultTitle(item) {
182+ var pathArray = item.ref.split( '/' );
183+
184+ // first directory is in 4th element of split path array
185+ var firstPartTitle = pathArray[3];
186+
187+ // last directory is in 2nd to last element of array
188+ var lastPartTitle = item.doc.title;
189+
190+ var fullTitle = firstPartTitle + '<span class="gray90 inline-block pl2">' + ' / ' + lastPartTitle + '</span>';
191+
192+ return fullTitle;
193+}
194+
195+function formatSearchResultItem(item, terms) {
196+ console.log(item);
197+ var li = document.createElement("li");
198+ var createA = document.createElement("a");
199+ li.appendChild(createA);
200+ var teaserTitle = formatSearchResultTitle(item);
201+ li.classList.add("search-results__item");
202+ li.classList.add("ph3");
203+ var hrefA = item.ref;
204+ createA.setAttribute('href', hrefA);
205+ createA.setAttribute('class','no-underline block pl6 pv3');
206+ createA.innerHTML = `<span class="fs45 capitalize">${teaserTitle}</span>`;
207+ createA.innerHTML += `<span class='pr7 fs5 none float-right'>→</span>`;
208+ createA.innerHTML += `<div class="fs35 truncate mr10">${makeTeaser(item.doc.body, terms)}</div>`;
209+ return li;
210+}
211+
212+function initSearch() {
213+ var searchInput = document.getElementById("search");
214+ var searchForm = document.getElementById("search-form");
215+ var inputReset = searchForm.nextElementSibling;
216+
217+ if (!searchInput) {
218+ return;
219+ }
220+
221+ var searchResults = document.querySelector(".search-results");
222+ var searchResultsHeader = document.querySelector(".search-results__header");
223+ var searchResultsItems = document.querySelector(".search-results__items");
224+ var MAX_ITEMS = 200;
225+ var options = {
226+ bool: "AND",
227+ expand: true, // turn on partial word search
228+ fields: {
229+ title: {boost: 2},
230+ body: {boost: 1},
231+ }
232+ };
233+ var currentTerm = "";
234+ var index = elasticlunr.Index.load(window.searchIndex);
235+ // toggle search window
236+ var toggleSearchWindow = document.getElementById('js-search-window-toggle');
237+ var bodyEl = document.body;
238+ var searchOverlay = document.getElementById('search-overlay');
239+ var searchWindow = document.getElementById('search-window');
240+ var searchActive = bodyEl.classList.contains('has-active-search-window');
241+
242+ if (toggleSearchWindow !== null) {
243+ toggleSearchWindow.onclick = function() {
244+ inputReset.style.display="none";
245+
246+ //activate search
247+ bodyEl.classList.toggle('has-active-search-window');
248+ //put input in focus when activating search
249+ searchInput.focus();
250+
251+ searchInput.value = "";
252+ searchResultsHeader.value = "";
253+ searchResults.style.display = searchInput.value.trim() === "" ? "none" : "block";
254+ }
255+ searchOverlay.addEventListener('click', function(e){
256+ if (!searchWindow.contains(e.target)){
257+ bodyEl.classList.toggle('has-active-search-window');
258+ }
259+ })
260+ }
261+
262+
263+ searchInput.addEventListener("keyup", debounce(function() {
264+ inputReset.style.display="block";
265+
266+ searchResultsHeader.value = "";
267+ var term = searchInput.value.trim();
268+ /*
269+ //removed to fix: a state where there’s a valid search term, but no results
270+
271+ if (term === currentTerm || !index) {
272+ return;
273+ }*/
274+
275+ searchResults.style.display = term === "" ? "none" : "block";
276+ searchResults.style.overflowY = term === "" ? "hidden" : "scroll";
277+ searchResultsItems.innerHTML = "";
278+ if (term === "") {
279+ inputReset.style.display="none";
280+ return;
281+ }
282+
283+ var results = index.search(term, options).filter(function (r) {
284+ return r.doc.body !== "";
285+ });
286+
287+ if (results.length === 0) {
288+ searchResultsHeader.innerText = `No search results for '${term}'.`;
289+ return;
290+ }
291+
292+ currentTerm = term;
293+ searchResultsHeader.innerText = `${results.length} search results for '${term}':`;
294+ for (var i = 0; i < Math.min(results.length, MAX_ITEMS); i++) {
295+ if (!results[i].doc.body) {
296+ continue;
297+ }
298+ searchResultsItems.appendChild(formatSearchResultItem(results[i], term.split(" ")));
299+ }
300+ }, 150));
301+
302+ // reset input button
303+ inputReset.onclick=function(){
304+ document.getElementById('search-form').reset();
305+ }
306+ searchForm.addEventListener("reset", function(event){
307+ var e = document.createEvent('KeyboardEvent');
308+ e.initEvent("keyup", false, true);
309+ searchInput.dispatchEvent(e);
310+ });
311+
312+}
313+
314+if (document.readyState === "complete" ||
315+ (document.readyState !== "loading" && !document.documentElement.doScroll)
316+) {
317+ initToggleSearchWindow();
318+} else {
319+ document.addEventListener("DOMContentLoaded", function () {
320+ initSearch();
321+ initHotKeys();
48 });
322 });
49}
323}
templates/base.html+4
@@ -32,6 +32,10 @@
32 {% block content %}
32 {% block content %}
33 {% endblock content %}
33 {% endblock content %}
34 <!-- scripts -->
34 <!-- scripts -->
35+ {% if config.build_search_index %}
36+ <script type="text/javascript" src="{{ get_url(path="elasticlunr.min.js") | safe }}"></script>
37+ <script type="text/javascript" src="{{ get_url(path="search_index.en.js") | safe }}"></script>
38+ {% endif %}
35 <script src="{{ config.base_url }}/scripts.js"></script>
39 <script src="{{ config.base_url }}/scripts.js"></script>
36 </body>
40 </body>
37</html>
41</html>
templates/doc.html+2
@@ -3,6 +3,8 @@
3{% block content %}
3{% block content %}
4<!-- header -->
4<!-- header -->
5{% include "partials/navigation.html" %}
5{% include "partials/navigation.html" %}
6+<!-- search overlay (btn to activate located in navigations-docs partial) -->
7+{% include "partials/search-overlay.html" %}
6<!-- content -->
8<!-- content -->
7<div class="main-wrapper">
9<div class="main-wrapper">
8 <div class="wrapper">
10 <div class="wrapper">
templates/partials/navigation-docs.html+5−2
@@ -1,12 +1,15 @@
1{% block content %}
1{% block content %}
2<div id="js-content-nav-toggle" class="content-nav-icon"></div>
2<div id="js-content-nav-toggle" class="content-nav-icon"></div>
3<nav class="content-nav">
3<nav class="content-nav">
4+ <button id="js-search-window-toggle" type="button" class="search-bar content-nav__search-bar inline align-center" style="outline:none">
5+ <div class="inline-block"><span class="header__search-icon mh3 inline-block fs6">⚲</span></div><span class="gray50">Search</span>
6+ </button>
4− <ul class="list">
7+ <ul class="list content-nav__index">
5 {% block menu %}
8 {% block menu %}
6 {% set index = get_section(path="docs/_index.md") %}
9 {% set index = get_section(path="docs/_index.md") %}
7 {% for s in index.subsections %}
10 {% for s in index.subsections %}
8 {% set subsection = get_section(path=s) %}
11 {% set subsection = get_section(path=s) %}
9− <li class="list__item">
12+ <li class="list__item content-nav__index__item mt5">
10 <a class="text-600" href="{{ subsection.permalink }}">
13 <a class="text-600" href="{{ subsection.permalink }}">
11 {{ subsection.title }}
14 {{ subsection.title }}
12 </a>
15 </a>
templates/partials/search-overlay.html+7−5
@@ -1,12 +1,14 @@
1{% block content %}
1{% block content %}
2−<div id="search-overlay" class="search-overlay">
2+<div id="search-overlay" class="search-overlay fix vh100 w-full b-transparent z10 none top0">
3 <div class="wrapper">
3 <div class="wrapper">
4 <div id="search-window" class="pop-up">
4 <div id="search-window" class="pop-up">
5− <div class="search-bar">
5+ <div class="search-bar padding-none border-none z1 inline align-center w-full">
6+ <span class="header__search-icon ml3 abs z1 fs6 gray50">⚲</span>
6− <input id="search" autocomplete="off" type="search" class="search-bar__input" placeholder="Go to...">
7+ <form id="search-form" class="w-full"><input id="search" autocomplete="off" type="search" class="search-bar__input border-none" placeholder="Go to..."></form>
8+ <a id="search-reset" class="fs6 pr3 gray50 no-underline pointer:hover">&#57503;</a>
7 </div>
9 </div>
8− <div class="search-results">
10+ <div id="search-results" class="search-results bt1 b-gray40">
9− <div class="search-results__header mb2"></div>
11+ <div class="search-results__header ml9 mv2 gray60 fs3"></div>
10 <ul class="list search-results__items"></ul>
12 <ul class="list search-results__items"></ul>
11 </div>
13 </div>
12 </div>
14 </div>
templates/sections/docs.html+2
@@ -3,6 +3,8 @@
3{% block content %}
3{% block content %}
4<!-- header -->
4<!-- header -->
5{% include "partials/navigation.html" %}
5{% include "partials/navigation.html" %}
6+<!-- search overlay (btn to activate located in navigations-docs partial) -->
7+{% include "partials/search-overlay.html" %}
6<!-- content -->
8<!-- content -->
7<div class="main-wrapper">
9<div class="main-wrapper">
8 <div class="wrapper">
10 <div class="wrapper">
templates/sections/docs/chapters.html+2
@@ -3,6 +3,8 @@
3{% block content %}
3{% block content %}
4<!-- header -->
4<!-- header -->
5{% include "partials/navigation.html" %}
5{% include "partials/navigation.html" %}
6+<!-- search overlay (btn to activate located in navigations-docs partial) -->
7+{% include "partials/search-overlay.html" %}
6<!-- content -->
8<!-- content -->
7<div class="main-wrapper">
9<div class="main-wrapper">
8 <div class="wrapper">
10 <div class="wrapper">