panelset 0.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 (39) hide show
  1. package/dist/index.d.ts +111 -0
  2. package/dist/panelset.css +1 -0
  3. package/dist/panelset.js +317 -0
  4. package/dist/panelset.js.map +1 -0
  5. package/package.json +49 -0
  6. package/src/docs/assets/scripts/copybutton.js +44 -0
  7. package/src/docs/assets/scripts/example-async.js +161 -0
  8. package/src/docs/assets/scripts/example-closable.js +27 -0
  9. package/src/docs/assets/scripts/example-megamenu.js +84 -0
  10. package/src/docs/assets/scripts/example.js +29 -0
  11. package/src/docs/assets/scripts/main.js +7 -0
  12. package/src/docs/assets/styles/_base.scss +13 -0
  13. package/src/docs/assets/styles/_code.scss +121 -0
  14. package/src/docs/assets/styles/_demos.scss +180 -0
  15. package/src/docs/assets/styles/_landingpage.scss +41 -0
  16. package/src/docs/assets/styles/_layout.scss +80 -0
  17. package/src/docs/assets/styles/_sidebar.scss +67 -0
  18. package/src/docs/assets/styles/_typography.scss +116 -0
  19. package/src/docs/assets/styles/_variables.scss +32 -0
  20. package/src/docs/assets/styles/docs.scss +64 -0
  21. package/src/docs/views/api-reference.pug +474 -0
  22. package/src/docs/views/configuration.pug +173 -0
  23. package/src/docs/views/events.pug +222 -0
  24. package/src/docs/views/examples/async.pug +268 -0
  25. package/src/docs/views/examples/basic.pug +155 -0
  26. package/src/docs/views/examples/closable.pug +97 -0
  27. package/src/docs/views/getting-started.pug +99 -0
  28. package/src/docs/views/index.pug +38 -0
  29. package/src/docs/views/templates/includes/_head.pug +11 -0
  30. package/src/docs/views/templates/includes/_mixins.pug +100 -0
  31. package/src/docs/views/templates/includes/_scripts.pug +14 -0
  32. package/src/docs/views/templates/includes/_sidebar.pug +18 -0
  33. package/src/docs/views/templates/layouts/_base.pug +36 -0
  34. package/src/docs/views/transitions.pug +141 -0
  35. package/src/lib/index.ts +685 -0
  36. package/src/lib/styles/_base.scss +99 -0
  37. package/src/lib/styles/_loading.scss +47 -0
  38. package/src/lib/styles/_variables.scss +19 -0
  39. package/src/lib/styles/panelset.scss +3 -0
@@ -0,0 +1,161 @@
1
+ function renderRecipe(data) {
2
+ return `
3
+ <h2>${data.name}</h2>
4
+ <div class="recipe-meta">
5
+ <span>⏱️ ${data.prepTimeMinutes + data.cookTimeMinutes} mins</span>
6
+ <span>👥 ${data.servings} servings</span>
7
+ <span>🔥 ${data.caloriesPerServing} cal</span>
8
+ <span>⭐ ${data.rating} (${data.reviewCount} reviews)</span>
9
+ </div>
10
+ <div class="recipe">
11
+ <img class="recipe-image" src="${data.image}" alt="${data.name}">
12
+ <div class="recipe-ingredients">
13
+ <h3>Ingredients</h3>
14
+ <ul>
15
+ ${data.ingredients.map(ing => `<li>${ing}</li>`).join('')}
16
+ </ul>
17
+ </div>
18
+ </div>
19
+ <div class="recipe-instructions">
20
+ <h3>Instructions</h3>
21
+ <ol>
22
+ ${data.instructions.map(step => `<li>${step}</li>`).join('')}
23
+ </ol>
24
+ </div>
25
+ `;
26
+ }
27
+
28
+ document.addEventListener('click', e => {
29
+ const button = e.target.closest('button[data-panel]');
30
+ if (!button) return;
31
+
32
+ const panelId = button.dataset.panel;
33
+ const container = document.getElementById(panelId)?.closest('[data-panelset]');
34
+
35
+ container?.panelSet?.show(panelId, true, { trigger: button });
36
+ });
37
+
38
+ function slowfetch(url, options = {}, delayMs = 1500) {
39
+ return new Promise((resolve, reject) => {
40
+ setTimeout(() => {
41
+ fetch(url, options)
42
+ .then(resolve)
43
+ .catch(reject);
44
+ }, delayMs);
45
+ });
46
+ }
47
+
48
+ function randomIntFromInterval(min, max) {
49
+ return Math.floor(Math.random() * (max - min + 1) + min);
50
+ }
51
+
52
+ const asyncDemo = document.getElementById('async-demo');
53
+ const asyncPanelSet = asyncDemo.panelSet;
54
+
55
+ // NORMAL EASY WAY
56
+
57
+ asyncPanelSet.onBeforeActivate((targetPanel, signal) => {
58
+ if (targetPanel.id === 'async-panel-2') {
59
+ return slowfetch(`https://dummyjson.com/recipes/${randomIntFromInterval(1, 10)}`, { signal })
60
+ .then(response => response.json())
61
+ .then(data => {
62
+ targetPanel.innerHTML = renderRecipe(data);
63
+ });
64
+ }
65
+ }, { once: false });
66
+
67
+
68
+
69
+
70
+
71
+ // EVENT WAY:
72
+ // THIS IS COMMENTED OUT, BUT LEFT HERE FOR REFERENCE
73
+
74
+ // slowDemo.addEventListener('ps:beforeactivate', (e) => {
75
+ // const { panelId, targetPanel, signal } = e.detail;
76
+
77
+ // // Skip if already loaded
78
+ // if (targetPanel.dataset.loaded === 'true') {
79
+ // console.log(`Panel ${panelId} already loaded`);
80
+ // return;
81
+ // }
82
+
83
+ // if (targetPanel.id === 'async-panel-2') {
84
+ // // Just assign the fetch promise directly - no wrapping needed
85
+ // e.detail.promise = slowfetch(`https://dummyjson.com/recipes/${randomIntFromInterval(1, 10)}`, { signal })
86
+ // .then(response => response.json())
87
+ // .then(data => {
88
+ // targetPanel.innerHTML = renderRecipe(data);
89
+ // targetPanel.dataset.loaded = 'true';
90
+ // })
91
+ // .catch(error => {
92
+ // if (error.name === 'AbortError') {
93
+ // console.log(`Load cancelled for ${panelId}`);
94
+ // } else {
95
+ // console.error(`Load failed for ${panelId}:`, error);
96
+ // }
97
+ // // Re-throw so PanelSet can handle it
98
+ // throw error;
99
+ // });
100
+ // }
101
+ // });
102
+
103
+
104
+
105
+ // document.addEventListener('ps:beforeactivate', (e) => {
106
+ // const { instance, container, panelId, targetPanel, signal } = e.detail;
107
+
108
+ // if (panelId == 'async-panel-2') {
109
+
110
+ // // Skip if already loaded
111
+ // if (targetPanel.dataset.loaded === 'true') {
112
+ // console.log(`Panel ${panelId} already loaded`);
113
+ // return;
114
+ // }
115
+ // console.log(`Loading content for ${panelId}...`);
116
+ // // Simulate slow API call
117
+ // e.detail.promise = new Promise((resolve, reject) => {
118
+ // const timeout = setTimeout(() => {
119
+ // // Simulate loaded content
120
+ // targetPanel.innerHTML = `
121
+ // <h3>Loaded: ${panelId}</h3>
122
+ // <p>This content was loaded asynchronously!</p>
123
+ // <p>Loaded at: ${new Date().toLocaleTimeString()}</p>
124
+ // `;
125
+ // targetPanel.dataset.loaded = 'true';
126
+ // resolve();
127
+ // }, 2000); // 2 second delay
128
+
129
+ // // Handle abort (rapid clicks)
130
+ // signal.addEventListener('abort', () => {
131
+ // console.log(`Load cancelled for ${panelId}`);
132
+ // clearTimeout(timeout);
133
+ // reject(new Error('Aborted'));
134
+ // });
135
+ // });
136
+
137
+ // }
138
+
139
+ // })
140
+
141
+ // Log all events
142
+
143
+ document.addEventListener('ps:ready', (e) => {
144
+ console.log('This panelset is ready:', e.detail.panelId);
145
+ });
146
+
147
+ document.addEventListener('ps:beforeactivate', (e) => {
148
+ console.log('Before doing any transitions:', e.detail.panelId);
149
+ });
150
+
151
+ document.addEventListener('ps:activationstart', (e) => {
152
+ console.log('Started a transition:', e.detail.panelId);
153
+ });
154
+
155
+ document.addEventListener('ps:activationcomplete', (e) => {
156
+ console.log('Completed a transition:', e.detail.panelId);
157
+ });
158
+
159
+ document.addEventListener('ps:activationaborted', (e) => {
160
+ console.log('Aborted the loading of a panel:', e.detail.panelId);
161
+ });
@@ -0,0 +1,27 @@
1
+ document.addEventListener('click', e => {
2
+ const button = e.target.closest('button[data-panel]');
3
+ if (!button) return;
4
+ const panelId = button.dataset.panel;
5
+ const container = document.getElementById(panelId)?.closest('[data-panelset]');
6
+
7
+ const panelSet = container?.panelSet;
8
+ panelSet.show(panelId);
9
+ });
10
+
11
+ document.addEventListener('click', e => {
12
+ const actions = ['close', 'open', 'toggle'];
13
+
14
+ for (const action of actions) {
15
+ const btn = e.target.closest(`[data-panelset-${action}]`);
16
+ if (btn) {
17
+ const selector = btn.getAttribute(`data-panelset-${action}`);
18
+ const container = document.querySelector(selector);
19
+
20
+ if (container?.panelSet) {
21
+ const withTransition = !btn.hasAttribute('data-no-transition');
22
+ container.panelSet[action](withTransition);
23
+ }
24
+ return;
25
+ }
26
+ }
27
+ });
@@ -0,0 +1,84 @@
1
+
2
+ // Delegated button handler
3
+ // Delegated button handler with megamenu support
4
+ document.addEventListener('click', e => {
5
+ const button = e.target.closest('button[data-panel]');
6
+ if (!button) return;
7
+
8
+ const panelId = button.getAttribute('data-panel');
9
+ const panel = document.getElementById(panelId);
10
+ const container = panel?.closest('[data-panelset]');
11
+
12
+ if (!container?.panelSet) return;
13
+
14
+ const panelSet = container.panelSet;
15
+ const withTransition = !button.hasAttribute('data-no-transition');
16
+
17
+ // Megamenu behavior for closable panelsets
18
+ if (container.hasAttribute('data-closable')) {
19
+ const isClosed = container.classList.contains('is-closed');
20
+ const isClosing = container.classList.contains('is-closing');
21
+ const isOpening = container.classList.contains('is-opening');
22
+ const pendingPanelId = panelSet.pendingPanel?.id;
23
+
24
+ if (isClosed || isClosing) {
25
+ // Closed/closing → open to clicked panel
26
+ panelSet.show(panelId, withTransition, { trigger: button });
27
+ panelSet.open(withTransition);
28
+ } else if (pendingPanelId === panelId) {
29
+ // Same panel clicked
30
+ if (isOpening) {
31
+ // Opening → reverse to close
32
+ panelSet.close(withTransition);
33
+ } else if (!isClosing) {
34
+ // Fully open, not closing → close it
35
+ panelSet.close(withTransition);
36
+ }
37
+ // If closing → do nothing (let it finish)
38
+ } else {
39
+ // Different panel → switch
40
+ panelSet.show(panelId, withTransition, { trigger: button });
41
+ }
42
+ } else {
43
+ // Non-closable → just switch panels
44
+ panelSet.show(panelId, withTransition, { trigger: button });
45
+ }
46
+ });
47
+
48
+ document.addEventListener('click', e => {
49
+ // Close action
50
+ const closeBtn = e.target.closest('[data-panelset-close]');
51
+ alert('clicked close');
52
+ if (closeBtn) {
53
+ const selector = closeBtn.getAttribute('data-panelset-close');
54
+ const container = selector ? document.querySelector(selector) : closeBtn.closest('[data-panelset]');
55
+ if (container?.panelSet) {
56
+ const withTransition = !closeBtn.hasAttribute('data-no-transition');
57
+ container.panelSet.close(withTransition);
58
+ }
59
+ return;
60
+ }
61
+
62
+ // Open action
63
+ const openBtn = e.target.closest('[data-panelset-open]');
64
+ if (openBtn) {
65
+ const selector = openBtn.getAttribute('data-panelset-open');
66
+ const container = selector ? document.querySelector(selector) : openBtn.closest('[data-panelset]');
67
+ if (container?.panelSet) {
68
+ const withTransition = !openBtn.hasAttribute('data-no-transition');
69
+ container.panelSet.open(withTransition);
70
+ }
71
+ return;
72
+ }
73
+
74
+ // Toggle action
75
+ const toggleBtn = e.target.closest('[data-panelset-toggle]');
76
+ if (toggleBtn) {
77
+ const selector = toggleBtn.getAttribute('data-panelset-toggle');
78
+ const container = selector ? document.querySelector(selector) : toggleBtn.closest('[data-panelset]');
79
+ if (container?.panelSet) {
80
+ const withTransition = !toggleBtn.hasAttribute('data-no-transition');
81
+ container.panelSet.toggle(withTransition);
82
+ }
83
+ }
84
+ });
@@ -0,0 +1,29 @@
1
+ document.addEventListener('click', e => {
2
+ const button = e.target.closest('button[data-panel]');
3
+ if (!button) return;
4
+ const panelId = button.dataset.panel;
5
+ const container = document.getElementById(panelId)?.closest('[data-panelset]');
6
+ container?.panelSet?.show(panelId, true, { trigger: button });
7
+ });
8
+
9
+ // Log all events
10
+
11
+ document.addEventListener('ps:ready', (e) => {
12
+ console.log('This panelset is ready:', e.detail.panelId);
13
+ });
14
+
15
+ document.addEventListener('ps:beforeactivate', (e) => {
16
+ console.log('Before doing any transitions:', e.detail.panelId);
17
+ });
18
+
19
+ document.addEventListener('ps:activationstart', (e) => {
20
+ console.log('Started a transition:', e.detail.panelId);
21
+ });
22
+
23
+ document.addEventListener('ps:activationcomplete', (e) => {
24
+ console.log('Completed a transition:', e.detail.panelId);
25
+ });
26
+
27
+ document.addEventListener('ps:activationaborted', (e) => {
28
+ console.log('Aborted the loading of a panel:', e.detail.panelId);
29
+ });
@@ -0,0 +1,7 @@
1
+ document.addEventListener('click', e => {
2
+ const button = e.target.closest('button[data-panel]');
3
+ if (!button) return;
4
+ const panelId = button.dataset.panel;
5
+ const container = document.getElementById(panelId)?.closest('[data-panelset]');
6
+ container?.panelSet?.show(panelId, true, { trigger: button });
7
+ });
@@ -0,0 +1,13 @@
1
+
2
+ // Reset & Base
3
+ * {
4
+ margin: 0;
5
+ padding: 0;
6
+ box-sizing: border-box;
7
+ }
8
+
9
+ body {
10
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
11
+ color: var(--doc-gray-800);
12
+ background: #fff;
13
+ }
@@ -0,0 +1,121 @@
1
+
2
+ code, .code {
3
+ background: var(--doc-gray-100);
4
+ padding: 0.2em 0.4em;
5
+ border-radius: 3px;
6
+ font-size: 1em;
7
+ display: inline-block;
8
+ }
9
+
10
+ p code, li code , dl code, dt.code {
11
+ background: var(--doc-code-transparent);
12
+ padding: 0em 0.3em;
13
+ font-size: 0.9em ;
14
+ }
15
+
16
+ pre {
17
+ background: var(--doc-gray-50);
18
+ border: 1px solid var(--doc-gray-200);
19
+ border-radius: 6px;
20
+ padding: 1rem;
21
+ overflow-x: auto;
22
+ margin-bottom: 1rem;
23
+
24
+ code {
25
+ background: none;
26
+ padding: 0;
27
+ }
28
+ }
29
+
30
+ dl.horizontal {
31
+ display: grid;
32
+ grid-template-columns: max-content auto;
33
+ gap: 1rem;
34
+ }
35
+
36
+ .code-block {
37
+ position: relative;
38
+
39
+ .code-header {
40
+ background: var(--doc-gray-100);
41
+ padding: 0.5rem 0.5rem 0.5rem 1rem;
42
+ border: 1px solid var(--doc-gray-200);
43
+ border-bottom: none;
44
+ border-radius: 6px 6px 0 0;
45
+ font-size: 0.875rem;
46
+ font-weight: 600;
47
+ color: var(--doc-gray-600);
48
+ display: flex;
49
+ justify-content: space-between;
50
+ align-items: center;
51
+ }
52
+
53
+ pre {
54
+ margin: 0;
55
+ border-radius: 0 0 6px 6px;
56
+ }
57
+
58
+ .lane:nth-child(even) & pre {
59
+ background: white;
60
+ }
61
+ .lane:nth-child(even) .example & pre {
62
+ background: var(--doc-gray-50);
63
+ }
64
+
65
+ }
66
+
67
+ // HIGHLIGHTING CODE
68
+
69
+ .hljs-ln td.hljs-ln-numbers {
70
+ -webkit-touch-callout: none;
71
+ -webkit-user-select: none;
72
+ -khtml-user-select: none;
73
+ -moz-user-select: none;
74
+ -ms-user-select: none;
75
+ user-select: none;
76
+ text-align: right;
77
+ color: #ccc;
78
+ padding-right: 1.5rem;
79
+ }
80
+ .hljs-ln-code {
81
+ padding-left: 10px;
82
+ }
83
+
84
+ pre code.hljs {
85
+ padding: 0;
86
+ background: none;
87
+ }
88
+
89
+ .hljs-attribute,
90
+ .hljs-doctag,
91
+ .hljs-keyword,
92
+ .hljs-meta .hljs-keyword,
93
+ .hljs-name,
94
+ .hljs-selector-tag,
95
+ .hljs-section,
96
+ .hljs-title {
97
+ font-weight: 600;
98
+ }
99
+
100
+ button.copy {
101
+ background: var(--doc-gray-200);
102
+ border: none;
103
+ border-radius: 3px;
104
+ padding: 0.2rem 0.5rem;
105
+ font-size: 1rem;
106
+ font-weight: 400;
107
+ color: var(--doc-gray-600);
108
+ cursor: pointer;
109
+
110
+ transition: background 0.2s, color 0.2s;
111
+ outline-width: 2px;
112
+
113
+ &:hover {
114
+ background: white;
115
+ color: var(--doc-primary-dark);
116
+ }
117
+ &.copied {
118
+ background: var(--doc-primary);
119
+ color: white;
120
+ }
121
+ }
@@ -0,0 +1,180 @@
1
+ // Demo block
2
+
3
+ .example {
4
+ position: relative;
5
+ display: flex;
6
+ flex-direction: column;
7
+ padding: var(--standard-padding);
8
+ border: 2px solid var(--doc-gray-200);
9
+ border-radius: 16px;
10
+ gap: 1rem;
11
+ background: white;
12
+
13
+ h2 {
14
+ margin: 0;
15
+ font-size: 1.5rem;
16
+
17
+ }
18
+
19
+ @media (max-width: 768px) {
20
+ padding: 1rem;
21
+ }
22
+
23
+ &.white {
24
+ background: white;
25
+ }
26
+ .code-block + * {
27
+ margin-top: 1rem;
28
+ }
29
+ }
30
+
31
+ .api {
32
+ gap: 0;
33
+ h3.code {
34
+ font-size: 1.2rem;
35
+ color: var(--doc-primary-dark);
36
+ margin-bottom: 0.5rem;
37
+ }
38
+ h4 {
39
+ font-size: 1.1rem;
40
+ margin-bottom: 0.25rem;
41
+ margin-top: 1rem;
42
+ }
43
+ dt {
44
+ font-weight: 600;
45
+ margin-top: 0.5rem;
46
+ font-size: 0.9em;
47
+ }
48
+ }
49
+
50
+ .card-row {
51
+ display: flex;
52
+ gap: 1.5rem;
53
+ flex-wrap: wrap;
54
+ }
55
+
56
+ .card {
57
+ flex: 1;
58
+ min-width: 200px;
59
+ border: 1px solid var(--doc-gray-200);
60
+ border-radius: 8px;
61
+ padding: 1rem;
62
+ background: var(--doc-gray-50);
63
+
64
+ h4 {
65
+ margin-top: 0;
66
+ margin-bottom: 0.5rem;
67
+ }
68
+ }
69
+
70
+ // Demo Styles
71
+ .demo {
72
+ background: var(--doc-gray-50);
73
+ border-radius: 8px;
74
+ padding: var(--standard-padding);
75
+ }
76
+
77
+ .demo-controls {
78
+ display: flex;
79
+ gap: 1rem;
80
+ margin-bottom: 1.5rem;
81
+ flex-wrap: wrap;
82
+ &.center {
83
+ justify-content: center;
84
+ }
85
+
86
+ button {
87
+ background: var(--doc-primary);
88
+ color: white;
89
+ border: none;
90
+ padding: 0.5rem 1rem;
91
+ border-radius: 24px;
92
+ font-size: 1rem;
93
+ cursor: pointer;
94
+ transition: background 0.2s;
95
+
96
+ &:hover {
97
+ background: var(--doc-primary-dark);
98
+ }
99
+
100
+ &:disabled {
101
+ opacity: 0.5;
102
+ cursor: not-allowed;
103
+ }
104
+ &.secondary {
105
+ background: var(--doc-secondary);
106
+ }
107
+ }
108
+ }
109
+
110
+ // SLOW DATA
111
+
112
+ .recipe {
113
+ display: flex;
114
+ gap: 24px;
115
+ align-items: center;
116
+
117
+ & > * {
118
+ flex: 1;
119
+ }
120
+ }
121
+
122
+ .recipe-meta {
123
+ display: flex;
124
+ gap: 16px;
125
+ margin: 16px 0;
126
+ padding: 12px;
127
+ background: white;
128
+ border-radius: 8px;
129
+ }
130
+
131
+ img.testimage {
132
+ display: block;
133
+ margin-top: 1em;
134
+ width: 100%;
135
+ }
136
+
137
+ img.recipe-image {
138
+ width: 100%;
139
+ max-width: 256px;
140
+ border-radius: 8px;
141
+ object-fit: cover;
142
+ margin: 0 auto;
143
+ }
144
+
145
+ .recipe-meta span {
146
+ font-size: 14px;
147
+ }
148
+
149
+ .recipe {
150
+ display: grid;
151
+ grid-template-columns: repeat(2, 1fr);
152
+ grid-template-rows: 1fr;
153
+ grid-column-gap: 2rem;
154
+ grid-row-gap: 0px;
155
+ margin-bottom: 2rem;
156
+ }
157
+
158
+ img.recipe-image {
159
+ grid-area: 1 / 1 / 2 / 2;
160
+ }
161
+ .recipe-ingredients {
162
+ grid-area: 1 / 2 / 2 / 3;
163
+ }
164
+
165
+ @media (max-width: 768px) {
166
+ .recipe {
167
+ display: flex;
168
+ flex-direction: column;
169
+
170
+ img {
171
+ width: 100%;
172
+ max-width: unset;
173
+ max-height: 256px;
174
+ }
175
+ }
176
+ .recipe-ingredients {
177
+ margin-top: 2rem;
178
+ width: 100%;
179
+ }
180
+ }
@@ -0,0 +1,41 @@
1
+ // Landing Page Styles
2
+
3
+ html.intro {
4
+ .docs-content {
5
+ // background: linear-gradient(135deg, var(--doc-primary) 0%, var(--doc-primary-dark) 100%);
6
+
7
+ // min-height: 100vh;
8
+ display: flex;
9
+ flex-direction: column;
10
+ padding: var(--standard-padding);
11
+ text-align: center;
12
+
13
+ .container {
14
+ width: 100%;
15
+ margin: auto auto;
16
+ gap: var(--standard-padding);
17
+ }
18
+ .example {
19
+ text-align: left;
20
+ border: none;
21
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
22
+ }
23
+
24
+ p.lead {
25
+ font-size: 1.25rem;
26
+ opacity: 0.9;
27
+ }
28
+
29
+ }
30
+ }
31
+
32
+ .badge {
33
+ display: inline-block;
34
+ background: var(--doc-gray-200);
35
+ padding: 0.25rem 0.75rem;
36
+ border-radius: 999px;
37
+ font-size: 1rem;
38
+ font-weight: 400;
39
+ margin-bottom: 1rem;
40
+ line-height: 1.5em;
41
+ }