@klodd/ds 5.6.0 → 5.7.1
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/SKILL.md +5 -4
- package/bin/verify.js +147 -7
- package/deprecated-classes.txt +10 -0
- package/js/sheet.js +96 -0
- package/js/toast.js +31 -0
- package/package.json +2 -1
package/SKILL.md
CHANGED
|
@@ -6,10 +6,11 @@ description: Design memory for the @klodd/ds shared design system used by the tw
|
|
|
6
6
|
# @klodd/ds — Design Memory
|
|
7
7
|
|
|
8
8
|
## Status
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
- Sprint
|
|
12
|
-
- Sprint
|
|
9
|
+
Designsystem-auditen (2026-05-20, se `audits/00-summary.md`) slutförd -
|
|
10
|
+
alla tre sprintar klara:
|
|
11
|
+
- Sprint 1: dead-code-städning + topbar-bump (5.4.3)
|
|
12
|
+
- Sprint 2: component-token-konsolidering, ADR 0024-backfill, light-theme-removal (5.5.0)
|
|
13
|
+
- Sprint 3: base.css-extraktion till paketets base/interactions.css (5.6.0)
|
|
13
14
|
|
|
14
15
|
## Vad detta är
|
|
15
16
|
Gemensamt designsystem på npm (`@klodd/ds@5.x`). Tre-lagers tokens,
|
package/bin/verify.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/* @klodd/ds verify -
|
|
3
|
-
* Kör i CI: npx @klodd/ds verify
|
|
4
|
-
* Fail:ar med exit code 1 om drift detekteras.
|
|
2
|
+
/* @klodd/ds verify - validerar att en app stammer mot paketet.
|
|
3
|
+
* Kör i CI: npx @klodd/ds verify. Exit 1 vid problem.
|
|
5
4
|
*
|
|
6
|
-
*
|
|
5
|
+
* Check 1 - fil-drift: diffar committade DS-filer mot paket-källan.
|
|
6
|
+
* exclude-fältet (v3.5.0+) i klodd-ds.json hoppas över.
|
|
7
|
+
* Check 2 - deprecade selektorer: letar i app-ägd kod (app/templates/
|
|
8
|
+
* + app/static/, exkl. synkade paket-filer i ds/) efter klass-
|
|
9
|
+
* ANVANDNING av namn listade i paketets deprecated-classes.txt.
|
|
10
|
+
* Kommentarer ignoreras - bara riktiga class=""/selektor/JS-strang-
|
|
11
|
+
* referenser flaggas. */
|
|
7
12
|
'use strict';
|
|
8
13
|
|
|
9
14
|
const fs = require('fs');
|
|
@@ -26,6 +31,7 @@ function hashFile(filePath) {
|
|
|
26
31
|
|
|
27
32
|
function listFilesRec(dir) {
|
|
28
33
|
const out = [];
|
|
34
|
+
if (!fs.existsSync(dir)) return out;
|
|
29
35
|
const stack = [{ abs: dir, rel: '' }];
|
|
30
36
|
while (stack.length) {
|
|
31
37
|
const { abs, rel } = stack.pop();
|
|
@@ -57,17 +63,151 @@ function diffDirs(sourceDir, targetRel, kind) {
|
|
|
57
63
|
return drifted;
|
|
58
64
|
}
|
|
59
65
|
|
|
66
|
+
// --- Deprecated-class-check ---
|
|
67
|
+
|
|
68
|
+
function escapeRe(s) {
|
|
69
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// deprecated-classes.txt: en CSS-klass per rad (utan punkt). Allt efter
|
|
73
|
+
// klassnamnet pa raden ar fri kommentar. Tomma rader + #-rader hoppas.
|
|
74
|
+
function loadDeprecatedClasses() {
|
|
75
|
+
const file = path.join(pkgDir, 'deprecated-classes.txt');
|
|
76
|
+
if (!fs.existsSync(file)) return [];
|
|
77
|
+
return fs.readFileSync(file, 'utf8')
|
|
78
|
+
.split('\n')
|
|
79
|
+
.map((line) => line.trim())
|
|
80
|
+
.filter((line) => line && !line.startsWith('#'))
|
|
81
|
+
.map((line) => line.split(/\s+/)[0]);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Returnerar varje rads kod med kommentarer borttagna. Flerrads
|
|
85
|
+
// /* */ (css/js), <!-- --> + {# #} (html) och radslut // (js).
|
|
86
|
+
function stripComments(content, lang) {
|
|
87
|
+
const blocks = {
|
|
88
|
+
css: [['/*', '*/']],
|
|
89
|
+
js: [['/*', '*/']],
|
|
90
|
+
html: [['<!--', '-->'], ['{#', '#}']],
|
|
91
|
+
}[lang] || [];
|
|
92
|
+
const lineMarker = lang === 'js' ? '//' : null;
|
|
93
|
+
const out = [];
|
|
94
|
+
let close = null;
|
|
95
|
+
for (const raw of content.split('\n')) {
|
|
96
|
+
let code = '';
|
|
97
|
+
let i = 0;
|
|
98
|
+
while (i < raw.length) {
|
|
99
|
+
if (close) {
|
|
100
|
+
const end = raw.indexOf(close, i);
|
|
101
|
+
if (end === -1) { i = raw.length; }
|
|
102
|
+
else { i = end + close.length; close = null; }
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
let bestPos = -1;
|
|
106
|
+
let bestOpenLen = 0;
|
|
107
|
+
let bestClose = null;
|
|
108
|
+
for (const [open, cl] of blocks) {
|
|
109
|
+
const p = raw.indexOf(open, i);
|
|
110
|
+
if (p !== -1 && (bestPos === -1 || p < bestPos)) {
|
|
111
|
+
bestPos = p;
|
|
112
|
+
bestOpenLen = open.length;
|
|
113
|
+
bestClose = cl;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const lc = lineMarker ? raw.indexOf(lineMarker, i) : -1;
|
|
117
|
+
if (lc !== -1 && (bestPos === -1 || lc < bestPos)) {
|
|
118
|
+
code += raw.slice(i, lc);
|
|
119
|
+
i = raw.length;
|
|
120
|
+
} else if (bestPos !== -1) {
|
|
121
|
+
code += raw.slice(i, bestPos);
|
|
122
|
+
i = bestPos + bestOpenLen;
|
|
123
|
+
close = bestClose;
|
|
124
|
+
} else {
|
|
125
|
+
code += raw.slice(i);
|
|
126
|
+
i = raw.length;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
out.push(code);
|
|
130
|
+
}
|
|
131
|
+
return out;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Sann om raden (kommentarsbefriad) ANVANDER klassen: class=""-attribut
|
|
135
|
+
// (html), .selektor (css) eller klass-strang i JS.
|
|
136
|
+
function usesClass(code, cls, lang) {
|
|
137
|
+
const c = escapeRe(cls);
|
|
138
|
+
if (lang === 'css') {
|
|
139
|
+
return new RegExp('\\.' + c + '(?![\\w-])').test(code);
|
|
140
|
+
}
|
|
141
|
+
if (lang === 'js') {
|
|
142
|
+
return new RegExp("['\"`](?:[^'\"`]*\\.)?" + c + '(?![\\w-])').test(code);
|
|
143
|
+
}
|
|
144
|
+
if (lang === 'html') {
|
|
145
|
+
const attrs = code.match(/class\s*=\s*("[^"]*"|'[^']*')/gi) || [];
|
|
146
|
+
return attrs.some((a) => new RegExp('(^|[^\\w-])' + c + '(?![\\w-])').test(a));
|
|
147
|
+
}
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Synkade paket-filer (css/ds/<X>, js/ds/<X> dar <X> finns i paketet)
|
|
152
|
+
// hoppas - checken galler app-agd kod. App-agda domain-filer i ds/
|
|
153
|
+
// (ekonom.css, jubb.css, bundles/ osv.) behalls i kontrollen.
|
|
154
|
+
function grepDeprecated(classNames) {
|
|
155
|
+
const hits = [];
|
|
156
|
+
if (classNames.length === 0) return hits;
|
|
157
|
+
const LANG = { '.html': 'html', '.css': 'css', '.js': 'js' };
|
|
158
|
+
const pkgCss = new Set(listFilesRec(path.join(pkgDir, 'css')));
|
|
159
|
+
const pkgJs = new Set(listFilesRec(path.join(pkgDir, 'js')));
|
|
160
|
+
function isSyncedPackageFile(rel) {
|
|
161
|
+
if (rel.startsWith('css/ds/')) return pkgCss.has(rel.slice('css/ds/'.length));
|
|
162
|
+
if (rel.startsWith('js/ds/')) return pkgJs.has(rel.slice('js/ds/'.length));
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
for (const dir of ['app/templates', 'app/static']) {
|
|
166
|
+
const abs = path.join(process.cwd(), dir);
|
|
167
|
+
if (!fs.existsSync(abs)) continue;
|
|
168
|
+
for (const rel of listFilesRec(abs)) {
|
|
169
|
+
const lang = LANG[path.extname(rel)];
|
|
170
|
+
if (!lang) continue;
|
|
171
|
+
if (dir === 'app/static' && isSyncedPackageFile(rel)) continue;
|
|
172
|
+
const code = stripComments(fs.readFileSync(path.join(abs, rel), 'utf8'), lang);
|
|
173
|
+
code.forEach((line, idx) => {
|
|
174
|
+
for (const cls of classNames) {
|
|
175
|
+
if (usesClass(line, cls, lang)) {
|
|
176
|
+
hits.push(`${dir}/${rel}:${idx + 1} ${cls}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return hits;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// --- Check 1: fil-drift mot paket-kallan ---
|
|
60
186
|
const cssDrift = diffDirs(path.join(pkgDir, 'css'), config.cssTarget, 'css');
|
|
61
187
|
const jsDrift = diffDirs(path.join(pkgDir, 'js'), config.jsTarget, 'js');
|
|
62
188
|
const allDrift = [...cssDrift, ...jsDrift];
|
|
63
189
|
|
|
190
|
+
// --- Check 2: deprecade selektorer i app-koden ---
|
|
191
|
+
const deprecatedHits = grepDeprecated(loadDeprecatedClasses());
|
|
192
|
+
|
|
193
|
+
let failed = false;
|
|
194
|
+
|
|
64
195
|
if (allDrift.length > 0) {
|
|
196
|
+
failed = true;
|
|
65
197
|
console.error('FAIL: @klodd/ds drift detekterad:');
|
|
66
198
|
allDrift.forEach((d) => console.error(' ' + d));
|
|
67
|
-
console.error('
|
|
68
|
-
process.exit(1);
|
|
199
|
+
console.error('Kör `npm run build:ds` för att synka.\n');
|
|
69
200
|
}
|
|
70
201
|
|
|
202
|
+
if (deprecatedHits.length > 0) {
|
|
203
|
+
failed = true;
|
|
204
|
+
console.error(`FAIL: ${deprecatedHits.length} referens(er) till deprecade klasser:`);
|
|
205
|
+
deprecatedHits.forEach((h) => console.error(' ' + h));
|
|
206
|
+
console.error('Se @klodd/ds deprecated-classes.txt för ersättningar.\n');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (failed) process.exit(1);
|
|
210
|
+
|
|
71
211
|
const excludedCount = exclude.size > 0 ? ` (${exclude.size} excluderade fran kontroll)` : '';
|
|
72
|
-
console.log(`OK: alla DS-filer matchar @klodd/ds-paketet${excludedCount}.`);
|
|
212
|
+
console.log(`OK: alla DS-filer matchar @klodd/ds-paketet${excludedCount}, 0 deprecade selektorer.`);
|
|
73
213
|
process.exit(0);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# @klodd/ds deprecated-classes.txt
|
|
2
|
+
# En CSS-klass per rad (utan punkt). Text efter klassnamnet på raden
|
|
3
|
+
# är fri kommentar. Tomma rader och #-rader hoppas över.
|
|
4
|
+
# npx @klodd/ds verify greppar app/templates/ + app/static/ efter
|
|
5
|
+
# varje klassnamn och exit 1 vid träff.
|
|
6
|
+
|
|
7
|
+
sub-row (→ expandable-row__item matched-row, ADR 0002)
|
|
8
|
+
hero-amount (→ hero__amount, ADR 0019. /design-demon fixad, 0 träffar - kvar som påminnelse)
|
|
9
|
+
btn--negative (raderad, ingen ersättning)
|
|
10
|
+
tx-row (→ list-row)
|
package/js/sheet.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/*--------------------------------------------------------------
|
|
2
|
+
sheet.js - bottom-sheet open/close for <dialog class="sheet">.
|
|
3
|
+
|
|
4
|
+
Native <dialog> skoter ESC, focus-trap och inert-bakgrund. Denna
|
|
5
|
+
modul lagger till:
|
|
6
|
+
- openSheet(id) / closeSheet(dlg), exponerade som globals
|
|
7
|
+
window.__sheetOpen / window.__sheetClose
|
|
8
|
+
- click-delegation: [data-sheet]-trigger oppnar sheet, klick pa
|
|
9
|
+
backdrop eller [data-sheet-close] stanger
|
|
10
|
+
- drag-state-reset: nollar inline transform/transition som
|
|
11
|
+
sheet-drag.js kan lamna kvar om en sheet stangs mitt i en drag
|
|
12
|
+
|
|
13
|
+
Laddas fore app-lokal JS som anropar window.__sheetOpen.
|
|
14
|
+
--------------------------------------------------------------*/
|
|
15
|
+
(function () {
|
|
16
|
+
// Defensiv state-reset mot drag-physics-laeckage. sheet-drag.js
|
|
17
|
+
// satter inline style.transform + style.transition under drag och
|
|
18
|
+
// cleanar via transitionend-event. Om dialogen stangs innan
|
|
19
|
+
// transitionen avslutas (ESC, backdrop-klick, programmatic close
|
|
20
|
+
// eller form-submit mid-animation) eldas transitionend ALDRIG och
|
|
21
|
+
// inline-style laecker till nasta showModal() - sheet hamnar dar
|
|
22
|
+
// drag avbrots (ofta utanfor viewport eller halvvags upp). Reset i
|
|
23
|
+
// open + close + close-event-listener ger clean state oavsett hur
|
|
24
|
+
// dialogen stangts.
|
|
25
|
+
function resetDragState(dlg) {
|
|
26
|
+
dlg.style.transform = '';
|
|
27
|
+
dlg.style.transition = '';
|
|
28
|
+
}
|
|
29
|
+
function openSheet(id) {
|
|
30
|
+
const dlg = document.querySelector('dialog.sheet[data-sheet-id="' + id + '"]');
|
|
31
|
+
if (!dlg) return null;
|
|
32
|
+
resetDragState(dlg);
|
|
33
|
+
if (typeof dlg.showModal === 'function' && !dlg.open) {
|
|
34
|
+
dlg.showModal();
|
|
35
|
+
} else {
|
|
36
|
+
// Fallback for browsers utan <dialog>.
|
|
37
|
+
dlg.setAttribute('open', '');
|
|
38
|
+
}
|
|
39
|
+
// Auto-focus + select-all pa input markerad med
|
|
40
|
+
// data-sheet-autofocus. setTimeout 50ms sa showModal-animationen
|
|
41
|
+
// hunnit borja innan focus-grabben (annars hoppar caret).
|
|
42
|
+
const focusEl = dlg.querySelector('[data-sheet-autofocus]');
|
|
43
|
+
if (focusEl) {
|
|
44
|
+
setTimeout(function () {
|
|
45
|
+
try {
|
|
46
|
+
focusEl.focus({ preventScroll: true });
|
|
47
|
+
if (typeof focusEl.select === 'function') focusEl.select();
|
|
48
|
+
} catch (_) { /* ignore */ }
|
|
49
|
+
}, 50);
|
|
50
|
+
}
|
|
51
|
+
return dlg;
|
|
52
|
+
}
|
|
53
|
+
function closeSheet(dlg) {
|
|
54
|
+
if (!dlg) return;
|
|
55
|
+
resetDragState(dlg);
|
|
56
|
+
if (typeof dlg.close === 'function' && dlg.open) {
|
|
57
|
+
dlg.close();
|
|
58
|
+
} else {
|
|
59
|
+
dlg.removeAttribute('open');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Exponera for andra moduler (sheet-drag, optimistic-UI).
|
|
63
|
+
window.__sheetOpen = openSheet;
|
|
64
|
+
window.__sheetClose = closeSheet;
|
|
65
|
+
|
|
66
|
+
// ESC-stangning + native dialog.close() gar inte via closeSheet
|
|
67
|
+
// ovan. Lyssna pa close-event i capture-phase (close-event har
|
|
68
|
+
// bubbles: false per spec, sa capture ar enda sattet att fanga det
|
|
69
|
+
// med delegation pa document-niva). Reset drag-geometri sa nasta
|
|
70
|
+
// open startar med clean inline-style.
|
|
71
|
+
document.addEventListener('close', function (e) {
|
|
72
|
+
if (e.target instanceof HTMLDialogElement && e.target.classList.contains('sheet')) {
|
|
73
|
+
resetDragState(e.target);
|
|
74
|
+
}
|
|
75
|
+
}, true);
|
|
76
|
+
|
|
77
|
+
document.addEventListener('click', function (e) {
|
|
78
|
+
const trigger = e.target.closest('[data-sheet]');
|
|
79
|
+
if (trigger) {
|
|
80
|
+
openSheet(trigger.dataset.sheet);
|
|
81
|
+
e.preventDefault();
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
// Backdrop-click: e.target ar dialog-elementet sjalvt (klick
|
|
85
|
+
// utanfor content-omradet, pa backdrop som <dialog> renderar).
|
|
86
|
+
if (e.target.tagName === 'DIALOG' && e.target.classList.contains('sheet')) {
|
|
87
|
+
closeSheet(e.target);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
// Avbryt-knapp inne i sheet.
|
|
91
|
+
if (e.target.closest('[data-sheet-close]')) {
|
|
92
|
+
const dlg = e.target.closest('dialog.sheet');
|
|
93
|
+
closeSheet(dlg);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
})();
|
package/js/toast.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/*--------------------------------------------------------------
|
|
2
|
+
toast.js - transient feedback-meddelanden.
|
|
3
|
+
|
|
4
|
+
window.showToast(message, type, duration)
|
|
5
|
+
type 'info' (default) | 'success' | 'error' | 'warning'
|
|
6
|
+
duration ms innan auto-removal (default 3000)
|
|
7
|
+
|
|
8
|
+
Skapar ett .toast-element och stackar det i #toast-region. Kraver:
|
|
9
|
+
- ett element med id="toast-region" i DOM (mountpoint)
|
|
10
|
+
- .toast / .toast--out / .toast--{success,error,warning} CSS
|
|
11
|
+
(paketets components/feedback.css + .toast-region/.toast--out
|
|
12
|
+
i app-doman-CSS)
|
|
13
|
+
|
|
14
|
+
Laddas fore app-lokal JS som anropar window.showToast.
|
|
15
|
+
--------------------------------------------------------------*/
|
|
16
|
+
(function () {
|
|
17
|
+
window.showToast = function (message, type, duration) {
|
|
18
|
+
type = type || 'info';
|
|
19
|
+
duration = duration || 3000;
|
|
20
|
+
const region = document.getElementById('toast-region');
|
|
21
|
+
if (!region) return;
|
|
22
|
+
const el = document.createElement('div');
|
|
23
|
+
el.className = 'toast' + (type !== 'info' ? ' toast--' + type : '');
|
|
24
|
+
el.textContent = message;
|
|
25
|
+
region.appendChild(el);
|
|
26
|
+
setTimeout(function () {
|
|
27
|
+
el.classList.add('toast--out');
|
|
28
|
+
el.addEventListener('animationend', function () { el.remove(); }, { once: true });
|
|
29
|
+
}, duration);
|
|
30
|
+
};
|
|
31
|
+
})();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@klodd/ds",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.7.1",
|
|
4
4
|
"description": "Klodd shared design system - tokens, components, JS",
|
|
5
5
|
"main": "css/index.css",
|
|
6
6
|
"bin": {
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"css/",
|
|
11
11
|
"js/",
|
|
12
12
|
"bin/",
|
|
13
|
+
"deprecated-classes.txt",
|
|
13
14
|
"stylelint-plugin/",
|
|
14
15
|
"SKILL.md",
|
|
15
16
|
"references/"
|