strata-css 1.0.4 → 1.2.6

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.
@@ -1,123 +0,0 @@
1
- /*!
2
- * Strata Modal Component
3
- *
4
- * Usage:
5
- * Trigger: <button data-st-toggle="modal" data-st-target="#myModal">Open</button>
6
- * Dismiss: <button data-st-dismiss="modal">Close</button>
7
- * Static: <div class="modal" data-st-backdrop="static" ...>
8
- * API: Strata.Modal.open('#myModal') / Strata.Modal.close()
9
- *
10
- * Events fired on document:
11
- * st:modal:open — detail: { modal }
12
- * st:modal:close — detail: { modal }
13
- */
14
- ;(function (win, doc) {
15
- 'use strict'
16
-
17
- var currentModal = null
18
- var backdrop = null
19
-
20
- function ensureBackdrop() {
21
- if (!backdrop) {
22
- backdrop = doc.createElement('div')
23
- backdrop.className = 'modal-backdrop'
24
- doc.body.appendChild(backdrop)
25
- }
26
- return backdrop
27
- }
28
-
29
- function lockScroll() {
30
- var sbw = win.innerWidth - doc.documentElement.clientWidth
31
- doc.body.style.setProperty('--st-scrollbar-width', sbw + 'px')
32
- doc.body.classList.add('modal-open')
33
- }
34
-
35
- function unlockScroll() {
36
- doc.body.classList.remove('modal-open')
37
- doc.body.style.removeProperty('--st-scrollbar-width')
38
- }
39
-
40
- function openModal(modal) {
41
- if (currentModal === modal) return
42
- if (currentModal) closeModal()
43
-
44
- currentModal = modal
45
- modal.setAttribute('data-st-visible', 'true')
46
- modal.removeAttribute('aria-hidden')
47
- modal.setAttribute('aria-modal', 'true')
48
-
49
- var bd = ensureBackdrop()
50
- void bd.offsetHeight // force reflow so transition plays
51
- bd.setAttribute('data-st-visible', 'true')
52
-
53
- lockScroll()
54
-
55
- var focusTarget = modal.querySelector('[autofocus]') ||
56
- modal.querySelector('.modal-content')
57
- if (focusTarget) setTimeout(function() { focusTarget.focus() }, 50)
58
-
59
- doc.dispatchEvent(new CustomEvent('st:modal:open', { detail: { modal: modal } }))
60
- }
61
-
62
- function closeModal() {
63
- if (!currentModal) return
64
-
65
- var modal = currentModal
66
- currentModal = null
67
-
68
- modal.setAttribute('data-st-visible', 'false')
69
- modal.setAttribute('aria-hidden', 'true')
70
- modal.removeAttribute('aria-modal')
71
-
72
- if (backdrop) backdrop.setAttribute('data-st-visible', 'false')
73
-
74
- unlockScroll()
75
-
76
- doc.dispatchEvent(new CustomEvent('st:modal:close', { detail: { modal: modal } }))
77
- }
78
-
79
- doc.addEventListener('click', function(e) {
80
- var trigger = e.target.closest('[data-st-toggle="modal"]')
81
- if (trigger) {
82
- var sel = trigger.getAttribute('data-st-target') || trigger.getAttribute('href')
83
- if (sel) {
84
- var target = doc.querySelector(sel)
85
- if (target) openModal(target)
86
- }
87
- return
88
- }
89
-
90
- if (e.target.closest('[data-st-dismiss="modal"]')) {
91
- closeModal()
92
- return
93
- }
94
-
95
- if (currentModal && e.target === currentModal) {
96
- var isStatic = currentModal.getAttribute('data-st-backdrop') === 'static'
97
- if (isStatic) {
98
- currentModal.classList.add('modal-static')
99
- var m = currentModal
100
- setTimeout(function() { m.classList.remove('modal-static') }, 300)
101
- } else {
102
- closeModal()
103
- }
104
- }
105
- })
106
-
107
- doc.addEventListener('keydown', function(e) {
108
- if (e.key === 'Escape' && currentModal) {
109
- var isStatic = currentModal.getAttribute('data-st-backdrop') === 'static'
110
- if (!isStatic) closeModal()
111
- }
112
- })
113
-
114
- win.Strata = win.Strata || {}
115
- win.Strata.Modal = {
116
- open: function(selector) {
117
- var el = typeof selector === 'string' ? doc.querySelector(selector) : selector
118
- if (el) openModal(el)
119
- },
120
- close: closeModal
121
- }
122
-
123
- }(window, document))
@@ -1,334 +0,0 @@
1
- /**
2
- * Strata Skeleton JS Utility
3
- * Version: 1.0.0
4
- *
5
- * Smart detection and lifecycle management for skeleton loaders.
6
- * JS handles detection and toggling only.
7
- * CSS handles all visual rendering via @layer st-skeleton.
8
- *
9
- * Four attribute states:
10
- * "true" — element shimmers (CSS applies ::before overlay)
11
- * "false" — element revealed (no shimmer)
12
- * "null" — JS managed parent (no overlay, children shimmer individually)
13
- *
14
- * Key fix: replaced elements (img, video, iframe) cannot have ::before
15
- * pseudo-elements in browsers. JS marks their WRAPPER div with "true"
16
- * instead so the wrapper's ::before overlay covers the replaced content.
17
- */
18
-
19
- ;(function (global) {
20
- 'use strict'
21
-
22
- // ─── Element type sets ───────────────────────────────────────────────
23
-
24
- // Content elements that support ::before — become individual skeleton bars
25
- const CONTENT_TAGS = new Set([
26
- 'P', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6',
27
- 'SPAN', 'A', 'LABEL', 'STRONG', 'EM', 'SMALL',
28
- 'BUTTON', 'INPUT', 'TEXTAREA', 'SELECT',
29
- 'LI', 'DT', 'DD', 'BLOCKQUOTE', 'FIGCAPTION'
30
- // NOTE: IMG, VIDEO, IFRAME are NOT here —
31
- // replaced elements cannot have ::before.
32
- // Their parent WRAPPER is marked instead.
33
- ])
34
-
35
- // Replaced elements — ::before not supported by browsers
36
- // JS marks their wrapper parent instead
37
- const REPLACED_TAGS = new Set([
38
- 'IMG', 'VIDEO', 'IFRAME', 'PICTURE', 'CANVAS', 'SVG'
39
- ])
40
-
41
- // Structural elements — skipped, children scanned
42
- // UNLESS they contain only replaced elements, in which case
43
- // the structural element itself becomes the skeleton bar
44
- const STRUCTURAL_TAGS = new Set([
45
- 'DIV', 'SECTION', 'ARTICLE', 'MAIN', 'ASIDE',
46
- 'HEADER', 'FOOTER', 'NAV', 'UL', 'OL', 'DL',
47
- 'FIGURE', 'FORM', 'FIELDSET'
48
- ])
49
-
50
- // Tags to ignore completely
51
- const IGNORE_TAGS = new Set([
52
- 'SCRIPT', 'STYLE', 'NOSCRIPT', 'META', 'LINK',
53
- 'HEAD', 'HTML', 'BODY', 'BR', 'HR', 'WBR'
54
- ])
55
-
56
- // ─── Registry ────────────────────────────────────────────────────────
57
-
58
- const registry = new Map()
59
-
60
- // ─── Smart Detection ─────────────────────────────────────────────────
61
-
62
- /**
63
- * Check if an element has direct text nodes (not just whitespace)
64
- */
65
- function hasDirectText(el) {
66
- return Array.from(el.childNodes).some(
67
- node => node.nodeType === Node.TEXT_NODE
68
- && node.textContent.trim().length > 0
69
- )
70
- }
71
-
72
- /**
73
- * Check if a structural element contains ONLY replaced elements.
74
- * If so, the structural element itself becomes the skeleton bar
75
- * because its ::before overlay covers the replaced content.
76
- *
77
- * Example: <div class="card-img-wrap"><img src="..."></div>
78
- * → card-img-wrap gets "true", its ::before covers the img
79
- */
80
- function hasOnlyReplacedChildren(el) {
81
- const meaningful = Array.from(el.children).filter(
82
- c => !IGNORE_TAGS.has(c.tagName)
83
- )
84
- return meaningful.length > 0
85
- && meaningful.every(c => REPLACED_TAGS.has(c.tagName))
86
- }
87
-
88
- /**
89
- * Walk the DOM from a parent and find all leaf nodes.
90
- *
91
- * Rules:
92
- * - Content tags (p, h3, button, a etc.) → mark as skeleton bar
93
- * - Structural wrapper containing ONLY replaced elements (img, video) →
94
- * mark the WRAPPER as skeleton bar (its ::before covers the media)
95
- * - Structural wrapper with direct text → mark as skeleton bar
96
- * - Structural wrapper with mixed children → recurse
97
- * - Replaced elements (img, video) as direct children → skip
98
- * (handled by their parent wrapper rule above)
99
- * - data-st-skeleton="false" → skip element and all descendants
100
- * - data-st-skeleton="null" → skip (another managed parent)
101
- */
102
- function detectLeaves(parent) {
103
- const leaves = []
104
-
105
- function walk(el) {
106
- // Skip opted-out elements entirely
107
- if (el.getAttribute('data-st-skeleton') === 'false') return
108
-
109
- // Skip other managed JS parents
110
- if (el !== parent && el.getAttribute('data-st-skeleton') === 'null') return
111
-
112
- // Skip ignored tags
113
- if (IGNORE_TAGS.has(el.tagName)) return
114
-
115
- const tag = el.tagName
116
-
117
- // Content element — supports ::before, mark as leaf
118
- if (CONTENT_TAGS.has(tag)) {
119
- leaves.push(el)
120
- return
121
- }
122
-
123
- // Replaced element as direct child of managed parent
124
- // Cannot mark it — no ::before support. Skip it.
125
- // Ideally developer wraps their images in a div.
126
- if (REPLACED_TAGS.has(tag)) {
127
- return
128
- }
129
-
130
- // Structural element — decide whether to mark or recurse
131
- if (STRUCTURAL_TAGS.has(tag)) {
132
-
133
- // CASE 1: Contains only replaced elements (img, video etc.)
134
- // Mark THIS structural element — its ::before covers the media
135
- if (hasOnlyReplacedChildren(el)) {
136
- leaves.push(el)
137
- return
138
- }
139
-
140
- // CASE 2: Has direct text — mark as leaf
141
- if (hasDirectText(el)) {
142
- leaves.push(el)
143
- return
144
- }
145
-
146
- // CASE 3: Mixed children — recurse into children
147
- Array.from(el.children).forEach(child => walk(child))
148
- return
149
- }
150
-
151
- // Unknown tag — recurse into children
152
- Array.from(el.children).forEach(child => walk(child))
153
- }
154
-
155
- // Walk children — not the parent itself
156
- Array.from(parent.children).forEach(child => walk(child))
157
-
158
- return leaves
159
- }
160
-
161
- // ─── Core ─────────────────────────────────────────────────────────────
162
-
163
- /**
164
- * Register a parent for JS management.
165
- * Sets parent to "null" — neutral container, no parent overlay.
166
- * Runs smart detection and marks leaf children with "true".
167
- */
168
- function manage(parent, options = {}) {
169
- if (!parent || registry.has(parent)) return
170
-
171
- const leaves = detectLeaves(parent)
172
- registry.set(parent, { leaves, options })
173
-
174
- // Set parent to "null" — removes parent ::before overlay
175
- // so only individual leaf shimmers show
176
- parent.setAttribute('data-st-skeleton', 'null')
177
-
178
- // Mark detected leaves as skeleton
179
- leaves.forEach(leaf => leaf.setAttribute('data-st-skeleton', 'true'))
180
- }
181
-
182
- /**
183
- * Show skeleton — parent to "null", leaves to "true"
184
- */
185
- function show(selector) {
186
- const targets = resolveTargets(selector)
187
- targets.forEach(parent => {
188
- const entry = registry.get(parent)
189
- if (!entry) return
190
- parent.setAttribute('data-st-skeleton', 'null')
191
- entry.leaves.forEach(leaf => leaf.setAttribute('data-st-skeleton', 'true'))
192
- })
193
- }
194
-
195
- /**
196
- * Reveal content — parent to "false", leaves to "false"
197
- * Attribute stays in DOM — only value changes.
198
- */
199
- function reveal(selector, options = {}) {
200
- if (typeof selector === 'object' && !isSelector(selector)) {
201
- options = selector
202
- selector = null
203
- }
204
-
205
- const targets = resolveTargets(selector)
206
- const stagger = options.stagger || 0
207
- const onReveal = options.onReveal || null
208
-
209
- targets.forEach((parent, index) => {
210
- const entry = registry.get(parent)
211
- if (!entry) return
212
-
213
- setTimeout(() => {
214
- parent.setAttribute('data-st-skeleton', 'false')
215
- entry.leaves.forEach(leaf => leaf.setAttribute('data-st-skeleton', 'false'))
216
-
217
- if (onReveal && index === targets.length - 1) {
218
- setTimeout(onReveal, 0)
219
- }
220
- }, stagger * index)
221
- })
222
- }
223
-
224
- /**
225
- * Toggle between skeleton and revealed
226
- */
227
- function toggle(selector) {
228
- const targets = resolveTargets(selector)
229
- targets.forEach(parent => {
230
- const val = parent.getAttribute('data-st-skeleton')
231
- val === 'false' ? show(parent) : reveal(parent)
232
- })
233
- }
234
-
235
- /**
236
- * Reveal one element by index within a selector group
237
- */
238
- function revealAt(selector, index) {
239
- const targets = resolveTargets(selector)
240
- const parent = targets[index]
241
- if (parent) reveal(parent)
242
- }
243
-
244
- /**
245
- * Check if element is currently in skeleton state
246
- */
247
- function isSkeleton(el) {
248
- const val = el.getAttribute('data-st-skeleton')
249
- return val === 'true' || val === 'null'
250
- }
251
-
252
- // ─── Init ─────────────────────────────────────────────────────────────
253
-
254
- /**
255
- * Initialise the skeleton utility.
256
- *
257
- * No selector: auto-discovers all [data-st-skeleton="true"] top-level parents.
258
- * With selector: manages all matching elements.
259
- */
260
- function init(selector, options = {}) {
261
- let parents
262
-
263
- if (!selector) {
264
- // Auto-discover top-level skeleton parents
265
- // Filter out elements that are children of another skeleton parent
266
- parents = Array.from(
267
- document.querySelectorAll('[data-st-skeleton="true"]')
268
- ).filter(el => !el.parentElement.closest('[data-st-skeleton]'))
269
-
270
- } else {
271
- parents = resolveRaw(selector)
272
-
273
- // Set initial attribute if missing
274
- parents.forEach(parent => {
275
- if (!parent.hasAttribute('data-st-skeleton')) {
276
- parent.setAttribute('data-st-skeleton', 'true')
277
- }
278
- })
279
- }
280
-
281
- parents.forEach(parent => manage(parent, options))
282
- return api
283
- }
284
-
285
- // ─── Helpers ──────────────────────────────────────────────────────────
286
-
287
- function resolveTargets(selector) {
288
- if (!selector) return Array.from(registry.keys())
289
- if (selector instanceof Element) return registry.has(selector) ? [selector] : []
290
- if (selector instanceof NodeList || Array.isArray(selector)) {
291
- return Array.from(selector).filter(el => registry.has(el))
292
- }
293
- if (typeof selector === 'string') {
294
- return Array.from(document.querySelectorAll(selector))
295
- .filter(el => registry.has(el))
296
- }
297
- return []
298
- }
299
-
300
- function resolveRaw(selector) {
301
- if (!selector) return []
302
- if (selector instanceof Element) return [selector]
303
- if (selector instanceof NodeList || Array.isArray(selector)) return Array.from(selector)
304
- if (typeof selector === 'string') return Array.from(document.querySelectorAll(selector))
305
- return []
306
- }
307
-
308
- function isSelector(val) {
309
- return typeof val === 'string'
310
- || val instanceof Element
311
- || val instanceof NodeList
312
- }
313
-
314
- // ─── Public API ───────────────────────────────────────────────────────
315
-
316
- const api = {
317
- init,
318
- show,
319
- reveal,
320
- toggle,
321
- revealAt,
322
- isSkeleton,
323
- manage: (selector, options) => {
324
- resolveRaw(selector).forEach(el => manage(el, options))
325
- return api
326
- }
327
- }
328
-
329
- // ─── Export ───────────────────────────────────────────────────────────
330
-
331
- if (!global.Strata) global.Strata = {}
332
- global.Strata.skeleton = api
333
-
334
- })(window)