@profitlich/template-toolkit 5.6.0 → 6.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/CLAUDE.project.md CHANGED
@@ -65,6 +65,36 @@ Eine JS-Datei ist das eine oder das andere, nie beides:
65
65
 
66
66
  Vermischt man beides, wird ein Import zur versteckten Ladeanweisung: Er sieht ungenutzt aus, ist aber das Einzige, was die Funktion startet. Wer ihn entfernt — oder ein Linter, der ihn meldet — legt sie lautlos still.
67
67
 
68
+ ### Debug-Code
69
+
70
+ Debug-Code bleibt im Quelltext — man braucht ihn beim nächsten Mal wieder. Er darf nur die **Produktion** nicht erreichen; auf Dev **und Staging** bleibt er vollständig erhalten.
71
+
72
+ Dafür zwei Hebel, je nach Fall:
73
+
74
+ - **`console.*`** — entfernt Terser beim Produktions-Build über `drop_console: mode === 'production'` in der `vite.config.js`. Kein Zutun nötig.
75
+ - **Alles andere** (Debug-Ausgaben ins DOM, Messungen, Overlays) — in `if (__DEBUG__) { … }` einschliessen. Vite ersetzt die Konstante zur Bauzeit per `define: { __DEBUG__: mode !== 'production' }`; daraus wird `if (false)`, und der Minifier wirft den Zweig weg.
76
+
77
+ An `mode` hängen, **nicht** an `import.meta.env.DEV` — Letzteres ist auf Staging bereits `false` und würde den Debug-Code dort verschlucken.
78
+
79
+ **Debug-Funktionen auf Modulebene schreiben, nicht als private Klassenmethoden.** Der Minifier entfernt zwar in beiden Fällen den Aufruf, schüttelt ungenutzte private Klassenmethoden aber nicht ab — deren Rumpf bliebe im Produktions-Bundle liegen. Als Funktion verschwindet er vollständig.
80
+
81
+ Ganze Dateien, die nur der Entwicklung dienen, werden gar nicht erst geladen: `{% if craft.app.env != 'production' %}` um die Registrierung, wie bei `Dev.js`.
82
+
83
+ **Debug-Anzeigen schaltbar machen** — nicht dauerhaft einblenden. Die Dev-Toolbar nimmt dafür projekteigene Checkboxen entgegen:
84
+
85
+ ```js
86
+ // src/Dev.js
87
+ initDev(config, {
88
+ toggles: [{ key: 'navigateSpace', name: 'Navigate Space' }],
89
+ });
90
+ ```
91
+
92
+ Der Wert steht danach als `body[data-dev-navigate-space="true"]` bereit — für reine CSS-Anzeigen genügt das, weiteres JS braucht es nicht. Module, die auf den Wechsel *reagieren* müssen statt ihn nur darzustellen, hören auf `eventDevToggle` und lesen `event.detail.key` und `event.detail.value`.
93
+
94
+ Der Schalter ersetzt nicht `__DEBUG__`, er ergänzt es: `__DEBUG__` entscheidet, ob der Code überhaupt ausgeliefert wird, der Schalter, ob man ihn gerade sehen will.
95
+
96
+ Ist ESLint eingerichtet, braucht `__DEBUG__` einen Eintrag unter `languageOptions.globals`, sonst meldet `no-undef`.
97
+
68
98
  ### SCSS
69
99
 
70
100
  - Nie direkte `px`-, `vw`- oder `rem`-Werte — ausschliesslich Toolkit-Funktionen: `size()`, `columns()`, `font()`, `marginPadding()`.
package/dev/Dev.js CHANGED
@@ -1,16 +1,20 @@
1
1
  import { Toolbar } from './toolbar/Toolbar.js';
2
2
 
3
3
  let _config = {};
4
+ let _options = {};
4
5
 
5
6
  /**
6
7
  * Hinterlegt die Projekt-Config für die Dev-Toolbar.
7
8
  * @param {Object} [config={}] - Inhalt von `src/config.json` des Projekts.
9
+ * @param {import('./toolbar/Toolbar.js').ToolbarOptions} [options={}] - Projekteigene
10
+ * Ergänzungen der Toolbar, derzeit `toggles`.
8
11
  */
9
- export function initDev(config = {}) {
12
+ export function initDev(config = {}, options = {}) {
10
13
  _config = config;
14
+ _options = options;
11
15
  }
12
16
 
13
17
  document.addEventListener('DOMContentLoaded', () => {
14
- new Toolbar();
18
+ new Toolbar(_options, _config);
15
19
  console.info('Dev initialized.');
16
20
  });
@@ -1,7 +1,38 @@
1
1
  import GUI from 'lil-gui';
2
+ import { canvasGridLines } from 'canvas-grid-lines';
2
3
  import { MediaQueries } from '../../utils/MediaQueries.js';
4
+ import { gridColumnsTriple } from './gridColumns.js';
3
5
  import './toolbar.scss';
4
6
 
7
+ /**
8
+ * Übersetzt die Auswahl im Grid-Menü in die Rasterart von `canvas-grid-lines`.
9
+ * Die Namen der Auswahl bleiben, wie sie waren — sie stehen im gespeicherten
10
+ * State der Toolbar und wären sonst bei jedem ein Rückschritt auf `aus`.
11
+ *
12
+ * Die Farben standen früher als Sass-Variablen in `toolbar.scss`. Ein Canvas
13
+ * bekommt sie zur Laufzeit, deshalb stehen sie jetzt hier.
14
+ */
15
+ const GRID_MODES = {
16
+ lines: { gridType: 'columns', color: 'rgba(0, 0, 255, 0.5)' },
17
+ ribbons: { gridType: 'ribbons', color: 'rgba(191, 255, 254, 0.5)' },
18
+ };
19
+
20
+ /**
21
+ * Beschreibt eine projekteigene Checkbox in der Dev-Toolbar.
22
+ * @typedef {Object} ToolbarToggle
23
+ * @property {string} key - Schlüssel im State und in `localStorage.devTools`.
24
+ * @property {string} name - Beschriftung in der Toolbar.
25
+ * @property {string} [attribute] - Data-Attribut am `<body>`. Ohne Angabe aus dem
26
+ * Schlüssel abgeleitet: `navigateSpace` → `data-dev-navigate-space`.
27
+ * @property {boolean} [default=false] - Startwert, solange nichts gespeichert ist.
28
+ */
29
+
30
+ /**
31
+ * Optionen, mit denen ein Projekt die Toolbar erweitert.
32
+ * @typedef {Object} ToolbarOptions
33
+ * @property {ToolbarToggle[]} [toggles=[]] - Zusätzliche Checkboxen.
34
+ */
35
+
5
36
  /**
6
37
  * Dev-Toolbar (lil-gui) mit Grid-Overlay, Bildgrössen- und Inhaltstyp-Labels.
7
38
  * State wird in `localStorage.devTools` persistiert. Toggle via `Ctrl`.
@@ -12,17 +43,34 @@ export class Toolbar {
12
43
  #state;
13
44
  #pictureElements;
14
45
  #contentTypeContainer;
46
+ #toggles;
47
+ #config;
48
+ #gridElement;
49
+ #grid = null;
50
+
51
+ /**
52
+ * @param {ToolbarOptions} [options={}] - Projekteigene Ergänzungen.
53
+ * @param {Object} [config={}] - Inhalt von `src/config.json` des Projekts.
54
+ * Das Grid-Overlay leitet Spaltenzahl, Gutter und Seitenränder daraus ab.
55
+ */
56
+ constructor(options = {}, config = {}) {
57
+ this.#toggles = options.toggles ?? [];
58
+ this.#config = config;
15
59
 
16
- constructor() {
17
60
  // State aus localStorage laden
18
61
  const defaults = { visible: false, grid: 'aus', imageSize: false, sizes: false, contentType: false };
62
+ for (const toggle of this.#toggles) {
63
+ defaults[toggle.key] = toggle.default ?? false;
64
+ }
19
65
  const saved = localStorage.getItem('devTools');
20
66
  this.#state = saved ? { ...defaults, ...JSON.parse(saved) } : defaults;
21
67
 
22
- // Grid-Overlay DOM-Element erstellen
23
- const gridOverlay = document.createElement('div');
24
- gridOverlay.classList.add('dev-toolbar__grid');
25
- document.body.prepend(gridOverlay);
68
+ // Grid-Overlay DOM-Element erstellen. Das Canvas darin entsteht erst
69
+ // beim ersten Einschalten — es ist eine Bitmap in Viewportgrösse, und
70
+ // die soll nicht zahlen, wer das Raster gar nicht anschaut.
71
+ this.#gridElement = document.createElement('div');
72
+ this.#gridElement.classList.add('dev-toolbar__grid');
73
+ document.body.prepend(this.#gridElement);
26
74
 
27
75
  // Content-Type-Label-Overlay erstellen
28
76
  this.#contentTypeContainer = document.createElement('div');
@@ -51,6 +99,14 @@ export class Toolbar {
51
99
  .name('Inhaltstypen')
52
100
  .onChange(() => this.#onStateChange());
53
101
 
102
+ // Projekteigene Schalter ans Ende, damit die Reihenfolge der festen
103
+ // Einträge über alle Projekte hinweg gleich bleibt
104
+ for (const toggle of this.#toggles) {
105
+ this.#gui.add(this.#state, toggle.key)
106
+ .name(toggle.name)
107
+ .onChange(() => this.#onStateChange());
108
+ }
109
+
54
110
  // State anwenden
55
111
  this.#applyState();
56
112
 
@@ -61,6 +117,7 @@ export class Toolbar {
61
117
 
62
118
  // Event-Listener
63
119
  window.addEventListener('resize', this.#onResize);
120
+ window.addEventListener('eventLayoutchange', this.#onLayoutChange);
64
121
  document.addEventListener('keydown', this.#handleKeyDown);
65
122
  }
66
123
 
@@ -76,11 +133,49 @@ export class Toolbar {
76
133
  #applyState() {
77
134
  document.body.setAttribute('data-dev-grid', this.#state.grid);
78
135
  document.body.setAttribute('data-dev-content-types', this.#state.contentType);
136
+ this.#updateGrid();
137
+ this.#applyToggles();
79
138
  this.#updateImageSize();
80
139
  this.#updateSizes();
81
140
  this.#updateContentTypeLabels();
82
141
  }
83
142
 
143
+ /**
144
+ * Baut das Raster beim ersten Einschalten und hält es danach auf dem Stand
145
+ * von Auswahl und Layout. Im Modus `aus` geschieht nichts — das Ausblenden
146
+ * erledigt allein `data-dev-grid` in der CSS.
147
+ */
148
+ #updateGrid() {
149
+ const mode = GRID_MODES[this.#state.grid];
150
+ if (!mode) return;
151
+
152
+ const columns = gridColumnsTriple(this.#config, this.#mediaQueries.layout);
153
+ if (!columns) return;
154
+
155
+ if (!this.#grid) {
156
+ [this.#grid] = canvasGridLines.initGrid({
157
+ targets: this.#gridElement,
158
+ gridType: mode.gridType,
159
+ columns,
160
+ color: mode.color,
161
+ lineWidth: 1,
162
+ units: 'devicepixel',
163
+ }) ?? [];
164
+ return;
165
+ }
166
+
167
+ // Reihenfolge: erst die Spalten, dann die Rasterart. Der gridType-Setter
168
+ // leitet sein Lückenmuster aus dem aktuellen columns-Wert ab, und der
169
+ // muss nach einem Breakpoint-Wechsel schon der neue sein.
170
+ this.#grid.columns = columns;
171
+ this.#grid.gridType = mode.gridType;
172
+ this.#grid.color = mode.color;
173
+ }
174
+
175
+ #onLayoutChange = () => {
176
+ this.#updateGrid();
177
+ }
178
+
84
179
  #updateContentTypeLabels() {
85
180
  this.#contentTypeContainer.innerHTML = '';
86
181
  if (!this.#state.contentType) return;
@@ -95,6 +190,25 @@ export class Toolbar {
95
190
  });
96
191
  }
97
192
 
193
+ /**
194
+ * Schreibt jeden projekteigenen Schalter als Data-Attribut ans `<body>` und
195
+ * meldet ihn zusätzlich per Event.
196
+ *
197
+ * Beides, weil beide Seiten gebraucht werden: Das Attribut genügt für reines
198
+ * CSS und gilt auch für Listener, die es zum Zeitpunkt des Umschaltens noch
199
+ * nicht gab; das Event erreicht Module, die auf den Wechsel reagieren müssen,
200
+ * statt ihn nur darzustellen.
201
+ */
202
+ #applyToggles() {
203
+ for (const toggle of this.#toggles) {
204
+ const value = this.#state[toggle.key];
205
+ document.body.setAttribute(toggleAttribute(toggle), String(value));
206
+ document.dispatchEvent(new CustomEvent('eventDevToggle', {
207
+ detail: { key: toggle.key, value },
208
+ }));
209
+ }
210
+ }
211
+
98
212
  #saveState() {
99
213
  localStorage.setItem('devTools', JSON.stringify(this.#state));
100
214
  }
@@ -159,3 +273,14 @@ export class Toolbar {
159
273
  }
160
274
  }
161
275
  }
276
+
277
+ /**
278
+ * Liefert das Data-Attribut eines Schalters — die explizite Angabe, sonst aus dem
279
+ * Schlüssel abgeleitet: `navigateSpace` → `data-dev-navigate-space`.
280
+ * @param {ToolbarToggle} toggle
281
+ * @returns {string}
282
+ */
283
+ function toggleAttribute(toggle) {
284
+ return toggle.attribute
285
+ ?? `data-dev-${toggle.key.replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`)}`;
286
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Rechnet die Spaltenkonfiguration eines Layouts in das Tripel um, das
3
+ * `canvas-grid-lines` für die Rasterarten `columns` und `ribbons` erwartet:
4
+ * `[total, band, gap]` in Rastereinheiten.
5
+ *
6
+ * Die Einheit ist der grösste gemeinsame Teiler von Spaltenbreite und Gutter —
7
+ * aus einem Verhältnis wie 80 : 20 werden damit die ganzen Zahlen 4 : 1, die
8
+ * das Paket als Wiederholungsmuster braucht. Beide Rasterarten teilen sich
9
+ * dieselbe Kantenfolge: `columns` strichelt sie, `ribbons` füllt dazwischen.
10
+ *
11
+ * @param {Object} config - Inhalt von `src/config.json` des Projekts.
12
+ * @param {string} layout - Layoutname, etwa `desktop`.
13
+ * @returns {number[]|undefined} `[total, band, gap]`, oder `undefined`, wenn die
14
+ * Angaben fehlen oder sich das Verhältnis nicht ganzzahlig ausdrücken lässt.
15
+ */
16
+ export function gridColumnsTriple(config, layout) {
17
+ const width = config?.layouts?.[layout];
18
+ const margins = config?.margins?.[layout];
19
+ const gutter = config?.gutter?.[layout];
20
+ const count = config?.columns?.[layout];
21
+
22
+ if (width == null || margins == null || gutter == null || count == null) {
23
+ console.warn(`Dev-Toolbar: Für das Layout "${layout}" fehlen Angaben in der config.json (layouts, margins, columns, gutter). Wird initDev die Config übergeben?`);
24
+ return undefined;
25
+ }
26
+
27
+ // Die Spaltenbreite ist `zaehler / count` und oft kein ganzer Designpixel —
28
+ // in `template-kirbycms` etwa 91,428…. Deshalb wird sie nie ausgerechnet:
29
+ // Das Verhältnis Spalte zu Gutter ist `zaehler/count : gutter`, und mit
30
+ // `count` erweitert wird daraus `zaehler : count · gutter`, also ein
31
+ // Verhältnis zweier ganzer Zahlen. Gekürzt ergibt das exakt das gesuchte
32
+ // Muster, ohne jede Rundung.
33
+ const zaehler = width - margins.left - margins.right - (count - 1) * gutter;
34
+
35
+ if (!Number.isInteger(zaehler) || !Number.isInteger(gutter) || !Number.isInteger(count) || zaehler <= 0) {
36
+ console.warn(`Dev-Toolbar: Layout "${layout}" ergibt keine brauchbare Spaltenaufteilung (Rest ${zaehler}, Gutter ${gutter}, Spalten ${count}) — das Raster bleibt aus.`);
37
+ return undefined;
38
+ }
39
+
40
+ const unit = greatestCommonDivisor(zaehler, count * gutter);
41
+ const band = zaehler / unit;
42
+ const gap = (count * gutter) / unit;
43
+
44
+ return [count * band + (count - 1) * gap, band, gap];
45
+ }
46
+
47
+ function greatestCommonDivisor(a, b) {
48
+ return b === 0 ? a : greatestCommonDivisor(b, a % b);
49
+ }
@@ -7,69 +7,43 @@
7
7
  }
8
8
 
9
9
  // grid
10
- $color--dev-grid: rgba(0, 0, 255, 0.5);
11
- $color--dev-grid-ribbons: rgba(191, 255, 254, 0.5);
12
- $color--dev-grid-center: rgba(0, 0, 0, 0.2);
13
-
14
- @mixin dev-toolbar-grid($layout, $columns, $gutter, $margin-left, $margin-right) {
15
-
16
- $gutter: size($layout, $gutter);
17
- $margin-left: size($layout, $margin-left);
18
- $margin-right: size($layout, $margin-right);
19
-
20
- .dev-toolbar__grid::after {
21
- // the grid is made through repition
22
- // 'important' because background settings are more specific than the grid settings
23
- background-size: calc((100% - ($columns * $gutter)) * calc(1 / $columns) + $gutter) 1px !important;
24
- margin: 0 calc($margin-right - calc($gutter / 2)) 0 calc($margin-left - calc($gutter / 2));
25
- // The grid is calculated starting from the middle
26
- // Thats why its necessary to add the half of the column gap to the left and right side
27
- width: calc(100% - $margin-right + calc($gutter / 2) - $margin-left + calc($gutter / 2));
28
- }
29
-
30
- body[data-dev-grid="lines"] {
31
- .dev-toolbar__grid::after {
32
- background:
33
- // center of the column spacing
34
- linear-gradient(90deg, transparent 0.5px, transparent 0.5px) 0 0,
35
- // Left column line
36
- // position: from the column spacing center to the right by half a column spacing
37
- linear-gradient(90deg, $color--dev-grid 0.5px, transparent 0.5px) calc($gutter / 2) 0,
38
- // right column line
39
- linear-gradient(90deg, $color--dev-grid 0.5px, transparent 0.5px) calc($gutter / 2 * -1) 0;
40
- }
41
- }
42
-
43
- body[data-dev-grid="ribbons"] {
44
- .dev-toolbar__grid::after {
45
- background:
46
- // ceft column ribbon
47
- // position: from the column spacing center to the right by half a column spacing
48
- linear-gradient(90deg, $color--dev-grid-ribbons calc(100% - $gutter), transparent 0.5px) calc($gutter / 2) 0,
49
- }
50
- }
51
- }
52
-
10
+ //
11
+ // Gezeichnet wird das Raster von `canvas-grid-lines`; Toolbar.js hängt das
12
+ // Canvas in dieses Element. Hier steht nur noch, wo das Element liegt.
13
+ // Die Linienfarben stehen als GRID_MODES in Toolbar.js — ein Canvas nimmt sie
14
+ // zur Laufzeit entgegen, nicht als Sass-Variable.
53
15
  .dev-toolbar__grid {
54
- display: none;
55
- width: 100%;
16
+ // Spannt genau die Rasterbreite auf: Seitenränder aus `$margins`, Breite
17
+ // aus `left`/`right`. Die frühere Verschiebung um eine halbe Gutter-Breite
18
+ // entfällt — sie richtete nur die Kachelwiederholung des Gradienten aus.
56
19
  height: 100%;
57
- left: 0;
58
20
  pointer-events: none;
59
21
  position: fixed;
60
22
  top: 0;
61
23
 
62
- body:not([data-dev-grid='aus']) & {
63
- display: block;
64
- }
24
+ // Nicht `display: none`: Ein Container ohne Ausdehnung schickt
25
+ // canvas-grid-lines auf seinen Lazy-Pfad, das Raster bliebe ungezeichnet.
26
+ visibility: hidden;
27
+
28
+ // Immer vor dem Inhalt: Ein Messraster, das hinter dem liegt, was man
29
+ // vermessen will, taugt nicht — früher galt das nur für `lines`, `ribbons`
30
+ // lag dahinter. Der z-index macht zugleich den Stacking Context auf, ohne
31
+ // den das Canvas hinter den Hintergrund des Body rutschte.
32
+ //
33
+ // Gleicher Wert wie `.lil-gui.lil-root`, und das ist Absicht: Bei
34
+ // Gleichstand entscheidet die Dokumentreihenfolge, und die Toolbar hängt
35
+ // sich nach dem Overlay ein. Das Panel bleibt also bedienbar.
36
+ z-index: 9999999;
65
37
 
66
- body[data-dev-grid='lines'] & {
67
- z-index: 9999999;
38
+ body:not([data-dev-grid='aus']) & {
39
+ visibility: visible;
68
40
  }
69
41
 
70
- &::after {
71
- content: "";
72
- height: 100%;
42
+ // Bewusst statt eines Imports von `canvas-grid-lines/css`: Dessen Regel
43
+ // greift über `[data-grid-initialised] canvas` auf jedes Canvas im Dokument
44
+ // und würde die Rasterebenen im Projekt mit `z-index: -1` nach hinten
45
+ // ziehen. Hier bleibt sie auf das Overlay beschränkt.
46
+ canvas {
73
47
  left: 0;
74
48
  position: absolute;
75
49
  top: 0;
@@ -78,7 +52,10 @@ $color--dev-grid-center: rgba(0, 0, 0, 0.2);
78
52
 
79
53
  @each $layout, $breite in $layouts {
80
54
  @include mediaquery($layout) using ($layout) {
81
- @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'));
55
+ .dev-toolbar__grid {
56
+ left: size($layout, map.get(map.get($margins, $layout), 'left'));
57
+ right: size($layout, map.get(map.get($margins, $layout), 'right'));
58
+ }
82
59
  }
83
60
  }
84
61
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@profitlich/template-toolkit",
3
- "version": "5.6.0",
3
+ "version": "6.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
  "sass": "scss/forward.scss",
@@ -39,6 +39,7 @@
39
39
  ],
40
40
  "dependencies": {
41
41
  "basic-ftp": "^5.0.0",
42
+ "canvas-grid-lines": "^10.3.0",
42
43
  "chokidar": "^4.0.0",
43
44
  "cli-progress": "^3.12.0",
44
45
  "dotenv": "^17.0.0",
@@ -64,7 +65,7 @@
64
65
  },
65
66
  "repository": {
66
67
  "type": "git",
67
- "url": "git+https://github.com/profitlich-ch/profitlich-template-toolkit.git"
68
+ "url": "git+https://github.com/profitlich-ch/template-toolkit.git"
68
69
  },
69
70
  "license": "MIT"
70
71
  }
@@ -1,228 +0,0 @@
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