@profitlich/template-toolkit 1.0.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 +124 -0
- package/components/menu-toggle/menu-toggle.scss +14 -0
- package/components/mux-player/MuxPlayer.js +106 -0
- package/components/mux-player/mux-player.scss +3 -0
- package/dev/Dev.js +19 -0
- package/dev/formie-fill-in/FormieFillIn.js +91 -0
- package/dev/toolbar/Toolbar.js +102 -0
- package/dev/toolbar/toolbar.scss +102 -0
- package/package.json +51 -0
- package/scripts/copy-files.js +74 -0
- package/scripts/deploy.js +112 -0
- package/scss/core/hamburger.scss +153 -0
- package/scss/core/layout.scss +238 -0
- package/scss/core/mediaqueries.scss +42 -0
- package/scss/forward.scss +4 -0
- package/utils/BodyScrolled.js +31 -0
- package/utils/MediaQueries.js +56 -0
- package/utils/Vh100.js +31 -0
|
@@ -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
|
+
}
|
package/dev/Dev.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Toolbar } from './toolbar/Toolbar.js';
|
|
2
|
+
import { FormieFillIn } from './formie-fill-in/FormieFillIn.js';
|
|
3
|
+
|
|
4
|
+
let _config = {};
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {Object} config - Aus config.json des Projekts
|
|
8
|
+
*/
|
|
9
|
+
export function initDev(config = {}) {
|
|
10
|
+
_config = config;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
14
|
+
new Toolbar();
|
|
15
|
+
if (_config.formie) {
|
|
16
|
+
new FormieFillIn(_config.formie);
|
|
17
|
+
}
|
|
18
|
+
console.info('Dev initialized.');
|
|
19
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FormieFillIn – Füllt Formie-Formulare in der Entwicklungsumgebung automatisch aus.
|
|
3
|
+
*
|
|
4
|
+
* Initialisierung im Projekt-Dev-Script:
|
|
5
|
+
* import config from '../config.json';
|
|
6
|
+
* import { FormieFillIn } from 'profitlich-template-toolkit/dev/formie-fill-in/FormieFillIn';
|
|
7
|
+
* new FormieFillIn(config.formie);
|
|
8
|
+
*
|
|
9
|
+
* Konfiguration in config.json (zwei Ebenen: Form-Handle → Feld-Handle → Wert):
|
|
10
|
+
* "formie": {
|
|
11
|
+
* "kontaktformular": {
|
|
12
|
+
* "vorname": "Max",
|
|
13
|
+
* "email": "max@example.com",
|
|
14
|
+
* "nachricht": "Testanfrage"
|
|
15
|
+
* }
|
|
16
|
+
* }
|
|
17
|
+
*/
|
|
18
|
+
export class FormieFillIn {
|
|
19
|
+
#formConfig;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {Object} formConfig - Aus config.json, z.B. { kontaktformular: { email: 'test@example.com' } }
|
|
23
|
+
*/
|
|
24
|
+
constructor(formConfig) {
|
|
25
|
+
if (!formConfig || typeof formConfig !== 'object') return;
|
|
26
|
+
|
|
27
|
+
this.#formConfig = formConfig;
|
|
28
|
+
document.addEventListener('onFormieInit', this.#onFormieInit);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
#onFormieInit = () => {
|
|
32
|
+
const forms = document.querySelectorAll('form[data-fui-form]');
|
|
33
|
+
forms.forEach((formElement) => {
|
|
34
|
+
const handle = this.#getFormHandle(formElement);
|
|
35
|
+
if (!handle || !this.#formConfig[handle]) return;
|
|
36
|
+
|
|
37
|
+
this.#fillForm(formElement, this.#formConfig[handle]);
|
|
38
|
+
console.info(`FormieFillIn: "${handle}" ausgefüllt.`);
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
#getFormHandle(formElement) {
|
|
43
|
+
try {
|
|
44
|
+
const data = JSON.parse(formElement.getAttribute('data-fui-form'));
|
|
45
|
+
return data?.handle ?? null;
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
#fillForm(formElement, fields) {
|
|
52
|
+
for (const [fieldHandle, value] of Object.entries(fields)) {
|
|
53
|
+
const name = `fields[${fieldHandle}]`;
|
|
54
|
+
const inputs = formElement.querySelectorAll(`[name="${name}"], [name="${name}[]"]`);
|
|
55
|
+
|
|
56
|
+
if (!inputs.length) {
|
|
57
|
+
console.warn(`FormieFillIn: Feld "${fieldHandle}" nicht gefunden.`);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
this.#setFieldValue(inputs, value);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
#setFieldValue(inputs, value) {
|
|
66
|
+
const first = inputs[0];
|
|
67
|
+
const type = first.type?.toLowerCase();
|
|
68
|
+
|
|
69
|
+
if (type === 'radio') {
|
|
70
|
+
inputs.forEach((radio) => {
|
|
71
|
+
radio.checked = radio.value === String(value);
|
|
72
|
+
if (radio.checked) radio.dispatchEvent(new Event('change', { bubbles: true }));
|
|
73
|
+
});
|
|
74
|
+
} else if (type === 'checkbox') {
|
|
75
|
+
if (inputs.length === 1) {
|
|
76
|
+
first.checked = Boolean(value);
|
|
77
|
+
} else {
|
|
78
|
+
// Checkbox-Gruppe: value ist Array mit den gewünschten Werten
|
|
79
|
+
const selected = Array.isArray(value) ? value.map(String) : [String(value)];
|
|
80
|
+
inputs.forEach((checkbox) => {
|
|
81
|
+
checkbox.checked = selected.includes(checkbox.value);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
inputs.forEach((cb) => cb.dispatchEvent(new Event('change', { bubbles: true })));
|
|
85
|
+
} else {
|
|
86
|
+
first.value = value;
|
|
87
|
+
first.dispatchEvent(new Event('input', { bubbles: true }));
|
|
88
|
+
first.dispatchEvent(new Event('change', { bubbles: true }));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import GUI from 'lil-gui';
|
|
2
|
+
import { MediaQueries } from '../../utils/MediaQueries.js';
|
|
3
|
+
import './toolbar.scss';
|
|
4
|
+
|
|
5
|
+
export class Toolbar {
|
|
6
|
+
#mediaQueries = MediaQueries.getInstance();
|
|
7
|
+
#gui;
|
|
8
|
+
#state;
|
|
9
|
+
#pictureElements;
|
|
10
|
+
|
|
11
|
+
constructor() {
|
|
12
|
+
// State aus localStorage laden
|
|
13
|
+
const saved = localStorage.getItem('devTools');
|
|
14
|
+
this.#state = saved
|
|
15
|
+
? JSON.parse(saved)
|
|
16
|
+
: { visible: false, grid: 'aus', imageSize: false };
|
|
17
|
+
|
|
18
|
+
// Grid-Overlay DOM-Element erstellen
|
|
19
|
+
const gridOverlay = document.createElement('div');
|
|
20
|
+
gridOverlay.classList.add('dev-toolbar__grid');
|
|
21
|
+
document.body.prepend(gridOverlay);
|
|
22
|
+
|
|
23
|
+
// Picture-Elemente sammeln
|
|
24
|
+
this.#pictureElements = document.getElementsByTagName('picture');
|
|
25
|
+
|
|
26
|
+
// lil-gui initialisieren
|
|
27
|
+
this.#gui = new GUI({ title: this.#getViewportText() });
|
|
28
|
+
|
|
29
|
+
this.#gui.add(this.#state, 'grid', ['aus', 'lines', 'ribbons'])
|
|
30
|
+
.name('Grid')
|
|
31
|
+
.onChange(() => this.#onStateChange());
|
|
32
|
+
|
|
33
|
+
this.#gui.add(this.#state, 'imageSize')
|
|
34
|
+
.name('Bildgrössen')
|
|
35
|
+
.onChange(() => this.#onStateChange());
|
|
36
|
+
|
|
37
|
+
// State anwenden
|
|
38
|
+
this.#applyState();
|
|
39
|
+
|
|
40
|
+
// Panel ein-/ausblenden
|
|
41
|
+
if (!this.#state.visible) {
|
|
42
|
+
this.#gui.hide();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Event-Listener
|
|
46
|
+
window.addEventListener('resize', this.#onResize);
|
|
47
|
+
document.addEventListener('keydown', this.#handleKeyDown);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
#getViewportText() {
|
|
51
|
+
return `${this.#mediaQueries.layout} @ ${window.innerWidth}×${window.innerHeight}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
#onStateChange() {
|
|
55
|
+
this.#applyState();
|
|
56
|
+
this.#saveState();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
#applyState() {
|
|
60
|
+
const gridActive = this.#state.grid !== 'aus';
|
|
61
|
+
document.body.setAttribute('data-dev', String(gridActive));
|
|
62
|
+
document.body.setAttribute('data-dev-grid', this.#state.grid);
|
|
63
|
+
this.#updateImageSize();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
#saveState() {
|
|
67
|
+
localStorage.setItem('devTools', JSON.stringify(this.#state));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
#updateImageSize() {
|
|
71
|
+
if (!this.#pictureElements) return;
|
|
72
|
+
const pictures = Array.from(this.#pictureElements);
|
|
73
|
+
if (this.#state.imageSize) {
|
|
74
|
+
const windowWidth = window.innerWidth;
|
|
75
|
+
pictures.forEach((picture) => {
|
|
76
|
+
picture.setAttribute('data-size', `${Math.round(picture.offsetWidth / windowWidth * 100)}vw`);
|
|
77
|
+
});
|
|
78
|
+
} else {
|
|
79
|
+
pictures.forEach((picture) => {
|
|
80
|
+
picture.setAttribute('data-size', '');
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
#onResize = () => {
|
|
86
|
+
this.#gui.title(this.#getViewportText());
|
|
87
|
+
this.#updateImageSize();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
#handleKeyDown = (event) => {
|
|
91
|
+
if (event.key === 'Control') {
|
|
92
|
+
event.preventDefault();
|
|
93
|
+
this.#state.visible = !this.#state.visible;
|
|
94
|
+
if (this.#state.visible) {
|
|
95
|
+
this.#gui.show();
|
|
96
|
+
} else {
|
|
97
|
+
this.#gui.hide();
|
|
98
|
+
}
|
|
99
|
+
this.#saveState();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
@use "sass:map";
|
|
2
|
+
@use "../../scss/forward" as *;
|
|
3
|
+
|
|
4
|
+
// grid
|
|
5
|
+
$color--dev-grid: rgba(0, 0, 255, 0.5);
|
|
6
|
+
$color--dev-grid-ribbons: rgba(191, 255, 254, 0.5);
|
|
7
|
+
$color--dev-grid-center: rgba(0, 0, 0, 0.2);
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@mixin dev-toolbar-grid($layout, $columns, $gutter, $margin-left, $margin-right) {
|
|
11
|
+
|
|
12
|
+
$gutter: size($layout, $gutter);
|
|
13
|
+
$margin-left: size($layout, $margin-left);
|
|
14
|
+
$margin-right: size($layout, $margin-right);
|
|
15
|
+
|
|
16
|
+
body[data-dev='true'] {
|
|
17
|
+
.dev-toolbar__grid::after {
|
|
18
|
+
// the grid is made through repition
|
|
19
|
+
// 'important' because background settings are more specific than the grid settings
|
|
20
|
+
background-size: calc((100% - ($columns * $gutter)) * calc(1 / $columns) + $gutter) 1px !important;
|
|
21
|
+
margin: 0 ($margin-right - calc($gutter / 2)) 0 ($margin-left - calc($gutter / 2));
|
|
22
|
+
// The grid is calculated starting from the middle
|
|
23
|
+
// Thats why its necessary to add the half of the column gap to the left and right side
|
|
24
|
+
width: calc(100% - $margin-right + calc($gutter / 2) - $margin-left + calc($gutter / 2));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
body[data-dev='true'][data-dev-grid="lines"] {
|
|
29
|
+
.dev-toolbar__grid::after {
|
|
30
|
+
background:
|
|
31
|
+
// center of the column spacing
|
|
32
|
+
linear-gradient(90deg, transparent 0.5px, transparent 0.5px) 0 0,
|
|
33
|
+
// Left column line
|
|
34
|
+
// position: from the column spacing center to the right by half a column spacing
|
|
35
|
+
linear-gradient(90deg, $color--dev-grid 0.5px, transparent 0.5px) calc($gutter / 2) 0,
|
|
36
|
+
// right column line
|
|
37
|
+
linear-gradient(90deg, $color--dev-grid 0.5px, transparent 0.5px) calc($gutter / 2 * -1) 0;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
body[data-dev='true'][data-dev-grid="ribbons"] {
|
|
42
|
+
.dev-toolbar__grid::after {
|
|
43
|
+
background:
|
|
44
|
+
// ceft column ribbon
|
|
45
|
+
// position: from the column spacing center to the right by half a column spacing
|
|
46
|
+
linear-gradient(90deg, $color--dev-grid-ribbons calc(100% - $gutter), transparent 0.5px) calc($gutter / 2) 0,
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// lil-gui z-index
|
|
52
|
+
.lil-gui.root {
|
|
53
|
+
z-index: 9999999;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.dev-toolbar__grid {
|
|
57
|
+
display: none;
|
|
58
|
+
width: 100%;
|
|
59
|
+
height: 100%;
|
|
60
|
+
left: 0;
|
|
61
|
+
pointer-events: none;
|
|
62
|
+
position: fixed;
|
|
63
|
+
top: 0;
|
|
64
|
+
|
|
65
|
+
body[data-dev='true'] & {
|
|
66
|
+
display: block;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
body[data-dev-grid='lines'] & {
|
|
70
|
+
z-index: 9999999;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
&::after {
|
|
74
|
+
content: "";
|
|
75
|
+
height: 100%;
|
|
76
|
+
left: 0;
|
|
77
|
+
position: absolute;
|
|
78
|
+
top: 0;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@each $layout, $breite in $layouts {
|
|
83
|
+
@include mediaquery($layout) using ($layout) {
|
|
84
|
+
@include dev-toolbar-grid($layout, map.get($columns, $layout), map.get($gutter, $layout), map.get(map.get($margins, $layout), 'left'), map.get(map.get($margins, $layout), 'right'));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
picture {
|
|
89
|
+
position: relative;
|
|
90
|
+
|
|
91
|
+
&::before{
|
|
92
|
+
content: attr(data-size);
|
|
93
|
+
font-family: sans-serif;
|
|
94
|
+
position: absolute;
|
|
95
|
+
top: 10px;
|
|
96
|
+
left: 10px;
|
|
97
|
+
color: white;
|
|
98
|
+
font-size: 20px;
|
|
99
|
+
text-shadow: 0 0 10px black;
|
|
100
|
+
z-index: 10;
|
|
101
|
+
}
|
|
102
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@profitlich/template-toolkit",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Shared SCSS layout system, JS utilities, components and build scripts for profitlich template repos",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
"./dev": "./dev/Dev.js",
|
|
8
|
+
"./dev/toolbar": "./dev/toolbar/Toolbar.js",
|
|
9
|
+
"./dev/formie-fill-in/FormieFillIn": "./dev/formie-fill-in/FormieFillIn.js",
|
|
10
|
+
"./scss/forward": "./scss/forward.scss",
|
|
11
|
+
"./scss/core/layout": "./scss/core/layout.scss",
|
|
12
|
+
"./scss/core/mediaqueries": "./scss/core/mediaqueries.scss",
|
|
13
|
+
"./scss/core/hamburger": "./scss/core/hamburger.scss",
|
|
14
|
+
"./utils/MediaQueries": "./utils/MediaQueries.js",
|
|
15
|
+
"./utils/Vh100": "./utils/Vh100.js",
|
|
16
|
+
"./utils/BodyScrolled": "./utils/BodyScrolled.js",
|
|
17
|
+
"./components/menu-toggle/MenuToggle": "./components/menu-toggle/MenuToggle.js",
|
|
18
|
+
"./components/mux-player/MuxPlayer": "./components/mux-player/MuxPlayer.js",
|
|
19
|
+
"./scripts/copy-files": "./scripts/copy-files.js",
|
|
20
|
+
"./scripts/deploy": "./scripts/deploy.js"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dev/",
|
|
24
|
+
"scss/",
|
|
25
|
+
"utils/",
|
|
26
|
+
"components/",
|
|
27
|
+
"scripts/"
|
|
28
|
+
],
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"basic-ftp": "^5.0.0",
|
|
31
|
+
"chokidar": "^4.0.0",
|
|
32
|
+
"cli-progress": "^3.12.0",
|
|
33
|
+
"dotenv": "^17.0.0",
|
|
34
|
+
"fs-extra": "^11.3.0",
|
|
35
|
+
"glob": "^11.0.0",
|
|
36
|
+
"lil-gui": "^0.21.0"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"@mux/mux-player": ">=3.0.0"
|
|
40
|
+
},
|
|
41
|
+
"peerDependenciesMeta": {
|
|
42
|
+
"@mux/mux-player": {
|
|
43
|
+
"optional": true
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"repository": {
|
|
47
|
+
"type": "git",
|
|
48
|
+
"url": "git+https://github.com/profitlich-ch/profitlich-template-toolkit.git"
|
|
49
|
+
},
|
|
50
|
+
"license": "MIT"
|
|
51
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import { glob } from 'glob';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import chokidar from 'chokidar';
|
|
5
|
+
|
|
6
|
+
async function runTask(task) {
|
|
7
|
+
const files = await glob(task.src, { nodir: true, dot: true });
|
|
8
|
+
if (files.length === 0) return;
|
|
9
|
+
|
|
10
|
+
for (const file of files) {
|
|
11
|
+
const relativePath = path.relative(task.base, file);
|
|
12
|
+
const destPath = path.join(task.dest, relativePath);
|
|
13
|
+
await fs.copy(file, destPath);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function copyAll(copyTasks) {
|
|
18
|
+
console.log('🚀 Starting initial copy of all files...');
|
|
19
|
+
// empty folders to prevent orphaned files
|
|
20
|
+
for (const task of copyTasks) {
|
|
21
|
+
await fs.emptyDir(task.dest);
|
|
22
|
+
}
|
|
23
|
+
// run tasks parallely
|
|
24
|
+
await Promise.all(copyTasks.map(task => runTask(task)));
|
|
25
|
+
console.log('✅ Initial copy complete.');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function watchFiles(copyTasks, watchDir = 'src') {
|
|
29
|
+
console.log(`👀 Watching for file changes in ${watchDir}/`);
|
|
30
|
+
|
|
31
|
+
// set lock variable to prevent parallel copy tasks
|
|
32
|
+
let isCopying = false;
|
|
33
|
+
|
|
34
|
+
const watcher = chokidar.watch(watchDir, { ignored: /(^|[\/\\])\../, persistent: true });
|
|
35
|
+
|
|
36
|
+
watcher.on('all', async (event, filePath) => {
|
|
37
|
+
if (['add', 'change', 'unlink'].includes(event)) {
|
|
38
|
+
|
|
39
|
+
// only proceed if copy job is not running
|
|
40
|
+
if (isCopying) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
console.log(`[${event}] ${filePath}. Re-copying all files...`);
|
|
45
|
+
|
|
46
|
+
// secure copy job with try...finally
|
|
47
|
+
try {
|
|
48
|
+
isCopying = true; // activate lock
|
|
49
|
+
await copyAll(copyTasks);
|
|
50
|
+
} catch (err) {
|
|
51
|
+
console.error("Error during copy:", err);
|
|
52
|
+
} finally {
|
|
53
|
+
isCopying = false; // deactivate lock, allow new copy job
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Run the copy-files script.
|
|
61
|
+
* @param {Array} copyTasks - array of { name, src, dest, base } objects
|
|
62
|
+
* @param {object} [options] - optional settings
|
|
63
|
+
* @param {string} [options.watchDir] - directory to watch (default: 'src')
|
|
64
|
+
*/
|
|
65
|
+
export function run(copyTasks, options = {}) {
|
|
66
|
+
const command = process.argv[2];
|
|
67
|
+
const watchDir = options.watchDir || 'src';
|
|
68
|
+
|
|
69
|
+
if (command === 'dev') {
|
|
70
|
+
copyAll(copyTasks).then(() => watchFiles(copyTasks, watchDir));
|
|
71
|
+
} else if (command === 'build') {
|
|
72
|
+
copyAll(copyTasks);
|
|
73
|
+
}
|
|
74
|
+
}
|