profitlich-template-toolkit 0.1.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.
@@ -0,0 +1,124 @@
1
+ import './menu-toggle.scss';
2
+
3
+ export class MenuToggle {
4
+ static #instance;
5
+ #menuButton;
6
+ #menu;
7
+ #menuLinkClass;
8
+ #menuItemClass;
9
+ #scrollbarWidth;
10
+ #shiftElement;
11
+ #y;
12
+ #bodyClickHandler;
13
+ #resizeHandler;
14
+ #escapeHandler;
15
+ isActive;
16
+
17
+ constructor(menuButtonId, menuId, menuLinkClass, menuItemClass, shiftElementId) {
18
+ this.#menuButton = document.getElementById(menuButtonId);
19
+ this.#menu = document.getElementById(menuId);
20
+ this.#menuLinkClass = menuLinkClass;
21
+ this.#menuItemClass = menuItemClass;
22
+ this.#shiftElement = shiftElementId ? document.getElementById(shiftElementId) : null;
23
+ this.#scrollbarWidth = 0;
24
+ this.isActive = false;
25
+ this.#y = 0;
26
+
27
+ this.#bodyClickHandler = this.#onBodyClick.bind(this);
28
+ this.#resizeHandler = this.#onResize.bind(this);
29
+ this.#escapeHandler = this.#onEscape.bind(this);
30
+
31
+ this.#menuButton.addEventListener('click', (event) => {
32
+ event.preventDefault();
33
+ event.stopPropagation();
34
+ this.#toggleMenu();
35
+ });
36
+
37
+ this.#menu.addEventListener('click', (event) => {
38
+ if (this.isActive && event.target.matches(this.#menuLinkClass)) {
39
+ this.#toggleMenu();
40
+ }
41
+ });
42
+ }
43
+
44
+ static getInstance(menuButtonId, menuId, menuLinkClass, menuItemClass, shiftElementId) {
45
+ if (!MenuToggle.#instance) {
46
+ if (!menuButtonId || !menuId || !menuLinkClass || !menuItemClass) {
47
+ throw new Error("MenuToggle muss beim ersten Aufruf mit menuButtonId, menuId, menuLinkClass und menuItemClass initialisiert werden.");
48
+ }
49
+ MenuToggle.#instance = new MenuToggle(menuButtonId, menuId, menuLinkClass, menuItemClass, shiftElementId);
50
+ }
51
+ return MenuToggle.#instance;
52
+ }
53
+
54
+ #toggleMenu() {
55
+ if (this.isActive) {
56
+ this.isActive = false;
57
+ document.body.removeEventListener('click', this.#bodyClickHandler);
58
+ window.removeEventListener('resize', this.#resizeHandler);
59
+ this.#setBodyAttribute('data-menu-active', 'false');
60
+ if (this.#shiftElement) {
61
+ this.#shiftElement.style.marginRight = '';
62
+ this.#shiftElement.style.width = '';
63
+ }
64
+ document.body.style.paddingRight = '';
65
+ document.body.style.top = '';
66
+ window.scrollTo(0, this.#y);
67
+ this.#toggleEscape(false);
68
+ } else {
69
+ this.isActive = true;
70
+ document.body.addEventListener('click', this.#bodyClickHandler);
71
+ window.addEventListener('resize', this.#resizeHandler);
72
+ this.#scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
73
+ this.#y = window.scrollY;
74
+ this.#setBodyAttribute('data-menu-active', 'true');
75
+ document.body.style.paddingRight = `${this.#scrollbarWidth}px`;
76
+ document.body.style.top = `-${this.#y}px`;
77
+ if (this.#shiftElement) {
78
+ const marginOriginal = parseFloat(window.getComputedStyle(this.#menuButton).marginRight);
79
+ this.#shiftElement.style.marginRight = `${marginOriginal + this.#scrollbarWidth}px`;
80
+ this.#adjustShiftElementWidth();
81
+ }
82
+ this.#toggleEscape(true);
83
+ }
84
+ const event = new CustomEvent('eventMenuestatus', {
85
+ detail: { menueStatus: this.isActive }
86
+ });
87
+ document.dispatchEvent(event);
88
+ }
89
+
90
+ #adjustShiftElementWidth() {
91
+ const contentWidth = document.documentElement.clientWidth;
92
+ this.#shiftElement.style.width = `${contentWidth - this.#scrollbarWidth}px`;
93
+ }
94
+
95
+ #onBodyClick(event) {
96
+ if (this.isActive && !event.target.closest('.' + this.#menuItemClass)) {
97
+ this.#toggleMenu();
98
+ }
99
+ }
100
+
101
+ #onResize() {
102
+ if (this.#shiftElement) {
103
+ this.#adjustShiftElementWidth();
104
+ }
105
+ }
106
+
107
+ #setBodyAttribute(attr, value) {
108
+ document.body.setAttribute(attr, value);
109
+ }
110
+
111
+ #toggleEscape(status) {
112
+ if (status) {
113
+ document.addEventListener('keydown', this.#escapeHandler);
114
+ } else {
115
+ document.removeEventListener('keydown', this.#escapeHandler);
116
+ }
117
+ }
118
+
119
+ #onEscape(event) {
120
+ if (event.key === 'Escape') {
121
+ this.#toggleMenu();
122
+ }
123
+ }
124
+ }
@@ -0,0 +1,14 @@
1
+ body {
2
+
3
+ &[data-menu-active="true"] {
4
+ position: fixed;
5
+ width: 100%;
6
+
7
+ // Bedeckt das Menü nur einen Teil der Seite, ist also ein Teil der Seite weiterhin zu sehen,
8
+ // soll die Maus Interaktionen ausserhalb des Menüs ignorieren
9
+ .main {
10
+ pointer-events: none;
11
+ }
12
+ }
13
+
14
+ }
@@ -0,0 +1,106 @@
1
+ import '@mux/mux-player';
2
+ import './mux-player.scss';
3
+
4
+ export class MuxPlayer {
5
+ #lazyLoadObserver;
6
+ #playPauseObserver;
7
+
8
+ constructor() {
9
+ this.#lazyLoadObserver = new IntersectionObserver(this.#handleLazyLoad.bind(this));
10
+ this.#playPauseObserver = new IntersectionObserver(this.#handlePlayPause.bind(this));
11
+ }
12
+
13
+ observeLazyElements() {
14
+ document.querySelectorAll('.mux-player').forEach(el => {
15
+ this.#lazyLoadObserver.observe(el);
16
+ });
17
+ }
18
+
19
+ #handleLazyLoad(entries) {
20
+ for (const entry of entries) {
21
+ if (entry.isIntersecting) {
22
+ this.#lazyLoadObserver.unobserve(entry.target);
23
+ const container = entry.target;
24
+ const playbackId = container.dataset.playbackId;
25
+ const aspectRatio = container.dataset.aspectRatio || '16 / 9';
26
+ const autoplay = container.dataset.autoplay === "true";
27
+
28
+ const player = document.createElement('mux-player');
29
+ player.playbackId = playbackId;
30
+ player.streamType = 'on-demand';
31
+ player.playsInline = true;
32
+ player.style.aspectRatio = aspectRatio;
33
+
34
+ // Autoplay-Attribut am Player-Element setzen, damit es später gefunden werden kann
35
+ if (autoplay) {
36
+ player.setAttribute('data-autoplay', 'true');
37
+ }
38
+
39
+ const wrapper = container.closest('[data-video-wrapper]');
40
+
41
+ // Wenn ein Wrapper gefunden wird, richte den MutationObserver ein
42
+ if (wrapper) {
43
+ const visibilityObserver = new MutationObserver(() => {
44
+ this.#handleVisibilityChange(player);
45
+ });
46
+
47
+ // Beobachte den gefundenen Wrapper
48
+ visibilityObserver.observe(wrapper, { attributes: true, childList: true, subtree: true });
49
+
50
+ // Container durch Player ersetzen BEVOR die Sichtbarkeitsprüfung
51
+ container.replaceWith(player);
52
+
53
+ // Führe eine initiale Prüfung durch, falls es schon sichtbar ist
54
+ this.#handleVisibilityChange(player);
55
+ } else if (autoplay) {
56
+ // Fallback für normale Autoplay-Videos ohne speziellen Wrapper
57
+ player.muted = true;
58
+ player.style.setProperty('--controls', 'none');
59
+ container.replaceWith(player);
60
+
61
+ player.addEventListener('loadeddata', () => {
62
+ this.#playPauseObserver.observe(player);
63
+ });
64
+ } else {
65
+ // Für Videos ohne Autoplay
66
+ container.replaceWith(player);
67
+ }
68
+ }
69
+ }
70
+ }
71
+
72
+ #handleVisibilityChange(player) {
73
+ // Prüfe direkt am Player, ob er durch CSS sichtbar ist
74
+ const isVisible = window.getComputedStyle(player).display !== 'none';
75
+
76
+ if (isVisible && player.hasAttribute('data-autoplay')) {
77
+ // Wenn der Player sichtbar ist und Autoplay haben soll
78
+ player.muted = true;
79
+ player.style.setProperty('--controls', 'none');
80
+
81
+ // Warte auf loadeddata bevor der IntersectionObserver aktiviert wird
82
+ if (player.readyState >= 2) { // HAVE_CURRENT_DATA oder höher
83
+ this.#playPauseObserver.observe(player);
84
+ } else {
85
+ player.addEventListener('loadeddata', () => {
86
+ this.#playPauseObserver.observe(player);
87
+ }, { once: true });
88
+ }
89
+ } else if (!isVisible) {
90
+ // Wenn der Player unsichtbar wird, pausiere ihn und stoppe die Beobachtung
91
+ this.#playPauseObserver.unobserve(player);
92
+ player.pause();
93
+ }
94
+ }
95
+
96
+ #handlePlayPause(entries) {
97
+ for (const entry of entries) {
98
+ const player = entry.target;
99
+ if (entry.isIntersecting) {
100
+ player.play().catch(e => console.error("Player-Fehler:", e));
101
+ } else {
102
+ player.pause();
103
+ }
104
+ }
105
+ }
106
+ }
@@ -0,0 +1,3 @@
1
+ mux-player {
2
+ display: block;
3
+ }
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "profitlich-template-toolkit",
3
+ "version": "0.1.0",
4
+ "description": "Shared SCSS layout system, JS utilities and components for profitlich template repos",
5
+ "type": "module",
6
+ "exports": {
7
+ "./scss/core/layout": "./scss/core/layout.scss",
8
+ "./scss/core/mediaqueries": "./scss/core/mediaqueries.scss",
9
+ "./scss/core/hamburger": "./scss/core/hamburger.scss",
10
+ "./utils/MediaQueries": "./utils/MediaQueries.js",
11
+ "./utils/Vh100": "./utils/Vh100.js",
12
+ "./utils/BodyScrolled": "./utils/BodyScrolled.js",
13
+ "./components/menu-toggle/MenuToggle": "./components/menu-toggle/MenuToggle.js",
14
+ "./components/mux-player/MuxPlayer": "./components/mux-player/MuxPlayer.js"
15
+ },
16
+ "files": [
17
+ "scss/",
18
+ "utils/",
19
+ "components/"
20
+ ],
21
+ "peerDependencies": {
22
+ "@mux/mux-player": ">=3.0.0"
23
+ },
24
+ "peerDependenciesMeta": {
25
+ "@mux/mux-player": {
26
+ "optional": true
27
+ }
28
+ },
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "https://github.com/profitlich-ch/profitlich-template-toolkit"
32
+ },
33
+ "license": "MIT"
34
+ }
@@ -0,0 +1,153 @@
1
+ @mixin hamburger($hamburger-breite, $hamburger-hoehe, $hamburger-line-width, $hamburger-color, $hamburger-color-active) {
2
+
3
+ // Settings
4
+ // ==================================================
5
+ $hamburger-padding-x : 15px !default;
6
+ $hamburger-padding-y : 15px !default;
7
+ $hamburger-line-gap : calc(($hamburger-hoehe - 3 * $hamburger-line-width) / 2) !default;
8
+ $hamburger-color : #000 !default;
9
+ $hamburger-layer-border-radius : 0 !default;
10
+ $hamburger-hover-opacity : 1 !default;
11
+ $hamburger-active-layer-color : $hamburger-color-active !default;
12
+ $hamburger-active-hover-opacity: $hamburger-hover-opacity !default;
13
+ $hamburger-animation-timing: 0.195s;
14
+ $hamburger-compression-speed: 0.12s;
15
+ $hamburger-turn-speed: $hamburger-animation-timing - $hamburger-compression-speed;
16
+
17
+ // To use CSS filters as the hover effect instead of opacity,
18
+ // set $hamburger-hover-use-filter as true and
19
+ // change the value of $hamburger-hover-filter accordingly.
20
+ $hamburger-hover-use-filter : false !default;
21
+ $hamburger-hover-filter : opacity(50%) !default;
22
+ $hamburger-active-hover-filter: $hamburger-hover-filter !default;
23
+
24
+
25
+ // padding: $hamburger-padding-y $hamburger-padding-x;
26
+
27
+
28
+ &:hover {
29
+ @if $hamburger-hover-use-filter==true {
30
+ filter: $hamburger-hover-filter;
31
+ }
32
+
33
+ @else {
34
+ opacity: $hamburger-hover-opacity;
35
+ }
36
+ }
37
+
38
+ body[data-menu-active='true'] & {
39
+ &:hover {
40
+ @if $hamburger-hover-use-filter==true {
41
+ filter: $hamburger-active-hover-filter;
42
+ }
43
+
44
+ @else {
45
+ opacity: $hamburger-active-hover-opacity;
46
+ }
47
+ }
48
+
49
+ .hamburger-inner,
50
+ .hamburger-inner::before,
51
+ .hamburger-inner::after {
52
+ background-color: $hamburger-active-layer-color;
53
+ transition: 0s $hamburger-compression-speed linear;
54
+ }
55
+ }
56
+
57
+ .hamburger-box {
58
+ width: $hamburger-breite;
59
+ height: calc($hamburger-line-width * 3 + $hamburger-line-gap * 2);
60
+ display: inline-block;
61
+ position: relative;
62
+ }
63
+
64
+ .hamburger-inner {
65
+ display: block;
66
+ top: 50%;
67
+ margin-top: calc($hamburger-line-width / -2);
68
+
69
+ &,
70
+ &::before,
71
+ &::after {
72
+ width: $hamburger-breite;
73
+ height: $hamburger-line-width;
74
+ background-color: $hamburger-color;
75
+ border-radius: $hamburger-layer-border-radius;
76
+ position: absolute;
77
+ transition-property: transform;
78
+ transition-duration: 0.15s;
79
+ transition-timing-function: ease;
80
+ }
81
+
82
+ &::before,
83
+ &::after {
84
+ content: "";
85
+ display: block;
86
+ }
87
+
88
+ &::before {
89
+ top: calc(($hamburger-line-gap + $hamburger-line-width) * -1);
90
+ }
91
+
92
+ &::after {
93
+ bottom: calc(($hamburger-line-gap + $hamburger-line-width) * -1);
94
+ }
95
+ }
96
+
97
+
98
+
99
+ // Aus Basis
100
+ display: block;
101
+ cursor: pointer;
102
+
103
+ transition-property: opacity, filter;
104
+ transition-duration: 0.15s;
105
+ transition-timing-function: linear;
106
+
107
+ // Normalize (<button>)
108
+ font: inherit;
109
+ color: inherit;
110
+ text-transform: none;
111
+ background-color: transparent;
112
+ border: 0;
113
+ margin: 0;
114
+ overflow: visible;
115
+ line-height: 0;
116
+
117
+ .hamburger-inner {
118
+ transition-duration: $hamburger-turn-speed;
119
+ transition-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);
120
+
121
+ &::before {
122
+ transition: top $hamburger-turn-speed $hamburger-compression-speed ease,
123
+ opacity $hamburger-turn-speed ease;
124
+ }
125
+
126
+ &::after {
127
+ transition: bottom $hamburger-turn-speed $hamburger-compression-speed ease,
128
+ transform $hamburger-turn-speed cubic-bezier(0.55, 0.055, 0.675, 0.19);
129
+ }
130
+ }
131
+
132
+ body[data-menu-active='true'] & {
133
+ .hamburger-inner {
134
+ transform: rotate(45deg);
135
+ transition-delay: $hamburger-compression-speed;
136
+ transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
137
+
138
+ &::before {
139
+ top: 0;
140
+ opacity: 0;
141
+ transition: top $hamburger-turn-speed ease,
142
+ opacity $hamburger-turn-speed $hamburger-compression-speed ease;
143
+ }
144
+
145
+ &::after {
146
+ bottom: 0;
147
+ transform: rotate(-90deg);
148
+ transition: bottom $hamburger-turn-speed ease,
149
+ transform $hamburger-turn-speed $hamburger-compression-speed cubic-bezier(0.215, 0.61, 0.355, 1);
150
+ }
151
+ }
152
+ }
153
+ }
@@ -0,0 +1,239 @@
1
+ @use "sass:list";
2
+ @use "sass:map";
3
+ @use "sass:meta";
4
+ @use "sass:math";
5
+
6
+ @use "config";
7
+
8
+ // Media queries get the breakpoints
9
+ // Breakpints get the layouts
10
+ // Depending on the project, only a part of the layout names are used,
11
+ // so there needs to be an intervention in the breakpoints to deliver a number even if a layout is not present.
12
+ // Example: There are only the layouts smartphone, tablet and desktop.
13
+ // Then tabletPortrait must deliver the value of tablet
14
+
15
+ $tabletPortrait: null;
16
+ @if map.get(config.$layouts, tabletPortrait) {
17
+ $tabletPortrait: tabletPortrait;
18
+ // no tabletPortrait size is defined
19
+ } @else {
20
+ @if map.get(config.$layouts, tablet) {
21
+ $tabletPortrait: tablet;
22
+ // neither tablet size is defined
23
+ } @else {
24
+ $tabletPortrait: desktop;
25
+ }
26
+ }
27
+
28
+ $bigFrom: null;
29
+ @if map.get(config.$layouts, big) {
30
+ $bigFrom: big;
31
+ } @else {
32
+ $bigFrom: 999999px;
33
+ }
34
+
35
+ $breakpointsVariables: (
36
+ smartphoneTo: map.get(config.$breakpoints, $tabletPortrait) - 1px,
37
+ tabletPortraitFrom: map.get(config.$breakpoints, $tabletPortrait) + 0px,
38
+ tabletPortraitTo: map.get(config.$breakpoints, tabletLandscape) - 1px,
39
+ tabletLandscapeFrom:map.get(config.$breakpoints, tabletLandscape) + 0px,
40
+ tabletLandscapeTo: map.get(config.$breakpoints, desktop) - 1px,
41
+ desktopFrom: map.get(config.$breakpoints, desktop) + 0px,
42
+ desktopTo: map.get(config.$breakpoints, big) - 1px,
43
+ bigFrom: map.get(config.$breakpoints, big) + 0px,
44
+ );
45
+
46
+ // Gibt es kein Layout big, soll die media query desktop keine max-width haben
47
+ $mediaqueryDesktop: null;
48
+ @if map.get(config.$layouts, big) {
49
+ $mediaqueryDesktop: (min-width: map.get($breakpointsVariables, desktopFrom), max-width: map.get($breakpointsVariables, desktopTo));
50
+ } @else {
51
+ $mediaqueryDesktop: (min-width: map.get($breakpointsVariables, desktopFrom));
52
+ }
53
+
54
+ $mediaqueries: (
55
+ smartphone: (max-width: map.get($breakpointsVariables, smartphoneTo)),
56
+ tablet: (min-width: map.get($breakpointsVariables, tabletPortraitFrom), max-width: map.get($breakpointsVariables, tabletLandscapeTo)),
57
+ tabletFrom: (min-width: map.get($breakpointsVariables, tabletPortraitFrom)),
58
+ tabletTo: (max-width: map.get($breakpointsVariables, tabletLandscapeTo)),
59
+ tabletPortraitFrom: (min-width: map.get($breakpointsVariables, tabletPortraitFrom)),
60
+ tabletPortrait: (min-width: map.get($breakpointsVariables, tabletPortraitFrom), max-width: map.get($breakpointsVariables, tabletPortraitTo)),
61
+ tabletPortraitTo: (max-width: map.get($breakpointsVariables, tabletPortraitTo)),
62
+ tabletLandscapeFrom:(min-width: map.get($breakpointsVariables, tabletLandscapeFrom)),
63
+ tabletLandscape: (min-width: map.get($breakpointsVariables, tabletLandscapeFrom), max-width: map.get($breakpointsVariables, tabletLandscapeTo)),
64
+ tabletLandscapeTo: (max-width: map.get($breakpointsVariables, tabletLandscapeTo)),
65
+ desktopFrom: (min-width: map.get($breakpointsVariables, desktopFrom)),
66
+ desktop: $mediaqueryDesktop,
67
+ desktopTo: (max-width: map.get($breakpointsVariables, desktopTo)),
68
+ big: (min-width: map.get($breakpointsVariables, bigFrom)),
69
+ );
70
+
71
+
72
+ // Font
73
+ @mixin font($layout, $fontSize, $lineHeight, $bold:'false') {
74
+
75
+ font-size: size($layout, $fontSize);
76
+ line-height: size($layout, $lineHeight);
77
+
78
+ @if ($bold != 'false') {
79
+ font-weight: $bold;
80
+ }
81
+
82
+ }
83
+
84
+
85
+ // columns
86
+ // Calculates the width (as a fraction of 100%)
87
+ // Per default the function assumes that the given space does not contain column gaps;
88
+ // with the variable $columnGapsAddedBasis they can be specified.
89
+ // $columnGapsAddedBasis is to be used when the space to be calculated contains additional column gaps outside the $columnsBasis.
90
+ // $columnGapsAdditional is to be used when additionally to the $columns column gaps are to be added (e.g left and/or right).
91
+ // $shift is for adding a value to the result, for example a margin
92
+ // $inverse is for calculating the width as a negative number, for example for a negative shift in a stage system
93
+ @function columns($layout, $columns, $columnsBasis, $columnGapsAddedBasis:0, $columnGapsAdditional:0, $margin:0, $shift:0, $invers:false) {
94
+ $columnGap: size($layout, map.get(config.$gutter, $layout) );
95
+ $margin: size($layout, $margin);
96
+ $shift: size($layout, $shift);
97
+ @if ( $invers == false ) {
98
+ $invers: 1;
99
+ } @else {
100
+ $invers: -1;
101
+ }
102
+
103
+ // available space
104
+ // minus column gaps within the space
105
+ // equals space for content
106
+ // divided by number of columns in space
107
+ // equals width of each column
108
+ // times number of desired columns
109
+ // equals width of desired columns
110
+ // plus column gaps between desired columns
111
+ // equals width of desired columns including gaps
112
+ // plus additional column gaps
113
+ @return calc(
114
+ (
115
+ (
116
+ // available space
117
+ 100%
118
+ // all column gaps in the given space
119
+ - $margin - $columnGap * ($columnsBasis + $columnGapsAddedBasis - 1)
120
+ )
121
+ // number of columns in the given space
122
+ / $columnsBasis
123
+ // desired columns
124
+ * ($columns * $invers)
125
+ // columns gaps between desired columns plus additional column gaps
126
+ // inverse to get a negative value
127
+ + ($columnGap * ($columns - 1 + $columnGapsAdditional) * $invers)
128
+ // Shift
129
+ + ($shift * $invers)
130
+ )
131
+ );
132
+ }
133
+
134
+
135
+
136
+ // Size
137
+ // converts a number to a size in px or vw
138
+ // depending on the unit set for the layout
139
+ // accepts single value or a list of values
140
+ @function size($layout, $numbers) {
141
+ // Check whether second argument is a number or a list
142
+ @if (meta.type-of($numbers) == 'list') {
143
+ $result-list: (); // Empty list to store results
144
+
145
+ // Iterate over each element in the list
146
+ @each $number in $numbers {
147
+ // Test if the current element is a number
148
+ @if (meta.type-of($number) == 'number') {
149
+ // add calculated value to the result list
150
+ $result-list: list.append($result-list, _calculate-single-size($number, $layout));
151
+ } @else {
152
+ // Error if the current element is not a number
153
+ @error "Listenelement '#{$number}' übergeben an size() ist keine Zahl. Alle Elemente müssen Zahlen sein.";
154
+ }
155
+ }
156
+ @return $result-list; // Return the list of calculated sizes
157
+ } @else if (meta.type-of($numbers) == 'number') {
158
+ // If second argument is a single number, calculate and return the size
159
+ @return _calculate-single-size($numbers, $layout);
160
+ } @else {
161
+ // Error if the second argument is neither a number nor a list
162
+ @error "Das zweite Argument der Funktion size() muss eine Zahl oder eine Liste von Zahlen sein. Empfangen: #{type-of($numbers)} '#{$numbers}'.";
163
+ }
164
+ }
165
+
166
+ // Round a number to specified decimal places
167
+ @function _decimal-round($number, $digits: 0) {
168
+ $n: math.pow(10, $digits);
169
+ @return math.div(math.round($number * $n), $n);
170
+ }
171
+
172
+ // Helper function to calculate a single size value (px or vw)
173
+ // This function is used by the main size() function
174
+ @function _calculate-single-size($number, $layout) {
175
+ // Get the width and unit type from the config
176
+ $width: map.get(config.$layouts, $layout);
177
+ $unit-type: map.get(config.$units, $layout);
178
+
179
+ // Error handling for missing width or unit type
180
+ @if ($width == null) {
181
+ @error "Layout '#{$layout}' ist nicht in 'config.$layouts' definiert.";
182
+ }
183
+ @if ($unit-type == null) {
184
+ @error "Der Einheitentyp für Layout '#{$layout}' ist nicht in 'config.$units' definiert.";
185
+ }
186
+
187
+
188
+ @if ($unit-type == 'px') {
189
+ @return $number * 1px;
190
+ } @else if ($unit-type == 'vw') {
191
+ @return _decimal-round(math.div($number, $width) * 100, 2) * 1vw;
192
+ } @else {
193
+ @error "Nicht unterstützter Einheitstyp '#{$unit-type}' für Layout '#{$layout}'. Nur 'px' oder 'vw' werden unterstützt.";
194
+ }
195
+ }
196
+
197
+
198
+ // Distance
199
+ @mixin marginPadding($layout, $mode, $position, $top: 0, $right: 0, $bottom: 0, $left: 0) {
200
+
201
+ // @include mode($layout, padding/margin, all/top/right/bottom/left, 0, 0, 0, 0)
202
+
203
+ @if ( map.get(config.$units, $layout) == 'px' ) {
204
+ $top: $top + px;
205
+ $right: $right + px;
206
+ $bottom: $bottom + px;
207
+ $left: $left + px;
208
+ } @else {
209
+
210
+ @if meta.type-of($top) == 'number'{
211
+ $top: size($layout, $top);
212
+ }
213
+ @if meta.type-of($right) == 'number'{
214
+ $right: size($layout, $right);
215
+ }
216
+ @if meta.type-of($bottom) == 'number'{
217
+ $bottom: size($layout, $bottom);
218
+ }
219
+ @if meta.type-of($left) == 'number'{
220
+ $left: size($layout, $left);
221
+ }
222
+ }
223
+
224
+ @if $position == all {
225
+ #{$mode}: $top $right $bottom $left;
226
+ }
227
+ @if $position == top {
228
+ #{$mode}-top: $top;
229
+ }
230
+ @if $position == right {
231
+ #{$mode}-right: $right;
232
+ }
233
+ @if $position == bottom {
234
+ #{$mode}-bottom: $bottom;
235
+ }
236
+ @if $position == left {
237
+ #{$mode}-left: $left;
238
+ }
239
+ }
@@ -0,0 +1,42 @@
1
+ @use "layout";
2
+
3
+ @use "sass:map";
4
+ @use "sass:list";
5
+
6
+ // Zusammenlegen aller Meda Queries in eine
7
+ // https://www.sitepoint.com/sass-mixin-media-merging
8
+
9
+ @mixin mediaquery($mediaquery) {
10
+ // Hole die map $mediaquery aus der map $mediaqueries
11
+ $queries: map.get(layout.$mediaqueries, $mediaquery);
12
+
13
+ // Wenn es keine $mediaquery in der map gibt, gebe einen Fehler aus
14
+ @if not $queries {
15
+ @error "No value could be retrieved from `#{$mediaquery}`. "
16
+ + "Please make sure it is defined in `$mediaqueries` map.";
17
+ }
18
+
19
+ // Include the media mixin with $queries
20
+ @include media($queries) {
21
+ @content($mediaquery);
22
+ }
23
+ }
24
+
25
+ @mixin media($queries) {
26
+ // Erst wenn alle mediaqueries geschachtelt geschrieben sind,
27
+ // wird der @content ausgegeben
28
+ @if list.length($queries) == 0 {
29
+ @content;
30
+ } @else {
31
+ $first-key: list.nth(map.keys($queries), 1);
32
+
33
+ // Schreibt nur die mediaquery, nicht den Inhalt
34
+ @media ($first-key: map.get($queries, $first-key)) {
35
+ $queries: map.remove($queries, $first-key);
36
+
37
+ @include media($queries) {
38
+ @content;
39
+ }
40
+ }
41
+ }
42
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * after scroll set body attribute data-scrolled to true
3
+ */
4
+
5
+ export class BodyScrolled {
6
+ static #instance;
7
+
8
+ constructor() {
9
+ document.addEventListener('scroll', function setScrolled() {
10
+ document.body.setAttribute('data-body-scrolled', 'true');
11
+ this.removeEventListener('scroll', setScrolled);
12
+
13
+ // create event
14
+ let event = new CustomEvent('eventBodyScrolled', {
15
+ detail: {
16
+ scrolled: true
17
+ }
18
+ });
19
+ // dispatch the event
20
+ window.dispatchEvent(event);
21
+ });
22
+
23
+ }
24
+
25
+ static getInstance() {
26
+ if (!BodyScrolled.#instance) {
27
+ BodyScrolled.#instance = new BodyScrolled();
28
+ }
29
+ return BodyScrolled.#instance;
30
+ }
31
+ }
@@ -0,0 +1,56 @@
1
+ /**Media queries
2
+ * https://kinsta.com/blog/javamediaqueryipt-media-query/
3
+ * option 3 on the linked page
4
+ */
5
+
6
+ export class MediaQueries {
7
+ static #instance;
8
+ layout = 'desktop';
9
+ #breakpoints;
10
+
11
+ constructor(breakpoints) {
12
+ this.#breakpoints = breakpoints;
13
+ this.layout = 'desktop';
14
+ this.changeLayout();
15
+ this.#matchmedia();
16
+ }
17
+
18
+ static getInstance(breakpoints) {
19
+ if (!MediaQueries.#instance) {
20
+ if (!breakpoints) {
21
+ throw new Error("MediaQueries muss beim ersten Aufruf mit breakpoints initialisiert werden.");
22
+ }
23
+ MediaQueries.#instance = new MediaQueries(breakpoints);
24
+ }
25
+ return MediaQueries.#instance;
26
+ }
27
+
28
+ #matchmedia() {
29
+ for (let [layout, minSize] of Object.entries(this.#breakpoints)) {
30
+ if (minSize) {
31
+ var matchmedia = window.matchMedia('(min-width: ' + minSize + 'px)');
32
+ matchmedia.addEventListener('change', (e) => this.changeLayout());
33
+ }
34
+ }
35
+ }
36
+
37
+ // media query handler function
38
+ changeLayout() {
39
+ for (let [layout, minSize] of Object.entries(this.#breakpoints)) {
40
+ var matchmedia = window.matchMedia('(min-width: ' + minSize + 'px)');
41
+ if (!matchmedia || matchmedia.matches) {
42
+ this.layout = layout;
43
+ }
44
+ }
45
+ document.body.setAttribute('data-layout', this.layout);
46
+
47
+ // create event
48
+ let event = new CustomEvent('eventLayoutchange', {
49
+ detail: {
50
+ layout: this.layout
51
+ }
52
+ });
53
+ // dispatch the event
54
+ window.dispatchEvent(event);
55
+ }
56
+ }
package/utils/Vh100.js ADDED
@@ -0,0 +1,31 @@
1
+ /**
2
+ * 100vh problem
3
+ * https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
4
+ */
5
+
6
+ export class Vh100 {
7
+ static #instance;
8
+ vh = 0;
9
+
10
+ constructor() {
11
+ this.#calculate();
12
+ // We listen to the resize event
13
+ window.addEventListener('resize', () => {
14
+ this.#calculate();
15
+ });
16
+ }
17
+
18
+ #calculate() {
19
+ // First we get the viewport height and we multiple it by 1% to get a value for a vh unit
20
+ this.vh = window.innerHeight * 0.01;
21
+ // Then we set the value in the --vh custom property to the root of the document
22
+ document.documentElement.style.setProperty('--vh', `${this.vh}px`);
23
+ }
24
+
25
+ static getInstance() {
26
+ if (!Vh100.#instance) {
27
+ Vh100.#instance = new Vh100();
28
+ }
29
+ return Vh100.#instance;
30
+ }
31
+ }