@profitlich/template-toolkit 4.2.0 → 5.2.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/components/menu-toggle/MenuToggle.js +49 -34
- package/components/menu-toggle/menu-toggle.scss +9 -13
- package/components/mux-player/MuxPlayer.js +13 -0
- package/dev/Dev.js +2 -1
- package/dev/toolbar/Toolbar.js +4 -0
- package/package.json +2 -2
- package/scripts/copy-files.js +23 -3
- package/scripts/deploy.js +25 -2
- package/utils/BodyScrolled.js +6 -2
- package/utils/MailAdresses.js +12 -0
- package/utils/MediaQueries.js +16 -3
- package/utils/Vh100.js +6 -2
- package/utils/VwBody.js +4 -0
- package/utils/images-loaded/ImagesLoaded.js +5 -0
- package/vite/capsizeSassFunctions.js +1 -1
- package/vite/jsonToScss.js +9 -0
|
@@ -1,16 +1,30 @@
|
|
|
1
1
|
import './menu-toggle.scss';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {Object} MenuToggleOptions
|
|
5
|
+
* @property {string} menuButtonSelector CSS-Selektor (`querySelector`) des Buttons, der das Menü öffnet/schliesst.
|
|
6
|
+
* @property {string} menuSelector CSS-Selektor (`querySelector`) des Menü-Containers.
|
|
7
|
+
* @property {string} menuLinkSelector CSS-Selektor für Menü-Links, die das Menü beim Klick schliessen (z.B. '.menu-link').
|
|
8
|
+
* @property {string} menuItemSelector CSS-Selektor des Menü-Wrappers; Klicks ausserhalb schliessen das Menü (z.B. '.menu').
|
|
9
|
+
* @property {string} [shiftElementSelector] Optional: CSS-Selektor des Elements, das beim Öffnen um die Scrollbar-Breite verschoben/verbreitert wird, damit z.B. ein fixierter Header nicht springt.
|
|
10
|
+
* @property {number} [shiftDelay=0] Verzögerung in Sekunden, bevor Scrollbar gemessen und Body fixiert wird – nützlich, wenn vorher noch eine CSS-Animation läuft, die die Scrollbar entfernt.
|
|
11
|
+
* @property {boolean} [deferPositionFixed=false] Setzt `data-menu-fixed` erst nach `shiftDelay` statt sofort. Nötig, wenn das Fixieren eine laufende Öffnungs-Animation stören würde.
|
|
12
|
+
* @property {boolean} [lockScroll=true] Wenn `false`, bleibt das fixierende Element scrollbar; kein Scrollbar-Ausgleich und kein Shift. Für Menüs, die nur einen Teil der Seite bedecken und Hintergrund-Scroll erlauben sollen.
|
|
13
|
+
* @property {string} [fixElementSelector] Optional: CSS-Selektor des Elements, das beim Öffnen fixiert wird (`position: fixed`, Scrollbar-Ausgleich, Scroll-Position-Trick). Default: `document.body`.
|
|
14
|
+
*/
|
|
15
|
+
|
|
3
16
|
export class MenuToggle {
|
|
4
17
|
static #instance;
|
|
5
18
|
#menuButton;
|
|
6
19
|
#menu;
|
|
7
20
|
#menuLinkSelector;
|
|
8
|
-
#
|
|
21
|
+
#menuItemSelector;
|
|
9
22
|
#scrollbarWidth;
|
|
10
23
|
#shiftElement;
|
|
11
24
|
#shiftDelay;
|
|
12
25
|
#deferPositionFixed;
|
|
13
|
-
#
|
|
26
|
+
#lockScroll;
|
|
27
|
+
#fixElement;
|
|
14
28
|
#y;
|
|
15
29
|
#bodyClickHandler;
|
|
16
30
|
#resizeHandler;
|
|
@@ -18,34 +32,28 @@ export class MenuToggle {
|
|
|
18
32
|
isActive;
|
|
19
33
|
|
|
20
34
|
/**
|
|
21
|
-
* @param {
|
|
22
|
-
* @param {string} options.menuButtonId ID des Buttons, der das Menü öffnet/schliesst.
|
|
23
|
-
* @param {string} options.menuId ID des Menü-Containers.
|
|
24
|
-
* @param {string} options.menuLinkSelector Selektor für Menü-Links, die das Menü beim Klick schliessen (z.B. '.menu-link').
|
|
25
|
-
* @param {string} options.menuItemClass Klassenname (ohne Punkt) des Menü-Wrappers; Klicks ausserhalb schliessen das Menü.
|
|
26
|
-
* @param {string?} options.shiftElementId Optional: Element, das beim Öffnen um die Scrollbar-Breite verschoben/verbreitert wird, damit z.B. ein fixierter Header nicht springt.
|
|
27
|
-
* @param {number} options.shiftDelay Verzögerung in Sekunden, bevor Scrollbar gemessen und Body fixiert wird – nützlich, wenn vorher noch eine CSS-Animation läuft, die die Scrollbar entfernt.
|
|
28
|
-
* @param {boolean} options.deferPositionFixed Setzt `data-menu-fixed` erst nach `shiftDelay` statt sofort. Nötig, wenn das Fixieren eine laufende Öffnungs-Animation stören würde.
|
|
29
|
-
* @param {boolean} options.fixBody Wenn `false`, bleibt der Body scrollbar; kein Scrollbar-Ausgleich und kein Shift. Für Menüs, die nur einen Teil der Seite bedecken und Hintergrund-Scroll erlauben sollen.
|
|
35
|
+
* @param {MenuToggleOptions} options
|
|
30
36
|
*/
|
|
31
37
|
constructor({
|
|
32
|
-
|
|
33
|
-
|
|
38
|
+
menuButtonSelector,
|
|
39
|
+
menuSelector,
|
|
34
40
|
menuLinkSelector,
|
|
35
|
-
|
|
36
|
-
|
|
41
|
+
menuItemSelector,
|
|
42
|
+
shiftElementSelector = null,
|
|
37
43
|
shiftDelay = 0,
|
|
38
44
|
deferPositionFixed = false,
|
|
39
|
-
|
|
45
|
+
lockScroll = true,
|
|
46
|
+
fixElementSelector = null,
|
|
40
47
|
}) {
|
|
41
|
-
this.#menuButton = document.
|
|
42
|
-
this.#menu = document.
|
|
48
|
+
this.#menuButton = document.querySelector(menuButtonSelector);
|
|
49
|
+
this.#menu = document.querySelector(menuSelector);
|
|
43
50
|
this.#menuLinkSelector = menuLinkSelector;
|
|
44
|
-
this.#
|
|
45
|
-
this.#shiftElement =
|
|
51
|
+
this.#menuItemSelector = menuItemSelector;
|
|
52
|
+
this.#shiftElement = shiftElementSelector ? document.querySelector(shiftElementSelector) : null;
|
|
46
53
|
this.#shiftDelay = shiftDelay * 1000;
|
|
47
54
|
this.#deferPositionFixed = deferPositionFixed;
|
|
48
|
-
this.#
|
|
55
|
+
this.#lockScroll = lockScroll;
|
|
56
|
+
this.#fixElement = fixElementSelector ? document.querySelector(fixElementSelector) : document.body;
|
|
49
57
|
this.#scrollbarWidth = 0;
|
|
50
58
|
this.isActive = false;
|
|
51
59
|
this.#y = 0;
|
|
@@ -68,13 +76,20 @@ export class MenuToggle {
|
|
|
68
76
|
|
|
69
77
|
this.#setBodyAttribute('data-menu-active', 'false');
|
|
70
78
|
this.#setBodyAttribute('data-menu-moving', 'false');
|
|
71
|
-
this.#
|
|
79
|
+
this.#fixElement.setAttribute('data-menu-fixed', 'false');
|
|
72
80
|
}
|
|
73
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Holt die Singleton-Instanz. Beim ersten Aufruf müssen die Pflicht-Optionen
|
|
84
|
+
* (`menuButtonSelector`, `menuSelector`, `menuLinkSelector`, `menuItemSelector`)
|
|
85
|
+
* gesetzt sein; spätere Aufrufe ignorieren übergebene Optionen.
|
|
86
|
+
* @param {MenuToggleOptions} [options]
|
|
87
|
+
* @returns {MenuToggle}
|
|
88
|
+
*/
|
|
74
89
|
static getInstance(options) {
|
|
75
90
|
if (!MenuToggle.#instance) {
|
|
76
|
-
if (!options || !options.
|
|
77
|
-
throw new Error("MenuToggle muss beim ersten Aufruf mit
|
|
91
|
+
if (!options || !options.menuButtonSelector || !options.menuSelector || !options.menuLinkSelector || !options.menuItemSelector) {
|
|
92
|
+
throw new Error("MenuToggle muss beim ersten Aufruf mit menuButtonSelector, menuSelector, menuLinkSelector und menuItemSelector initialisiert werden.");
|
|
78
93
|
}
|
|
79
94
|
MenuToggle.#instance = new MenuToggle(options);
|
|
80
95
|
}
|
|
@@ -87,14 +102,14 @@ export class MenuToggle {
|
|
|
87
102
|
document.body.removeEventListener('click', this.#bodyClickHandler);
|
|
88
103
|
window.removeEventListener('resize', this.#resizeHandler);
|
|
89
104
|
this.#setBodyAttribute('data-menu-active', 'false');
|
|
90
|
-
if (this.#
|
|
91
|
-
this.#
|
|
105
|
+
if (this.#lockScroll) {
|
|
106
|
+
this.#fixElement.setAttribute('data-menu-fixed', 'false');
|
|
92
107
|
if (this.#shiftElement) {
|
|
93
108
|
this.#shiftElement.style.marginRight = '';
|
|
94
109
|
this.#shiftElement.style.width = '';
|
|
95
110
|
}
|
|
96
|
-
|
|
97
|
-
|
|
111
|
+
this.#fixElement.style.paddingRight = '';
|
|
112
|
+
this.#fixElement.style.top = '';
|
|
98
113
|
window.scrollTo(0, this.#y);
|
|
99
114
|
}
|
|
100
115
|
this.#toggleEscape(false);
|
|
@@ -102,19 +117,19 @@ export class MenuToggle {
|
|
|
102
117
|
this.isActive = true;
|
|
103
118
|
document.body.addEventListener('click', this.#bodyClickHandler);
|
|
104
119
|
this.#setBodyAttribute('data-menu-active', 'true');
|
|
105
|
-
if (this.#
|
|
120
|
+
if (this.#lockScroll) {
|
|
106
121
|
window.addEventListener('resize', this.#resizeHandler);
|
|
107
122
|
this.#setBodyAttribute('data-menu-moving', 'true');
|
|
108
123
|
if (!this.#deferPositionFixed) {
|
|
109
|
-
this.#
|
|
124
|
+
this.#fixElement.setAttribute('data-menu-fixed', 'true');
|
|
110
125
|
}
|
|
111
126
|
setTimeout(() => {
|
|
112
127
|
this.#scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
|
|
113
128
|
this.#y = window.scrollY;
|
|
114
|
-
|
|
115
|
-
|
|
129
|
+
this.#fixElement.style.paddingRight = `${this.#scrollbarWidth}px`;
|
|
130
|
+
this.#fixElement.style.top = `-${this.#y}px`;
|
|
116
131
|
if (this.#deferPositionFixed) {
|
|
117
|
-
this.#
|
|
132
|
+
this.#fixElement.setAttribute('data-menu-fixed', 'true');
|
|
118
133
|
}
|
|
119
134
|
if (this.#shiftElement) {
|
|
120
135
|
const marginOriginal = parseFloat(window.getComputedStyle(this.#menuButton).marginRight);
|
|
@@ -138,7 +153,7 @@ export class MenuToggle {
|
|
|
138
153
|
}
|
|
139
154
|
|
|
140
155
|
#onBodyClick(event) {
|
|
141
|
-
if (this.isActive && !event.target.closest(
|
|
156
|
+
if (this.isActive && !event.target.closest(this.#menuItemSelector)) {
|
|
142
157
|
this.#toggleMenu();
|
|
143
158
|
}
|
|
144
159
|
}
|
|
@@ -1,16 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
width: 100%;
|
|
6
|
-
}
|
|
1
|
+
[data-menu-fixed="true"] {
|
|
2
|
+
position: fixed;
|
|
3
|
+
width: 100%;
|
|
4
|
+
}
|
|
7
5
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
6
|
+
body[data-menu-active="true"] {
|
|
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;
|
|
14
11
|
}
|
|
15
|
-
|
|
16
12
|
}
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import '@mux/mux-player';
|
|
2
2
|
import './mux-player.scss';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {Object} MuxPlayerOptions
|
|
6
|
+
* @property {boolean} [loop=false] Wiederholt das Video nach Ende.
|
|
7
|
+
* @property {boolean} [noLowRes=false] Setzt `minResolution` anhand der physischen Container-Grösse, damit auf hochauflösenden Displays keine zu kleine Auflösung gespielt wird.
|
|
8
|
+
* @property {((player: HTMLElement, container: HTMLElement) => void)} [onPlayerCreated] Callback, der nach jedem Lazy-Load-Setup aufgerufen wird (unabhängig von Autoplay).
|
|
9
|
+
* @property {boolean} [disableTracking=false] Setzt das `disable-tracking`-Attribut am Player — verhindert das Mux-Data-Tracking.
|
|
10
|
+
* @property {string} [envKey] Mux-Data-Env-Key. Nur wirksam, wenn `disableTracking=false`.
|
|
11
|
+
* @property {Object} [metadata] Mux-Data-Metadata-Objekt. Nur wirksam, wenn `envKey` gesetzt ist.
|
|
12
|
+
*/
|
|
13
|
+
|
|
4
14
|
/**
|
|
5
15
|
* Lädt Mux-Videos lazy und steuert Autoplay per IntersectionObserver.
|
|
6
16
|
*
|
|
@@ -30,6 +40,9 @@ export class MuxPlayer {
|
|
|
30
40
|
#envKey;
|
|
31
41
|
#metadata;
|
|
32
42
|
|
|
43
|
+
/**
|
|
44
|
+
* @param {MuxPlayerOptions} [options]
|
|
45
|
+
*/
|
|
33
46
|
constructor({
|
|
34
47
|
loop = false,
|
|
35
48
|
noLowRes = false,
|
package/dev/Dev.js
CHANGED
|
@@ -3,7 +3,8 @@ import { Toolbar } from './toolbar/Toolbar.js';
|
|
|
3
3
|
let _config = {};
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
6
|
+
* Hinterlegt die Projekt-Config für die Dev-Toolbar.
|
|
7
|
+
* @param {Object} [config={}] - Inhalt von `src/config.json` des Projekts.
|
|
7
8
|
*/
|
|
8
9
|
export function initDev(config = {}) {
|
|
9
10
|
_config = config;
|
package/dev/toolbar/Toolbar.js
CHANGED
|
@@ -2,6 +2,10 @@ import GUI from 'lil-gui';
|
|
|
2
2
|
import { MediaQueries } from '../../utils/MediaQueries.js';
|
|
3
3
|
import './toolbar.scss';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Dev-Toolbar (lil-gui) mit Grid-Overlay, Bildgrössen- und Inhaltstyp-Labels.
|
|
7
|
+
* State wird in `localStorage.devTools` persistiert. Toggle via `Ctrl`.
|
|
8
|
+
*/
|
|
5
9
|
export class Toolbar {
|
|
6
10
|
#mediaQueries = MediaQueries.getInstance();
|
|
7
11
|
#gui;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@profitlich/template-toolkit",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.2.0",
|
|
4
4
|
"description": "Shared SCSS layout system, JS utilities, components and build scripts for profitlich template repos",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sass": "scss/forward.scss",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
47
|
"@capsizecss/core": ">=4.0.0",
|
|
48
|
-
"@capsizecss/unpack": ">=
|
|
48
|
+
"@capsizecss/unpack": ">=4.0.0",
|
|
49
49
|
"@mux/mux-player": ">=3.0.0"
|
|
50
50
|
},
|
|
51
51
|
"peerDependenciesMeta": {
|
package/scripts/copy-files.js
CHANGED
|
@@ -3,6 +3,14 @@ import { glob } from 'glob';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import chokidar from 'chokidar';
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {Object} CopyTask
|
|
8
|
+
* @property {string} name - Anzeigename des Tasks (für Logs).
|
|
9
|
+
* @property {string} src - Glob-Pattern der Quelldateien.
|
|
10
|
+
* @property {string} dest - Zielverzeichnis.
|
|
11
|
+
* @property {string} base - Basis-Pfad; relative Pfade werden ausgehend davon erhalten.
|
|
12
|
+
*/
|
|
13
|
+
|
|
6
14
|
async function runTask(task) {
|
|
7
15
|
const files = await glob(task.src, { nodir: true, dot: true });
|
|
8
16
|
if (files.length === 0) return;
|
|
@@ -14,6 +22,11 @@ async function runTask(task) {
|
|
|
14
22
|
}
|
|
15
23
|
}
|
|
16
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Leert alle Ziel-Verzeichnisse und kopiert anschliessend alle Quelldateien
|
|
27
|
+
* jedes Tasks parallel ins jeweilige Ziel.
|
|
28
|
+
* @param {CopyTask[]} copyTasks
|
|
29
|
+
*/
|
|
17
30
|
export async function copyAll(copyTasks) {
|
|
18
31
|
console.log('🚀 Starting initial copy of all files...');
|
|
19
32
|
// empty folders to prevent orphaned files
|
|
@@ -25,6 +38,11 @@ export async function copyAll(copyTasks) {
|
|
|
25
38
|
console.log('✅ Initial copy complete.');
|
|
26
39
|
}
|
|
27
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Beobachtet `watchDir` und führt bei jeder Änderung `copyAll` aus.
|
|
43
|
+
* @param {CopyTask[]} copyTasks
|
|
44
|
+
* @param {string} [watchDir='src']
|
|
45
|
+
*/
|
|
28
46
|
export function watchFiles(copyTasks, watchDir = 'src') {
|
|
29
47
|
console.log(`👀 Watching for file changes in ${watchDir}/`);
|
|
30
48
|
|
|
@@ -58,9 +76,11 @@ export function watchFiles(copyTasks, watchDir = 'src') {
|
|
|
58
76
|
|
|
59
77
|
/**
|
|
60
78
|
* Run the copy-files script.
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* @param {
|
|
79
|
+
* Liest `process.argv[2]`: `dev` startet einmal `copyAll` und danach `watchFiles`,
|
|
80
|
+
* `build` führt `copyAll` einmal aus.
|
|
81
|
+
* @param {CopyTask[]} copyTasks
|
|
82
|
+
* @param {Object} [options]
|
|
83
|
+
* @param {string} [options.watchDir='src'] - Im Dev-Mode beobachtetes Verzeichnis.
|
|
64
84
|
*/
|
|
65
85
|
export function run(copyTasks, options = {}) {
|
|
66
86
|
const command = process.argv[2];
|
package/scripts/deploy.js
CHANGED
|
@@ -6,6 +6,20 @@ import fs from 'fs/promises';
|
|
|
6
6
|
import readline from 'readline';
|
|
7
7
|
import cliProgress from 'cli-progress';
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {Object} UploadTask
|
|
11
|
+
* @property {string} name - Anzeigename des Tasks (für Logs).
|
|
12
|
+
* @property {string} localPattern - Glob-Pattern der hochzuladenden Dateien.
|
|
13
|
+
* @property {string} localBase - Basis-Pfad; relative Pfade werden auf den `remoteDir` gemappt.
|
|
14
|
+
* @property {string} remoteDir - Ziel-Verzeichnis auf dem FTP-Server.
|
|
15
|
+
* @property {string|string[]} [ignore] - Glob-Pattern, die vom Upload ausgeschlossen werden.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {Object} DeployOptions
|
|
20
|
+
* @property {number} [parallel=3] - Anzahl paralleler FTP-Verbindungen. `1` deaktiviert Parallelität.
|
|
21
|
+
*/
|
|
22
|
+
|
|
9
23
|
// Helper for making sure the upload progress bars go to 100%
|
|
10
24
|
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
|
|
11
25
|
|
|
@@ -35,6 +49,13 @@ async function createClient(accessOptions) {
|
|
|
35
49
|
return client;
|
|
36
50
|
}
|
|
37
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Lädt die in `uploadTasks` definierten Dateien per FTP hoch.
|
|
54
|
+
* Liest `FTP_HOST_<MODE>`, `FTP_USER_<MODE>`, `FTP_PASSWORD_<MODE>` aus dem env.
|
|
55
|
+
* @param {'staging'|'production'} mode
|
|
56
|
+
* @param {UploadTask[]} uploadTasks
|
|
57
|
+
* @param {DeployOptions} [options]
|
|
58
|
+
*/
|
|
38
59
|
export async function runDeploy(mode, uploadTasks, options = {}) {
|
|
39
60
|
const parallel = options.parallel ?? 3;
|
|
40
61
|
const modeUpper = mode.toUpperCase();
|
|
@@ -198,8 +219,10 @@ export async function runDeploy(mode, uploadTasks, options = {}) {
|
|
|
198
219
|
|
|
199
220
|
/**
|
|
200
221
|
* Run the deploy script.
|
|
201
|
-
*
|
|
202
|
-
*
|
|
222
|
+
* Liest `process.argv[2]` (`'staging'` oder `'production'`) als Mode.
|
|
223
|
+
* Im Production-Mode wird vorher um Bestätigung gefragt.
|
|
224
|
+
* @param {UploadTask[]} uploadTasks
|
|
225
|
+
* @param {DeployOptions} [options]
|
|
203
226
|
*/
|
|
204
227
|
export function run(uploadTasks, options = {}) {
|
|
205
228
|
const mode = process.argv[2];
|
package/utils/BodyScrolled.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Setzt beim ersten Scroll-Event das Body-Attribut `data-body-scrolled="true"`
|
|
3
|
+
* und feuert das Custom-Event `eventBodyScrolled`. Einmal pro Seitenaufruf.
|
|
3
4
|
*/
|
|
4
|
-
|
|
5
5
|
export class BodyScrolled {
|
|
6
6
|
static #instance;
|
|
7
7
|
|
|
@@ -22,6 +22,10 @@ export class BodyScrolled {
|
|
|
22
22
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Holt die Singleton-Instanz und initialisiert beim ersten Aufruf den Scroll-Listener.
|
|
27
|
+
* @returns {BodyScrolled}
|
|
28
|
+
*/
|
|
25
29
|
static getInstance() {
|
|
26
30
|
if (!BodyScrolled.#instance) {
|
|
27
31
|
BodyScrolled.#instance = new BodyScrolled();
|
package/utils/MailAdresses.js
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Setzt `href` und Linktext aller Elemente mit `data-eml="local-part"` auf
|
|
3
|
+
* `local-part@domain` (Spam-Schutz: Adresse erst per JS zusammengesetzt).
|
|
4
|
+
*/
|
|
1
5
|
export class MailAdresses {
|
|
2
6
|
static #instance;
|
|
3
7
|
|
|
4
8
|
#domain;
|
|
5
9
|
|
|
10
|
+
/**
|
|
11
|
+
* @param {string} domain - Domain-Teil der E-Mail-Adressen (z.B. 'example.com')
|
|
12
|
+
*/
|
|
6
13
|
constructor(domain) {
|
|
7
14
|
this.#domain = domain;
|
|
8
15
|
this.init();
|
|
@@ -18,6 +25,11 @@ export class MailAdresses {
|
|
|
18
25
|
});
|
|
19
26
|
}
|
|
20
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Holt die Singleton-Instanz. Beim ersten Aufruf muss `domain` übergeben werden.
|
|
30
|
+
* @param {string} [domain]
|
|
31
|
+
* @returns {MailAdresses}
|
|
32
|
+
*/
|
|
21
33
|
static getInstance(domain) {
|
|
22
34
|
if (!MailAdresses.#instance) {
|
|
23
35
|
MailAdresses.#instance = new MailAdresses(domain);
|
package/utils/MediaQueries.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
1
|
+
/**
|
|
2
|
+
* Verwaltet Layout-Wechsel anhand von Breakpoints. Setzt `data-layout` am Body
|
|
3
|
+
* und feuert das Custom-Event `eventLayoutchange` bei jedem Wechsel.
|
|
4
|
+
* Siehe https://kinsta.com/blog/javamediaqueryipt-media-query/ (Option 3).
|
|
5
|
+
*
|
|
6
|
+
* @typedef {Object<string, number|null>} BreakpointsConfig
|
|
7
|
+
* Map: Layout-Name → minimale Viewport-Breite in Pixeln (oder `null` für das kleinste Layout).
|
|
8
|
+
* Reihenfolge der Keys bestimmt die Layout-Priorität (späteres Match überschreibt früheres).
|
|
4
9
|
*/
|
|
5
10
|
|
|
6
11
|
export class MediaQueries {
|
|
@@ -8,6 +13,9 @@ export class MediaQueries {
|
|
|
8
13
|
layout = 'desktop';
|
|
9
14
|
#breakpoints;
|
|
10
15
|
|
|
16
|
+
/**
|
|
17
|
+
* @param {BreakpointsConfig} breakpoints
|
|
18
|
+
*/
|
|
11
19
|
constructor(breakpoints) {
|
|
12
20
|
this.#breakpoints = breakpoints;
|
|
13
21
|
this.layout = 'desktop';
|
|
@@ -15,6 +23,11 @@ export class MediaQueries {
|
|
|
15
23
|
this.#matchmedia();
|
|
16
24
|
}
|
|
17
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Holt die Singleton-Instanz. Beim ersten Aufruf müssen `breakpoints` übergeben werden.
|
|
28
|
+
* @param {BreakpointsConfig} [breakpoints]
|
|
29
|
+
* @returns {MediaQueries}
|
|
30
|
+
*/
|
|
18
31
|
static getInstance(breakpoints) {
|
|
19
32
|
if (!MediaQueries.#instance) {
|
|
20
33
|
if (!breakpoints) {
|
package/utils/Vh100.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Setzt die CSS Custom Property `--vh` auf 1% der aktuellen Viewport-Höhe und
|
|
3
|
+
* aktualisiert bei Resize. Workaround für mobiles 100vh.
|
|
3
4
|
* https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
|
|
4
5
|
*/
|
|
5
|
-
|
|
6
6
|
export class Vh100 {
|
|
7
7
|
static #instance;
|
|
8
8
|
vh = 0;
|
|
@@ -22,6 +22,10 @@ export class Vh100 {
|
|
|
22
22
|
document.documentElement.style.setProperty('--vh', `${this.vh}px`);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Holt die Singleton-Instanz und initialisiert beim ersten Aufruf den Resize-Listener.
|
|
27
|
+
* @returns {Vh100}
|
|
28
|
+
*/
|
|
25
29
|
static getInstance() {
|
|
26
30
|
if (!Vh100.#instance) {
|
|
27
31
|
Vh100.#instance = new Vh100();
|
package/utils/VwBody.js
CHANGED
|
@@ -18,6 +18,10 @@ export class VwBody {
|
|
|
18
18
|
document.documentElement.style.setProperty('--vw-body', `${this.vw}px`);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Holt die Singleton-Instanz und initialisiert beim ersten Aufruf den Resize-Listener.
|
|
23
|
+
* @returns {VwBody}
|
|
24
|
+
*/
|
|
21
25
|
static getInstance() {
|
|
22
26
|
if (!VwBody.#instance) {
|
|
23
27
|
VwBody.#instance = new VwBody();
|
|
@@ -9,6 +9,11 @@ export class ImagesLoaded {
|
|
|
9
9
|
#attributeName;
|
|
10
10
|
#attributeValue;
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* @param {string} selector CSS-Selektor der zu beobachtenden `<img>`-Elemente.
|
|
14
|
+
* @param {string} [loadedAttribute='data-loaded'] Attribut, das den Ladestatus signalisiert.
|
|
15
|
+
* @param {string} [loadedValue='true'] Wert, der bei erfolgreichem Laden gesetzt wird.
|
|
16
|
+
*/
|
|
12
17
|
constructor(selector, loadedAttribute = 'data-loaded', loadedValue = 'true') {
|
|
13
18
|
this.#selector = selector;
|
|
14
19
|
this.#attributeName = loadedAttribute;
|
package/vite/jsonToScss.js
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wandelt ein JSON-Objekt in SCSS-Variablen-Deklarationen um.
|
|
3
|
+
* Top-Level-Keys werden zu `$key: value;`-Zeilen, verschachtelte Objekte zu
|
|
4
|
+
* Sass-Maps `(key: value, ...)`. Hex-Farben werden ohne Quotes ausgegeben.
|
|
5
|
+
* Der Key `README` wird übersprungen.
|
|
6
|
+
*
|
|
7
|
+
* @param {Object} json - Quell-Objekt (z.B. Inhalt von `src/config.json`).
|
|
8
|
+
* @returns {string} SCSS-Source mit einer `$variable: ...;`-Zeile pro Top-Level-Key.
|
|
9
|
+
*/
|
|
1
10
|
export function jsonToScss(json) {
|
|
2
11
|
const toValue = (v) => {
|
|
3
12
|
if (typeof v === 'object' && v !== null) {
|