@profitlich/template-toolkit 2.21.2 → 3.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.
@@ -4,26 +4,48 @@ export class MenuToggle {
4
4
  static #instance;
5
5
  #menuButton;
6
6
  #menu;
7
- #menuLinkClass;
7
+ #menuLinkSelector;
8
8
  #menuItemClass;
9
9
  #scrollbarWidth;
10
10
  #shiftElement;
11
11
  #shiftDelay;
12
12
  #deferPositionFixed;
13
+ #fixBody;
13
14
  #y;
14
15
  #bodyClickHandler;
15
16
  #resizeHandler;
16
17
  #escapeHandler;
17
18
  isActive;
18
19
 
19
- constructor(menuButtonId, menuId, menuLinkClass, menuItemClass, shiftElementId, shiftDelay = 0, deferPositionFixed = false) {
20
+ /**
21
+ * @param {Object} options
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.
30
+ */
31
+ constructor({
32
+ menuButtonId,
33
+ menuId,
34
+ menuLinkSelector,
35
+ menuItemClass,
36
+ shiftElementId = null,
37
+ shiftDelay = 0,
38
+ deferPositionFixed = false,
39
+ fixBody = true,
40
+ }) {
20
41
  this.#menuButton = document.getElementById(menuButtonId);
21
42
  this.#menu = document.getElementById(menuId);
22
- this.#menuLinkClass = menuLinkClass;
43
+ this.#menuLinkSelector = menuLinkSelector;
23
44
  this.#menuItemClass = menuItemClass;
24
45
  this.#shiftElement = shiftElementId ? document.getElementById(shiftElementId) : null;
25
46
  this.#shiftDelay = shiftDelay * 1000;
26
47
  this.#deferPositionFixed = deferPositionFixed;
48
+ this.#fixBody = fixBody;
27
49
  this.#scrollbarWidth = 0;
28
50
  this.isActive = false;
29
51
  this.#y = 0;
@@ -39,7 +61,7 @@ export class MenuToggle {
39
61
  });
40
62
 
41
63
  this.#menu.addEventListener('click', (event) => {
42
- if (this.isActive && event.target.matches(this.#menuLinkClass)) {
64
+ if (this.isActive && event.target.matches(this.#menuLinkSelector)) {
43
65
  this.#toggleMenu();
44
66
  }
45
67
  });
@@ -49,12 +71,12 @@ export class MenuToggle {
49
71
  this.#setBodyAttribute('data-menu-fixed', 'false');
50
72
  }
51
73
 
52
- static getInstance(menuButtonId, menuId, menuLinkClass, menuItemClass, shiftElementId, shiftDelay, deferPositionFixed) {
74
+ static getInstance(options) {
53
75
  if (!MenuToggle.#instance) {
54
- if (!menuButtonId || !menuId || !menuLinkClass || !menuItemClass) {
55
- throw new Error("MenuToggle muss beim ersten Aufruf mit menuButtonId, menuId, menuLinkClass und menuItemClass initialisiert werden.");
76
+ if (!options || !options.menuButtonId || !options.menuId || !options.menuLinkSelector || !options.menuItemClass) {
77
+ throw new Error("MenuToggle muss beim ersten Aufruf mit menuButtonId, menuId, menuLinkSelector und menuItemClass initialisiert werden.");
56
78
  }
57
- MenuToggle.#instance = new MenuToggle(menuButtonId, menuId, menuLinkClass, menuItemClass, shiftElementId, shiftDelay, deferPositionFixed);
79
+ MenuToggle.#instance = new MenuToggle(options);
58
80
  }
59
81
  return MenuToggle.#instance;
60
82
  }
@@ -65,39 +87,43 @@ export class MenuToggle {
65
87
  document.body.removeEventListener('click', this.#bodyClickHandler);
66
88
  window.removeEventListener('resize', this.#resizeHandler);
67
89
  this.#setBodyAttribute('data-menu-active', 'false');
68
- this.#setBodyAttribute('data-menu-fixed', 'false');
69
- if (this.#shiftElement) {
70
- this.#shiftElement.style.marginRight = '';
71
- this.#shiftElement.style.width = '';
90
+ if (this.#fixBody) {
91
+ this.#setBodyAttribute('data-menu-fixed', 'false');
92
+ if (this.#shiftElement) {
93
+ this.#shiftElement.style.marginRight = '';
94
+ this.#shiftElement.style.width = '';
95
+ }
96
+ document.body.style.paddingRight = '';
97
+ document.body.style.top = '';
98
+ window.scrollTo(0, this.#y);
72
99
  }
73
- document.body.style.paddingRight = '';
74
- document.body.style.top = '';
75
- window.scrollTo(0, this.#y);
76
100
  this.#toggleEscape(false);
77
101
  } else {
78
102
  this.isActive = true;
79
103
  document.body.addEventListener('click', this.#bodyClickHandler);
80
- window.addEventListener('resize', this.#resizeHandler);
81
104
  this.#setBodyAttribute('data-menu-active', 'true');
82
- this.#setBodyAttribute('data-menu-moving', 'true');
83
- if (!this.#deferPositionFixed) {
84
- this.#setBodyAttribute('data-menu-fixed', 'true');
85
- }
86
- setTimeout(() => {
87
- this.#scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
88
- this.#y = window.scrollY;
89
- document.body.style.paddingRight = `${this.#scrollbarWidth}px`;
90
- document.body.style.top = `-${this.#y}px`;
91
- if (this.#deferPositionFixed) {
105
+ if (this.#fixBody) {
106
+ window.addEventListener('resize', this.#resizeHandler);
107
+ this.#setBodyAttribute('data-menu-moving', 'true');
108
+ if (!this.#deferPositionFixed) {
92
109
  this.#setBodyAttribute('data-menu-fixed', 'true');
93
110
  }
94
- if (this.#shiftElement) {
95
- const marginOriginal = parseFloat(window.getComputedStyle(this.#menuButton).marginRight);
96
- this.#shiftElement.style.marginRight = `${marginOriginal + this.#scrollbarWidth}px`;
97
- this.#adjustShiftElementWidth();
98
- }
99
- this.#setBodyAttribute('data-menu-moving', 'false');
100
- }, this.#shiftDelay);
111
+ setTimeout(() => {
112
+ this.#scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
113
+ this.#y = window.scrollY;
114
+ document.body.style.paddingRight = `${this.#scrollbarWidth}px`;
115
+ document.body.style.top = `-${this.#y}px`;
116
+ if (this.#deferPositionFixed) {
117
+ this.#setBodyAttribute('data-menu-fixed', 'true');
118
+ }
119
+ if (this.#shiftElement) {
120
+ const marginOriginal = parseFloat(window.getComputedStyle(this.#menuButton).marginRight);
121
+ this.#shiftElement.style.marginRight = `${marginOriginal + this.#scrollbarWidth}px`;
122
+ this.#adjustShiftElementWidth();
123
+ }
124
+ this.#setBodyAttribute('data-menu-moving', 'false');
125
+ }, this.#shiftDelay);
126
+ }
101
127
  this.#toggleEscape(true);
102
128
  }
103
129
  const event = new CustomEvent('eventMenuestatus', {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@profitlich/template-toolkit",
3
- "version": "2.21.2",
3
+ "version": "3.0.0",
4
4
  "description": "Shared SCSS layout system, JS utilities, components and build scripts for profitlich template repos",
5
5
  "type": "module",
6
6
  "exports": {
package/scripts/deploy.js CHANGED
@@ -9,14 +9,21 @@ import cliProgress from 'cli-progress';
9
9
  // Helper for making sure the upload progress bars go to 100%
10
10
  const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
11
11
 
12
- async function isNewer(client, localFile, remotePath) {
12
+ function formatBytes(bytes) {
13
+ if (bytes >= 1024 ** 3) return `${(bytes / 1024 ** 3).toFixed(2)} GB`;
14
+ if (bytes >= 1024 ** 2) return `${(bytes / 1024 ** 2).toFixed(2)} MB`;
15
+ if (bytes >= 1024) return `${(bytes / 1024).toFixed(2)} KB`;
16
+ return `${bytes} B`;
17
+ }
18
+
19
+ async function getFileStatus(client, localFile, remotePath) {
13
20
  const localStat = await fs.stat(localFile);
14
21
  try {
15
22
  const remoteDate = await client.lastMod(remotePath);
16
- return localStat.mtimeMs > remoteDate.getTime();
23
+ return { shouldUpload: localStat.mtimeMs > remoteDate.getTime(), size: localStat.size };
17
24
  } catch {
18
25
  // File doesn't exist remotely (550) or server doesn't support MDTM — upload it
19
- return true;
26
+ return { shouldUpload: true, size: localStat.size };
20
27
  }
21
28
  }
22
29
 
@@ -71,8 +78,9 @@ export async function runDeploy(mode, uploadTasks, options = {}) {
71
78
  for (const file of files) {
72
79
  const relativeFile = path.relative(task.localBase, file);
73
80
  const remotePath = path.join(task.remoteDir, relativeFile).replace(/\\/g, '/');
74
- if (await isNewer(mainClient, file, remotePath)) {
75
- filesToUpload.push({ file, remotePath });
81
+ const { shouldUpload, size } = await getFileStatus(mainClient, file, remotePath);
82
+ if (shouldUpload) {
83
+ filesToUpload.push({ file, remotePath, size });
76
84
  }
77
85
  }
78
86
  } else {
@@ -88,8 +96,9 @@ export async function runDeploy(mode, uploadTasks, options = {}) {
88
96
  if (!file) break;
89
97
  const relativeFile = path.relative(task.localBase, file);
90
98
  const remotePath = path.join(task.remoteDir, relativeFile).replace(/\\/g, '/');
91
- if (await isNewer(workerClient, file, remotePath)) {
92
- filesToUpload.push({ file, remotePath });
99
+ const { shouldUpload, size } = await getFileStatus(workerClient, file, remotePath);
100
+ if (shouldUpload) {
101
+ filesToUpload.push({ file, remotePath, size });
93
102
  }
94
103
  }
95
104
  } finally {
@@ -105,21 +114,28 @@ export async function runDeploy(mode, uploadTasks, options = {}) {
105
114
  continue;
106
115
  }
107
116
 
117
+ const totalBytes = filesToUpload.reduce((sum, { size }) => sum + size, 0);
118
+ let uploadedBytes = 0;
119
+
108
120
  const progressBar = new cliProgress.SingleBar({
109
- format: ' Upload |{bar}| {percentage}% {value}/{total} files {duration_formatted}',
121
+ format: ' Upload |{bar}| {percentage}% {value}/{total} files {uploaded}/{totalSize} {duration_formatted}',
110
122
  barCompleteChar: '█',
111
123
  barIncompleteChar: '░',
112
124
  hideCursor: true
113
125
  });
114
126
 
115
127
  activeProgressBar = progressBar;
116
- progressBar.start(filesToUpload.length, 0);
128
+ progressBar.start(filesToUpload.length, 0, {
129
+ uploaded: formatBytes(0),
130
+ totalSize: formatBytes(totalBytes)
131
+ });
117
132
 
118
133
  if (parallel <= 1) {
119
- for (const { file, remotePath } of filesToUpload) {
134
+ for (const { file, remotePath, size } of filesToUpload) {
120
135
  await mainClient.ensureDir(path.dirname(remotePath));
121
136
  await mainClient.uploadFrom(file, remotePath);
122
- progressBar.increment();
137
+ uploadedBytes += size;
138
+ progressBar.increment(1, { uploaded: formatBytes(uploadedBytes) });
123
139
  }
124
140
  } else {
125
141
  // Pre-create all required remote directories with the main client
@@ -154,7 +170,8 @@ export async function runDeploy(mode, uploadTasks, options = {}) {
154
170
  workerClient = await createClient(accessOptions);
155
171
  }
156
172
  }
157
- progressBar.increment();
173
+ uploadedBytes += item.size;
174
+ progressBar.increment(1, { uploaded: formatBytes(uploadedBytes) });
158
175
  }
159
176
  } finally {
160
177
  workerClient.close();