@pushword/js-helper 0.0.123 → 0.0.127

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushword/js-helper",
3
- "version": "0.0.123",
3
+ "version": "0.0.127",
4
4
  "description": "Pushword front end helpers. ",
5
5
  "author": "Robin@PiedWeb <contact@piedweb.com>",
6
6
  "license": "MIT",
@@ -8,18 +8,20 @@
8
8
  "prettier": "prettier ./src/*.{js,scss} --write"
9
9
  },
10
10
  "dependencies": {
11
- "vite": "^7.1.12",
12
- "@tailwindcss/vite": "^4.1.16",
13
- "vite-plugin-static-copy": "^3.1.4",
14
- "vite-plugin-symfony": "^8.2.2",
15
- "@tailwindcss/typography": "^0.5",
16
- "tailwindcss-animated": "^2",
11
+ "140.css": "^1.0",
17
12
  "@tailwindcss/forms": "^0.5",
18
- "140.css": "^1.0.1",
13
+ "@tailwindcss/typography": "^0.5",
14
+ "@tailwindcss/vite": "^4.1",
15
+ "alpinejs": "^3.15.2",
19
16
  "codemirror": "^6.0",
20
17
  "core-js": "^3.38",
21
18
  "easymde": "^2.18",
22
- "glightbox": "^3.3"
19
+ "glightbox": "^3.3",
20
+ "tailwindcss-animated": "^2",
21
+ "vite": "^7.1.12",
22
+ "vite-plugin-compression2": "^2.3.1",
23
+ "vite-plugin-static-copy": "^3.1",
24
+ "vite-plugin-symfony": "^8.2"
23
25
  },
24
26
  "repository": {
25
27
  "type": "git",
@@ -31,6 +33,5 @@
31
33
  "bugs": {
32
34
  "url": "https://github.com/Pushword/Pushword/issues"
33
35
  },
34
- "homepage": "https://pushword.piedweb.com",
35
- "gitHead": "242ec54483dca1be93d7a62d8d554c9c08008c28"
36
+ "homepage": "https://pushword.piedweb.com"
36
37
  }
@@ -0,0 +1,324 @@
1
+ /**
2
+ * ShowMore - Expand/collapse content blocks
3
+ *
4
+ * Features:
5
+ * - [x] Open/close show-more blocks
6
+ * 1. Page loads → blocks are fully visible (no max-height - best for SEO)
7
+ * 2. On scroll → `addClassForNormalUser` adds `max-h-[250px]` via `data-acinb`, collapsing them
8
+ * Exception → blocks previously opened by user (in localStorage) stay open
9
+ * - [x] Auto-open when URL contains hash pointing inside block
10
+ * - [x] Auto-open when page loads with scroll position (browser back/refresh)
11
+ * - [x] Auto-open on hash change (SPA navigation)
12
+ * - [x] Ctrl+F: Auto-open when browser finds text in collapsed block
13
+ */
14
+
15
+ const STORAGE_KEY = 'showmore_opened'
16
+
17
+ const ShowMore = {
18
+ _initialized: false,
19
+ _scrollPos: 0,
20
+ _userClosed: new WeakSet(), // Track blocks manually closed by user
21
+ _openedIds: new Set(), // IDs of blocks user has ever opened
22
+
23
+ /**
24
+ * Load opened block IDs from localStorage
25
+ */
26
+ _loadOpenedIds() {
27
+ try {
28
+ const stored = localStorage.getItem(STORAGE_KEY)
29
+ if (stored) {
30
+ JSON.parse(stored).forEach((id) => this._openedIds.add(id))
31
+ }
32
+ } catch (e) {
33
+ // localStorage not available or corrupted
34
+ }
35
+ },
36
+
37
+ /**
38
+ * Save opened block ID to localStorage
39
+ */
40
+ _saveOpenedId(id) {
41
+ if (!id) return
42
+ this._openedIds.add(id)
43
+ this._persistOpenedIds()
44
+ },
45
+
46
+ /**
47
+ * Remove closed block ID from localStorage
48
+ */
49
+ _removeOpenedId(id) {
50
+ if (!id) return
51
+ this._openedIds.delete(id)
52
+ this._persistOpenedIds()
53
+ },
54
+
55
+ /**
56
+ * Persist opened IDs to localStorage
57
+ */
58
+ _persistOpenedIds() {
59
+ try {
60
+ localStorage.setItem(STORAGE_KEY, JSON.stringify([...this._openedIds]))
61
+ } catch (e) {
62
+ // localStorage not available
63
+ }
64
+ },
65
+
66
+ /**
67
+ * Check if user has ever opened this block
68
+ */
69
+ _hasUserOpened(wrapper) {
70
+ const input = wrapper.querySelector('input.show-hide-input')
71
+ return input && this._openedIds.has(input.id)
72
+ },
73
+
74
+ /**
75
+ * Open a show-more block
76
+ * @param {HTMLElement} el - Element inside the .show-more wrapper (typically .show-more-btn)
77
+ * @param {boolean} auto - Whether this is an automatic open (not user-initiated)
78
+ */
79
+ open(el, auto = false) {
80
+ const wrapper = el.closest('.show-more')
81
+ if (!wrapper) return
82
+
83
+ // Don't auto-open if user manually closed this block
84
+ if (auto && this._userClosed.has(wrapper)) return
85
+
86
+ const content = wrapper.children[1]
87
+ if (!content) return
88
+
89
+ this._scrollPos = wrapper.getBoundingClientRect().top + window.scrollY
90
+ content.classList.remove('overflow-hidden')
91
+ content.style.maxHeight = content.scrollHeight + 'px'
92
+ wrapper.dataset.showMoreOpen = 'true'
93
+
94
+ // Toggle checkbox to update arrow icon state
95
+ const checkbox = wrapper.querySelector('input.show-hide-input')
96
+ if (checkbox) {
97
+ checkbox.checked = true
98
+ // Save to localStorage when user manually opens (not auto)
99
+ if (!auto) {
100
+ this._saveOpenedId(checkbox.id)
101
+ }
102
+ }
103
+ },
104
+
105
+ /**
106
+ * Close a show-more block
107
+ * @param {HTMLElement} el - Element inside the .show-more wrapper
108
+ */
109
+ close(el) {
110
+ const wrapper = el.closest('.show-more')
111
+ if (!wrapper) return
112
+
113
+ const content = wrapper.children[1]
114
+ if (!content) return
115
+
116
+ // Mark as user-closed to prevent auto-reopening
117
+ this._userClosed.add(wrapper)
118
+
119
+ content.classList.add('overflow-hidden')
120
+ content.style.maxHeight = '250px'
121
+ delete wrapper.dataset.showMoreOpen
122
+
123
+ // Toggle checkbox to update arrow icon state
124
+ const checkbox = wrapper.querySelector('input.show-hide-input')
125
+ if (checkbox) {
126
+ checkbox.checked = false
127
+ // Remove from localStorage when user closes
128
+ this._removeOpenedId(checkbox.id)
129
+ }
130
+
131
+ if (this._scrollPos) {
132
+ window.scrollTo({ top: Math.max(0, this._scrollPos - 20), behavior: 'smooth' })
133
+ } else {
134
+ wrapper.scrollIntoView({ behavior: 'smooth', block: 'start' })
135
+ }
136
+ },
137
+
138
+ /**
139
+ * Check if block is open
140
+ * @param {HTMLElement} wrapper
141
+ * @returns {boolean}
142
+ */
143
+ isOpen(wrapper) {
144
+ return wrapper.dataset.showMoreOpen === 'true'
145
+ },
146
+
147
+ /**
148
+ * Open the show-more block containing the given element
149
+ * @param {HTMLElement} element - Element inside a show-more block
150
+ * @param {boolean} auto - Whether this is an automatic open
151
+ */
152
+ openContaining(element, auto = true) {
153
+ if (!element) return
154
+
155
+ const wrapper = element.closest('.show-more')
156
+ if (!wrapper || this.isOpen(wrapper)) return
157
+
158
+ // Use the wrapper itself as fallback if btn not found yet (hidden)
159
+ const btn = wrapper.querySelector('.show-more-btn') || wrapper
160
+ this.open(btn, auto)
161
+ },
162
+
163
+ /**
164
+ * Open show-more block containing the hash target and scroll to it
165
+ * @param {string} hash - The hash (with #) to navigate to
166
+ */
167
+ scrollToHash(hash) {
168
+ if (!hash) return
169
+
170
+ try {
171
+ const target = document.querySelector(hash)
172
+ if (target) {
173
+ this.openContaining(target)
174
+ setTimeout(() => {
175
+ target.scrollIntoView({ behavior: 'smooth' })
176
+ }, 100)
177
+ }
178
+ } catch (e) {
179
+ // Invalid selector, ignore
180
+ }
181
+ },
182
+
183
+ /**
184
+ * Check if a show-more block is currently collapsed
185
+ * @param {HTMLElement} wrapper - The .show-more wrapper element
186
+ * @returns {boolean}
187
+ */
188
+ isCollapsed(wrapper) {
189
+ const content = wrapper.children[1]
190
+ return content && content.classList.contains('overflow-hidden')
191
+ },
192
+
193
+ /**
194
+ * Open all visible collapsed show-more blocks
195
+ * Useful when page loads with scroll position
196
+ */
197
+ openVisibleBlocks() {
198
+ document.querySelectorAll('.show-more').forEach((wrapper) => {
199
+ if (!this.isCollapsed(wrapper)) return
200
+ if (this._userClosed.has(wrapper)) return
201
+ // Only auto-open if user has previously opened this block
202
+ if (!this._hasUserOpened(wrapper)) return
203
+
204
+ const rect = wrapper.getBoundingClientRect()
205
+ // Check if block intersects with viewport (with some margin)
206
+ const isVisible = rect.top < window.innerHeight + 100 && rect.bottom > -100
207
+
208
+ if (isVisible) {
209
+ this.openContaining(wrapper, true)
210
+ }
211
+ })
212
+ },
213
+
214
+ /**
215
+ * Initialize ShowMore functionality
216
+ * Safe to call multiple times - will only initialize once
217
+ */
218
+ init() {
219
+ if (this._initialized) return
220
+ this._initialized = true
221
+
222
+ // Load previously opened block IDs from localStorage
223
+ this._loadOpenedIds()
224
+
225
+ // Open if URL has hash pointing inside a show-more block
226
+ if (location.hash) {
227
+ this.scrollToHash(location.hash)
228
+ }
229
+
230
+ // Open if page loaded with scroll (e.g., browser back/refresh)
231
+ // Listen for scroll events to catch smooth scroll restoration
232
+ let scrollCheckCount = 0
233
+ const maxScrollChecks = 10
234
+ const checkScrollAndOpen = () => {
235
+ if (window.scrollY > 0) {
236
+ this.openVisibleBlocks()
237
+ }
238
+ }
239
+
240
+ // Immediate check
241
+ checkScrollAndOpen()
242
+
243
+ // Listen for scroll events (catches smooth scroll restoration)
244
+ const onScrollRestore = () => {
245
+ scrollCheckCount++
246
+ checkScrollAndOpen()
247
+ if (scrollCheckCount >= maxScrollChecks) {
248
+ window.removeEventListener('scroll', onScrollRestore)
249
+ }
250
+ }
251
+ window.addEventListener('scroll', onScrollRestore, { passive: true })
252
+
253
+ // Also use scrollend event if available (modern browsers)
254
+ if ('onscrollend' in window) {
255
+ window.addEventListener(
256
+ 'scrollend',
257
+ () => {
258
+ checkScrollAndOpen()
259
+ window.removeEventListener('scroll', onScrollRestore)
260
+ },
261
+ { once: true },
262
+ )
263
+ }
264
+
265
+ // Fallback: remove listener after 2 seconds
266
+ setTimeout(() => {
267
+ window.removeEventListener('scroll', onScrollRestore)
268
+ }, 2000)
269
+
270
+ // Handle hash changes (SPA navigation)
271
+ window.addEventListener('hashchange', () => {
272
+ if (location.hash) {
273
+ this.scrollToHash(location.hash)
274
+ }
275
+ })
276
+
277
+ // Handle clicks on anchor links pointing to content inside show-more
278
+ document.addEventListener('click', (e) => {
279
+ const link = e.target.closest('a[href^="#"]')
280
+ if (link) {
281
+ const hash = link.getAttribute('href')
282
+ if (hash && hash.length > 1) {
283
+ // Delay to let default navigation happen first
284
+ setTimeout(() => this.scrollToHash(hash), 10)
285
+ }
286
+ }
287
+ })
288
+
289
+ // Ctrl+F: open all collapsed blocks so browser can find text
290
+ document.addEventListener('keydown', (e) => {
291
+ if ((e.ctrlKey || e.metaKey) && e.key === 'f') {
292
+ this.openAllCollapsed()
293
+ }
294
+ })
295
+ },
296
+
297
+ /**
298
+ * Open all collapsed show-more blocks
299
+ * Useful for Ctrl+F to make all content searchable
300
+ */
301
+ openAllCollapsed() {
302
+ document.querySelectorAll('.show-more').forEach((wrapper) => {
303
+ if (this.isCollapsed(wrapper)) {
304
+ this.openContaining(wrapper, true)
305
+ }
306
+ })
307
+ },
308
+ }
309
+
310
+ // Auto-init when DOM is ready
311
+ export function initShowMore() {
312
+ if (document.readyState === 'loading') {
313
+ document.addEventListener('DOMContentLoaded', () => ShowMore.init())
314
+ } else {
315
+ ShowMore.init()
316
+ }
317
+ }
318
+
319
+ // Expose globally for inline onclick handlers in templates
320
+ if (typeof window !== 'undefined') {
321
+ window.ShowMore = ShowMore
322
+ }
323
+
324
+ export default ShowMore
package/src/alpine.js ADDED
@@ -0,0 +1,5 @@
1
+ import Alpine from 'alpinejs'
2
+
3
+ // Initialize Alpine.js
4
+ window.Alpine = Alpine
5
+ Alpine.start()
package/src/app.css CHANGED
@@ -1,10 +1,12 @@
1
1
  @import 'tailwindcss';
2
-
3
2
  @import 'tailwindcss-animated';
4
-
5
3
  @plugin '@tailwindcss/typography';
4
+ @import './tailwind.prose.scss';
5
+ @import './utility.css'; /* node_modules/@pushword/js-helper/src/utility.css */
6
6
  /* @plugin "@tailwindcss/forms"; */
7
7
 
8
+ @import './../node_modules/glightbox/dist/css/glightbox.css';
9
+
8
10
  /*
9
11
  Tailwind content sources for all packages
10
12
  -----------------------------------------
@@ -12,6 +14,8 @@ Tailwind content sources for all packages
12
14
 
13
15
  @source "./../../../../vendor/pushword/**/templates/";
14
16
  @source "./../../**/templates/";
17
+ @source "./../../**/src/templates/";
18
+ @source "./../../conversation/src/templates/";
15
19
  @source "./../../../../templates";
16
20
  @source "./../../../../var/TailwindGeneratorCache/*";
17
21
  @source "./../../../../vendor/pushword/core/src/Twig/**.php";
@@ -19,65 +23,6 @@ Tailwind content sources for all packages
19
23
  @source "./../../../../vendor/pushword/admin/src/templates/markdown_cheatsheet.html.twig";
20
24
  @source "./../../admin/src/templates/markdown_cheatsheet.html.twig";
21
25
 
22
- /* Tailwind v4 CSS-first configuration */
23
- @theme {
24
- /* Custom minHeight values */
25
- --min-height-screen-1-4: 25vh;
26
- --min-height-screen-3-4: 75vh;
27
- --min-height-screen-1-3: 33vh;
28
- --min-height-screen-2-3: 66vh;
29
- --min-height-screen-1-2: 50vh;
30
-
31
- /* Custom colors */
32
- --color-primary: var(--primary);
33
- --color-secondary: var(--secondary);
34
- }
35
-
36
- /* Custom variants for first-letter */
37
- @variant first-letter (&:first-letter);
38
-
39
- /* Custom variants for first-child */
40
- @variant first-child (&:first-child);
41
-
42
- /* Custom utilities - Bleed plugin replacement */
43
- @utility bleed {
44
- width: 100vw;
45
- margin-inline-start: 50%;
46
- margin-inline-end: unset;
47
- transform: translateX(-50%);
48
- max-width: none;
49
- }
50
-
51
- @utility bleed-disable {
52
- width: inherit;
53
- margin-inline-start: inherit;
54
- margin-inline-end: inherit;
55
- transform: default;
56
- }
57
-
58
- /* Custom utilities - Justify safe center */
59
- @utility justify-safe-center {
60
- justify-content: safe center;
61
- }
62
-
63
- /* Typography customization */
64
- .prose a:not(.not-prose a),
65
- .prose span[data-rot]:not(.not-prose span[data-rot]),
66
- .prose .link:not(.not-prose .link) {
67
- text-decoration: none;
68
- color: var(--primary);
69
- font-weight: 500;
70
- border-bottom: 1px solid;
71
- }
72
- .prose a:hover:not(.not-prose a:hover),
73
- .prose span[data-rot]:hover:not(.not-prose span[data-rot]:hover),
74
- .prose .link:hover:not(.not-prose .link:hover) {
75
- opacity: 0.75;
76
- }
77
-
78
- .img-stretched {
79
- @apply w-screen relative left-1/2 right-1/2 mr-[-50vw] ml-[-50vw];
80
- }
81
- .img-background {
82
- @apply px-6 sm:px-12 py-1 text-center bg-gray-200 rounded;
26
+ [x-cloak] {
27
+ display: none !important;
83
28
  }
package/src/app.js CHANGED
@@ -9,10 +9,14 @@ import {
9
9
  convertFormFromRot13,
10
10
  } from './helpers.js'
11
11
  import { allClickable } from './clickable.js'
12
+ import { initShowMore } from './ShowMore.js'
12
13
 
13
14
  //import { HorizontalScroll } from '@pushword/js-helper/src/horizontalScroll.js';
14
15
  //window.HorizontalScroll = HorizontalScroll;
15
16
 
17
+ // Initialize ShowMore (exposes window.ShowMore and sets up event listeners)
18
+ initShowMore()
19
+
16
20
  let lightbox
17
21
  function onDomChanged() {
18
22
  liveBlock()
@@ -29,8 +33,8 @@ function onDomChanged() {
29
33
  }
30
34
 
31
35
  function onPageLoaded() {
32
- onDomChanged()
33
36
  lightbox = new Glightbox()
37
+ onDomChanged()
34
38
  }
35
39
 
36
40
  document.addEventListener('DOMContentLoaded', onPageLoaded())
package/src/helpers.js CHANGED
@@ -223,18 +223,9 @@ export function addClassForNormalUser(attribute = 'data-acinb') {
223
223
  }
224
224
  },
225
225
  )
226
- if (window.location.hash) {
227
- const targetElement = document.querySelector(window.location.hash)
228
- if (targetElement) {
229
- // open show more block if exists
230
- const showMoreTarget = targetElement.closest('.show-more')
231
- if (showMoreTarget) {
232
- showMoreTarget.querySelector('.show-more-btn label').click()
233
- }
234
- targetElement.scrollIntoView({
235
- behavior: 'smooth',
236
- })
237
- }
226
+ // Handle hash navigation - delegate to ShowMore if available
227
+ if (window.location.hash && window.ShowMore) {
228
+ window.ShowMore.scrollToHash(window.location.hash)
238
229
  }
239
230
  }
240
231
  }
@@ -260,7 +251,12 @@ export async function uncloakLinks(
260
251
  var href = element.getAttribute(attribute)
261
252
  element.removeAttribute(attribute)
262
253
  for (var i = 0, n = element.attributes.length; i < n; i++) {
263
- link.setAttribute(element.attributes[i].nodeName, element.attributes[i].nodeValue)
254
+ const attr = element.attributes[i]
255
+ if (attr.nodeName.startsWith('@') || attr.nodeName.startsWith(':')) {
256
+ console.log("You can't use @alpine.js attribute on", element)
257
+ continue
258
+ }
259
+ link.setAttribute(attr.nodeName, attr.nodeValue)
264
260
  }
265
261
  link.innerHTML = element.innerHTML
266
262
  link.setAttribute('href', responsiveImage(convertShortchutForLink(rot13ToText(href))))
@@ -3,32 +3,32 @@
3
3
  */
4
4
  .text-color-inherit,
5
5
  .link-text {
6
- color: inherit !important;
6
+ color: inherit !important;
7
7
  }
8
8
 
9
9
  .no-decoration,
10
10
  .ninja {
11
- text-decoration: none !important;
12
- color: inherit !important;
13
- cursor: auto !important;
14
- border-bottom: 0 !important;
11
+ text-decoration: none !important;
12
+ color: inherit !important;
13
+ cursor: auto !important;
14
+ border-bottom: 0 !important;
15
15
  }
16
16
 
17
17
  .link-btn {
18
- padding: 6px 12px;
19
- margin-bottom: 0;
20
- border-radius: 3px;
21
- border: 1px solid transparent;
22
- cursor: pointer;
23
- color: #fff !important;
24
- background-color: var(--primary);
25
- border-color: var(--primary);
26
- outline: none;
27
- text-decoration: none !important;
18
+ padding: 6px 12px;
19
+ margin-bottom: 0;
20
+ border-radius: 3px;
21
+ border: 1px solid transparent;
22
+ cursor: pointer;
23
+ color: #fff !important;
24
+ background-color: var(--primary);
25
+ border-color: var(--primary);
26
+ outline: none;
27
+ text-decoration: none !important;
28
28
  }
29
29
 
30
30
  .link-btn:hover {
31
- color: #fff !important;
31
+ color: #fff !important;
32
32
  }
33
33
 
34
34
  /**
@@ -36,8 +36,8 @@
36
36
  */
37
37
 
38
38
  html {
39
- font-family: var(--font-family);
40
- scroll-behavior: smooth;
39
+ font-family: var(--font-family);
40
+ scroll-behavior: smooth;
41
41
  }
42
42
 
43
43
  /**
@@ -46,21 +46,21 @@ html {
46
46
 
47
47
  .show-hide-input:checked ~ ul,
48
48
  .show-hide-input:checked ~ nav {
49
- max-height: 100vh;
50
- padding-top: 1rem;
49
+ max-height: 100vh;
50
+ padding-top: 1rem;
51
51
  }
52
52
 
53
53
  /** Pure CSS accordion and show more
54
54
  Example https: //play.tailwindcss.com/VxdXWMH64M
55
55
 
56
56
  **/
57
- .show-hide-input:checked~div {
58
- max-height: 100%;
57
+ .show-hide-input:checked ~ div {
58
+ max-height: 100%;
59
59
  }
60
60
 
61
- .show-more>.show-hide-input:checked~div.show-more-btn {
62
- max-height: 0;
63
- overflow: hidden;
64
- padding-top: 0;
65
- margin-top: 0;
66
- }
61
+ .show-more > .show-hide-input:checked ~ div.show-more-btn {
62
+ max-height: 0;
63
+ overflow: hidden;
64
+ padding-top: 0;
65
+ margin-top: 0;
66
+ }
@@ -0,0 +1,62 @@
1
+ /* Tailwind v4 CSS-first configuration */
2
+ @theme {
3
+ /* Custom minHeight values */
4
+ --min-height-screen-1-4: 25vh;
5
+ --min-height-screen-3-4: 75vh;
6
+ --min-height-screen-1-3: 33vh;
7
+ --min-height-screen-2-3: 66vh;
8
+ --min-height-screen-1-2: 50vh;
9
+
10
+ /* Custom colors */
11
+ --color-primary: var(--primary);
12
+ --color-secondary: var(--secondary);
13
+ }
14
+
15
+ /* Custom variants for first-letter */
16
+ @variant first-letter (&:first-letter);
17
+
18
+ /* Custom variants for first-child */
19
+ @variant first-child (&:first-child);
20
+
21
+ /* Custom utilities - Bleed plugin replacement */
22
+ @utility bleed {
23
+ width: 100vw;
24
+ margin-inline-start: 50%;
25
+ margin-inline-end: unset;
26
+ transform: translateX(-50%);
27
+ max-width: none;
28
+ }
29
+
30
+ @utility bleed-disable {
31
+ width: inherit;
32
+ margin-inline-start: inherit;
33
+ margin-inline-end: inherit;
34
+ transform: default;
35
+ }
36
+
37
+ /* Custom utilities - Justify safe center */
38
+ @utility justify-safe-center {
39
+ justify-content: safe center;
40
+ }
41
+
42
+ /* Typography customization */
43
+ .prose a:not(.not-prose a),
44
+ .prose span[data-rot]:not(.not-prose span[data-rot]),
45
+ .prose .link:not(.not-prose .link) {
46
+ text-decoration: none;
47
+ color: var(--primary);
48
+ font-weight: 500;
49
+ border-bottom: 1px solid;
50
+ }
51
+ .prose a:hover:not(.not-prose a:hover),
52
+ .prose span[data-rot]:hover:not(.not-prose span[data-rot]:hover),
53
+ .prose .link:hover:not(.not-prose .link:hover) {
54
+ opacity: 0.75;
55
+ }
56
+
57
+ .img-stretched {
58
+ @apply w-screen relative left-1/2 right-1/2 mr-[-50vw] ml-[-50vw];
59
+ }
60
+ .img-background {
61
+ @apply px-6 sm:px-12 py-1 text-center bg-gray-200 rounded;
62
+ }