@profitlich/template-toolkit 5.4.0 → 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.
- package/CLAUDE.craftcms.md +15 -0
- package/CLAUDE.kirbycms.md +5 -0
- package/CLAUDE.project.md +80 -0
- package/package.json +4 -1
- package/scripts/copy-files.js +14 -2
- package/scripts/sync-conventions.js +89 -0
|
@@ -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"`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@profitlich/template-toolkit",
|
|
3
|
-
"version": "5.
|
|
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/",
|
package/scripts/copy-files.js
CHANGED
|
@@ -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
|
+
}
|