@konce-pt/theme 0.9.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/dist/emit.d.ts ADDED
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Emitery motywu. Każdy bierze dokument i zwraca tekst pliku — nic nie zapisuje i nic nie wie
3
+ * o systemie plików, bo edytor działa w przeglądarce.
4
+ *
5
+ * Jedna zasada wiąże je wszystkie z podglądem: podgląd wstrzykuje **dokładnie ten tekst**, który
6
+ * zwraca `emitCss`. Nie „równoważny" — ten sam. Narzędzie, którego podgląd rozjeżdża się
7
+ * z artefaktem, nie ma racji bytu.
8
+ */
9
+ import type { KptTokenMap } from './resolve.ts';
10
+ import type { KptThemeDoc, KptThemeMode } from './schema.ts';
11
+ export interface KptEmitOptions {
12
+ /** Selektor motywu ciemnego. Domyślnie ten, którego używa biblioteka. */
13
+ darkSelector?: string;
14
+ /** Dołóż blok `@media (prefers-color-scheme: dark)` — biblioteka sama go nie ma. */
15
+ prefersColorScheme?: boolean;
16
+ /** Patrz `KptThemeExport.compat`. */
17
+ compat?: 'resolved' | 'layered';
18
+ /** Wyemituj komplet tokenów zamiast różnicy. Do podglądu całości i do diagnostyki. */
19
+ complete?: boolean;
20
+ }
21
+ /**
22
+ * Co trafia do pliku, dla jednego motywu.
23
+ *
24
+ * `resolved` (domyślne) emituje **całą różnicę wartościami dosłownymi**, prymitywy w to wliczając,
25
+ * i powtarza je w obu blokach. Powtarzanie nie jest marnotrawstwem: w wersjach `@konce-pt/tokens`
26
+ * sprzed poprawki `tokens.dark.css` deklarował komplet prymitywów wewnątrz `[data-theme="dark"]`,
27
+ * więc nadpisanie prymitywu w samym `:root` działało w motywie jasnym, a w ciemnym było po cichu
28
+ * przebijane. Blok ciemny idący później wygrywa z nim kolejnością i plik zachowuje się tak samo
29
+ * na każdej wersji biblioteki.
30
+ *
31
+ * Prymitywów **nie wolno pomijać**, choć kusi to przy kolorach: warstwa semantyczna niesie cały
32
+ * kolor, ale nie niesie typografii ani odstępów. Żaden token nie powołuje się na `--kpt-font-size-*`
33
+ * — komponenty czytają je wprost w swoich regułach — więc plik bez prymitywów gubiłby zmianę kroju
34
+ * i skali pisma w całości. Przy promieniach szkoda byłaby częściowa i przez to gorsza: sześć
35
+ * tokenów komponentowych powołuje się na `--kpt-radius-*`, więc część rogów by się zmieniła,
36
+ * a część nie.
37
+ *
38
+ * `layered` emituje prymitywy raz, w `:root`, i tylko te tokeny, których wartości **nie** tłumaczy
39
+ * już kaskada po nich — plik krótszy i czytelniejszy, ale wymagający wersji biblioteki z poprawką
40
+ * ciemnego arkusza.
41
+ */
42
+ export declare function emitPlan(doc: KptThemeDoc, mode: KptThemeMode, options?: KptEmitOptions): KptTokenMap;
43
+ /**
44
+ * Arkusz motywu. Blok ciemny emitujemy **zawsze**, nawet identyczny z jasnym: bez niego
45
+ * `data-theme="dark"` po cichu wraca do wartości stockowych, a to jest najtrudniejszy do
46
+ * znalezienia rodzaj usterki motywu.
47
+ */
48
+ export declare function emitCss(doc: KptThemeDoc, options?: KptEmitOptions): string;
49
+ /** Ten sam motyw jako mixiny SCSS — dla konsumentów kompilujących style ze źródeł. */
50
+ export declare function emitScss(doc: KptThemeDoc, options?: KptEmitOptions): string;
51
+ /**
52
+ * Motyw jako moduł TypeScriptu, razem z funkcją nakładającą go w runtime.
53
+ *
54
+ * Plik jest samowystarczalny — zero importów — bo trafia do repozytorium konsumenta, gdzie
55
+ * `@konce-pt/theme` nie musi być zainstalowany. To jest droga dla aplikacji, w których motyw
56
+ * wybiera użytkownik końcowy, a nie build.
57
+ */
58
+ export declare function emitTs(doc: KptThemeDoc, options?: KptEmitOptions): string;
59
+ /**
60
+ * Eksport w formacie W3C Design Tokens. Tryby jasny i ciemny idą jako dwie grupy najwyższego
61
+ * poziomu, bo **DTCG nie standaryzuje trybów** — każdy eksporter koduje je inaczej i nie ma
62
+ * zapisu, który byłby tu „poprawny". Dwie grupy są przynajmniej jednoznaczne i czytelne dla
63
+ * człowieka, a importer po drugiej stronie i tak wymaga wskazania, co jest czym.
64
+ *
65
+ * To jest eksport do narzędzi projektowych, nie synchronizacja.
66
+ */
67
+ export declare function emitDtcg(doc: KptThemeDoc, options?: KptEmitOptions): string;
68
+ /** Ile tokenów zmienia motyw względem stocku — do podsumowania w interfejsie. */
69
+ export declare function themeSize(doc: KptThemeDoc): {
70
+ light: number;
71
+ dark: number;
72
+ semantic: number;
73
+ };
74
+ /** Wartości stockowe — wystawione, żeby edytor mógł pokazać „wróć do domyślnej". */
75
+ export declare const stockValue: (name: string, mode: KptThemeMode) => string | undefined;
76
+ export declare const rawValue: (name: string, mode: KptThemeMode) => string | undefined;
package/dist/emit.js ADDED
@@ -0,0 +1,276 @@
1
+ /**
2
+ * Emitery motywu. Każdy bierze dokument i zwraca tekst pliku — nic nie zapisuje i nic nie wie
3
+ * o systemie plików, bo edytor działa w przeglądarce.
4
+ *
5
+ * Jedna zasada wiąże je wszystkie z podglądem: podgląd wstrzykuje **dokładnie ten tekst**, który
6
+ * zwraca `emitCss`. Nie „równoważny" — ten sam. Narzędzie, którego podgląd rozjeżdża się
7
+ * z artefaktem, nie ma racji bytu.
8
+ */
9
+ import { isKnownToken, isPrimitiveToken, resolveTheme, stockTheme, themeDiff } from "./resolve.js";
10
+ import { KPT_RAW, KPT_SEMANTIC_TOKENS } from "./generated/tokens.js";
11
+ const DEFAULT_DARK_SELECTOR = '[data-theme="dark"]';
12
+ const SEMANTIC = new Set(KPT_SEMANTIC_TOKENS);
13
+ const optionsOf = (doc, options = {}) => ({
14
+ darkSelector: options.darkSelector ?? doc.export?.darkSelector ?? DEFAULT_DARK_SELECTOR,
15
+ prefersColorScheme: options.prefersColorScheme ?? doc.export?.prefersColorScheme ?? false,
16
+ compat: options.compat ?? doc.export?.compat ?? 'resolved',
17
+ complete: options.complete ?? false,
18
+ });
19
+ /**
20
+ * Co trafia do pliku, dla jednego motywu.
21
+ *
22
+ * `resolved` (domyślne) emituje **całą różnicę wartościami dosłownymi**, prymitywy w to wliczając,
23
+ * i powtarza je w obu blokach. Powtarzanie nie jest marnotrawstwem: w wersjach `@konce-pt/tokens`
24
+ * sprzed poprawki `tokens.dark.css` deklarował komplet prymitywów wewnątrz `[data-theme="dark"]`,
25
+ * więc nadpisanie prymitywu w samym `:root` działało w motywie jasnym, a w ciemnym było po cichu
26
+ * przebijane. Blok ciemny idący później wygrywa z nim kolejnością i plik zachowuje się tak samo
27
+ * na każdej wersji biblioteki.
28
+ *
29
+ * Prymitywów **nie wolno pomijać**, choć kusi to przy kolorach: warstwa semantyczna niesie cały
30
+ * kolor, ale nie niesie typografii ani odstępów. Żaden token nie powołuje się na `--kpt-font-size-*`
31
+ * — komponenty czytają je wprost w swoich regułach — więc plik bez prymitywów gubiłby zmianę kroju
32
+ * i skali pisma w całości. Przy promieniach szkoda byłaby częściowa i przez to gorsza: sześć
33
+ * tokenów komponentowych powołuje się na `--kpt-radius-*`, więc część rogów by się zmieniła,
34
+ * a część nie.
35
+ *
36
+ * `layered` emituje prymitywy raz, w `:root`, i tylko te tokeny, których wartości **nie** tłumaczy
37
+ * już kaskada po nich — plik krótszy i czytelniejszy, ale wymagający wersji biblioteki z poprawką
38
+ * ciemnego arkusza.
39
+ */
40
+ export function emitPlan(doc, mode, options = {}) {
41
+ return plansOf(doc, options)[mode];
42
+ }
43
+ /** Para planów dla obu motywów. Liczona raz, bo `balance` i tak potrzebuje obu naraz. */
44
+ function plansOf(doc, options = {}) {
45
+ if (optionsOf(doc, options).complete) {
46
+ const all = resolveTheme(doc);
47
+ return { light: { ...all.light }, dark: { ...all.dark } };
48
+ }
49
+ return balance(doc, options);
50
+ }
51
+ /**
52
+ * Oba bloki z **tym samym zestawem kluczy**. To nie jest ozdoba, tylko warunek poprawności.
53
+ *
54
+ * Plik motywu ładuje się PO `tokens.dark.css`, a `:root` i `[data-theme="dark"]` mają dokładnie
55
+ * tę samą specyficzność — przy remisie rozstrzyga kolejność w dokumencie. Nasz `:root` stoi więc
56
+ * wyżej niż biblioteczne nadpisanie ciemne. Token zmieniony tylko w motywie jasnym i wypisany
57
+ * w samym `:root` **przecieknąłby do ciemnego**: tam nikt by go już nie przykrył.
58
+ *
59
+ * Dlatego dla każdego tokenu, który trafia do jednego bloku, drugi blok dostaje jego własną
60
+ * wartość — najczęściej stockową, czyli dosłownie przywrócenie tego, co biblioteka i tak miała.
61
+ * Wygląda na nadmiarowe i takie jest w połowie przypadków; druga połowa to różnica między motywem
62
+ * działającym a ciemnym, który po cichu przejmuje barwy jasnego.
63
+ */
64
+ function balance(doc, options = {}) {
65
+ const plans = {
66
+ light: planFor(doc, 'light', options),
67
+ dark: planFor(doc, 'dark', options),
68
+ };
69
+ const all = new Set([...Object.keys(plans.light), ...Object.keys(plans.dark)]);
70
+ const full = resolveTheme(doc);
71
+ for (const mode of ['light', 'dark']) {
72
+ for (const name of all) {
73
+ plans[mode][name] ??= full[mode][name] ?? '';
74
+ }
75
+ }
76
+ return plans;
77
+ }
78
+ function planFor(doc, mode, options = {}) {
79
+ const resolved = optionsOf(doc, options);
80
+ const diff = themeDiff(doc, mode);
81
+ if (resolved.compat === 'resolved')
82
+ return diff;
83
+ // Warstwowo: najpierw same prymitywy, potem to, czego one nie wyjaśniają.
84
+ const primitiveOnly = { kptTheme: doc.kptTheme, overrides: { primitive: {} } };
85
+ for (const [name, value] of Object.entries(diff)) {
86
+ if (isPrimitiveToken(name))
87
+ primitiveOnly.overrides.primitive[name] = value;
88
+ }
89
+ const afterPrimitives = resolveTheme(primitiveOnly)[mode];
90
+ const plan = { ...primitiveOnly.overrides.primitive };
91
+ for (const [name, value] of Object.entries(diff)) {
92
+ if (!isPrimitiveToken(name) && afterPrimitives[name] !== value)
93
+ plan[name] = value;
94
+ }
95
+ return plan;
96
+ }
97
+ const sorted = (map) => Object.entries(map).sort(([a], [b]) => a.localeCompare(b));
98
+ const block = (selector, map, indent = ' ') => {
99
+ const body = sorted(map)
100
+ .map(([name, value]) => `${indent}${name}: ${value};`)
101
+ .join('\n');
102
+ return `${selector} {\n${body}\n}`;
103
+ };
104
+ /**
105
+ * Nagłówek arkusza — razem z instrukcją ładowania.
106
+ *
107
+ * Instrukcja jedzie w komentarzu, a nie w osobnym pliku, bo osobny plik ginie przy pierwszym
108
+ * skopiowaniu samego arkusza — a arkusz kopiuje się częściej, niż pobiera całą paczkę. Kolejność
109
+ * jest tu jedyną rzeczą, którą da się zrobić źle bez żadnego komunikatu błędu: plik wczytany przed
110
+ * `tokens.css` po prostu nic nie robi.
111
+ */
112
+ const header = (doc) => {
113
+ const name = doc.meta?.name ? `"${doc.meta.name}" ` : '';
114
+ const based = doc.meta?.basedOn ? ` · ${doc.meta.basedOn.package} ${doc.meta.basedOn.version}` : '';
115
+ const stamp = doc.meta?.updatedAt ? ` · ${doc.meta.updatedAt}` : '';
116
+ return [
117
+ `/* Koncept UI theme ${name}— ui.konce.pt/theme`,
118
+ ` * kpt-theme ${doc.kptTheme}${based}${stamp}`,
119
+ ' *',
120
+ ' * Load AFTER @konce-pt/tokens/css and @konce-pt/tokens/css/dark.',
121
+ ' *',
122
+ ' * Angular CLI — angular.json > styles[], in this order:',
123
+ ' * node_modules/@konce-pt/tokens/dist/css/tokens.css',
124
+ ' * node_modules/@konce-pt/tokens/dist/css/tokens.dark.css',
125
+ ' * node_modules/@konce-pt/styles/index.css',
126
+ ' * src/kpt-theme.css <- this file',
127
+ ' * src/styles.scss',
128
+ " * Do NOT import it from main.ts — under the CLI that yields an orphaned chunk and no styles.",
129
+ ' *',
130
+ ' * Vite / React:',
131
+ " * import '@konce-pt/tokens/css';",
132
+ " * import '@konce-pt/tokens/css/dark';",
133
+ " * import '@konce-pt/styles';",
134
+ " * import '@konce-pt/styles/components'; // React only — this is what styles kpt-*",
135
+ " * import './kpt-theme.css'; // last one wins",
136
+ ' *',
137
+ ' * Dark mode is an attribute on the root element and the library does not read',
138
+ ' * prefers-color-scheme — the application sets it:',
139
+ ' * document.documentElement.dataset.theme =',
140
+ " * matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';",
141
+ ' */',
142
+ ].join('\n');
143
+ };
144
+ /**
145
+ * Arkusz motywu. Blok ciemny emitujemy **zawsze**, nawet identyczny z jasnym: bez niego
146
+ * `data-theme="dark"` po cichu wraca do wartości stockowych, a to jest najtrudniejszy do
147
+ * znalezienia rodzaj usterki motywu.
148
+ */
149
+ export function emitCss(doc, options = {}) {
150
+ const resolved = optionsOf(doc, options);
151
+ const { light, dark } = plansOf(doc, options);
152
+ const parts = [header(doc), '', block(':root', light), '', block(resolved.darkSelector, dark)];
153
+ if (resolved.prefersColorScheme) {
154
+ // Biblioteka nie czyta `prefers-color-scheme` — aplikacja albo ustawia `data-theme` sama,
155
+ // albo bierze ten blok. Negacja na `light` zostawia jawny wybór użytkownika ważniejszym
156
+ // od ustawienia systemu.
157
+ const inner = block(':root:not([data-theme="light"])', dark, ' ')
158
+ .split('\n')
159
+ .map((line) => ` ${line}`)
160
+ .join('\n');
161
+ parts.push('', '@media (prefers-color-scheme: dark) {', inner, '}');
162
+ }
163
+ return `${parts.join('\n')}\n`;
164
+ }
165
+ /** Ten sam motyw jako mixiny SCSS — dla konsumentów kompilujących style ze źródeł. */
166
+ export function emitScss(doc, options = {}) {
167
+ const resolved = optionsOf(doc, options);
168
+ const plans = plansOf(doc, options);
169
+ const declare = (map) => sorted(map)
170
+ .map(([name, value]) => ` ${name}: ${value};`)
171
+ .join('\n');
172
+ return `// Koncept UI theme${doc.meta?.name ? ` "${doc.meta.name}"` : ''} — ui.konce.pt/theme
173
+ // Użycie: @use './kpt-theme'; @include kpt-theme.light; @include kpt-theme.dark;
174
+
175
+ @mixin light {
176
+ :root {
177
+ ${declare(plans.light)}
178
+ }
179
+ }
180
+
181
+ @mixin dark {
182
+ ${resolved.darkSelector} {
183
+ ${declare(plans.dark)}
184
+ }
185
+ }
186
+ `;
187
+ }
188
+ /**
189
+ * Motyw jako moduł TypeScriptu, razem z funkcją nakładającą go w runtime.
190
+ *
191
+ * Plik jest samowystarczalny — zero importów — bo trafia do repozytorium konsumenta, gdzie
192
+ * `@konce-pt/theme` nie musi być zainstalowany. To jest droga dla aplikacji, w których motyw
193
+ * wybiera użytkownik końcowy, a nie build.
194
+ */
195
+ export function emitTs(doc, options = {}) {
196
+ const plans = plansOf(doc, options);
197
+ const entries = (mode) => sorted(plans[mode])
198
+ .map(([name, value]) => ` ${JSON.stringify(name)}: ${JSON.stringify(value)},`)
199
+ .join('\n');
200
+ return `// Koncept UI theme${doc.meta?.name ? ` "${doc.meta.name}"` : ''} — ui.konce.pt/theme
201
+ // Wygenerowane. Zero zależności — plik jest samowystarczalny.
202
+
203
+ export const KPT_THEME = {
204
+ light: {
205
+ ${entries('light')}
206
+ },
207
+ dark: {
208
+ ${entries('dark')}
209
+ },
210
+ } as const;
211
+
212
+ export type KptThemeMode = keyof typeof KPT_THEME;
213
+
214
+ /** Nakłada motyw na element (domyślnie <html>) przez style inline. Zwraca funkcję cofającą. */
215
+ export function applyKptTheme(mode: KptThemeMode, element: HTMLElement = document.documentElement): () => void {
216
+ const applied = Object.entries(KPT_THEME[mode]);
217
+ for (const [name, value] of applied) element.style.setProperty(name, value);
218
+ return () => {
219
+ for (const [name] of applied) element.style.removeProperty(name);
220
+ };
221
+ }
222
+ `;
223
+ }
224
+ const COLOR = /^(oklch|rgb|hsl|color|#)/i;
225
+ const DIMENSION = /^-?(?:\d*\.\d+|\d+)(px|rem|em|%)$/;
226
+ /** Typ DTCG zgadnięty z kształtu wartości — źródła biblioteki nie niosą typów w tej postaci. */
227
+ const dtcgType = (value) => {
228
+ if (COLOR.test(value.trim()))
229
+ return 'color';
230
+ if (DIMENSION.test(value.trim()))
231
+ return 'dimension';
232
+ if (/^-?(?:\d*\.\d+|\d+)$/.test(value.trim()))
233
+ return 'number';
234
+ if (value.includes('px ') || value.includes('rem '))
235
+ return 'shadow';
236
+ return 'other';
237
+ };
238
+ /**
239
+ * Eksport w formacie W3C Design Tokens. Tryby jasny i ciemny idą jako dwie grupy najwyższego
240
+ * poziomu, bo **DTCG nie standaryzuje trybów** — każdy eksporter koduje je inaczej i nie ma
241
+ * zapisu, który byłby tu „poprawny". Dwie grupy są przynajmniej jednoznaczne i czytelne dla
242
+ * człowieka, a importer po drugiej stronie i tak wymaga wskazania, co jest czym.
243
+ *
244
+ * To jest eksport do narzędzi projektowych, nie synchronizacja.
245
+ */
246
+ export function emitDtcg(doc, options = {}) {
247
+ const plans = plansOf(doc, options);
248
+ const group = (mode) => {
249
+ const out = {};
250
+ for (const [name, value] of sorted(plans[mode])) {
251
+ // Prefiks zdejmujemy także tokenom własnym: w DTCG nazwa jest ścieżką w drzewie, a nie
252
+ // custom property, więc `--` po drugiej stronie jest śmieciem, nie informacją.
253
+ out[name.replace(/^--(kpt-)?/, '')] = {
254
+ $value: value,
255
+ $type: dtcgType(value),
256
+ ...(isKnownToken(name) ? {} : { $description: 'Token własny motywu (spoza biblioteki).' }),
257
+ };
258
+ }
259
+ return out;
260
+ };
261
+ return `${JSON.stringify({
262
+ $description: `Koncept UI theme${doc.meta?.name ? ` "${doc.meta.name}"` : ''} — export, not sync.`,
263
+ light: group('light'),
264
+ dark: group('dark'),
265
+ }, null, 2)}\n`;
266
+ }
267
+ /** Ile tokenów zmienia motyw względem stocku — do podsumowania w interfejsie. */
268
+ export function themeSize(doc) {
269
+ const light = themeDiff(doc, 'light');
270
+ const dark = themeDiff(doc, 'dark');
271
+ const semantic = new Set([...Object.keys(light), ...Object.keys(dark)].filter((n) => SEMANTIC.has(n)));
272
+ return { light: Object.keys(light).length, dark: Object.keys(dark).length, semantic: semantic.size };
273
+ }
274
+ /** Wartości stockowe — wystawione, żeby edytor mógł pokazać „wróć do domyślnej". */
275
+ export const stockValue = (name, mode) => stockTheme()[mode][name];
276
+ export const rawValue = (name, mode) => KPT_RAW[name]?.[mode];
@@ -0,0 +1,24 @@
1
+ /** Stopnie pełnej skali, tak jak rozkłada je rampa neutralna biblioteki. */
2
+ export declare const KPT_RAMP_STEPS: readonly number[];
3
+ /** Jasność OKLCH na każdym stopniu — krzywa wzięta z rampy neutralnej. */
4
+ export declare const KPT_RAMP_LIGHTNESS: Readonly<Record<number, number>>;
5
+ /** Kształt nasycenia na każdym stopniu, 0–1: uśredniony z czterech ramp akcentowych. */
6
+ export declare const KPT_RAMP_CHROMA: Readonly<Record<number, number>>;
7
+ /** Największe nasycenie w rampach akcentowych — skala, do której odnosi się krzywa. */
8
+ export declare const KPT_RAMP_PEAK_CHROMA = 0.18325;
9
+ /** Stopnie rodziny akcentowej per motyw, odczytane z danger/success/warning/info. */
10
+ export declare const KPT_ACCENT_PATTERN: Readonly<Record<string, {
11
+ light: number;
12
+ dark: number;
13
+ }>>;
14
+ /** Nazwy tokenów warstwy semantycznej (te i tylko te zmienia motyw). */
15
+ export declare const KPT_SEMANTIC_TOKENS: readonly string[];
16
+ /**
17
+ * Wartości stockowe wszystkich tokenów w obu motywach, w postaci SUROWEJ: `{--kpt-nazwa}` to
18
+ * referencja do innego tokenu. Rozwiązuje je `resolveTheme` — dzięki temu nadpisanie prymitywu
19
+ * przestawia wszystko, co się na niego powołuje, tak samo jak zrobiłaby to kaskada CSS.
20
+ */
21
+ export declare const KPT_RAW: Readonly<Record<string, {
22
+ light: string;
23
+ dark: string;
24
+ }>>;