@profitlich/template-toolkit 5.3.1 → 5.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.
@@ -0,0 +1,15 @@
1
+ ### Craft CMS
2
+
3
+ **CSP Nonce:** Beim Einbinden eines Scripts immer den Nonce mitgeben, sonst blockt die Content-Security-Policy es:
4
+
5
+ ```twig
6
+ {% do craft.vite.register("src/modules/module-name/Module.js", false, { 'nonce': csp('script-src') }) %}
7
+ ```
8
+
9
+ Dasselbe gilt für per `view.registerCss()` eingebettetes CSS: `{ nonce: csp('style-src') }`.
10
+
11
+ **Project Config:** Feld- und Struktur-Änderungen können direkt in den YAML-Dateien unter `config/project/` gemacht werden, danach `ddev craft project-config/apply` (Kontrolle vorher mit `project-config/diff`). `allowAdminChanges` ist üblicherweise nur in der Dev-Umgebung aktiv, Änderungen über die Oberfläche gehen also ausschliesslich lokal.
12
+
13
+ Project-Config-Änderungen lassen sich nicht sinnvoll auf mehrere Commits aufteilen, weil `project.yaml` mit seinem `dateModified` an allem hängt.
14
+
15
+ **Templates sind generiert:** `templates/` entsteht aus `src/` (`ddev npm run copy`, während der Entwicklung `ddev npm run dev`) und ist gitignoriert. Änderungen gehören immer nach `src/`; eine Bearbeitung in `templates/` ist beim nächsten Copy-Lauf verloren.
@@ -0,0 +1,5 @@
1
+ ### Kirby CMS
2
+
3
+ *Gerüst — kirby-spezifische Konventionen hier eintragen.*
4
+
5
+ Was hierher gehört: Regeln, die nur für Kirby-Projekte gelten (Snippet- und Template-Aufbau, Blueprints, Panel-Eigenheiten, Einbindung von Assets). Was für alle Projekte gilt, steht in `CLAUDE.project.md`; was nur die Toolkit-Entwicklung betrifft, in `CLAUDE.md`.
@@ -0,0 +1,80 @@
1
+ ## Geerbte Konventionen
2
+
3
+ Dieser Abschnitt stammt aus `@profitlich/template-toolkit` und wird bei jedem `copy`-Lauf aus der installierten Paketversion neu geschrieben. Änderungen hier gehen verloren.
4
+
5
+ **Rangfolge:** Was **unterhalb** dieses Blocks steht, ist projektspezifisch und geht im Konfliktfall vor. Eine Abweichung ist erlaubt — sie soll nur als Abweichung sichtbar sein und nicht die geerbte Regel stillschweigend ersetzen.
6
+
7
+ **Wohin gehört eine neue Regel?** Betrifft sie den Stack (ddev, Vite, SCSS-Funktionen, Build, Deployment), gehört sie ins Toolkit und wird mit dessen nächstem Release verteilt. Betrifft sie nur eine CMS-Sorte, gehört sie in deren Datei im Toolkit. Betrifft sie nur dieses Projekt, gehört sie unter diesen Block.
8
+
9
+ ### Stack
10
+
11
+ - **ddev** — lokale Entwicklungsumgebung.
12
+ - **npm immer über ddev** aufrufen: `ddev npm run dev`, `ddev npm install`, `ddev npm run release:staging`. Nie auf dem Host: Der Container legt die Node-Version über `nodejs_version` in `.ddev/config.yaml` fest, damit lokal dasselbe gebaut wird wie auf dem Server. Dass ein Build auf dem Host durchläuft, heisst nicht, dass er dort hingehört.
13
+ - **Bricht ein Build mit `Killed` ab, ohne Fehlermeldung**, ist der Speicher der Colima-VM ausgegangen und nicht der Code kaputt: Die VM hat keinen Swap, der Linux-OOM-Killer schickt sofort `SIGKILL`. Prüfen mit `ddev exec free -m`; Abhilfe ist `colima stop && colima start --memory 8` oder das Schliessen speicherhungriger Programme — nicht das Ausweichen auf den Host.
14
+ - **Vite** — Build-Tool für JS und SCSS.
15
+
16
+ ### JavaScript
17
+
18
+ - **Klassen bevorzugen** mit private Fields per `#`-Prefix — nie Underscore-Konvention (`_field`).
19
+ - **Singleton-Pattern** für Utilities, die global einmalig sind (analog zu `MediaQueries`, `MenuToggle`).
20
+
21
+ ### Custom Events und Data-Attributes
22
+
23
+ - Zustandskommunikation zwischen Komponenten über `CustomEvent`, nicht direkte Methodenaufrufe.
24
+ - Event-Namenskonvention: `event` + PascalCase → `eventMenuStatus`, `eventBodyScrolled`.
25
+ - DOM-Zustand per `data-*`-Attribut, nie als CSS-Klassen-Toggle: `document.body.setAttribute('data-menu-active', 'true')`.
26
+
27
+ ### SCSS
28
+
29
+ - Nie direkte `px`-, `vw`- oder `rem`-Werte — ausschliesslich Toolkit-Funktionen: `size()`, `columns()`, `font()`, `marginPadding()`.
30
+ - `src/config.json` ist die einzige Quelle für Breakpoints, Layouts, Farben — nie im Code hardcodieren.
31
+ - Jedes Modul/Snippet hat eine eigene `.js`-Datei, die das zugehörige SCSS importiert — auch wenn sie sonst keine Logik enthält.
32
+ - Keine globalen Styles in Modul- oder Snippet-SCSS-Dateien.
33
+
34
+ #### vw-Basis
35
+
36
+ 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.
37
+
38
+ **Wichtig bei vwBasis "body"**: Werte aus `size()`, `columns()`, `marginPadding()` sind dann CSS-`calc()`-Ausdrücke. Sass kann sie ausserhalb eines `calc()`-Wrappers nicht arithmetisch kombinieren. Statt `($a - $b)` mit Sass-Parens → `calc($a - $b)`. Auch im Default-Modus ist diese Schreibweise unschädlich, also generell als Konvention nutzen.
39
+
40
+ #### Capsize
41
+
42
+ Optionale pixel-präzise Schriftpositionierung via `@capsizecss/core` (Em-Trims an `::before`/`::after`). Capsize wird zur Sass-Compile-Zeit über eine Custom-Function direkt aufgerufen — keine Algorithmus-Reimplementierung, Updates aus dem Capsize-Paket fliessen mit.
43
+
44
+ Setup im Konsumenten:
45
+
46
+ 1. `@capsizecss/unpack` und `@capsizecss/core` als devDependency installieren.
47
+ 2. In `src/config.json` Top-Level-Feld `fonts` ergänzen: Map Name → Pfad zur Font-Datei.
48
+ 3. In `vite.config.js`:
49
+
50
+ ```js
51
+ import { createCapsizeFunctions } from '@profitlich/template-toolkit/vite/capsizeSassFunctions';
52
+ // defineConfig async:
53
+ const capsizeFunctions = await createCapsizeFunctions(configJson.fonts ?? {});
54
+ // dann: css.preprocessorOptions.scss.functions = capsizeFunctions;
55
+ ```
56
+
57
+ 4. Pro `@include font(...)` als 4. Argument den Font-Namen aus `fonts` mitgeben — Trims werden emittiert. Ohne Argument: kein Capsize-Output (Default).
58
+
59
+ `font-weight` wird nicht mehr vom `font()`-Mixin gesetzt — direkt im CSS deklarieren (meist breakpoint-übergreifend).
60
+
61
+ **SCSS-Organisation**: `scss/core/capsize.scss` enthält das `capsize`-Mixin (emittiert Pseudo-Elemente). `font()` in `layout.scss` ruft es intern auf, wenn das 4. Argument gesetzt ist. Du kannst das Mixin auch direkt nutzen, falls du Capsize ohne `font()` brauchst:
62
+
63
+ ```scss
64
+ .foo { @include capsize("soehne", 40, 45); }
65
+ ```
66
+
67
+ **Cap-Höhe als SCSS-Wert**: `capsize-cap-height($name, $fontSize)` liefert die Cap-Höhe als unitless Zahl (Design-Pixel). Nutzbar für eigene Berechnungen, z. B. paddings, die zum Grid passen sollen:
68
+
69
+ ```scss
70
+ $cap: capsize-cap-height("soehne", 40); // → font-spezifischer Wert
71
+ padding-top: size($layout, 40 - $cap + 8);
72
+ ```
73
+
74
+ ### Vite Entry
75
+
76
+ 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.
77
+
78
+ ### Bilder
79
+
80
+ Kein `lazysizes`. Ausschliesslich natives `loading="lazy"`.
@@ -6,6 +6,7 @@ import './menu-toggle.scss';
6
6
  * @property {string} menuSelector CSS-Selektor (`querySelector`) des Menü-Containers.
7
7
  * @property {string} menuLinkSelector CSS-Selektor für Menü-Links, die das Menü beim Klick schliessen (z.B. '.menu-link').
8
8
  * @property {string} menuItemSelector CSS-Selektor des Menü-Wrappers; Klicks ausserhalb schliessen das Menü (z.B. '.menu').
9
+ * @property {boolean} [linkClickClosesMenu=true] Wenn `false`, bleibt das Menü beim Klick auf einen `menuLinkSelector`-Link offen. Default `true` = Menü schliesst beim Link-Klick.
9
10
  * @property {string} [shiftElementSelector] Optional: CSS-Selektor des Elements, das beim Öffnen um die Scrollbar-Breite verschoben/verbreitert wird, damit z.B. ein fixierter Header nicht springt.
10
11
  * @property {number} [shiftDelay=0] 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.
11
12
  * @property {boolean} [deferPositionFixed=false] Setzt `data-menu-fixed` erst nach `shiftDelay` statt sofort. Nötig, wenn das Fixieren eine laufende Öffnungs-Animation stören würde.
@@ -19,6 +20,7 @@ export class MenuToggle {
19
20
  #menu;
20
21
  #menuLinkSelector;
21
22
  #menuItemSelector;
23
+ #linkClickClosesMenu;
22
24
  #scrollbarWidth;
23
25
  #shiftElement;
24
26
  #shiftDelay;
@@ -40,6 +42,7 @@ export class MenuToggle {
40
42
  menuSelector,
41
43
  menuLinkSelector,
42
44
  menuItemSelector,
45
+ linkClickClosesMenu = true,
43
46
  shiftElementSelector = null,
44
47
  shiftDelay = 0,
45
48
  deferPositionFixed = false,
@@ -50,6 +53,7 @@ export class MenuToggle {
50
53
  this.#menu = document.querySelector(menuSelector);
51
54
  this.#menuLinkSelector = menuLinkSelector;
52
55
  this.#menuItemSelector = menuItemSelector;
56
+ this.#linkClickClosesMenu = linkClickClosesMenu;
53
57
  this.#shiftElement = shiftElementSelector ? document.querySelector(shiftElementSelector) : null;
54
58
  this.#shiftDelay = shiftDelay * 1000;
55
59
  this.#deferPositionFixed = deferPositionFixed;
@@ -71,7 +75,7 @@ export class MenuToggle {
71
75
  });
72
76
 
73
77
  this.#menu.addEventListener('click', (event) => {
74
- if (this.isActive && event.target.matches(this.#menuLinkSelector)) {
78
+ if (this.#linkClickClosesMenu && this.isActive && event.target.matches(this.#menuLinkSelector)) {
75
79
  this.#toggleMenu();
76
80
  }
77
81
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@profitlich/template-toolkit",
3
- "version": "5.3.1",
3
+ "version": "5.5.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",
@@ -27,6 +27,9 @@
27
27
  "./vite/capsizeSassFunctions": "./vite/capsizeSassFunctions.js"
28
28
  },
29
29
  "files": [
30
+ "CLAUDE.project.md",
31
+ "CLAUDE.craftcms.md",
32
+ "CLAUDE.kirbycms.md",
30
33
  "components/",
31
34
  "dev/",
32
35
  "scss/",
@@ -2,6 +2,7 @@ import fs from 'fs-extra';
2
2
  import { glob } from 'glob';
3
3
  import path from 'path';
4
4
  import chokidar from 'chokidar';
5
+ import { syncConventions } from './sync-conventions.js';
5
6
 
6
7
  /**
7
8
  * @typedef {Object} CopyTask
@@ -78,17 +79,28 @@ export function watchFiles(copyTasks, watchDir = 'src') {
78
79
  * Run the copy-files script.
79
80
  * Liest `process.argv[2]`: `dev` startet einmal `copyAll` und danach `watchFiles`,
80
81
  * `build` führt `copyAll` einmal aus.
82
+ *
83
+ * Übernimmt vorab die geerbten Konventionen in die `CLAUDE.md` des Projekts —
84
+ * hier statt in jedem Projekt einzeln verdrahtet, damit bestehende Projekte
85
+ * nichts anpassen müssen. Ohne Marken in der Zieldatei passiert nichts.
81
86
  * @param {CopyTask[]} copyTasks
82
87
  * @param {Object} [options]
83
88
  * @param {string} [options.watchDir='src'] - Im Dev-Mode beobachtetes Verzeichnis.
89
+ * @param {string} [options.template] - Sorte für den Konventionsblock, z.B. `craftcms`.
90
+ * @param {boolean} [options.syncConventions=true] - Konventionsblock schreiben.
84
91
  */
85
92
  export function run(copyTasks, options = {}) {
86
93
  const command = process.argv[2];
87
94
  const watchDir = options.watchDir || 'src';
88
95
 
96
+ const vorlauf = options.syncConventions === false
97
+ ? Promise.resolve()
98
+ : syncConventions({ template: options.template })
99
+ .catch(err => console.error('Konventionen nicht übernommen:', err));
100
+
89
101
  if (command === 'dev') {
90
- copyAll(copyTasks).then(() => watchFiles(copyTasks, watchDir));
102
+ vorlauf.then(() => copyAll(copyTasks)).then(() => watchFiles(copyTasks, watchDir));
91
103
  } else if (command === 'build') {
92
- copyAll(copyTasks);
104
+ vorlauf.then(() => copyAll(copyTasks));
93
105
  }
94
106
  }
@@ -0,0 +1,89 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+
5
+ const paketWurzel = fileURLToPath(new URL('../', import.meta.url));
6
+ const paketDatei = path.join(paketWurzel, 'package.json');
7
+
8
+ const markeStart = '<!-- toolkit:start';
9
+ const markeEnde = '<!-- toolkit:end -->';
10
+
11
+ /**
12
+ * Schreibt die geerbten Konventionen in die `CLAUDE.md` des Projekts.
13
+ *
14
+ * Claude liest keine Dateien aus `node_modules`. Die Konventionen müssen also
15
+ * im Repo liegen — kopiert statt verlinkt. Ersetzt wird ausschliesslich der
16
+ * Bereich zwischen den Marken; alles davor und dahinter ist Projekteigentum
17
+ * und bleibt unangetastet.
18
+ *
19
+ * Der Block hängt an der *installierten* Toolkit-Version, nicht am neuesten
20
+ * Stand: Ein Projekt zieht Änderungen erst, wenn es die Version hebt und einmal
21
+ * baut. Die Änderung erscheint dann als git-Diff in der Projektdatei — sichtbar
22
+ * und überprüfbar, statt still.
23
+ *
24
+ * Fehlt die Zieldatei oder fehlen die Marken, passiert nichts. Das Skript legt
25
+ * bewusst nichts an: Ein Projekt ohne Marken hat sich gegen den Block
26
+ * entschieden, und diese Entscheidung zu überschreiben wäre schlimmer, als sie
27
+ * zu ignorieren.
28
+ *
29
+ * Zwei Quellen: `CLAUDE.project.md` gilt für alle Projekte, `CLAUDE.<template>.md`
30
+ * zusätzlich für die jeweilige Sorte. Getrennte Dateien, ein Transportweg — das
31
+ * Paket ist an eine Version gebunden, ein lokal geklontes Template-Repo dagegen
32
+ * kann veraltet sein, ohne dass es auffällt.
33
+ *
34
+ * @param {Object} [options]
35
+ * @param {string} [options.target='CLAUDE.md'] - Zieldatei, relativ zum Projektverzeichnis.
36
+ * @param {string} [options.template] - Sorte, z.B. `craftcms` oder `kirbycms`.
37
+ * @returns {Promise<boolean>} `true`, wenn geschrieben wurde.
38
+ */
39
+ export async function syncConventions(options = {}) {
40
+ const zielPfad = path.resolve(options.target ?? 'CLAUDE.md');
41
+
42
+ const quellDateien = [path.join(paketWurzel, 'CLAUDE.project.md')];
43
+ if (options.template) {
44
+ quellDateien.push(path.join(paketWurzel, `CLAUDE.${options.template}.md`));
45
+ }
46
+
47
+ const abschnitte = [];
48
+ for (const datei of quellDateien) {
49
+ // Ältere Toolkit-Versionen bringen die Dateien nicht mit; eine unbekannte
50
+ // Sorte soll den Lauf nicht abbrechen, sondern nur nichts beitragen
51
+ if (await fs.pathExists(datei)) {
52
+ abschnitte.push((await fs.readFile(datei, 'utf-8')).trim());
53
+ }
54
+ }
55
+
56
+ if (abschnitte.length === 0) return false;
57
+
58
+ if (!await fs.pathExists(zielPfad)) {
59
+ console.log(`ℹ️ ${path.basename(zielPfad)} nicht gefunden — Konventionen nicht übernommen.`);
60
+ return false;
61
+ }
62
+
63
+ const ziel = await fs.readFile(zielPfad, 'utf-8');
64
+ const start = ziel.indexOf(markeStart);
65
+ const ende = start === -1 ? -1 : ziel.indexOf(markeEnde, start);
66
+
67
+ if (start === -1 || ende === -1) {
68
+ console.log(`ℹ️ Keine Toolkit-Marken in ${path.basename(zielPfad)} — Konventionen nicht übernommen.`);
69
+ console.log(` Einfügen, wo der Block stehen soll: ${markeStart} --> … ${markeEnde}`);
70
+ return false;
71
+ }
72
+
73
+ const { version } = await fs.readJson(paketDatei);
74
+ const sorte = options.template ? ` + ${options.template}` : '';
75
+
76
+ const block = `${markeStart} ${version}${sorte} — erzeugt aus @profitlich/template-toolkit, nicht von Hand ändern -->\n`
77
+ + `${abschnitte.join('\n\n')}\n`
78
+ + markeEnde;
79
+
80
+ const neu = ziel.slice(0, start) + block + ziel.slice(ende + markeEnde.length);
81
+
82
+ // Nur schreiben, wenn sich etwas ändert — sonst stünde nach jedem Build eine
83
+ // veränderte Datei im git status, ohne dass sich der Inhalt unterscheidet
84
+ if (neu === ziel) return false;
85
+
86
+ await fs.writeFile(zielPfad, neu);
87
+ console.log(`🔄 ${path.basename(zielPfad)}: Konventionen aus Toolkit ${version} übernommen.`);
88
+ return true;
89
+ }
@@ -1,14 +1,22 @@
1
+ @use "sass:meta";
2
+
1
3
  // Emittiert die Capsize-Pseudo-Elemente (::before/::after mit Em-Trims)
2
4
  // für den aktuellen Selektor. Die Inhalte (welche Pseudo-Elemente, welche
3
5
  // Properties) kommen vollständig aus @capsizecss/core via der
4
6
  // Sass-Custom-Function capsize-pseudo-elements (registriert im
5
7
  // vite/capsizeSassFunctions.js des Toolkits).
6
8
  @mixin capsize($name, $fontSize, $lineHeight) {
7
- $pseudo: capsize-pseudo-elements($name, $fontSize, $lineHeight);
8
- @each $selector-suffix, $props in $pseudo {
9
- &#{$selector-suffix} {
10
- @each $prop, $val in $props {
11
- #{$prop}: $val;
9
+ // Capsize ist opt-in: ohne registrierte Custom-Function wird übersprungen,
10
+ // damit ein fehlender Vite-Setup nicht zu kryptischem Sass-Fehler führt.
11
+ @if not meta.function-exists("capsize-pseudo-elements") {
12
+ @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
+ } @else {
14
+ $pseudo: capsize-pseudo-elements($name, $fontSize, $lineHeight);
15
+ @each $selector-suffix, $props in $pseudo {
16
+ &#{$selector-suffix} {
17
+ @each $prop, $val in $props {
18
+ #{$prop}: $val;
19
+ }
12
20
  }
13
21
  }
14
22
  }
@@ -88,7 +88,12 @@ $mediaqueries: (
88
88
  line-height: size($layout, $lineHeight);
89
89
 
90
90
  @if ($capsize != null) {
91
- @include capsize($capsize, $fontSize, $lineHeight);
91
+ // 4. Argument ist der Capsize-Font-Name (String). Frühere API hatte hier font-weight.
92
+ @if meta.type-of($capsize) != 'string' {
93
+ @warn "font(): 4. Argument (Capsize-Font-Name) muss ein String sein, erhalten #{meta.type-of($capsize)} \"#{$capsize}\". Vermutlich Altlast aus früherer API (font-weight). Capsize wird übersprungen.";
94
+ } @else {
95
+ @include capsize($capsize, $fontSize, $lineHeight);
96
+ }
92
97
  }
93
98
 
94
99
  }