@profitlich/template-toolkit 5.8.0 → 6.1.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 +38 -0
- package/dev/Dev.js +1 -1
- package/dev/toolbar/Toolbar.js +66 -5
- package/dev/toolbar/gridColumns.js +49 -0
- package/dev/toolbar/toolbar.scss +32 -55
- package/package.json +5 -3
- package/scss/core/capsize.scss +42 -0
- package/vite/capsizeSassFunctions.js +27 -0
- package/vite/postcssBreakpointDry.js +134 -0
- package/dev/toolbar/COLUMNS.md +0 -228
package/CLAUDE.project.md
CHANGED
|
@@ -102,6 +102,22 @@ Ist ESLint eingerichtet, braucht `__DEBUG__` einen Eintrag unter `languageOption
|
|
|
102
102
|
- Jedes Modul/Snippet hat eine eigene `.js`-Datei, die das zugehörige SCSS importiert — auch wenn sie sonst keine Logik enthält.
|
|
103
103
|
- Keine globalen Styles in Modul- oder Snippet-SCSS-Dateien.
|
|
104
104
|
|
|
105
|
+
#### Grundregel und Breakpoint-Blöcke
|
|
106
|
+
|
|
107
|
+
**In einen `mediaquery()`-Block gehört nur, was sich pro Breakpoint unterscheidet** – in der Praxis fast nur Aufrufe mit `$layout`. Feste Werte wie `display: flex` oder `position: fixed` stehen einmal in der Grundregel oben in der Datei. Einen Breakpoint-Block nie als Kopie eines anderen anlegen: Genau so wandern feste Werte in alle drei Blöcke.
|
|
108
|
+
|
|
109
|
+
Gleich *geschriebene* `$layout`-Aufrufe wie `font($layout, 14, 20)` sind keine Wiederholung – sie ergeben pro Breakpoint andere Werte und bleiben in den Blöcken.
|
|
110
|
+
|
|
111
|
+
Das PostCSS-Plugin `@profitlich/template-toolkit/vite/postcssBreakpointDry` meldet beim Build und im Dev-Server, wo es trotzdem passiert ist: Deklarationen, die mit gleichem Selektor und Wert in Media-Queries stehen, die zusammen alle Breiten abdecken, und solche, die nur die Grundregel wiederholen. Es warnt nur. Eingebunden in `postcss.config.js`:
|
|
112
|
+
|
|
113
|
+
```js
|
|
114
|
+
import postcssBreakpointDry from '@profitlich/template-toolkit/vite/postcssBreakpointDry';
|
|
115
|
+
|
|
116
|
+
export default {
|
|
117
|
+
plugins: [postcssBreakpointDry()],
|
|
118
|
+
};
|
|
119
|
+
```
|
|
120
|
+
|
|
105
121
|
#### vw-Basis
|
|
106
122
|
|
|
107
123
|
Per Default skalieren vw-basierte Werte mit der Viewport-Breite inkl. Scrollbar (`100vw`). Mit `"vwBasis": "body"` als Top-Level-Feld in `src/config.json` skalieren sie stattdessen mit der scrollbar-freien Body-Breite — sinnvoll, wenn Layout-Elemente in `%` gesetzt sind und Schriften/Abstände exakt mit diesen mitskalieren sollen. Dafür im Projekt `VwBody.getInstance()` (aus `@profitlich/template-toolkit/utils/VwBody`) aufrufen — setzt die Custom Property `--vw-body` per JS. Default-Verhalten ohne Feld unverändert.
|
|
@@ -142,6 +158,28 @@ $cap: capsize-cap-height("soehne", 40); // → font-spezifischer Wert
|
|
|
142
158
|
padding-top: size($layout, 40 - $cap + 8);
|
|
143
159
|
```
|
|
144
160
|
|
|
161
|
+
**Capsize und Breakpoints**: Die Trims sind em-Werte und hängen nur von der Schrift und vom Verhältnis Zeilenhöhe/Schriftgrösse ab, nicht von `$layout`. Bekommt ein Selektor in allen Breakpoints Capsize, gehört der gleichbleibende Teil in die Grundregel. Zwei Fälle:
|
|
162
|
+
|
|
163
|
+
- **Gleiches Verhältnis in allen Breakpoints** (z. B. überall 14/20): `capsize()` einmal in der Grundregel, in den Breakpoints `font()` ohne 4. Argument.
|
|
164
|
+
|
|
165
|
+
```scss
|
|
166
|
+
.foo { @include capsize("soehne", 14, 20); }
|
|
167
|
+
@include mediaquery(tablet) using ($layout) {
|
|
168
|
+
.foo { @include font($layout, 14, 20); }
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
- **Verschiedene Verhältnisse** (z. B. 40/70, 56/120, 111/120): `capsize-base` in der Grundregel. Es schreibt `content` und `display` der Pseudo-Elemente einmal. `font()` mit Capsize-Font gibt danach für denselben Selektor nur noch die Trims aus. Die Verschachtelung muss in Grundregel und Breakpoint gleich sein, sonst greift die Zuordnung nicht, und es wird wie bisher alles ausgegeben.
|
|
173
|
+
|
|
174
|
+
```scss
|
|
175
|
+
.foo { @include capsize-base; }
|
|
176
|
+
@include mediaquery(tablet) using ($layout) {
|
|
177
|
+
.foo { @include font($layout, 56, 120, "soehne"); } // → nur Trims
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Beides nur, wenn **jeder** Breakpoint Capsize bekommt. Sonst entstehen in den übrigen Pseudo-Elemente ohne Trims, die in einem Flex- oder Grid-Container als zusätzliche Items mitlaufen.
|
|
182
|
+
|
|
145
183
|
### Vite Entry
|
|
146
184
|
|
|
147
185
|
Einen neuen Entry in `rollupOptions.input` eintragen **nur wenn** das Script per Twig/PHP-Tag direkt eingebunden wird. Wird es von einem anderen Script importiert, braucht es keinen eigenen Entry.
|
package/dev/Dev.js
CHANGED
package/dev/toolbar/Toolbar.js
CHANGED
|
@@ -1,7 +1,22 @@
|
|
|
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
|
+
|
|
5
20
|
/**
|
|
6
21
|
* Beschreibt eine projekteigene Checkbox in der Dev-Toolbar.
|
|
7
22
|
* @typedef {Object} ToolbarToggle
|
|
@@ -29,12 +44,18 @@ export class Toolbar {
|
|
|
29
44
|
#pictureElements;
|
|
30
45
|
#contentTypeContainer;
|
|
31
46
|
#toggles;
|
|
47
|
+
#config;
|
|
48
|
+
#gridElement;
|
|
49
|
+
#grid = null;
|
|
32
50
|
|
|
33
51
|
/**
|
|
34
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.
|
|
35
55
|
*/
|
|
36
|
-
constructor(options = {}) {
|
|
56
|
+
constructor(options = {}, config = {}) {
|
|
37
57
|
this.#toggles = options.toggles ?? [];
|
|
58
|
+
this.#config = config;
|
|
38
59
|
|
|
39
60
|
// State aus localStorage laden
|
|
40
61
|
const defaults = { visible: false, grid: 'aus', imageSize: false, sizes: false, contentType: false };
|
|
@@ -44,10 +65,12 @@ export class Toolbar {
|
|
|
44
65
|
const saved = localStorage.getItem('devTools');
|
|
45
66
|
this.#state = saved ? { ...defaults, ...JSON.parse(saved) } : defaults;
|
|
46
67
|
|
|
47
|
-
// Grid-Overlay DOM-Element erstellen
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
document.
|
|
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);
|
|
51
74
|
|
|
52
75
|
// Content-Type-Label-Overlay erstellen
|
|
53
76
|
this.#contentTypeContainer = document.createElement('div');
|
|
@@ -94,6 +117,7 @@ export class Toolbar {
|
|
|
94
117
|
|
|
95
118
|
// Event-Listener
|
|
96
119
|
window.addEventListener('resize', this.#onResize);
|
|
120
|
+
window.addEventListener('eventLayoutchange', this.#onLayoutChange);
|
|
97
121
|
document.addEventListener('keydown', this.#handleKeyDown);
|
|
98
122
|
}
|
|
99
123
|
|
|
@@ -109,12 +133,49 @@ export class Toolbar {
|
|
|
109
133
|
#applyState() {
|
|
110
134
|
document.body.setAttribute('data-dev-grid', this.#state.grid);
|
|
111
135
|
document.body.setAttribute('data-dev-content-types', this.#state.contentType);
|
|
136
|
+
this.#updateGrid();
|
|
112
137
|
this.#applyToggles();
|
|
113
138
|
this.#updateImageSize();
|
|
114
139
|
this.#updateSizes();
|
|
115
140
|
this.#updateContentTypeLabels();
|
|
116
141
|
}
|
|
117
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
|
+
|
|
118
179
|
#updateContentTypeLabels() {
|
|
119
180
|
this.#contentTypeContainer.innerHTML = '';
|
|
120
181
|
if (!this.#state.contentType) return;
|
|
@@ -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
|
+
}
|
package/dev/toolbar/toolbar.scss
CHANGED
|
@@ -7,69 +7,43 @@
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
// grid
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
55
|
-
|
|
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
|
-
|
|
63
|
-
|
|
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='
|
|
67
|
-
|
|
38
|
+
body:not([data-dev-grid='aus']) & {
|
|
39
|
+
visibility: visible;
|
|
68
40
|
}
|
|
69
41
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
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": "
|
|
3
|
+
"version": "6.1.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",
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
"./scripts/copy-files": "./scripts/copy-files.js",
|
|
25
25
|
"./scripts/deploy": "./scripts/deploy.js",
|
|
26
26
|
"./vite/jsonToScss": "./vite/jsonToScss.js",
|
|
27
|
-
"./vite/capsizeSassFunctions": "./vite/capsizeSassFunctions.js"
|
|
27
|
+
"./vite/capsizeSassFunctions": "./vite/capsizeSassFunctions.js",
|
|
28
|
+
"./vite/postcssBreakpointDry": "./vite/postcssBreakpointDry.js"
|
|
28
29
|
},
|
|
29
30
|
"files": [
|
|
30
31
|
"CLAUDE.project.md",
|
|
@@ -39,6 +40,7 @@
|
|
|
39
40
|
],
|
|
40
41
|
"dependencies": {
|
|
41
42
|
"basic-ftp": "^5.0.0",
|
|
43
|
+
"canvas-grid-lines": "^10.3.0",
|
|
42
44
|
"chokidar": "^4.0.0",
|
|
43
45
|
"cli-progress": "^3.12.0",
|
|
44
46
|
"dotenv": "^17.0.0",
|
|
@@ -64,7 +66,7 @@
|
|
|
64
66
|
},
|
|
65
67
|
"repository": {
|
|
66
68
|
"type": "git",
|
|
67
|
-
"url": "git+https://github.com/profitlich-ch/
|
|
69
|
+
"url": "git+https://github.com/profitlich-ch/template-toolkit.git"
|
|
68
70
|
},
|
|
69
71
|
"license": "MIT"
|
|
70
72
|
}
|
package/scss/core/capsize.scss
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
|
+
@use "sass:map";
|
|
1
2
|
@use "sass:meta";
|
|
2
3
|
|
|
4
|
+
// Selektoren, deren fester Capsize-Teil per capsize-base bereits in der
|
|
5
|
+
// Grundregel steht. Jede SCSS-Datei ist eine eigene Sass-Kompilation, die Map
|
|
6
|
+
// gilt also nur innerhalb einer Datei.
|
|
7
|
+
$_base-selectors: ();
|
|
8
|
+
|
|
3
9
|
// Emittiert die Capsize-Pseudo-Elemente (::before/::after mit Em-Trims)
|
|
4
10
|
// für den aktuellen Selektor. Die Inhalte (welche Pseudo-Elemente, welche
|
|
5
11
|
// Properties) kommen vollständig aus @capsizecss/core via der
|
|
6
12
|
// Sass-Custom-Function capsize-pseudo-elements (registriert im
|
|
7
13
|
// vite/capsizeSassFunctions.js des Toolkits).
|
|
14
|
+
//
|
|
15
|
+
// Steht für den Selektor capsize-base in der Grundregel, entfallen hier die
|
|
16
|
+
// festen Properties, und es bleiben nur die Trims.
|
|
8
17
|
@mixin capsize($name, $fontSize, $lineHeight) {
|
|
9
18
|
// Capsize ist opt-in: ohne registrierte Custom-Function wird übersprungen,
|
|
10
19
|
// damit ein fehlender Vite-Setup nicht zu kryptischem Sass-Fehler führt.
|
|
@@ -12,12 +21,45 @@
|
|
|
12
21
|
@warn "Capsize ist nicht eingerichtet: Custom-Function capsize-pseudo-elements fehlt. Siehe template-toolkit/CLAUDE.md, Abschnitt Capsize. Aufruf für \"#{$name}\" wird übersprungen.";
|
|
13
22
|
} @else {
|
|
14
23
|
$pseudo: capsize-pseudo-elements($name, $fontSize, $lineHeight);
|
|
24
|
+
$static: ();
|
|
25
|
+
@if map.has-key($_base-selectors, "#{&}") {
|
|
26
|
+
$static: capsize-static-properties();
|
|
27
|
+
}
|
|
28
|
+
|
|
15
29
|
@each $selector-suffix, $props in $pseudo {
|
|
30
|
+
$skip: ();
|
|
31
|
+
@if map.has-key($static, $selector-suffix) {
|
|
32
|
+
$skip: map.get($static, $selector-suffix);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
&#{$selector-suffix} {
|
|
36
|
+
@each $prop, $val in $props {
|
|
37
|
+
@if not map.has-key($skip, $prop) {
|
|
38
|
+
#{$prop}: $val;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Emittiert den festen Teil der Capsize-Pseudo-Elemente (content, display)
|
|
47
|
+
// einmal in der Grundregel. Die Aufrufe von capsize() bzw. font() mit
|
|
48
|
+
// Capsize-Font in den Breakpoint-Blöcken geben danach nur noch die Trims aus.
|
|
49
|
+
//
|
|
50
|
+
// Nur verwenden, wenn der Selektor in allen Breakpoints Capsize bekommt –
|
|
51
|
+
// sonst entstehen dort Pseudo-Elemente ohne Trims.
|
|
52
|
+
@mixin capsize-base {
|
|
53
|
+
@if not meta.function-exists("capsize-static-properties") {
|
|
54
|
+
@warn "Capsize ist nicht eingerichtet: Custom-Function capsize-static-properties fehlt. Siehe template-toolkit/CLAUDE.md, Abschnitt Capsize. capsize-base wird übersprungen.";
|
|
55
|
+
} @else {
|
|
56
|
+
@each $selector-suffix, $props in capsize-static-properties() {
|
|
16
57
|
&#{$selector-suffix} {
|
|
17
58
|
@each $prop, $val in $props {
|
|
18
59
|
#{$prop}: $val;
|
|
19
60
|
}
|
|
20
61
|
}
|
|
21
62
|
}
|
|
63
|
+
$_base-selectors: map.set($_base-selectors, "#{&}", true) !global;
|
|
22
64
|
}
|
|
23
65
|
}
|
|
@@ -60,6 +60,33 @@ export async function createCapsizeFunctions(fontFiles) {
|
|
|
60
60
|
return new sass.SassMap(OrderedMap(entries));
|
|
61
61
|
},
|
|
62
62
|
|
|
63
|
+
// Liefert pro Pseudo-Element die Properties, die nicht von Schriftgrösse
|
|
64
|
+
// und Zeilenhöhe abhängen (heute content und display). Ermittelt aus zwei
|
|
65
|
+
// Aufrufen mit verschiedenem Verhältnis: Was in beiden gleich bleibt, ist
|
|
66
|
+
// fest. So bestimmt Capsize das Schema, nicht eine Liste im Toolkit.
|
|
67
|
+
'capsize-static-properties()': () => {
|
|
68
|
+
const fontMetrics = Object.values(metrics)[0];
|
|
69
|
+
if (!fontMetrics) {
|
|
70
|
+
throw new Error('capsize-static-properties: Keine Font-Metriken geladen.');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const narrow = createStyleObject({ fontSize: 10, leading: 12, fontMetrics });
|
|
74
|
+
const wide = createStyleObject({ fontSize: 10, leading: 30, fontMetrics });
|
|
75
|
+
|
|
76
|
+
const entries = [];
|
|
77
|
+
for (const [key, val] of Object.entries(narrow)) {
|
|
78
|
+
if (!key.startsWith('::')) continue;
|
|
79
|
+
const fixed = Object.fromEntries(
|
|
80
|
+
Object.entries(val).filter(([prop, value]) => wide[key]?.[prop] === value)
|
|
81
|
+
);
|
|
82
|
+
entries.push([
|
|
83
|
+
new sass.SassString(key, { quotes: false }),
|
|
84
|
+
_objToSassMap(fixed),
|
|
85
|
+
]);
|
|
86
|
+
}
|
|
87
|
+
return new sass.SassMap(OrderedMap(entries));
|
|
88
|
+
},
|
|
89
|
+
|
|
63
90
|
'capsize-cap-height($name, $fontSize)': (args) => {
|
|
64
91
|
const name = args[0].assertString('name').text;
|
|
65
92
|
const fontSize = args[1].assertNumber('fontSize').value;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* PostCSS-Plugin, das Wiederholungen über Breakpoint-Blöcke hinweg meldet.
|
|
5
|
+
*
|
|
6
|
+
* Läuft auf dem kompilierten CSS jeder SCSS-Datei. Verschachtelung ist dort
|
|
7
|
+
* aufgelöst und `$layout`-Werte sind ausgerechnet. Gewarnt wird in zwei Fällen:
|
|
8
|
+
*
|
|
9
|
+
* - Eine Deklaration steht mit gleichem Selektor und gleichem Wert in
|
|
10
|
+
* Media-Queries, die zusammen alle Breiten abdecken (typisch: smartphone,
|
|
11
|
+
* tablet und desktop). Sie gehört in die Grundregel.
|
|
12
|
+
* - Eine Deklaration in einer Media-Query wiederholt, was die Grundregel für
|
|
13
|
+
* denselben Selektor bereits setzt.
|
|
14
|
+
*
|
|
15
|
+
* Das Plugin warnt nur und bricht den Build nie ab. Dateien aus `node_modules`
|
|
16
|
+
* werden übersprungen.
|
|
17
|
+
*
|
|
18
|
+
* @returns {import('postcss').Plugin}
|
|
19
|
+
*/
|
|
20
|
+
export default function postcssBreakpointDry() {
|
|
21
|
+
return {
|
|
22
|
+
postcssPlugin: 'postcss-breakpoint-dry',
|
|
23
|
+
OnceExit(root, { result }) {
|
|
24
|
+
const file = root.source?.input.file ?? '';
|
|
25
|
+
if (/[\\/]node_modules[\\/]/.test(file)) return;
|
|
26
|
+
// Vite nennt bei PostCSS-Warnungen die Datei nicht, darum steht sie in der Meldung
|
|
27
|
+
const location = file ? `${path.relative(process.cwd(), file)}: ` : '';
|
|
28
|
+
|
|
29
|
+
// Selektor|Eigenschaft → Wert, jeweils der letzte Stand der Grundregeln
|
|
30
|
+
const base = new Map();
|
|
31
|
+
// Selektor|Eigenschaft → [{ value, interval, decl, baseValue }]
|
|
32
|
+
const media = new Map();
|
|
33
|
+
|
|
34
|
+
root.each((node) => {
|
|
35
|
+
if (node.type === 'rule') {
|
|
36
|
+
eachDecl(node, (key, value) => base.set(key, value));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (node.type !== 'atrule' || node.name !== 'media') return;
|
|
40
|
+
|
|
41
|
+
// Unbekannte Bedingungen wie (hover: hover) lassen sich nicht auf Breiten abbilden
|
|
42
|
+
const interval = parseInterval(node.params);
|
|
43
|
+
if (!interval) return;
|
|
44
|
+
|
|
45
|
+
node.each((rule) => {
|
|
46
|
+
if (rule.type !== 'rule') return;
|
|
47
|
+
eachDecl(rule, (key, value, decl) => {
|
|
48
|
+
if (!media.has(key)) media.set(key, []);
|
|
49
|
+
media.get(key).push({ value, interval, decl, baseValue: base.get(key) });
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
for (const [key, entries] of media) {
|
|
55
|
+
// Setzt eine andere Media-Query denselben Selektor anders, hängt das
|
|
56
|
+
// Ergebnis an der Reihenfolge – kein eindeutiger Fall
|
|
57
|
+
const values = new Set(entries.map((entry) => entry.value));
|
|
58
|
+
if (values.size > 1) continue;
|
|
59
|
+
|
|
60
|
+
const [selector, prop] = key.split('|');
|
|
61
|
+
const { value, decl } = entries[0];
|
|
62
|
+
// Pseudo-Elemente stammen meist aus font() mit Capsize-Font, behoben wird dort
|
|
63
|
+
const hint = /::?(before|after)\b/.test(selector) ? ' (bei Capsize: capsize() bzw. capsize-base in die Grundregel)' : '';
|
|
64
|
+
|
|
65
|
+
if (entries.every((entry) => entry.baseValue === value)) {
|
|
66
|
+
decl.warn(result, `${location}${selector} { ${prop}: ${value} } wiederholt die Grundregel und kann aus den Media-Queries entfallen${hint}`);
|
|
67
|
+
} else if (coversAllWidths(entries.map((entry) => entry.interval))) {
|
|
68
|
+
decl.warn(result, `${location}${selector} { ${prop}: ${value} } steht in Media-Queries, die zusammen alle Breiten abdecken, und gehört in die Grundregel${hint}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
postcssBreakpointDry.postcss = true;
|
|
76
|
+
|
|
77
|
+
/** Ruft `callback` für jede direkte Deklaration einer Regel auf, mit normalisiertem Schlüssel und Wert. */
|
|
78
|
+
function eachDecl(rule, callback) {
|
|
79
|
+
const selector = rule.selectors.map((part) => part.replace(/\s+/g, ' ').trim()).join(', ');
|
|
80
|
+
rule.each((decl) => {
|
|
81
|
+
if (decl.type !== 'decl') return;
|
|
82
|
+
const value = decl.value.replace(/\s+/g, ' ').trim() + (decl.important ? ' !important' : '');
|
|
83
|
+
callback(`${selector}|${decl.prop}`, value, decl);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Liest eine Media-Query als Breitenbereich in ganzen px, in der Form
|
|
89
|
+
* `(min-width: 740px)` oder `(width >= 740px)`. Liefert `null` für alles andere –
|
|
90
|
+
* etwa Listen, Medientypen oder weitere Merkmale wie `(hover: hover)`.
|
|
91
|
+
*/
|
|
92
|
+
function parseInterval(params) {
|
|
93
|
+
if (params.includes(',')) return null;
|
|
94
|
+
|
|
95
|
+
const interval = { min: 0, max: Infinity };
|
|
96
|
+
for (const condition of params.split(/\s+and\s+/i)) {
|
|
97
|
+
const bound = parseBound(condition.trim());
|
|
98
|
+
if (!bound) return null;
|
|
99
|
+
if (bound.min !== undefined) interval.min = Math.max(interval.min, bound.min);
|
|
100
|
+
if (bound.max !== undefined) interval.max = Math.min(interval.max, bound.max);
|
|
101
|
+
}
|
|
102
|
+
return interval;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function parseBound(condition) {
|
|
106
|
+
const legacy = condition.match(/^\(\s*(min|max)-width\s*:\s*(\d+)px\s*\)$/i);
|
|
107
|
+
if (legacy) {
|
|
108
|
+
const px = Number(legacy[2]);
|
|
109
|
+
return legacy[1].toLowerCase() === 'min' ? { min: px } : { max: px };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const range = condition.match(/^\(\s*width\s*(<=|>=|<|>)\s*(\d+)px\s*\)$/i);
|
|
113
|
+
if (range) {
|
|
114
|
+
const px = Number(range[2]);
|
|
115
|
+
switch (range[1]) {
|
|
116
|
+
case '>=': return { min: px };
|
|
117
|
+
case '>': return { min: px + 1 };
|
|
118
|
+
case '<=': return { max: px };
|
|
119
|
+
case '<': return { max: px - 1 };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Prüft, ob die Bereiche lückenlos von 0 bis unendlich reichen. */
|
|
126
|
+
function coversAllWidths(intervals) {
|
|
127
|
+
let reached = 0;
|
|
128
|
+
for (const { min, max } of [...intervals].sort((a, b) => a.min - b.min)) {
|
|
129
|
+
// max-width: 739px und min-width: 740px schliessen lückenlos aneinander an
|
|
130
|
+
if (min > reached) return false;
|
|
131
|
+
reached = Math.max(reached, max + 1);
|
|
132
|
+
}
|
|
133
|
+
return reached === Infinity;
|
|
134
|
+
}
|
package/dev/toolbar/COLUMNS.md
DELETED
|
@@ -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
|