@madj2k/fe-frontend-kit 2.0.31 → 2.0.33

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/index.js CHANGED
@@ -6,6 +6,7 @@ export { Madj2kResizeEnd } from './tools/resize-end';
6
6
  export { Madj2kScrolling } from './tools/scrolling';
7
7
  export { Madj2kSimpleFadeSlider } from './tools/simple-fade-slider';
8
8
  export { Madj2kToggledOverlay } from './tools/toggled-overlay';
9
+ export { Madj2kElementInViewport } from './tools/element-in-viewport';
9
10
 
10
11
  // Menus
11
12
  export { Madj2kFlyoutMenu } from './menus/flyout-menu';
package/index.scss CHANGED
@@ -12,3 +12,4 @@
12
12
  @forward 'tools/scrolling/index';
13
13
  @forward 'tools/simple-fade-slider/index';
14
14
  @forward 'tools/toggled-overlay/index';
15
+ @forward 'tools/element-in-viewport/index';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@madj2k/fe-frontend-kit",
3
- "version": "2.0.31",
3
+ "version": "2.0.33",
4
4
  "description": "Shared frontend utilities, menus and mixins for projects",
5
5
  "main": "index.js",
6
6
  "style": "index.scss",
package/readme.md CHANGED
@@ -366,6 +366,52 @@ Usage with Appear-On-Scroll (HTML):
366
366
  </div>
367
367
  ```
368
368
 
369
+ # JS: Element In Viewport
370
+ A lightweight helper class that adds a configurable class to any DOM element once it becomes visible in the viewport.
371
+ Perfect for triggering CSS-based animations (e.g., quote reveals, fade-ins, transitions) when an element enters view.
372
+ * Works with IntersectionObserver API
373
+ * Purely DOM-based (no keyframes required)
374
+ * Fully configurable (threshold, delay, class)
375
+ * Ideal for CMS-driven content (dynamic DOM)
376
+ * Designed for performance and flexibility
377
+
378
+ Init:
379
+ ```
380
+ document.querySelectorAll('.js-inview').forEach((el) => {
381
+ new Madj2kElementInViewport(el, {
382
+ visibleClass: 'is-in-viewport',
383
+ threshold: 0.5,
384
+ debug: false
385
+ });
386
+ });
387
+ ```
388
+
389
+ HTML-Example
390
+ ```
391
+ <section class="my-element js-inview">
392
+ <div class="my-element-content">Lorem ipsum dolor sit amet.</div>
393
+ </section>
394
+ ```
395
+
396
+ SCSS-Example
397
+ ```
398
+ .my-element {
399
+ .my-element-content {
400
+ opacity: 0;
401
+ transform: translateY(20%);
402
+ transition: opacity 0.6s ease, transform 0.6s ease;
403
+ }
404
+
405
+ &.is-in-viewport {
406
+ .my-element-content {
407
+ opacity: 1;
408
+ transform: translateY(0);
409
+ }
410
+ }
411
+ }
412
+ ```
413
+
414
+
369
415
  # JS: Toggled Overlay
370
416
  This class toggles the visibility of any target element referenced by the `aria-controls`
371
417
  attribute of a trigger element (button, link, etc.). It manages ARIA attributes for accessibility
@@ -91,8 +91,12 @@
91
91
  ///
92
92
  /// @license
93
93
  /// GNU General Public License v3.0 https://www.gnu.org/licenses/gpl-3.0.en.html
94
+ /// @deprecated
94
95
  ///
95
96
  @mixin toggle-icon($selector: '.toggle') {
97
+
98
+ @warn "toggle-icon() is deprecated. Use toggle() instead.";
99
+
96
100
  #{$selector}:has(.icon:first-child) {
97
101
  line-height: 1; // icon-only toggle buttons
98
102
  }
@@ -250,6 +250,12 @@
250
250
  align-items: center;
251
251
  gap: rem-calc($gap);
252
252
  height: 100%;
253
+
254
+ @warn "nav-toggle-group() is deprecated. Use toggle-group() instead.";
255
+
256
+ @include toggle-group(
257
+ $gap: $gap,
258
+ );
253
259
  }
254
260
 
255
261
 
@@ -309,49 +315,13 @@
309
315
  "icon-plus": unquote('"\\e908"')
310
316
  )
311
317
  ) {
312
- background-color: transparent;
313
- border: 0;
314
- margin: 0;
315
- padding: 0;
316
-
317
- &:has(.nav-toggle-icon) {
318
- display: flex;
319
- align-items: center;
320
-
321
- // variant with icons as font
322
- &[aria-expanded="true"] {
323
- @each $icon-class, $content in $icon-mappings {
324
- .#{$icon-class} {
325
- &:before {
326
- content: $content;
327
- }
328
- }
329
- }
330
- }
331
-
332
- // variant with icons in a sprite
333
- .nav-toggle-icon-opened {
334
- display: none;
335
- }
336
-
337
- &[aria-expanded="true"] {
338
- .nav-toggle-icon-opened {
339
- display: inline-block;
340
- }
341
- .nav-toggle-icon-closed {
342
- display: none;
343
- }
344
- }
345
- }
346
318
 
347
- &-icon:not(:has(svg)),
348
- &-icon svg {
349
- font-size: rem-calc($icon-size);
350
- width: rem-calc($icon-size);
351
- color: $icon-color;
352
- }
319
+ @warn "nav-toggle() is deprecated. Use toggle() instead.";
353
320
 
354
- &-icon svg {
355
- height: 100%;
356
- }
321
+ @include toggle(
322
+ $icon-size: $icon-size,
323
+ $icon-color: $icon-color,
324
+ $icon-mappings: $icon-mappings,
325
+ $icon-selector: '.nav-toggle-icon'
326
+ );
357
327
  }
@@ -43,6 +43,15 @@
43
43
  /// [...]
44
44
  /// </div>
45
45
  ///
46
+ /// @example html
47
+ /// <div class="csp-section">
48
+ /// [...]
49
+ /// </div>
50
+ /// <!-- With vertical padding instead of margin -->
51
+ /// <div class="csp-section csp-section-padding">
52
+ /// [...]
53
+ /// </div>
54
+ ///
46
55
  /// @example scss
47
56
  /// .layout-default {
48
57
  /// @include section-spacing();
@@ -56,6 +65,7 @@
56
65
  $block-class: 'csp-block',
57
66
  $not-last-class: 'csp-not-last',
58
67
  $utility-append-class: 'sp',
68
+ $padding-append-class: 'padding',
59
69
  $csp-blocks: (
60
70
  'text',
61
71
  'text-image',
@@ -101,9 +111,17 @@
101
111
  }
102
112
 
103
113
  .#{$section-class}.#{$not-last-class}:not(:has(+ .#{$section-class})),
114
+ .#{$section-class}.#{$section-class}-#{$padding-append-class},
115
+ .#{$section-class}:has(+ .#{$section-class}-#{$padding-append-class}),
104
116
  .#{$section-class}-#{$utility-append-class}.#{$not-last-class}:not(:has(+ .#{$section-class})) {
105
117
  margin-bottom: 0;
106
118
  }
119
+
120
+ .#{$section-class}-#{$padding-append-class},
121
+ .#{$section-class}-#{$padding-append-class}-#{$utility-append-class} {
122
+ padding-top: rem-calc($spacer-section);
123
+ padding-bottom: rem-calc($spacer-section);
124
+ }
107
125
  }
108
126
 
109
127
  // block-spacing
@@ -0,0 +1,141 @@
1
+
2
+ /* ==================================================
3
+ * Toggle
4
+ * ==================================================*/
5
+ /// Creates a button group container for toggles.
6
+ ///
7
+ /// Used to align toggle buttons and icons horizontally with spacing.
8
+ /// Useful in responsive navigation headers.
9
+ ///
10
+ /// @group Toggle
11
+ ///
12
+ /// @param {Length} $gap - The spacing between toggle elements. Defaults to `$spacer`.
13
+ ///
14
+ /// @example html
15
+ /// <div class="toggle-group">
16
+ /// <button class="toggle" aria-expanded="false">
17
+ /// <i class="toggle-icon icon-hamburger"></i>
18
+ /// </button>
19
+ /// </div>
20
+ ///
21
+ /// @example scss
22
+ /// .toggle-group {
23
+ /// @include toggle-group($gap: 1rem);
24
+ /// }
25
+ ///
26
+ /// @author Steffen Kroggel <developer@steffenkroggel>
27
+ /// @license GNU General Public License v3.0 https://www.gnu.org/licenses/gpl-3.0.en.html
28
+ ///
29
+ @mixin toggle-group(
30
+ $gap: 16px
31
+ ) {
32
+ display: flex;
33
+ align-items: center;
34
+ gap: rem-calc($gap);
35
+ height: 100%;
36
+ }
37
+
38
+
39
+ /// Creates a button element for toggling.
40
+ ///
41
+ /// Features:
42
+ /// - Accessible button with `aria-expanded` state
43
+ /// - Icon switching based on toggle state (e.g., hamburger to close)
44
+ /// - Configurable icon size and colors
45
+ /// - No default background or borders for flexible styling
46
+ /// - Built-in accessibility outline
47
+ ///
48
+ /// @group Toggle
49
+ ///
50
+ /// @param {Length} $icon-size - Size of the icon (width/height). Defaults to `$spacer`.
51
+ /// @param {Color} $icon-color - Icon color (e.g., fill or text color). Defaults to `$color-primary`.
52
+ /// @param {Map} $icon-mappings - Map of icon toggle states (e.g., hamburger → close). Defaults to a predefined map.
53
+ ///
54
+ /// @example html with icon font
55
+ /// <button class="toggle" aria-expanded="false">
56
+ /// <i class="toggle-icon icon-hamburger icon"></i>
57
+ /// </button>
58
+ ///
59
+ /// @example html with icon sprite
60
+ /// <button class="toggle" aria-expanded="false">
61
+ /// <i class="toggle-icon">
62
+ /// <svg class="toggle-icon-opened" aria-hidden="true" focusable="false">
63
+ /// <use href="sprite.svg#search"></use>
64
+ /// </svg>
65
+ /// <svg class="toggle-icon-closed" aria-hidden="true" focusable="false">
66
+ /// <use href="sprite.svg#close"></use>
67
+ /// </svg>
68
+ /// </i>
69
+ /// </button>
70
+ ///
71
+ ///
72
+ /// @example scss
73
+ /// .toggle {
74
+ /// @include toggle(
75
+ /// $icon-size: 1.5rem,
76
+ /// $icon-color: #333,
77
+ /// $icon-mappings: (
78
+ /// "icon-hamburger": "icon-close",
79
+ /// "icon-plus": "icon-minus"
80
+ /// )
81
+ /// );
82
+ /// }
83
+ ///
84
+ /// @author Steffen Kroggel <developer@steffenkroggel>
85
+ /// @license GNU General Public License v3.0 https://www.gnu.org/licenses/gpl-3.0.en.html
86
+ ///
87
+ @mixin toggle(
88
+ $icon-size: 16px,
89
+ $icon-color: var(--bs-primary),
90
+ $icon-mappings: (
91
+ "icon-hamburger": unquote('"\\e905"'),
92
+ "icon-plus": unquote('"\\e908"')
93
+ ),
94
+ $icon-selector: '.toggle-icon'
95
+ ) {
96
+ background-color: transparent;
97
+ border: 0;
98
+ margin: 0;
99
+ padding: 0;
100
+
101
+ &:has(#{$icon-selector}) {
102
+ display: flex;
103
+ align-items: center;
104
+
105
+ // variant with icons as font
106
+ &[aria-expanded="true"] {
107
+ @each $icon-class, $content in $icon-mappings {
108
+ .#{$icon-class} {
109
+ &:before {
110
+ content: $content;
111
+ }
112
+ }
113
+ }
114
+ }
115
+
116
+ // variant with icons in a sprite
117
+ #{$icon-selector}-opened {
118
+ display: none;
119
+ }
120
+
121
+ &[aria-expanded="true"] {
122
+ #{$icon-selector}-opened {
123
+ display: inline-block;
124
+ }
125
+ #{$icon-selector}-closed {
126
+ display: none;
127
+ }
128
+ }
129
+ }
130
+
131
+ #{$icon-selector}:not(:has(svg)),
132
+ #{$icon-selector} svg {
133
+ font-size: rem-calc($icon-size);
134
+ width: rem-calc($icon-size);
135
+ color: $icon-color;
136
+ }
137
+
138
+ #{$icon-selector} svg {
139
+ height: 100%;
140
+ }
141
+ }
@@ -7,5 +7,6 @@
7
7
  @import './nav';
8
8
  @import './page';
9
9
  @import './section';
10
+ @import './toggle';
10
11
  @import './toggle-list';
11
12
  @import './unit';
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Madj2kElementInViewport
3
+ *
4
+ * Adds a CSS class to any DOM element once it becomes fully (or partially) visible
5
+ * in the viewport using the IntersectionObserver API.
6
+ *
7
+ * - Purely CSS-triggered transitions via class
8
+ * - Reusable for any type of element (quotes, sections, etc.)
9
+ * - Fully configurable: class name, threshold, delay
10
+ * - Designed for CMS contexts with dynamically loaded content
11
+ *
12
+ * @author Steffen Kroggel <developer@steffenkroggel.de>
13
+ * @copyright 2025 Steffen Kroggel
14
+ * @version 1.0.0
15
+ * @license GNU General Public License v3.0
16
+ * @see https://www.gnu.org/licenses/gpl-3.0.en.html
17
+ *
18
+ * @example
19
+ * // Single element with defaults
20
+ * const el = document.querySelector('.js-animate-in');
21
+ * new Madj2kElementInViewport(el);
22
+ *
23
+ * @example
24
+ * // Single element with custom config
25
+ * new Madj2kElementInViewport(el, {
26
+ * visibleClass: 'is-visible',
27
+ * threshold: 0.75,
28
+ * delay: 200,
29
+ * debug: true
30
+ * });
31
+ *
32
+ * @example
33
+ * // Multiple elements
34
+ * document.querySelectorAll('.js-element-in-viewport').forEach((el) => {
35
+ * new Madj2kElementInViewport(el, {
36
+ * visibleClass: 'is-in-viewport',
37
+ * threshold: 1
38
+ * });
39
+ * });
40
+ */
41
+
42
+ class Madj2kElementInViewport {
43
+ config = {
44
+ visibleClass: 'is-in-viewport',
45
+ threshold: 0.5,
46
+ delay: 0,
47
+ debug: false
48
+ };
49
+
50
+ /**
51
+ * @param {HTMLElement} element - DOM element to observe
52
+ * @param {Object} config - configuration options
53
+ * @param {string} [config.visibleClass='in-viewport'] - class to apply when element is in view
54
+ * @param {number} [config.threshold=1.0] - how much of the element must be visible (0–1)
55
+ * @param {number} [config.delay=0] - optional delay before applying class (in ms)
56
+ * @param {boolean} [config.debug=false] - enable debug logs
57
+ */
58
+ constructor(element, config = {}) {
59
+ if (!(element instanceof HTMLElement)) {
60
+ console.warn('[Madj2kElementInViewport] No valid element provided.');
61
+ return;
62
+ }
63
+
64
+ this.element = element;
65
+ this.config = { ...this.config, ...config };
66
+
67
+ this._log('Initialized with config:', this.config);
68
+ this._observe();
69
+ }
70
+
71
+ /**
72
+ * Initializes the IntersectionObserver
73
+ * @private
74
+ */
75
+ _observe() {
76
+ const observer = new IntersectionObserver(
77
+ ([entry], observerInstance) => {
78
+ if (entry.isIntersecting && entry.intersectionRatio >= this.config.threshold) {
79
+ this._log('Element in viewport:', this.element);
80
+
81
+ if (this.config.delay > 0) {
82
+ setTimeout(() => this._activate(observerInstance), this.config.delay);
83
+ } else {
84
+ this._activate(observerInstance);
85
+ }
86
+ }
87
+ },
88
+ {
89
+ threshold: this.config.threshold
90
+ }
91
+ );
92
+
93
+ observer.observe(this.element);
94
+ }
95
+
96
+ /**
97
+ * Applies the visible class and stops observing
98
+ * @param {IntersectionObserver} observer
99
+ * @private
100
+ */
101
+ _activate(observer) {
102
+ this.element.classList.add(this.config.visibleClass);
103
+ observer.unobserve(this.element);
104
+ this._log(`Class "${this.config.visibleClass}" added.`);
105
+ }
106
+
107
+ /**
108
+ * Logs debug messages
109
+ * @param {...any} args
110
+ * @private
111
+ */
112
+ _log(...args) {
113
+ if (this.config.debug) {
114
+ console.log('[Madj2kElementInViewport]', ...args);
115
+ }
116
+ }
117
+ }
@@ -0,0 +1,2 @@
1
+ import Madj2kElementInViewport from './element-in-viewport-2.0';
2
+ export { Madj2kElementInViewport };
@@ -0,0 +1 @@
1
+ @forward './element-in-viewport-2.0';