@andreyshpigunov/x 0.3.89 → 0.4.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/README.md +1 -1
- package/assets/css/app.css +1 -0
- package/assets/img/github-mark-white.png +0 -0
- package/assets/img/github-mark.png +0 -0
- package/assets/js/app.js +1 -0
- package/cheatsheet.html +427 -0
- package/dist/x.css +1 -1
- package/dist/x.js +1 -3
- package/index.html +20 -24
- package/package.json +52 -47
- package/postcss.config.cjs +0 -2
- package/src/components/x/animate.js +39 -45
- package/src/components/x/appear.js +19 -26
- package/src/components/x/autocomplete.js +22 -10
- package/src/components/x/buttons.css +40 -16
- package/src/components/x/colors.css +47 -41
- package/src/components/x/debug.css +2 -2
- package/src/components/x/device.js +39 -33
- package/src/components/x/dropdown.css +2 -3
- package/src/components/x/dropdown.js +16 -9
- package/src/components/x/flex.css +146 -109
- package/src/components/x/flow.css +12 -6
- package/src/components/x/form.css +3 -3
- package/src/components/x/form.js +12 -9
- package/src/components/x/grid.css +78 -42
- package/src/components/x/helpers.css +601 -438
- package/src/components/x/hover.js +20 -9
- package/src/components/x/icons.css +12 -12
- package/src/components/x/lazyload.js +17 -8
- package/src/components/x/lib.js +15 -1
- package/src/components/x/links.css +2 -6
- package/src/components/x/loadmore.js +17 -5
- package/src/components/x/modal.css +4 -22
- package/src/components/x/modal.js +14 -5
- package/src/components/x/reset.css +1 -15
- package/src/components/x/scroll.css +4 -9
- package/src/components/x/scroll.js +14 -1
- package/src/components/x/sheets.css +0 -3
- package/src/components/x/sheets.js +157 -37
- package/src/components/x/slider.css +10 -1
- package/src/components/x/slider.js +15 -0
- package/src/components/x/space.css +22 -2
- package/src/components/x/sticky.css +10 -15
- package/src/components/x/sticky.js +21 -4
- package/src/components/x/typo.css +14 -40
- package/src/css/app.css +7 -8
- package/src/css/x.css +191 -213
- package/src/js/app.js +8 -8
- package/src/js/x.js +37 -41
- package/assets/github-mark-white.png +0 -0
- package/assets/github-mark.png +0 -0
- package/assets/logo-inverse.png +0 -0
- package/babel.config.cjs +0 -4
- package/dist/app.css +0 -1
- package/dist/app.js +0 -1
- package/dist/index.html +0 -2182
- package/dist/logo.png +0 -0
- package/jest.config.mjs +0 -7
- package/jsdoc.json +0 -11
- package/vite.config.js +0 -31
- /package/assets/{alpha.png → img/alpha.png} +0 -0
- /package/assets/{apple-touch-icon.png → img/apple-touch-icon.png} +0 -0
- /package/assets/{logo.png → img/logo.png} +0 -0
- /package/assets/{logo.svg → img/logo.svg} +0 -0
|
@@ -51,21 +51,38 @@
|
|
|
51
51
|
* <div x-sheet="b1">Content B1</div>
|
|
52
52
|
* </div>
|
|
53
53
|
*
|
|
54
|
+
* Events:
|
|
55
|
+
* const sheetsContainer = document.querySelector('#mySheets');
|
|
56
|
+
*
|
|
57
|
+
* sheetsContainer.addEventListener('sheets:ready', (e) => {
|
|
58
|
+
* console.log('Sheets initialized', e.detail);
|
|
59
|
+
* });
|
|
60
|
+
*
|
|
61
|
+
* sheetsContainer.addEventListener('sheets:selected', (e) => {
|
|
62
|
+
* console.log('Tab selected:', e.detail.sheet);
|
|
63
|
+
* });
|
|
64
|
+
*
|
|
65
|
+
* sheetsContainer.addEventListener('sheets:destroy', (e) => {
|
|
66
|
+
* console.log('Sheets destroyed', e.detail);
|
|
67
|
+
* });
|
|
68
|
+
*
|
|
54
69
|
* Public API:
|
|
55
70
|
*
|
|
56
71
|
* @method init() - Initializes all [x-sheets] components on the page.
|
|
57
72
|
* Binds click events to [x-sheet-open] elements and activates tabs with .active class.
|
|
58
73
|
* Safe for multiple calls - automatically destroys previous listeners.
|
|
74
|
+
* Dispatches 'sheets:ready' event on each container when done.
|
|
59
75
|
* @example
|
|
60
76
|
* sheets.init();
|
|
61
77
|
*
|
|
62
78
|
* @method destroy() - Removes all sheets-related event listeners and resets state.
|
|
63
|
-
* Safe to call multiple times.
|
|
79
|
+
* Safe to call multiple times. Dispatches 'sheets:destroy' event on each container.
|
|
64
80
|
* @example
|
|
65
81
|
* sheets.destroy();
|
|
66
82
|
*
|
|
67
83
|
* @method show(xSheet) - Programmatically switches to a specific sheet.
|
|
68
84
|
* Activates the tab and corresponding content block by matching x-sheet value.
|
|
85
|
+
* Dispatches 'sheets:selected' event on the container when tab changes.
|
|
69
86
|
* @param {string} xSheet - The value of x-sheet and x-sheet-open to activate.
|
|
70
87
|
* @example
|
|
71
88
|
* sheets.show('tab2');
|
|
@@ -108,8 +125,26 @@
|
|
|
108
125
|
* - Test nested sheets behavior
|
|
109
126
|
* - Handle errors gracefully
|
|
110
127
|
*
|
|
128
|
+
* @example
|
|
129
|
+
* // Vanilla JS — plain HTML
|
|
130
|
+
* // index.html:
|
|
131
|
+
* // <div x-sheets id="mySheets">
|
|
132
|
+
* // <nav>
|
|
133
|
+
* // <button type="button" x-sheet-open="tab1" class="active">Tab 1</button>
|
|
134
|
+
* // <button type="button" x-sheet-open="tab2">Tab 2</button>
|
|
135
|
+
* // </nav>
|
|
136
|
+
* // <section x-sheet="tab1" class="active">Content 1</section>
|
|
137
|
+
* // <section x-sheet="tab2">Content 2</section>
|
|
138
|
+
* // </div>
|
|
139
|
+
* //
|
|
140
|
+
* // <script type="module">
|
|
141
|
+
* // import { sheets } from './src/components/x/sheets.js';
|
|
142
|
+
* // window.addEventListener('DOMContentLoaded', () => sheets.init());
|
|
143
|
+
* // window.addEventListener('pagehide', () => sheets.destroy());
|
|
144
|
+
* // </script>
|
|
145
|
+
*
|
|
111
146
|
* @author Andrey Shpigunov
|
|
112
|
-
* @version 0.
|
|
147
|
+
* @version 0.4
|
|
113
148
|
* @since 2025-07-18
|
|
114
149
|
*/
|
|
115
150
|
|
|
@@ -130,7 +165,7 @@ class Sheets {
|
|
|
130
165
|
* @private
|
|
131
166
|
*/
|
|
132
167
|
this._handlers = new Map();
|
|
133
|
-
|
|
168
|
+
|
|
134
169
|
/**
|
|
135
170
|
* Initialization flag to control safe reinitialization.
|
|
136
171
|
* @type {boolean}
|
|
@@ -138,7 +173,23 @@ class Sheets {
|
|
|
138
173
|
*/
|
|
139
174
|
this._initialized = false;
|
|
140
175
|
}
|
|
141
|
-
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Dispatch custom event on specific container
|
|
179
|
+
* @param {HTMLElement} container - The x-sheets container element
|
|
180
|
+
* @param {string} eventName - Event name (without 'sheets:' prefix)
|
|
181
|
+
* @param {Object} detail - Event detail data
|
|
182
|
+
* @private
|
|
183
|
+
*/
|
|
184
|
+
_dispatchEvent(container, eventName, detail = {}) {
|
|
185
|
+
const event = new CustomEvent(`sheets:${eventName}`, {
|
|
186
|
+
detail,
|
|
187
|
+
bubbles: true,
|
|
188
|
+
cancelable: false
|
|
189
|
+
});
|
|
190
|
+
container.dispatchEvent(event);
|
|
191
|
+
}
|
|
192
|
+
|
|
142
193
|
/**
|
|
143
194
|
* Validates sheet value to prevent XSS attacks.
|
|
144
195
|
* Only allows alphanumeric characters, hyphens, and underscores.
|
|
@@ -150,7 +201,7 @@ class Sheets {
|
|
|
150
201
|
if (typeof value !== 'string' || !value) return false;
|
|
151
202
|
return /^[a-zA-Z0-9_-]+$/.test(value);
|
|
152
203
|
}
|
|
153
|
-
|
|
204
|
+
|
|
154
205
|
/**
|
|
155
206
|
* Initializes all `[x-sheets]` components on the page.
|
|
156
207
|
*
|
|
@@ -158,44 +209,47 @@ class Sheets {
|
|
|
158
209
|
* - Activates the tab with `.active` class by default.
|
|
159
210
|
* - SECURITY: Validates sheet values before use.
|
|
160
211
|
* - Safe for multiple calls; previous listeners are removed.
|
|
212
|
+
* - Dispatches 'sheets:ready' event on each container when initialization is complete.
|
|
161
213
|
*/
|
|
162
214
|
init() {
|
|
163
215
|
if (this._initialized) {
|
|
164
216
|
this.destroy();
|
|
165
217
|
}
|
|
166
|
-
|
|
218
|
+
|
|
167
219
|
const sheets = lib.qsa('[x-sheets]');
|
|
168
|
-
if (!sheets.length)
|
|
169
|
-
|
|
220
|
+
if (!sheets.length) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
170
224
|
for (const sheet of sheets) {
|
|
171
225
|
if (!sheet) continue;
|
|
172
|
-
|
|
226
|
+
|
|
173
227
|
const tabs = lib.qsa('[x-sheet-open]:not([x-sheet-open] [x-sheet-open])', sheet);
|
|
174
|
-
|
|
228
|
+
|
|
175
229
|
for (const tab of tabs) {
|
|
176
230
|
if (!tab) continue;
|
|
177
|
-
|
|
231
|
+
|
|
178
232
|
const sheetValue = tab.getAttribute('x-sheet-open');
|
|
179
233
|
if (!sheetValue) {
|
|
180
234
|
console.warn('sheets.init: Tab has x-sheet-open attribute but no value', tab);
|
|
181
235
|
continue;
|
|
182
236
|
}
|
|
183
|
-
|
|
237
|
+
|
|
184
238
|
// SECURITY: Validate sheet value
|
|
185
239
|
if (!this._isValidSheetValue(sheetValue)) {
|
|
186
240
|
console.error('sheets.init: Invalid sheet value (security check failed):', sheetValue);
|
|
187
241
|
continue;
|
|
188
242
|
}
|
|
189
|
-
|
|
243
|
+
|
|
190
244
|
const handler = (e) => {
|
|
191
245
|
e.preventDefault();
|
|
192
246
|
this.show(sheetValue);
|
|
193
247
|
};
|
|
194
|
-
|
|
248
|
+
|
|
195
249
|
tab.addEventListener('click', handler);
|
|
196
250
|
this._handlers.set(tab, handler);
|
|
197
251
|
}
|
|
198
|
-
|
|
252
|
+
|
|
199
253
|
const active = lib.qs('[x-sheet-open].active', sheet);
|
|
200
254
|
if (active) {
|
|
201
255
|
const activeValue = active.getAttribute('x-sheet-open');
|
|
@@ -203,91 +257,157 @@ class Sheets {
|
|
|
203
257
|
this.show(activeValue);
|
|
204
258
|
}
|
|
205
259
|
}
|
|
260
|
+
|
|
261
|
+
// Dispatch ready event on this container
|
|
262
|
+
this._dispatchEvent(sheet, 'ready', {
|
|
263
|
+
container: sheet,
|
|
264
|
+
timestamp: Date.now(),
|
|
265
|
+
initialized: true
|
|
266
|
+
});
|
|
206
267
|
}
|
|
207
|
-
|
|
268
|
+
|
|
208
269
|
this._initialized = true;
|
|
209
270
|
}
|
|
210
|
-
|
|
271
|
+
|
|
211
272
|
/**
|
|
212
273
|
* Removes all event listeners and resets internal state.
|
|
213
|
-
* Safe to call multiple times.
|
|
274
|
+
* Safe to call multiple times. Dispatches 'sheets:destroy' event on each container.
|
|
214
275
|
*/
|
|
215
276
|
destroy() {
|
|
277
|
+
// Store containers before clearing handlers
|
|
278
|
+
const containers = new Set();
|
|
279
|
+
for (const [el] of this._handlers.entries()) {
|
|
280
|
+
const container = el.closest('[x-sheets]');
|
|
281
|
+
if (container) {
|
|
282
|
+
containers.add(container);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Remove all event listeners
|
|
216
287
|
for (const [el, handler] of this._handlers.entries()) {
|
|
217
288
|
el.removeEventListener('click', handler);
|
|
218
289
|
}
|
|
219
|
-
|
|
290
|
+
|
|
220
291
|
this._handlers.clear();
|
|
221
292
|
this._initialized = false;
|
|
293
|
+
|
|
294
|
+
// Dispatch destroy event on each container
|
|
295
|
+
for (const container of containers) {
|
|
296
|
+
this._dispatchEvent(container, 'destroy', {
|
|
297
|
+
container: container,
|
|
298
|
+
timestamp: Date.now(),
|
|
299
|
+
wasInitialized: true
|
|
300
|
+
});
|
|
301
|
+
}
|
|
222
302
|
}
|
|
223
|
-
|
|
303
|
+
|
|
224
304
|
/**
|
|
225
305
|
* Activates a tab and its corresponding content block by `x-sheet` key.
|
|
226
306
|
* SECURITY: Validates sheet value and uses CSS.escape() for selectors.
|
|
307
|
+
* Dispatches 'sheets:selected' event on the container when tab changes.
|
|
227
308
|
*
|
|
228
309
|
* @param {string} xSheet - The value of `x-sheet` and `x-sheet-open` to activate.
|
|
310
|
+
* @param {Object} options - Optional configuration.
|
|
311
|
+
* @param {boolean} options.silent - If true, prevents dispatching 'sheets:selected' event.
|
|
312
|
+
* @param {string} options.source - Source of the selection ('api' or 'user').
|
|
229
313
|
*/
|
|
230
|
-
show(xSheet) {
|
|
314
|
+
show(xSheet, options = { silent: false, source: 'api' }) {
|
|
231
315
|
if (!xSheet || typeof xSheet !== 'string') {
|
|
232
316
|
console.error('sheets.show: Sheet value is required and must be a string');
|
|
233
317
|
return;
|
|
234
318
|
}
|
|
235
|
-
|
|
319
|
+
|
|
236
320
|
// SECURITY: Validate sheet value
|
|
237
321
|
if (!this._isValidSheetValue(xSheet)) {
|
|
238
322
|
console.error('sheets.show: Invalid sheet value (security check failed):', xSheet);
|
|
239
323
|
return;
|
|
240
324
|
}
|
|
241
|
-
|
|
325
|
+
|
|
242
326
|
// SECURITY: Use CSS.escape() to prevent XSS in selectors
|
|
243
327
|
const escapedValue = CSS.escape(xSheet);
|
|
244
328
|
const targetBody = lib.qs(`[x-sheet="${escapedValue}"]`);
|
|
245
|
-
|
|
329
|
+
|
|
246
330
|
if (!targetBody) {
|
|
247
331
|
console.warn('sheets.show: Target content not found:', xSheet);
|
|
248
332
|
return;
|
|
249
333
|
}
|
|
250
|
-
|
|
251
|
-
const
|
|
252
|
-
if (!
|
|
334
|
+
|
|
335
|
+
const container = targetBody.closest('[x-sheets]');
|
|
336
|
+
if (!container) {
|
|
253
337
|
console.warn('sheets.show: Sheets container not found for:', xSheet);
|
|
254
338
|
return;
|
|
255
339
|
}
|
|
256
|
-
|
|
257
|
-
const selectedTab = lib.qs(`[x-sheet-open="${escapedValue}"]`,
|
|
258
|
-
|
|
340
|
+
|
|
341
|
+
const selectedTab = lib.qs(`[x-sheet-open="${escapedValue}"]`, container);
|
|
342
|
+
|
|
259
343
|
if (!selectedTab) {
|
|
260
344
|
console.warn('sheets.show: Tab not found:', xSheet);
|
|
261
345
|
return;
|
|
262
346
|
}
|
|
263
|
-
|
|
347
|
+
|
|
264
348
|
// Check if already active
|
|
265
349
|
if (
|
|
266
350
|
selectedTab.classList.contains('active') &&
|
|
267
351
|
targetBody.classList.contains('active')
|
|
268
352
|
) {
|
|
353
|
+
// Dispatch selected event even if already active (for consistency)
|
|
354
|
+
if (!options.silent) {
|
|
355
|
+
this._dispatchEvent(container, 'selected', {
|
|
356
|
+
sheet: xSheet,
|
|
357
|
+
tab: selectedTab,
|
|
358
|
+
content: targetBody,
|
|
359
|
+
container: container,
|
|
360
|
+
previousSheet: xSheet,
|
|
361
|
+
previousTab: selectedTab,
|
|
362
|
+
previousContent: targetBody,
|
|
363
|
+
wasActive: true,
|
|
364
|
+
source: options.source || 'api',
|
|
365
|
+
timestamp: Date.now()
|
|
366
|
+
});
|
|
367
|
+
}
|
|
269
368
|
return; // Already active
|
|
270
369
|
}
|
|
271
|
-
|
|
370
|
+
|
|
371
|
+
// Get previous active tab and body for event data
|
|
372
|
+
const previousTab = lib.qs('[x-sheet-open].active', container);
|
|
373
|
+
const previousBody = lib.qs('[x-sheet].active', container);
|
|
374
|
+
const previousSheet = previousTab ? previousTab.getAttribute('x-sheet-open') : null;
|
|
375
|
+
|
|
272
376
|
// Remove active class from all tabs and bodies in this container
|
|
273
|
-
const tabs = lib.qsa('[x-sheet-open]',
|
|
274
|
-
const bodies = lib.qsa('[x-sheet]',
|
|
275
|
-
|
|
377
|
+
const tabs = lib.qsa('[x-sheet-open]', container);
|
|
378
|
+
const bodies = lib.qsa('[x-sheet]', container);
|
|
379
|
+
|
|
276
380
|
for (const tab of tabs) {
|
|
277
381
|
if (tab && tab.classList.contains('active')) {
|
|
278
382
|
tab.classList.remove('active');
|
|
279
383
|
}
|
|
280
384
|
}
|
|
281
|
-
|
|
385
|
+
|
|
282
386
|
for (const body of bodies) {
|
|
283
387
|
if (body && body.classList.contains('active')) {
|
|
284
388
|
body.classList.remove('active');
|
|
285
389
|
}
|
|
286
390
|
}
|
|
287
|
-
|
|
391
|
+
|
|
288
392
|
// Add active class to selected tab and body
|
|
289
393
|
selectedTab.classList.add('active');
|
|
290
394
|
targetBody.classList.add('active');
|
|
395
|
+
|
|
396
|
+
// Dispatch selected event on the container
|
|
397
|
+
if (!options.silent) {
|
|
398
|
+
this._dispatchEvent(container, 'selected', {
|
|
399
|
+
sheet: xSheet,
|
|
400
|
+
tab: selectedTab,
|
|
401
|
+
content: targetBody,
|
|
402
|
+
container: container,
|
|
403
|
+
previousSheet: previousSheet,
|
|
404
|
+
previousTab: previousTab,
|
|
405
|
+
previousContent: previousBody,
|
|
406
|
+
wasActive: false,
|
|
407
|
+
source: options.source || 'api',
|
|
408
|
+
timestamp: Date.now()
|
|
409
|
+
});
|
|
410
|
+
}
|
|
291
411
|
}
|
|
292
412
|
}
|
|
293
413
|
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
/*----------------------------------------
|
|
2
|
+
slider.css
|
|
3
|
+
Slider
|
|
4
|
+
|
|
5
|
+
Created by Andrey Shpigunov at 13.04.2026
|
|
6
|
+
All right reserved.
|
|
7
|
+
----------------------------------------*/
|
|
8
|
+
|
|
9
|
+
|
|
1
10
|
:root {
|
|
2
11
|
--slider-point-color: #ffffff66;
|
|
3
12
|
--slider-point-color-active: #fff;
|
|
@@ -22,7 +31,7 @@
|
|
|
22
31
|
overscroll-behavior: contain; /* не отдаём overscroll родителю */
|
|
23
32
|
}
|
|
24
33
|
.slider_touch {
|
|
25
|
-
touch-action: pan-y
|
|
34
|
+
touch-action: pan-y; /* разрешаем только вертикальные жесты браузеру */
|
|
26
35
|
}
|
|
27
36
|
.slider-wrapper {
|
|
28
37
|
display: flex;
|
|
@@ -122,6 +122,21 @@
|
|
|
122
122
|
* - Touch events support for mobile devices
|
|
123
123
|
* - CSS transforms and transitions
|
|
124
124
|
*
|
|
125
|
+
* @example
|
|
126
|
+
* // Vanilla JS — plain HTML
|
|
127
|
+
* // index.html:
|
|
128
|
+
* // <div x-slider='{"gap":16,"rubber":true,"resetOnMouseout":true,"touch":true}'>
|
|
129
|
+
* // <div><img data-src="slide1.jpg" alt="Slide 1"></div>
|
|
130
|
+
* // <div><img data-src="slide2.jpg" alt="Slide 2"></div>
|
|
131
|
+
* // <div><img data-src="slide3.jpg" alt="Slide 3"></div>
|
|
132
|
+
* // </div>
|
|
133
|
+
* //
|
|
134
|
+
* // <script type="module">
|
|
135
|
+
* // import { slider } from './src/components/x/slider.js';
|
|
136
|
+
* // window.addEventListener('DOMContentLoaded', () => slider.init());
|
|
137
|
+
* // window.addEventListener('pagehide', () => slider.destroy());
|
|
138
|
+
* // </script>
|
|
139
|
+
*
|
|
125
140
|
* @author Andrey Shpigunov
|
|
126
141
|
* @version 0.3
|
|
127
142
|
* @since 2025-07-18
|
|
@@ -7,13 +7,25 @@ All right reserved.
|
|
|
7
7
|
----------------------------------------*/
|
|
8
8
|
|
|
9
9
|
|
|
10
|
+
/*
|
|
11
|
+
.space[0-10] (s,m,l,xl) - vertical space
|
|
12
|
+
*/
|
|
13
|
+
|
|
10
14
|
@for $i from 0 to 10 {
|
|
11
15
|
.space$(i) {
|
|
12
16
|
height: var(--space-$(i));
|
|
13
17
|
}
|
|
14
18
|
}
|
|
15
19
|
|
|
16
|
-
@media (
|
|
20
|
+
@media (min-width: 640px) {
|
|
21
|
+
@for $i from 0 to 10 {
|
|
22
|
+
.s\:space$(i) {
|
|
23
|
+
height: var(--space-$(i));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
@media (min-width: 768px) {
|
|
17
29
|
@for $i from 0 to 10 {
|
|
18
30
|
.m\:space$(i) {
|
|
19
31
|
height: var(--space-$(i));
|
|
@@ -21,10 +33,18 @@ All right reserved.
|
|
|
21
33
|
}
|
|
22
34
|
}
|
|
23
35
|
|
|
24
|
-
@media (
|
|
36
|
+
@media (min-width: 1024px) {
|
|
25
37
|
@for $i from 0 to 10 {
|
|
26
38
|
.l\:space$(i) {
|
|
27
39
|
height: var(--space-$(i));
|
|
28
40
|
}
|
|
29
41
|
}
|
|
30
42
|
}
|
|
43
|
+
|
|
44
|
+
@media (min-width: 1280px) {
|
|
45
|
+
@for $i from 0 to 10 {
|
|
46
|
+
.xl\:space$(i) {
|
|
47
|
+
height: var(--space-$(i));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -7,25 +7,20 @@ All right reserved.
|
|
|
7
7
|
----------------------------------------*/
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
/*
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
/* .sticky (m,l,xl) */
|
|
14
|
-
|
|
10
|
+
/* .sticky (s,m,l,xl) */
|
|
15
11
|
|
|
16
12
|
.sticky {
|
|
17
|
-
position: sticky
|
|
18
|
-
top: 0;
|
|
13
|
+
position: sticky;
|
|
19
14
|
}
|
|
20
|
-
@media (
|
|
21
|
-
.s\:sticky { position: sticky
|
|
15
|
+
@media (max-width: 640px) {
|
|
16
|
+
.s\:sticky { position: sticky }
|
|
22
17
|
}
|
|
23
|
-
@media (
|
|
24
|
-
.m\:sticky { position: sticky
|
|
18
|
+
@media (min-width: 768px) {
|
|
19
|
+
.m\:sticky { position: sticky }
|
|
25
20
|
}
|
|
26
|
-
@media (
|
|
27
|
-
.l\:sticky { position: sticky
|
|
21
|
+
@media (min-width: 1024px) {
|
|
22
|
+
.l\:sticky { position: sticky }
|
|
28
23
|
}
|
|
29
|
-
@media (
|
|
30
|
-
.xl\:sticky { position: sticky
|
|
24
|
+
@media (min-width: 1280px) {
|
|
25
|
+
.xl\:sticky { position: sticky }
|
|
31
26
|
}
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* top: 0;
|
|
24
24
|
* z-index: 100;
|
|
25
25
|
* }
|
|
26
|
-
* .sticky.
|
|
26
|
+
* .sticky.sticky--on {
|
|
27
27
|
* box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
28
28
|
* }
|
|
29
29
|
*
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
* CSS classes:
|
|
77
77
|
*
|
|
78
78
|
* - `.sticky` - Required class to mark element for observation
|
|
79
|
-
* - `.
|
|
79
|
+
* - `.sticky--on` - Added when element is in sticky state (not fully visible)
|
|
80
80
|
* - Removed when element returns to normal flow
|
|
81
81
|
*
|
|
82
82
|
* How it works:
|
|
@@ -85,7 +85,7 @@
|
|
|
85
85
|
* 2. Uses threshold: 1 (element must be fully visible)
|
|
86
86
|
* 3. Uses rootMargin: '-1px 0px 0px 0px' (triggers when top edge leaves viewport)
|
|
87
87
|
* 4. When intersectionRatio < 1, element is sticky
|
|
88
|
-
* 5. Toggles `
|
|
88
|
+
* 5. Toggles `sticky--on` class and dispatches events
|
|
89
89
|
*
|
|
90
90
|
* Observer configuration:
|
|
91
91
|
*
|
|
@@ -114,6 +114,23 @@
|
|
|
114
114
|
* - Modern browsers (Chrome 51+, Firefox 55+, Safari 12.1+, Edge 15+)
|
|
115
115
|
* - Falls back gracefully if not supported (no errors, just no functionality)
|
|
116
116
|
*
|
|
117
|
+
* @example
|
|
118
|
+
* // Vanilla JS — plain HTML
|
|
119
|
+
* // index.html:
|
|
120
|
+
* // <header class="sticky"><nav>Navigation</nav></header>
|
|
121
|
+
* // <main style="min-height: 200vh">Scroll content</main>
|
|
122
|
+
* //
|
|
123
|
+
* // <style>
|
|
124
|
+
* // .sticky { position: sticky; top: 0; z-index: 100; }
|
|
125
|
+
* // .sticky.sticky--on { box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
|
|
126
|
+
* // </style>
|
|
127
|
+
* //
|
|
128
|
+
* // <script type="module">
|
|
129
|
+
* // import { sticky } from './src/components/x/sticky.js';
|
|
130
|
+
* // window.addEventListener('DOMContentLoaded', () => sticky.init());
|
|
131
|
+
* // window.addEventListener('pagehide', () => sticky.destroy());
|
|
132
|
+
* // </script>
|
|
133
|
+
*
|
|
117
134
|
* @author Andrey Shpigunov
|
|
118
135
|
* @version 0.2
|
|
119
136
|
* @since 2025-07-17
|
|
@@ -133,7 +150,7 @@ class Sticky {
|
|
|
133
150
|
* Class to apply when sticky state is active.
|
|
134
151
|
* @type {string}
|
|
135
152
|
*/
|
|
136
|
-
this.activeClass = '
|
|
153
|
+
this.activeClass = 'sticky--on';
|
|
137
154
|
|
|
138
155
|
/**
|
|
139
156
|
* Root margin for IntersectionObserver.
|
|
@@ -7,9 +7,6 @@ All right reserved.
|
|
|
7
7
|
----------------------------------------*/
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
/* !- Typo */
|
|
11
|
-
|
|
12
|
-
|
|
13
10
|
/*
|
|
14
11
|
.h[1-6]
|
|
15
12
|
.mono
|
|
@@ -22,7 +19,6 @@ All right reserved.
|
|
|
22
19
|
.lh[0-9] (m,l,xl) - values: 1.0-1.9
|
|
23
20
|
*/
|
|
24
21
|
|
|
25
|
-
|
|
26
22
|
:root {
|
|
27
23
|
--headers-margin-top: 1em;
|
|
28
24
|
--headers-margin-bottom: .5em;
|
|
@@ -145,15 +141,15 @@ pre {
|
|
|
145
141
|
|
|
146
142
|
/* !- Lists */
|
|
147
143
|
|
|
148
|
-
ul,
|
|
149
|
-
ol,
|
|
144
|
+
ul,
|
|
145
|
+
ol,
|
|
150
146
|
dl {
|
|
151
147
|
margin: var(--paragraph-margin) 0;
|
|
152
148
|
padding-left: calc(var(--paragraph-margin) * 1.5);
|
|
153
149
|
|
|
154
|
-
& ul,
|
|
155
|
-
& ol,
|
|
156
|
-
& dl,
|
|
150
|
+
& ul,
|
|
151
|
+
& ol,
|
|
152
|
+
& dl,
|
|
157
153
|
& li {
|
|
158
154
|
margin: var(--space-1) 0;
|
|
159
155
|
}
|
|
@@ -210,19 +206,7 @@ table {
|
|
|
210
206
|
.fw$(i) { font-weight: $(i) }
|
|
211
207
|
}
|
|
212
208
|
|
|
213
|
-
@media (
|
|
214
|
-
@for $i from 10 to 19 {
|
|
215
|
-
.s\:fs$(i) { font-size: calc($(i)rem / 10) }
|
|
216
|
-
}
|
|
217
|
-
@for $i from 20 to 64 by 2 {
|
|
218
|
-
.s\:fs$(i) { font-size: calc($(i)rem / 10) }
|
|
219
|
-
}
|
|
220
|
-
@for $i from 100 to 900 by 100 {
|
|
221
|
-
.s\:fw$(i) { font-weight: $(i) }
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
@media (--medium) {
|
|
209
|
+
@media (min-width: 768px) {
|
|
226
210
|
@for $i from 10 to 19 {
|
|
227
211
|
.m\:fs$(i) { font-size: calc($(i)rem / 10) }
|
|
228
212
|
}
|
|
@@ -234,7 +218,7 @@ table {
|
|
|
234
218
|
}
|
|
235
219
|
}
|
|
236
220
|
|
|
237
|
-
@media (
|
|
221
|
+
@media (min-width: 1024px) {
|
|
238
222
|
@for $i from 10 to 19 {
|
|
239
223
|
.l\:fs$(i) { font-size: calc($(i)rem / 10) }
|
|
240
224
|
}
|
|
@@ -246,7 +230,7 @@ table {
|
|
|
246
230
|
}
|
|
247
231
|
}
|
|
248
232
|
|
|
249
|
-
@media (
|
|
233
|
+
@media (min-width: 1280px) {
|
|
250
234
|
@for $i from 10 to 19 {
|
|
251
235
|
.xl\:fs$(i) { font-size: calc($(i)rem / 10) }
|
|
252
236
|
}
|
|
@@ -265,22 +249,17 @@ table {
|
|
|
265
249
|
.ls$(i) { letter-spacing: calc($(i)em * 0.625) }
|
|
266
250
|
}
|
|
267
251
|
|
|
268
|
-
@media (
|
|
269
|
-
@for $i from 0 to 4 {
|
|
270
|
-
.s\:ls$(i) { letter-spacing: calc($(i)em * 0.625) }
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
@media (--medium) {
|
|
252
|
+
@media (min-width: 768px) {
|
|
274
253
|
@for $i from 0 to 4 {
|
|
275
254
|
.m\:ls$(i) { letter-spacing: calc($(i)em * 0.625) }
|
|
276
255
|
}
|
|
277
256
|
}
|
|
278
|
-
@media (
|
|
257
|
+
@media (min-width: 1024px) {
|
|
279
258
|
@for $i from 0 to 4 {
|
|
280
259
|
.l\:ls$(i) { letter-spacing: calc($(i)em * 0.625) }
|
|
281
260
|
}
|
|
282
261
|
}
|
|
283
|
-
@media (
|
|
262
|
+
@media (min-width: 1280px) {
|
|
284
263
|
@for $i from 0 to 4 {
|
|
285
264
|
.xl\:ls$(i) { letter-spacing: calc($(i)em * 0.625) }
|
|
286
265
|
}
|
|
@@ -293,22 +272,17 @@ table {
|
|
|
293
272
|
.lh$(i) { line-height: 1.$(i) }
|
|
294
273
|
}
|
|
295
274
|
|
|
296
|
-
@media (
|
|
297
|
-
@for $i from 0 to 9 {
|
|
298
|
-
.s\:lh$(i) { line-height: 1.$(i) }
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
@media (--medium) {
|
|
275
|
+
@media (min-width: 768px) {
|
|
302
276
|
@for $i from 0 to 9 {
|
|
303
277
|
.m\:lh$(i) { line-height: 1.$(i) }
|
|
304
278
|
}
|
|
305
279
|
}
|
|
306
|
-
@media (
|
|
280
|
+
@media (min-width: 1024px) {
|
|
307
281
|
@for $i from 0 to 9 {
|
|
308
282
|
.l\:lh$(i) { line-height: 1.$(i) }
|
|
309
283
|
}
|
|
310
284
|
}
|
|
311
|
-
@media (
|
|
285
|
+
@media (min-width: 1280px) {
|
|
312
286
|
@for $i from 0 to 9 {
|
|
313
287
|
.xl\:lh$(i) { line-height: 1.$(i) }
|
|
314
288
|
}
|