@profitlich/template-toolkit 2.4.2 → 2.5.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/dev/toolbar/COLUMNS.md +228 -0
- package/package.json +1 -1
- package/scripts/deploy.js +88 -23
- package/scss/core/layout.scss +1 -3
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# Plan: Exakte Spaltenlinien via Canvas (Variante E)
|
|
2
|
+
|
|
3
|
+
## Context
|
|
4
|
+
|
|
5
|
+
Im `lines`-Modus des Dev-Grids werden Spaltenlinien über `linear-gradient` + `background-size` als Kachelmuster erzeugt. Die Kachelbreite löst auf Bruchpixel-Werte auf → die `0.5px`-Gradient-Stops landen an leicht unterschiedlichen physischen Pixeln pro Kachel. Ergebnis: Linien erscheinen wackelnd oder inkonsistent.
|
|
6
|
+
|
|
7
|
+
**Lösung:** `<canvas>`-Element mit korrekter `devicePixelRatio`-Behandlung und dem "Half-Pixel"-Trick für pixelgenaue, scharf gerenderte Linien. Funktioniert auf 1×- und Retina-Displays gleich gut.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Betroffene Dateien
|
|
12
|
+
|
|
13
|
+
- [dev/toolbar/Toolbar.js](dev/toolbar/Toolbar.js)
|
|
14
|
+
- [dev/toolbar/toolbar.scss](dev/toolbar/toolbar.scss)
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Architektur
|
|
19
|
+
|
|
20
|
+
```text
|
|
21
|
+
.dev-toolbar__grid (position: fixed, 100vw×100vh)
|
|
22
|
+
└── <canvas> ← neu, ersetzt ::after für "lines"
|
|
23
|
+
::after ← bleibt für "ribbons" erhalten
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Das Canvas-Element wird als Kind von `#gridElement` angehängt. Es erbt dessen Fixed-Positioning und Visibility-Steuerung über `data-dev`. Für den `lines`-Modus zeichnet das Canvas; für `ribbons` wird das Canvas geleert und `::after` übernimmt.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## SCSS — `toolbar.scss`
|
|
31
|
+
|
|
32
|
+
### 1. CSS Custom Properties im Mixin ergänzen
|
|
33
|
+
|
|
34
|
+
Im Block `body[data-dev='true']` innerhalb von `dev-toolbar-grid`:
|
|
35
|
+
|
|
36
|
+
```scss
|
|
37
|
+
body[data-dev='true'] {
|
|
38
|
+
--dev-columns: #{$columns}; // unitless integer
|
|
39
|
+
--dev-gutter: #{$gutter}; // CSS-Wert (z. B. "1.5rem" oder "20px")
|
|
40
|
+
--dev-margin-left: #{$margin-left};
|
|
41
|
+
--dev-margin-right: #{$margin-right};
|
|
42
|
+
|
|
43
|
+
.dev-toolbar__grid::after { ... } // unverändert
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 2. `lines`-Gradient entfernen
|
|
48
|
+
|
|
49
|
+
Den gesamten Block (Zeilen 28–39) entfernen — das Canvas übernimmt die Darstellung:
|
|
50
|
+
|
|
51
|
+
```scss
|
|
52
|
+
// ENTFERNEN:
|
|
53
|
+
body[data-dev='true'][data-dev-grid="lines"] {
|
|
54
|
+
.dev-toolbar__grid::after { background: ...; }
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 3. Canvas-Styling ergänzen
|
|
59
|
+
|
|
60
|
+
```scss
|
|
61
|
+
.dev-toolbar__grid {
|
|
62
|
+
canvas {
|
|
63
|
+
display: block;
|
|
64
|
+
width: 100%;
|
|
65
|
+
height: 100%;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## JavaScript — `Toolbar.js`
|
|
73
|
+
|
|
74
|
+
### Neue private Fields
|
|
75
|
+
|
|
76
|
+
```javascript
|
|
77
|
+
#gridElement = null; // bisher nur lokale Variable, jetzt gespeichert
|
|
78
|
+
#canvas = null;
|
|
79
|
+
#ctx = null;
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### `constructor()` — Änderungen
|
|
83
|
+
|
|
84
|
+
1. `gridOverlay` → `this.#gridElement` (Referenz speichern)
|
|
85
|
+
2. `#initCanvas()` aufrufen nach `prepend`
|
|
86
|
+
|
|
87
|
+
```javascript
|
|
88
|
+
this.#gridElement = document.createElement('div');
|
|
89
|
+
this.#gridElement.classList.add('dev-toolbar__grid');
|
|
90
|
+
document.body.prepend(this.#gridElement);
|
|
91
|
+
this.#initCanvas();
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Neue Methode `#initCanvas()`
|
|
95
|
+
|
|
96
|
+
```javascript
|
|
97
|
+
#initCanvas() {
|
|
98
|
+
this.#canvas = document.createElement('canvas');
|
|
99
|
+
this.#gridElement.appendChild(this.#canvas);
|
|
100
|
+
this.#ctx = this.#canvas.getContext('2d');
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Neue Methode `#drawGrid()`
|
|
105
|
+
|
|
106
|
+
Wird aufgerufen bei: State-Wechsel (`#applyState`), Resize (`#onResize`).
|
|
107
|
+
|
|
108
|
+
```javascript
|
|
109
|
+
#drawGrid() {
|
|
110
|
+
const dpr = window.devicePixelRatio || 1;
|
|
111
|
+
const w = window.innerWidth;
|
|
112
|
+
const h = window.innerHeight;
|
|
113
|
+
|
|
114
|
+
this.#canvas.width = Math.round(w * dpr);
|
|
115
|
+
this.#canvas.height = Math.round(h * dpr);
|
|
116
|
+
|
|
117
|
+
this.#ctx.clearRect(0, 0, this.#canvas.width, this.#canvas.height);
|
|
118
|
+
|
|
119
|
+
if (this.#state.grid !== 'lines') return;
|
|
120
|
+
|
|
121
|
+
this.#ctx.setTransform(dpr, 0, 0, dpr, 0.5, 0); // scale + half-pixel shift
|
|
122
|
+
|
|
123
|
+
this.#drawColumnLines();
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
**Half-pixel-Trick:** `ctx.setTransform(dpr, 0, 0, dpr, 0.5, 0)` — der 0.5px-Shift zentriert 1px-Linien exakt auf physische Pixel bei dpr=1. Bei dpr=2 (Retina) ist 0.5 CSS-px = 1 physischer Pixel, ebenfalls scharf.
|
|
128
|
+
|
|
129
|
+
### Neue Methode `#drawColumnLines()`
|
|
130
|
+
|
|
131
|
+
```javascript
|
|
132
|
+
#drawColumnLines() {
|
|
133
|
+
const style = getComputedStyle(document.body);
|
|
134
|
+
const columns = parseInt(style.getPropertyValue('--dev-columns'));
|
|
135
|
+
const gutter = this.#resolveToPx(style.getPropertyValue('--dev-gutter').trim());
|
|
136
|
+
const marginLeft = this.#resolveToPx(style.getPropertyValue('--dev-margin-left').trim());
|
|
137
|
+
|
|
138
|
+
// Kachelbreite = (Viewport - Margins + Gutter) / Spalten
|
|
139
|
+
// (identisch zur background-size-Formel im SCSS)
|
|
140
|
+
const gridWidth = window.innerWidth - marginLeft
|
|
141
|
+
- this.#resolveToPx(style.getPropertyValue('--dev-margin-right').trim())
|
|
142
|
+
+ gutter;
|
|
143
|
+
const tileWidth = gridWidth / columns;
|
|
144
|
+
const colContent = tileWidth - gutter;
|
|
145
|
+
|
|
146
|
+
this.#ctx.strokeStyle = 'rgba(0, 0, 255, 0.5)';
|
|
147
|
+
this.#ctx.lineWidth = 1;
|
|
148
|
+
const h = window.innerHeight;
|
|
149
|
+
|
|
150
|
+
for (let i = 0; i < columns; i++) {
|
|
151
|
+
const leftEdge = marginLeft + i * tileWidth;
|
|
152
|
+
const rightEdge = leftEdge + colContent;
|
|
153
|
+
|
|
154
|
+
this.#ctx.beginPath();
|
|
155
|
+
this.#ctx.moveTo(Math.round(leftEdge), 0);
|
|
156
|
+
this.#ctx.lineTo(Math.round(leftEdge), h);
|
|
157
|
+
this.#ctx.stroke();
|
|
158
|
+
|
|
159
|
+
this.#ctx.beginPath();
|
|
160
|
+
this.#ctx.moveTo(Math.round(rightEdge), 0);
|
|
161
|
+
this.#ctx.lineTo(Math.round(rightEdge), h);
|
|
162
|
+
this.#ctx.stroke();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Neue Methode `#resolveToPx(cssValue)`
|
|
168
|
+
|
|
169
|
+
Löst beliebige CSS-Einheiten (rem, vw, calc, …) auf px auf, ohne Unit-Parsing-Logik:
|
|
170
|
+
|
|
171
|
+
```javascript
|
|
172
|
+
#resolveToPx(cssValue) {
|
|
173
|
+
if (/^-?\d+(\.\d+)?px$/.test(cssValue)) return parseFloat(cssValue);
|
|
174
|
+
|
|
175
|
+
const probe = document.createElement('div');
|
|
176
|
+
probe.style.cssText = `position:fixed;visibility:hidden;width:${cssValue};top:0;left:0;`;
|
|
177
|
+
document.body.appendChild(probe);
|
|
178
|
+
const px = probe.getBoundingClientRect().width;
|
|
179
|
+
probe.remove();
|
|
180
|
+
return px;
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### `#applyState()` — Ergänzung
|
|
185
|
+
|
|
186
|
+
```javascript
|
|
187
|
+
#applyState() {
|
|
188
|
+
const gridActive = this.#state.grid !== 'aus';
|
|
189
|
+
document.body.setAttribute('data-dev', String(gridActive));
|
|
190
|
+
document.body.setAttribute('data-dev-grid', this.#state.grid);
|
|
191
|
+
this.#updateImageSize();
|
|
192
|
+
this.#drawGrid(); // ← neu
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### `#onResize` — Ergänzung
|
|
197
|
+
|
|
198
|
+
```javascript
|
|
199
|
+
#onResize = () => {
|
|
200
|
+
this.#gui.title(this.#getViewportText());
|
|
201
|
+
this.#updateImageSize();
|
|
202
|
+
this.#drawGrid(); // ← neu
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## Positionsformel (Herleitung)
|
|
209
|
+
|
|
210
|
+
Das SCSS positioniert `::after` mit `margin-left = marginLeft - gutter/2`, sodass die Kacheln mittig zwischen Spalten beginnen. Die resultierenden Canvas-Positionen sind:
|
|
211
|
+
|
|
212
|
+
| | Formel | Beispiel (12 Spalten, gutter=20px, marginLeft=40px) |
|
|
213
|
+
| --- | --- | --- |
|
|
214
|
+
| Linke Kante Spalte i | `marginLeft + i × tileWidth` | i=0: 40px, i=1: 120px, … |
|
|
215
|
+
| Rechte Kante Spalte i | `marginLeft + i × tileWidth + colContent` | i=0: 100px, i=1: 180px, … |
|
|
216
|
+
|
|
217
|
+
Diese Formel ist identisch zur bestehenden `background-size`-Logik, nur ohne Kachel-Grenz-Problem.
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## Verifikation
|
|
222
|
+
|
|
223
|
+
1. `ddev npm run dev` starten
|
|
224
|
+
2. Dev-Toolbar öffnen (Ctrl), Grid auf "lines" schalten
|
|
225
|
+
3. **Browserfenster in verschiedene Breiten ziehen** → Linien dürfen nicht wackeln
|
|
226
|
+
4. **Safari (Retina) + Chrome (1×)** vergleichen → gleiche Schärfe
|
|
227
|
+
5. **Ribbons-Modus** prüfen → Canvas geleert, `::after`-Gradient unverändert
|
|
228
|
+
6. **Breakpoint-Wechsel** prüfen → CSS Custom Properties werden neu gelesen, Linienanzahl passt sich an
|
package/package.json
CHANGED
package/scripts/deploy.js
CHANGED
|
@@ -2,27 +2,50 @@ import ftp from 'basic-ftp';
|
|
|
2
2
|
import dotenv from 'dotenv';
|
|
3
3
|
import { glob } from 'glob';
|
|
4
4
|
import path from 'path';
|
|
5
|
+
import fs from 'fs/promises';
|
|
5
6
|
import readline from 'readline';
|
|
6
7
|
import cliProgress from 'cli-progress';
|
|
7
8
|
|
|
8
9
|
// Helper for making sure the upload progress bars go to 100%
|
|
9
10
|
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
async function isNewer(client, localFile, remotePath) {
|
|
13
|
+
const localStat = await fs.stat(localFile);
|
|
14
|
+
try {
|
|
15
|
+
const remoteDate = await client.lastMod(remotePath);
|
|
16
|
+
return localStat.mtimeMs > remoteDate.getTime();
|
|
17
|
+
} catch {
|
|
18
|
+
// File doesn't exist remotely (550) or server doesn't support MDTM — upload it
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function createClient(accessOptions) {
|
|
12
24
|
const client = new ftp.Client();
|
|
13
25
|
client.ftp.verbose = false;
|
|
26
|
+
await client.access(accessOptions);
|
|
27
|
+
return client;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function runDeploy(mode, uploadTasks, options = {}) {
|
|
31
|
+
const parallel = options.parallel ?? 3;
|
|
32
|
+
const modeUpper = mode.toUpperCase();
|
|
33
|
+
|
|
34
|
+
const accessOptions = {
|
|
35
|
+
host: process.env[`FTP_HOST_${modeUpper}`],
|
|
36
|
+
user: process.env[`FTP_USER_${modeUpper}`],
|
|
37
|
+
password: process.env[`FTP_PASSWORD_${modeUpper}`],
|
|
38
|
+
secure: true
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const mainClient = new ftp.Client();
|
|
42
|
+
mainClient.ftp.verbose = false;
|
|
14
43
|
let activeProgressBar = null;
|
|
15
44
|
|
|
16
45
|
try {
|
|
17
|
-
const modeUpper = mode.toUpperCase();
|
|
18
46
|
console.log(`🚀 Starting deployment for: ${modeUpper}`);
|
|
19
47
|
|
|
20
|
-
await
|
|
21
|
-
host: process.env[`FTP_HOST_${modeUpper}`],
|
|
22
|
-
user: process.env[`FTP_USER_${modeUpper}`],
|
|
23
|
-
password: process.env[`FTP_PASSWORD_${modeUpper}`],
|
|
24
|
-
secure: true
|
|
25
|
-
});
|
|
48
|
+
await mainClient.access(accessOptions);
|
|
26
49
|
|
|
27
50
|
for (const task of uploadTasks) {
|
|
28
51
|
console.log(`\nProcessing Task: ${task.name}`);
|
|
@@ -38,24 +61,65 @@ export async function runDeploy(mode, uploadTasks) {
|
|
|
38
61
|
continue;
|
|
39
62
|
}
|
|
40
63
|
|
|
41
|
-
|
|
64
|
+
// Determine which files are newer than their remote counterparts
|
|
65
|
+
process.stdout.write(` Checking ${files.length} files...`);
|
|
66
|
+
const filesToUpload = [];
|
|
67
|
+
for (const file of files) {
|
|
68
|
+
const relativeFile = path.relative(task.localBase, file);
|
|
69
|
+
const remotePath = path.join(task.remoteDir, relativeFile).replace(/\\/g, '/');
|
|
70
|
+
if (await isNewer(mainClient, file, remotePath)) {
|
|
71
|
+
filesToUpload.push({ file, remotePath });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
process.stdout.write(` ${filesToUpload.length} changed.\n`);
|
|
75
|
+
|
|
76
|
+
if (filesToUpload.length === 0) {
|
|
77
|
+
console.log(' All files are up to date, skipping.');
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const progressBar = new cliProgress.SingleBar({
|
|
42
82
|
format: ' Upload |{bar}| {percentage}% {value}/{total} files {duration_formatted}',
|
|
43
83
|
barCompleteChar: '█',
|
|
44
84
|
barIncompleteChar: '░',
|
|
45
85
|
hideCursor: true
|
|
46
86
|
});
|
|
47
87
|
|
|
48
|
-
activeProgressBar =
|
|
49
|
-
|
|
88
|
+
activeProgressBar = progressBar;
|
|
89
|
+
progressBar.start(filesToUpload.length, 0);
|
|
50
90
|
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
91
|
+
if (parallel <= 1) {
|
|
92
|
+
for (const { file, remotePath } of filesToUpload) {
|
|
93
|
+
await mainClient.ensureDir(path.dirname(remotePath));
|
|
94
|
+
await mainClient.uploadFrom(file, remotePath);
|
|
95
|
+
progressBar.increment();
|
|
96
|
+
}
|
|
97
|
+
} else {
|
|
98
|
+
// Pre-create all required remote directories with the main client
|
|
99
|
+
const uniqueDirs = [...new Set(filesToUpload.map(({ remotePath }) => path.dirname(remotePath)))];
|
|
100
|
+
for (const dir of uniqueDirs) {
|
|
101
|
+
await mainClient.ensureDir(dir);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Spawn parallel worker connections
|
|
105
|
+
const workerCount = Math.min(parallel, filesToUpload.length);
|
|
106
|
+
const workers = await Promise.all(
|
|
107
|
+
Array.from({ length: workerCount }, () => createClient(accessOptions))
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const queue = [...filesToUpload];
|
|
111
|
+
await Promise.all(workers.map(async (workerClient) => {
|
|
112
|
+
try {
|
|
113
|
+
while (true) {
|
|
114
|
+
const item = queue.shift();
|
|
115
|
+
if (!item) break;
|
|
116
|
+
await workerClient.uploadFrom(item.file, item.remotePath);
|
|
117
|
+
progressBar.increment();
|
|
118
|
+
}
|
|
119
|
+
} finally {
|
|
120
|
+
workerClient.close();
|
|
121
|
+
}
|
|
122
|
+
}));
|
|
59
123
|
}
|
|
60
124
|
|
|
61
125
|
activeProgressBar.stop();
|
|
@@ -71,15 +135,16 @@ export async function runDeploy(mode, uploadTasks) {
|
|
|
71
135
|
}
|
|
72
136
|
console.error('Deployment failed:', err);
|
|
73
137
|
} finally {
|
|
74
|
-
|
|
138
|
+
mainClient.close();
|
|
75
139
|
}
|
|
76
140
|
}
|
|
77
141
|
|
|
78
142
|
/**
|
|
79
143
|
* Run the deploy script.
|
|
80
144
|
* @param {Array} uploadTasks - array of { name, localPattern, localBase, remoteDir, ignore? }
|
|
145
|
+
* @param {Object} options - { parallel: number } (default: { parallel: 3 })
|
|
81
146
|
*/
|
|
82
|
-
export function run(uploadTasks) {
|
|
147
|
+
export function run(uploadTasks, options = {}) {
|
|
83
148
|
const mode = process.argv[2];
|
|
84
149
|
if (!mode || (mode !== 'staging' && mode !== 'production')) {
|
|
85
150
|
console.error('Error: a mode needs to be given, either "staging" or "production"');
|
|
@@ -99,7 +164,7 @@ export function run(uploadTasks) {
|
|
|
99
164
|
rl.close();
|
|
100
165
|
if (answer.toLowerCase() === 'yes') {
|
|
101
166
|
console.log('Confirmed. Starting upload...');
|
|
102
|
-
runDeploy(mode, uploadTasks);
|
|
167
|
+
runDeploy(mode, uploadTasks, options);
|
|
103
168
|
} else {
|
|
104
169
|
console.log('❌ Deployment aborted.');
|
|
105
170
|
process.exit(0);
|
|
@@ -107,6 +172,6 @@ export function run(uploadTasks) {
|
|
|
107
172
|
});
|
|
108
173
|
// In all other modes run deploy without prompt
|
|
109
174
|
} else {
|
|
110
|
-
runDeploy(mode, uploadTasks);
|
|
175
|
+
runDeploy(mode, uploadTasks, options);
|
|
111
176
|
}
|
|
112
177
|
}
|
package/scss/core/layout.scss
CHANGED
|
@@ -197,8 +197,6 @@ $mediaqueries: (
|
|
|
197
197
|
// Distance
|
|
198
198
|
@mixin marginPadding($layout, $mode, $position, $top: 0, $right: 0, $bottom: 0, $left: 0, $important: null) {
|
|
199
199
|
|
|
200
|
-
// @include mode($layout, padding/margin, all/top/right/bottom/left, 0, 0, 0, 0)
|
|
201
|
-
|
|
202
200
|
@if $important == true {
|
|
203
201
|
$important: '!important';
|
|
204
202
|
}
|
|
@@ -225,7 +223,7 @@ $mediaqueries: (
|
|
|
225
223
|
}
|
|
226
224
|
|
|
227
225
|
@if $position == all {
|
|
228
|
-
#{$mode}: $top $right $bottom $left $important;
|
|
226
|
+
#{$mode}: $top $right $bottom $left #{$important};
|
|
229
227
|
}
|
|
230
228
|
@if $position == top {
|
|
231
229
|
#{$mode}-top: $top;
|