@andreyshpigunov/x 0.3.72

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 (60) hide show
  1. package/.editorconfig +12 -0
  2. package/.github/workflows/publish.yml +28 -0
  3. package/README.md +37 -0
  4. package/assets/alpha.png +0 -0
  5. package/assets/apple-touch-icon.png +0 -0
  6. package/assets/github-mark-white.png +0 -0
  7. package/assets/github-mark.png +0 -0
  8. package/assets/logo-inverse.png +0 -0
  9. package/assets/logo.png +0 -0
  10. package/assets/logo.svg +9 -0
  11. package/babel.config.cjs +4 -0
  12. package/dist/app.css +167 -0
  13. package/dist/app.js +1 -0
  14. package/dist/x.css +167 -0
  15. package/dist/x.js +1 -0
  16. package/favicon.ico +0 -0
  17. package/favicon.svg +9 -0
  18. package/index.html +2214 -0
  19. package/index.js +1 -0
  20. package/jest.config.mjs +7 -0
  21. package/jsdoc.json +11 -0
  22. package/package.json +50 -0
  23. package/src/components/x/animate.js +296 -0
  24. package/src/components/x/appear.js +158 -0
  25. package/src/components/x/autocomplete.js +150 -0
  26. package/src/components/x/buttons.css +265 -0
  27. package/src/components/x/colors.css +64 -0
  28. package/src/components/x/debug.css +55 -0
  29. package/src/components/x/device.js +265 -0
  30. package/src/components/x/dropdown.css +164 -0
  31. package/src/components/x/dropdown.js +463 -0
  32. package/src/components/x/flex.css +163 -0
  33. package/src/components/x/flow.css +52 -0
  34. package/src/components/x/form.css +138 -0
  35. package/src/components/x/form.js +180 -0
  36. package/src/components/x/grid.css +109 -0
  37. package/src/components/x/helpers.css +928 -0
  38. package/src/components/x/hover.js +93 -0
  39. package/src/components/x/icons.css +58 -0
  40. package/src/components/x/lazyload.js +153 -0
  41. package/src/components/x/lib.js +679 -0
  42. package/src/components/x/links.css +114 -0
  43. package/src/components/x/loadmore.js +191 -0
  44. package/src/components/x/modal.css +286 -0
  45. package/src/components/x/modal.js +346 -0
  46. package/src/components/x/reset.css +213 -0
  47. package/src/components/x/scroll.css +100 -0
  48. package/src/components/x/scroll.js +301 -0
  49. package/src/components/x/sheets.css +15 -0
  50. package/src/components/x/sheets.js +147 -0
  51. package/src/components/x/slider.css +83 -0
  52. package/src/components/x/slider.js +330 -0
  53. package/src/components/x/space.css +56 -0
  54. package/src/components/x/sticky.css +28 -0
  55. package/src/components/x/sticky.js +156 -0
  56. package/src/components/x/typo.css +318 -0
  57. package/src/css/app.css +407 -0
  58. package/src/css/x.css +252 -0
  59. package/src/js/app.js +47 -0
  60. package/src/js/x.js +81 -0
@@ -0,0 +1,301 @@
1
+ /**
2
+ * @fileoverview Scroll utility for smooth scrolling to elements and active link highlighting.
3
+ *
4
+ * Automatically handles elements with `[x-scrollto]` attribute and provides manual scroll control.
5
+ * Supports offset, custom parent containers, class toggling for active states, and URL hash updates.
6
+ *
7
+ * Public API:
8
+ *
9
+ * - `scroll.init()` – Initializes `[x-scrollto]` links and sets up observers. Safe for multiple calls.
10
+ * - `scroll.destroy()` – Removes scroll observers and event listeners.
11
+ * - `scroll.scrollTo(target | params)` – Scrolls to element by id, selector, or element with options.
12
+ *
13
+ * Example usage:
14
+ *
15
+ * HTML:
16
+ * <a x-scrollto="top">Up</a>
17
+ * <a x-scrollto='{"target":"#section","offset":100}'>Go to section</a>
18
+ *
19
+ * JS:
20
+ * scroll.scrollTo('#section');
21
+ * scroll.scrollTo({ target: '#section', offset: 50 });
22
+ *
23
+ * Async:
24
+ * await scroll.scrollTo('#section');
25
+ *
26
+ * @author Andrey Shpigunov
27
+ * @version 0.3
28
+ * @since 2025-07-18
29
+ */
30
+
31
+ import { lib } from './lib';
32
+
33
+ /**
34
+ * Scroll utility class.
35
+ * Handles smooth scrolling to targets and active class management.
36
+ */
37
+ class Scroll {
38
+ /**
39
+ * Creates a Scroll instance.
40
+ */
41
+ constructor() {
42
+ /**
43
+ * Default scroll parent. Can be window or a DOM element.
44
+ * @type {Window|HTMLElement}
45
+ */
46
+ this.parent = window;
47
+
48
+ /**
49
+ * Default offset in pixels.
50
+ * @type {number}
51
+ */
52
+ this.offset = 0;
53
+
54
+ /**
55
+ * Default active class name.
56
+ * @type {string}
57
+ */
58
+ this.classActive = 'active';
59
+
60
+ /**
61
+ * Whether to update URL hash after scroll.
62
+ * @type {boolean}
63
+ */
64
+ this.hash = false;
65
+
66
+ /**
67
+ * Shortcut to scrollTo method.
68
+ * @type {Function}
69
+ */
70
+ this.to = this.scrollTo;
71
+
72
+ /**
73
+ * Internal registry of observed links and targets.
74
+ * @type {Object.<string, Object>}
75
+ * @private
76
+ */
77
+ this._linksHash = {};
78
+
79
+ /**
80
+ * Scroll event handlers for cleanup.
81
+ * @type {Map<HTMLElement|Window, Function>}
82
+ * @private
83
+ */
84
+ this._scrollHandlers = new Map();
85
+
86
+ /**
87
+ * Initialization flag to prevent duplicate setup.
88
+ * @type {boolean}
89
+ * @private
90
+ */
91
+ this._initialized = false;
92
+ }
93
+
94
+ /**
95
+ * Initializes all `[x-scrollto]` links on the page.
96
+ * Sets up click events and scroll observation for active class toggling.
97
+ * Safe to call multiple times.
98
+ */
99
+ init() {
100
+ if (this._initialized) {
101
+ this.destroy();
102
+ }
103
+
104
+ const links = lib.qsa('[x-scrollto]');
105
+ if (!links.length) return;
106
+
107
+ this._linksHash = {};
108
+
109
+ for (let link of links) {
110
+ try {
111
+ let item = {};
112
+ let attr = link.getAttribute('x-scrollto');
113
+
114
+ if (lib.isValidJSON(attr)) {
115
+ let json = JSON.parse(attr);
116
+
117
+ if (json.hasOwnProperty('target') && lib.qs(json.target)) {
118
+ item.link = link;
119
+ item.parent = json.parent || this.parent;
120
+ item.target = lib.qs(json.target);
121
+ item.offset = json.offset || this.offset;
122
+ item.classActive = json.classActive || this.classActive;
123
+ item.hash = json.hash ?? this.hash; // allow false override
124
+ } else {
125
+ console.error('Target required in JSON or element does not exist:', json);
126
+ continue;
127
+ }
128
+ } else {
129
+ if (lib.qs(attr)) {
130
+ item.link = link;
131
+ item.parent = this.parent;
132
+ item.target = lib.qs(attr);
133
+ item.offset = this.offset;
134
+ item.classActive = this.classActive;
135
+ item.hash = this.hash;
136
+ } else {
137
+ console.error(`Target "${attr}" not found.`);
138
+ continue;
139
+ }
140
+ }
141
+
142
+ if (item) {
143
+ const id = lib.makeId();
144
+ this._linksHash[id] = item;
145
+
146
+ link.addEventListener('click', event => {
147
+ event.preventDefault();
148
+ this.scrollTo({
149
+ parent: item.parent,
150
+ target: item.target,
151
+ offset: item.offset,
152
+ classActive: item.classActive,
153
+ hash: item.hash
154
+ });
155
+ });
156
+ }
157
+ } catch (err) {
158
+ console.error(err);
159
+ }
160
+ }
161
+
162
+ if (Object.keys(this._linksHash).length) {
163
+ this._setupScrollObservers();
164
+ }
165
+
166
+ this._initialized = true;
167
+ }
168
+
169
+ /**
170
+ * Removes scroll listeners and resets the state.
171
+ * Safe to call multiple times.
172
+ */
173
+ destroy() {
174
+ for (const [parent, handler] of this._scrollHandlers.entries()) {
175
+ parent.removeEventListener('scroll', handler);
176
+ }
177
+
178
+ this._scrollHandlers.clear();
179
+ this._linksHash = {};
180
+ this._initialized = false;
181
+ }
182
+
183
+ /**
184
+ * Scrolls to a specific element or target.
185
+ *
186
+ * @param {string|HTMLElement|Object} params - Target element, selector, or options object.
187
+ * Options format:
188
+ * {
189
+ * parent: string|HTMLElement, // Scroll parent (default: window)
190
+ * target: string|HTMLElement, // Target element or selector
191
+ * offset: number, // Offset in px
192
+ * classActive: string, // Class to toggle
193
+ * hash: boolean // Whether to update URL hash
194
+ * }
195
+ * @returns {Promise<void>} Resolves after scroll completes.
196
+ */
197
+ async scrollTo(params) {
198
+ return new Promise(resolve => {
199
+ const parent = typeof params.parent === 'string' ? lib.qs(params.parent) : params.parent || this.parent;
200
+ const offset = params.offset || this.offset;
201
+ const hash = params.hash ?? this.hash;
202
+
203
+ const target = typeof params === 'object' && !(params instanceof Element)
204
+ ? lib.qs(params.target)
205
+ : lib.qs(params);
206
+
207
+ if (!target) {
208
+ console.error('Target not found:', params.target || params);
209
+ resolve();
210
+ return;
211
+ }
212
+
213
+ let elementY, startingY, parentY, diff;
214
+
215
+ if (parent === window) {
216
+ startingY = window.pageYOffset;
217
+ elementY = startingY + target.getBoundingClientRect().top;
218
+ diff = elementY - startingY - offset;
219
+ } else {
220
+ startingY = parent.scrollTop;
221
+ parentY = parent.getBoundingClientRect().top;
222
+ elementY = startingY + target.getBoundingClientRect().top - parentY;
223
+ diff = elementY - startingY - offset;
224
+ }
225
+
226
+ parent.scrollTo({
227
+ top: startingY + diff,
228
+ left: 0,
229
+ behavior: 'smooth'
230
+ });
231
+
232
+ setTimeout(resolve, 400);
233
+
234
+ if (hash && target.id) {
235
+ lib.updateURL('#' + target.id);
236
+ } else if (hash) {
237
+ history.replaceState({}, document.title, window.location.href.split('#')[0]);
238
+ }
239
+ });
240
+ }
241
+
242
+ /**
243
+ * Sets up scroll event listeners for all unique parents to manage active classes.
244
+ * @private
245
+ */
246
+ _setupScrollObservers() {
247
+ const parents = new Set();
248
+
249
+ for (const key in this._linksHash) {
250
+ parents.add(this._linksHash[key].parent);
251
+ }
252
+
253
+ for (const parent of parents) {
254
+ const el = parent === window ? window : lib.qs(parent);
255
+ if (!el) continue;
256
+
257
+ const handler = () => {
258
+ this._scrollObserve();
259
+ };
260
+
261
+ el.addEventListener('scroll', handler, { passive: true });
262
+ this._scrollHandlers.set(el, handler);
263
+ }
264
+
265
+ // Initial trigger
266
+ this._scrollObserve();
267
+ }
268
+
269
+ /**
270
+ * Observes scroll position to manage active classes on links and targets.
271
+ * Adds or removes `classActive` when target enters viewport center.
272
+ * @private
273
+ */
274
+ _scrollObserve() {
275
+ Object.keys(this._linksHash).forEach(i => {
276
+ const item = this._linksHash[i];
277
+ const rect = item.target.getBoundingClientRect();
278
+ const threshold = document.documentElement.clientHeight / 4;
279
+
280
+ if (rect.top <= threshold && rect.bottom > threshold) {
281
+ if (item.classActive != null) {
282
+ item.link.classList.add(item.classActive);
283
+ item.target.classList.add(item.classActive);
284
+ }
285
+ } else {
286
+ if (item.classActive != null) {
287
+ item.link.classList.remove(item.classActive);
288
+ item.target.classList.remove(item.classActive);
289
+ }
290
+ }
291
+ });
292
+ }
293
+ }
294
+
295
+ /**
296
+ * Singleton export of Scroll.
297
+ * Use `scroll.init()` to initialize or reinitialize safely.
298
+ *
299
+ * @type {Scroll}
300
+ */
301
+ export const scroll = new Scroll();
@@ -0,0 +1,15 @@
1
+ /*----------------------------------------
2
+ sheets.css / x
3
+ Sheets
4
+
5
+ Created by Andrey Shpigunov at 20.03.2025
6
+ All right reserved.
7
+ ----------------------------------------*/
8
+
9
+
10
+ /* !- Sheets */
11
+
12
+
13
+ [x-sheet]:not(.active) {
14
+ display: none;
15
+ }
@@ -0,0 +1,147 @@
1
+ /**
2
+ * @fileoverview Tab sheets system for switching between content blocks.
3
+ *
4
+ * Provides simple tabs functionality using `[x-sheets]`, `[x-sheet-open]`, and `[x-sheet]` attributes.
5
+ * Handles nested sheets, multiple independent tab groups, and automatic activation of the `.active` tab.
6
+ *
7
+ * Public API:
8
+ *
9
+ * - `sheets.init()` – Initializes all `[x-sheets]` components. Safe for multiple calls.
10
+ * - `sheets.destroy()` – Removes all sheets-related event listeners and resets state.
11
+ * - `sheets.show(xSheet)` – Programmatically switches to a specific sheet.
12
+ *
13
+ * Example usage:
14
+ *
15
+ * HTML:
16
+ * <div x-sheets>
17
+ * <a x-sheet-open="sheetA" class="active">Tab A</a>
18
+ * <a x-sheet-open="sheetB">Tab B</a>
19
+ * <div x-sheet="sheetA">Content A</div>
20
+ * <div x-sheet="sheetB">Content B</div>
21
+ * </div>
22
+ *
23
+ * JS:
24
+ * sheets.show('sheetB');
25
+ *
26
+ * Behavior:
27
+ * - Activates tabs and corresponding content blocks by matching `x-sheet` and `x-sheet-open` values.
28
+ * - Nested sheets are supported; unrelated sheets are not affected.
29
+ *
30
+ * @author Andrey Shpigunov
31
+ * @version 0.3
32
+ * @since 2025-07-18
33
+ */
34
+
35
+ import { lib } from './lib';
36
+
37
+ /**
38
+ * Sheets system for tabs and content switching.
39
+ */
40
+ class Sheets {
41
+ /**
42
+ * Creates a Sheets instance.
43
+ */
44
+ constructor() {
45
+ /**
46
+ * Tracks all bound click handlers for cleanup.
47
+ * Key: Element, Value: Handler function.
48
+ * @type {Map<HTMLElement, Function>}
49
+ * @private
50
+ */
51
+ this._handlers = new Map();
52
+
53
+ /**
54
+ * Initialization flag to control safe reinitialization.
55
+ * @type {boolean}
56
+ * @private
57
+ */
58
+ this._initialized = false;
59
+ }
60
+
61
+ /**
62
+ * Initializes all `[x-sheets]` components on the page.
63
+ *
64
+ * - Binds click events to `[x-sheet-open]` elements.
65
+ * - Activates the tab with `.active` class by default.
66
+ * - Safe for multiple calls; previous listeners are removed.
67
+ */
68
+ init() {
69
+ if (this._initialized) {
70
+ this.destroy();
71
+ }
72
+
73
+ const sheets = lib.qsa('[x-sheets]');
74
+ if (!sheets.length) return;
75
+
76
+ for (let sheet of sheets) {
77
+ const tabs = lib.qsa('[x-sheet-open]:not([x-sheet-open] [x-sheet-open])', sheet);
78
+
79
+ for (let tab of tabs) {
80
+ const handler = (e) => {
81
+ e.preventDefault();
82
+ this.show(tab.getAttribute('x-sheet-open'));
83
+ };
84
+
85
+ tab.addEventListener('click', handler);
86
+ this._handlers.set(tab, handler);
87
+ }
88
+
89
+ const active = lib.qs('[x-sheet-open].active', sheet);
90
+ if (active) {
91
+ this.show(active.getAttribute('x-sheet-open'));
92
+ }
93
+ }
94
+
95
+ this._initialized = true;
96
+ }
97
+
98
+ /**
99
+ * Removes all event listeners and resets internal state.
100
+ * Safe to call multiple times.
101
+ */
102
+ destroy() {
103
+ for (const [el, handler] of this._handlers.entries()) {
104
+ el.removeEventListener('click', handler);
105
+ }
106
+
107
+ this._handlers.clear();
108
+ this._initialized = false;
109
+ }
110
+
111
+ /**
112
+ * Activates a tab and its corresponding content block by `x-sheet` key.
113
+ *
114
+ * @param {string} xSheet - The value of `x-sheet` and `x-sheet-open` to activate.
115
+ */
116
+ show(xSheet) {
117
+ const targetBody = lib.qs(`[x-sheet="${CSS.escape(xSheet)}"]`);
118
+ if (!targetBody) return;
119
+
120
+ const sheets = targetBody.closest('[x-sheets]');
121
+ if (!sheets) return;
122
+
123
+ const selectedTab = lib.qs(`[x-sheet-open="${CSS.escape(xSheet)}"]`, sheets);
124
+
125
+ if (
126
+ selectedTab?.classList.contains('active') &&
127
+ targetBody.classList.contains('active')
128
+ ) return; // Already active
129
+
130
+ const tabs = lib.qsa('[x-sheet-open]', sheets);
131
+ lib.removeClass(tabs, 'active');
132
+
133
+ const bodies = lib.qsa('[x-sheet]', sheets);
134
+ lib.removeClass(bodies, 'active');
135
+
136
+ lib.addClass(selectedTab, 'active');
137
+ lib.addClass(targetBody, 'active');
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Singleton export of Sheets system.
143
+ * Use `sheets.init()` to initialize or reinitialize safely.
144
+ *
145
+ * @type {Sheets}
146
+ */
147
+ export const sheets = new Sheets();
@@ -0,0 +1,83 @@
1
+ :root {
2
+ --slider-point-color: #ffffff66;
3
+ --slider-point-color-active: #fff;
4
+ --slider-point-size: .3rem;
5
+ --slider-indicators-gap: .3rem;
6
+ --slider-indicators-top: auto;
7
+ --slider-indicators-left: 1.2rem;
8
+ --slider-indicators-right: 1.2rem;
9
+ --slider-indicators-bottom: 1rem;
10
+ --slider-indicators-width: auto;
11
+ --slider-indicators-height: var(--slider-point-size);
12
+ --slider-indicators-init-opacity: 0;
13
+ --slider-object-fit: cover;
14
+ }
15
+
16
+ .slider {
17
+ display: flex;
18
+ position: relative;
19
+ overflow: hidden;
20
+ cursor: pointer;
21
+ touch-action: pan-y !important; /* разрешаем только вертикальные жесты браузеру */
22
+ overscroll-behavior: contain; /* не отдаём overscroll родителю */
23
+ }
24
+ .slider-wrapper {
25
+ display: flex;
26
+ width: 100%;
27
+ height: 100%;
28
+ pointer-events: auto;
29
+ will-change: transform;
30
+ transform: translateZ(0); /* создаёт отдельный композитный слой */
31
+ backface-visibility: hidden;
32
+ }
33
+ .slider-item {
34
+ display: flex;
35
+ position: relative;
36
+ min-width: 100%;
37
+ flex: 0 0 100%,
38
+ }
39
+ .slider-item a {
40
+ display: block;
41
+ width: 100%;
42
+ height: 100%;
43
+ }
44
+ .slider-item img {
45
+ width: 100%;
46
+ max-width: none;
47
+ height: 100%;
48
+ object-fit: var(--slider-object-fit);
49
+ pointer-events: auto;
50
+ }
51
+ .slider-indicators {
52
+ display: flex;
53
+ align-items: center;
54
+ position: absolute;
55
+ left: var(--slider-indicators-left);
56
+ right: var(--slider-indicators-right);
57
+ top: var(--slider-indicators-top);
58
+ bottom: var(--slider-indicators-bottom);
59
+ width: var(--slider-indicators-width);
60
+ height: var(--slider-indicators-height);
61
+ gap: var(--slider-indicators-gap);
62
+ opacity: var(--slider-indicators-init-opacity);
63
+ will-change: opacity;
64
+ transition: opacity .2s ease-out;
65
+ pointer-events: none;
66
+ }
67
+ .slider-indicators span {
68
+ display: flex;
69
+ flex: 1;
70
+ width: var(--slider-point-size);
71
+ min-width: var(--slider-point-size);
72
+ height: var(--slider-point-size);
73
+ border-radius: var(--slider-point-size);
74
+ background: var(--slider-point-color);
75
+ transition: background .1s ease-out;
76
+ }
77
+ .slider-indicators span.active {
78
+ background: var(--slider-point-color-active);
79
+ }
80
+ html.touch .slider .slider-indicators,
81
+ .slider:hover .slider-indicators {
82
+ opacity: 1;
83
+ }