@ngxsmk/theme 1.3.1 → 2.0.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/README.md +13 -1
- package/esm2022/lib/color.mjs +73 -76
- package/esm2022/lib/css.mjs +112 -30
- package/esm2022/lib/theme-injector.service.mjs +81 -0
- package/esm2022/lib/theme.service.mjs +19 -20
- package/esm2022/lib/token-adapter.mjs +80 -0
- package/esm2022/lib/tokens.mjs +16 -2
- package/esm2022/public-api.mjs +3 -1
- package/fesm2022/ngxsmk-theme.mjs +370 -124
- package/fesm2022/ngxsmk-theme.mjs.map +1 -1
- package/lib/color.d.ts +9 -15
- package/lib/color.d.ts.map +1 -1
- package/lib/css.d.ts.map +1 -1
- package/lib/theme-injector.service.d.ts +37 -0
- package/lib/theme-injector.service.d.ts.map +1 -0
- package/lib/theme.service.d.ts +12 -6
- package/lib/theme.service.d.ts.map +1 -1
- package/lib/token-adapter.d.ts +26 -0
- package/lib/token-adapter.d.ts.map +1 -0
- package/lib/tokens.d.ts +4 -0
- package/lib/tokens.d.ts.map +1 -1
- package/package.json +38 -13
- package/public-api.d.ts +2 -0
- package/public-api.d.ts.map +1 -1
- package/styles/ngxsmk.css +137 -76
- package/styles/ngxsmk.emerald.css +137 -76
- package/styles/ngxsmk.neutral.css +114 -53
- package/styles/ngxsmk.rose.css +137 -76
- package/styles/ngxsmk.violet.css +136 -75
- package/tailwind-v4.css +234 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { DOCUMENT } from '@angular/common';
|
|
2
2
|
import * as i0 from '@angular/core';
|
|
3
|
-
import { inject, signal, computed, effect
|
|
3
|
+
import { inject, Injectable, signal, computed, effect } from '@angular/core';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Design token model for the NGXSMK universal theme engine.
|
|
@@ -10,7 +10,7 @@ import { inject, signal, computed, effect, Injectable } from '@angular/core';
|
|
|
10
10
|
* SCSS/Tailwind/JSON exporters build on the same model).
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
function
|
|
13
|
+
function hexToRgb(hex) {
|
|
14
14
|
const value = hex.replace('#', '');
|
|
15
15
|
const full = value.length === 3
|
|
16
16
|
? value
|
|
@@ -24,90 +24,80 @@ function hexToHsl(hex) {
|
|
|
24
24
|
const r = parseInt(full.slice(0, 2), 16) / 255;
|
|
25
25
|
const g = parseInt(full.slice(2, 4), 16) / 255;
|
|
26
26
|
const b = parseInt(full.slice(4, 6), 16) / 255;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
27
|
+
return { r, g, b };
|
|
28
|
+
}
|
|
29
|
+
function srgbToLinear(c) {
|
|
30
|
+
return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
31
|
+
}
|
|
32
|
+
function linearToSrgb(c) {
|
|
33
|
+
return c <= 0.0031308 ? c * 12.92 : 1.055 * Math.pow(c, 1 / 2.4) - 0.055;
|
|
34
|
+
}
|
|
35
|
+
function hexToOklch(hex) {
|
|
36
|
+
const { r: sr, g: sg, b: sb } = hexToRgb(hex);
|
|
37
|
+
const r = srgbToLinear(sr);
|
|
38
|
+
const g = srgbToLinear(sg);
|
|
39
|
+
const b = srgbToLinear(sb);
|
|
40
|
+
const l = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b;
|
|
41
|
+
const m = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b;
|
|
42
|
+
const s = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b;
|
|
43
|
+
const l_ = Math.cbrt(l);
|
|
44
|
+
const m_ = Math.cbrt(m);
|
|
45
|
+
const s_ = Math.cbrt(s);
|
|
46
|
+
const L = 0.2104542553 * l_ + 0.793617785 * m_ - 0.0040720468 * s_;
|
|
47
|
+
const a = 1.9779984951 * l_ - 2.428592205 * m_ + 0.4505937099 * s_;
|
|
48
|
+
const b_ = 0.0259040371 * l_ + 0.7827717662 * m_ - 0.808675766 * s_;
|
|
49
|
+
const C = Math.sqrt(a * a + b_ * b_);
|
|
50
|
+
let h = (Math.atan2(b_, a) * 180) / Math.PI;
|
|
46
51
|
if (h < 0)
|
|
47
52
|
h += 360;
|
|
48
|
-
return {
|
|
53
|
+
return { l: L, c: C, h };
|
|
49
54
|
}
|
|
50
|
-
function
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
const
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
rgb = [c, 0, x];
|
|
69
|
-
const toHex = (v) => Math.round((v + m) * 255)
|
|
70
|
-
.toString(16)
|
|
71
|
-
.padStart(2, '0');
|
|
72
|
-
return `#${toHex(rgb[0])}${toHex(rgb[1])}${toHex(rgb[2])}`.toUpperCase();
|
|
55
|
+
function oklchToHex({ l: L, c: C, h }) {
|
|
56
|
+
const hRad = (h * Math.PI) / 180;
|
|
57
|
+
const a = C * Math.cos(hRad);
|
|
58
|
+
const b_ = C * Math.sin(hRad);
|
|
59
|
+
const l_ = L + 0.3963377774 * a + 0.2158037573 * b_;
|
|
60
|
+
const m_ = L - 0.1055613458 * a - 0.0638541728 * b_;
|
|
61
|
+
const s_ = L - 0.0894841775 * a - 1.291485548 * b_;
|
|
62
|
+
const l = l_ * l_ * l_;
|
|
63
|
+
const m = m_ * m_ * m_;
|
|
64
|
+
const s = s_ * s_ * s_;
|
|
65
|
+
const r = +4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s;
|
|
66
|
+
const g = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s;
|
|
67
|
+
const b = -0.0041960863 * l - 0.7034186148 * m + 1.707614701 * s;
|
|
68
|
+
const rClamped = Math.min(255, Math.max(0, Math.round(linearToSrgb(r) * 255)));
|
|
69
|
+
const gClamped = Math.min(255, Math.max(0, Math.round(linearToSrgb(g) * 255)));
|
|
70
|
+
const bClamped = Math.min(255, Math.max(0, Math.round(linearToSrgb(b) * 255)));
|
|
71
|
+
const toHex = (v) => v.toString(16).padStart(2, '0');
|
|
72
|
+
return `#${toHex(rClamped)}${toHex(gClamped)}${toHex(bClamped)}`.toUpperCase();
|
|
73
73
|
}
|
|
74
|
-
/** Shift a color's lightness by `delta`
|
|
74
|
+
/** Shift a color's lightness by `delta` (clamped 0–1). */
|
|
75
75
|
function shiftLightness(hex, delta) {
|
|
76
|
-
const
|
|
77
|
-
return
|
|
76
|
+
const oklch = hexToOklch(hex);
|
|
77
|
+
return oklchToHex({ ...oklch, l: Math.min(1.0, Math.max(0.0, oklch.l + delta)) });
|
|
78
78
|
}
|
|
79
|
-
/** Rotate a color's hue by `degrees`, keeping
|
|
79
|
+
/** Rotate a color's hue by `degrees`, keeping chroma and lightness. */
|
|
80
80
|
function rotateHue(hex, degrees) {
|
|
81
|
-
const
|
|
82
|
-
return
|
|
81
|
+
const oklch = hexToOklch(hex);
|
|
82
|
+
return oklchToHex({ ...oklch, h: (((oklch.h + degrees) % 360) + 360) % 360 });
|
|
83
83
|
}
|
|
84
|
-
/**
|
|
85
|
-
* Lightness stops for each scale step. Tuned so that step 500/600 stays close
|
|
86
|
-
* to typical brand-color lightness and the extremes remain usable as tinted
|
|
87
|
-
* backgrounds (50–100) and readable text (900–950).
|
|
88
|
-
*/
|
|
84
|
+
/** Lightness stops for each scale step in OKLCH (0–1). */
|
|
89
85
|
const LIGHTNESS_STOPS = {
|
|
90
|
-
50: 97,
|
|
91
|
-
100: 94,
|
|
92
|
-
200: 87,
|
|
93
|
-
300: 78,
|
|
94
|
-
400: 66,
|
|
95
|
-
500: 56,
|
|
96
|
-
600: 48,
|
|
97
|
-
700:
|
|
98
|
-
800: 32,
|
|
99
|
-
900: 25,
|
|
100
|
-
950: 16,
|
|
86
|
+
50: 0.97,
|
|
87
|
+
100: 0.94,
|
|
88
|
+
200: 0.87,
|
|
89
|
+
300: 0.78,
|
|
90
|
+
400: 0.66,
|
|
91
|
+
500: 0.56,
|
|
92
|
+
600: 0.48,
|
|
93
|
+
700: 0.4,
|
|
94
|
+
800: 0.32,
|
|
95
|
+
900: 0.25,
|
|
96
|
+
950: 0.16,
|
|
101
97
|
};
|
|
102
|
-
/**
|
|
103
|
-
* Derive a full 50–950 color scale from a single brand hex.
|
|
104
|
-
*
|
|
105
|
-
* The input color keeps its exact value at the step whose lightness stop is
|
|
106
|
-
* nearest to the input's own lightness, so `scale[step]` round-trips brand
|
|
107
|
-
* colors instead of shifting them.
|
|
108
|
-
*/
|
|
98
|
+
/** Derive a full 50–950 color scale from a single brand hex. */
|
|
109
99
|
function deriveScale(baseHex) {
|
|
110
|
-
const base =
|
|
100
|
+
const base = hexToOklch(baseHex);
|
|
111
101
|
let nearest = 500;
|
|
112
102
|
let nearestDelta = Number.POSITIVE_INFINITY;
|
|
113
103
|
for (const [step, stop] of Object.entries(LIGHTNESS_STOPS)) {
|
|
@@ -120,7 +110,14 @@ function deriveScale(baseHex) {
|
|
|
120
110
|
const scale = {};
|
|
121
111
|
for (const [step, stop] of Object.entries(LIGHTNESS_STOPS)) {
|
|
122
112
|
const key = Number(step);
|
|
123
|
-
|
|
113
|
+
if (key === nearest) {
|
|
114
|
+
scale[key] = oklchToHex(base);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
// Perceptually scale chroma using a sine taper so that extremes (50/950) don't clip/saturate unnaturally.
|
|
118
|
+
const scaleChroma = base.c * Math.sin(Math.PI * stop);
|
|
119
|
+
scale[key] = oklchToHex({ l: stop, c: scaleChroma, h: base.h });
|
|
120
|
+
}
|
|
124
121
|
}
|
|
125
122
|
return scale;
|
|
126
123
|
}
|
|
@@ -151,7 +148,7 @@ const DEFAULT_SEMANTIC = {
|
|
|
151
148
|
};
|
|
152
149
|
const DEFAULT_FONT_FAMILY = {
|
|
153
150
|
sans: "'Inter', system-ui, -apple-system, 'Segoe UI', sans-serif",
|
|
154
|
-
mono: "'
|
|
151
|
+
mono: "'Geist Mono', 'Geist Mono Fallback', monospace",
|
|
155
152
|
};
|
|
156
153
|
const DEFAULT_TYPOGRAPHY = {
|
|
157
154
|
display: {
|
|
@@ -216,6 +213,20 @@ const TRACKING = {
|
|
|
216
213
|
normal: '0',
|
|
217
214
|
wide: '0.08em',
|
|
218
215
|
};
|
|
216
|
+
/** Font-weight tokens — standalone scale for direct use outside typography presets. */
|
|
217
|
+
const FONT_WEIGHT = {
|
|
218
|
+
regular: '400',
|
|
219
|
+
medium: '500',
|
|
220
|
+
semibold: '600',
|
|
221
|
+
bold: '700',
|
|
222
|
+
};
|
|
223
|
+
/** Line-height tokens — unitless multipliers for flexible leading control. */
|
|
224
|
+
const LEADING = {
|
|
225
|
+
none: '1',
|
|
226
|
+
tight: '1.25',
|
|
227
|
+
normal: '1.5',
|
|
228
|
+
relaxed: '1.625',
|
|
229
|
+
};
|
|
219
230
|
const SPACING = {
|
|
220
231
|
'0': '0px',
|
|
221
232
|
'0-5': '0.125rem',
|
|
@@ -342,51 +353,34 @@ function scaleVars(prefix, scale) {
|
|
|
342
353
|
}
|
|
343
354
|
return vars;
|
|
344
355
|
}
|
|
345
|
-
/**
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
...scaleVars('brand', theme.brand),
|
|
349
|
-
...scaleVars('secondary', theme.secondary),
|
|
350
|
-
...scaleVars('neutral', theme.neutral),
|
|
351
|
-
'--ngxsmk-font-sans': theme.fontFamily.sans,
|
|
352
|
-
'--ngxsmk-font-mono': theme.fontFamily.mono,
|
|
353
|
-
'--ngxsmk-radius-base': theme.radius[theme.radiusBase],
|
|
354
|
-
'--ngxsmk-control-height': CONTROL_HEIGHT,
|
|
355
|
-
// Motion Tokens
|
|
356
|
-
'--ngxsmk-motion-duration': 'var(--ngxsmk-duration-normal)',
|
|
357
|
-
'--ngxsmk-motion-ease': 'var(--ngxsmk-ease-in-out)',
|
|
358
|
-
// Interactive-state motion: shared lift/press transforms. Neutralized to
|
|
359
|
-
// `none` under prefers-reduced-motion (see buildThemeCss).
|
|
360
|
-
'--ngxsmk-hover-lift': 'translateY(-1px)',
|
|
361
|
-
'--ngxsmk-press-scale': 'scale(0.98)',
|
|
362
|
-
// Focus ring: THE focus indicator for every component. Aliases the
|
|
363
|
-
// mode-aware --ngxsmk-shadow-focus; the error variant derives from the
|
|
364
|
-
// semantic error role so invalid controls ring red in both modes.
|
|
365
|
-
'--ngxsmk-focus-ring': 'var(--ngxsmk-shadow-focus)',
|
|
366
|
-
'--ngxsmk-focus-ring-error': '0 0 0 2px color-mix(in srgb, var(--ngxsmk-color-error) 15%, transparent), ' +
|
|
367
|
-
'0 0 0 4px color-mix(in srgb, var(--ngxsmk-color-error) 45%, transparent)',
|
|
368
|
-
// Component-specific Tokens (Buttons)
|
|
356
|
+
/** Default CSS custom properties for each component, emitted once in :root. */
|
|
357
|
+
const COMPONENT_TOKEN_DEFAULTS = {
|
|
358
|
+
button: {
|
|
369
359
|
'--ngxsmk-button-bg': 'var(--ngxsmk-color-primary)',
|
|
370
360
|
'--ngxsmk-button-color': 'var(--ngxsmk-color-on-primary)',
|
|
371
361
|
'--ngxsmk-button-font-family': 'var(--ngxsmk-font-sans)',
|
|
372
362
|
'--ngxsmk-button-font-weight': '500',
|
|
373
363
|
'--ngxsmk-button-radius': 'var(--ngxsmk-radius-base)',
|
|
374
364
|
'--ngxsmk-button-disabled-opacity': '0.5',
|
|
375
|
-
|
|
365
|
+
},
|
|
366
|
+
card: {
|
|
376
367
|
'--ngxsmk-card-bg': 'var(--ngxsmk-color-surface)',
|
|
377
368
|
'--ngxsmk-card-radius': 'var(--ngxsmk-radius-xl)',
|
|
378
369
|
'--ngxsmk-card-shadow': 'var(--ngxsmk-shadow-sm)',
|
|
379
370
|
'--ngxsmk-card-border-color': 'var(--ngxsmk-color-outline)',
|
|
380
|
-
|
|
371
|
+
},
|
|
372
|
+
accordion: {
|
|
381
373
|
'--ngxsmk-accordion-bg': 'var(--ngxsmk-color-surface)',
|
|
382
374
|
'--ngxsmk-accordion-radius': 'var(--ngxsmk-radius-lg)',
|
|
383
375
|
'--ngxsmk-accordion-border-color': 'var(--ngxsmk-color-outline)',
|
|
384
|
-
|
|
376
|
+
},
|
|
377
|
+
progress: {
|
|
385
378
|
'--ngxsmk-progress-height': '0.5rem',
|
|
386
379
|
'--ngxsmk-progress-bg': 'var(--ngxsmk-color-surface-variant)',
|
|
387
380
|
'--ngxsmk-progress-color': 'var(--ngxsmk-color-primary)',
|
|
388
381
|
'--ngxsmk-progress-radius': 'var(--ngxsmk-radius-full)',
|
|
389
|
-
|
|
382
|
+
},
|
|
383
|
+
switch: {
|
|
390
384
|
'--ngxsmk-switch-width': '2.25rem',
|
|
391
385
|
'--ngxsmk-switch-height': '1.25rem',
|
|
392
386
|
'--ngxsmk-switch-radius': 'var(--ngxsmk-radius-full)',
|
|
@@ -394,6 +388,74 @@ function staticVars(theme) {
|
|
|
394
388
|
'--ngxsmk-switch-thumb-size': '0.875rem',
|
|
395
389
|
'--ngxsmk-switch-thumb-bg': 'var(--ngxsmk-color-surface)',
|
|
396
390
|
'--ngxsmk-switch-checked-bg': 'var(--ngxsmk-color-primary)',
|
|
391
|
+
},
|
|
392
|
+
spreadsheet: {
|
|
393
|
+
'--ngxsmk-spreadsheet-bg': 'var(--ngxsmk-color-surface)',
|
|
394
|
+
'--ngxsmk-spreadsheet-header-bg': 'var(--ngxsmk-color-surface-variant)',
|
|
395
|
+
'--ngxsmk-spreadsheet-header-color': 'var(--ngxsmk-color-on-surface-variant)',
|
|
396
|
+
'--ngxsmk-spreadsheet-border': 'var(--ngxsmk-color-outline)',
|
|
397
|
+
'--ngxsmk-spreadsheet-grid-color': 'var(--ngxsmk-color-outline-variant)',
|
|
398
|
+
'--ngxsmk-spreadsheet-hover-bg': 'var(--ngxsmk-color-surface-hover)',
|
|
399
|
+
'--ngxsmk-spreadsheet-selected-bg': 'color-mix(in srgb, var(--ngxsmk-color-primary) 12%, transparent)',
|
|
400
|
+
'--ngxsmk-spreadsheet-active-border': 'var(--ngxsmk-color-primary)',
|
|
401
|
+
'--ngxsmk-spreadsheet-font': 'var(--ngxsmk-font-sans)',
|
|
402
|
+
'--ngxsmk-spreadsheet-font-mono': 'var(--ngxsmk-font-mono)',
|
|
403
|
+
'--ngxsmk-spreadsheet-row-height': '2.25rem',
|
|
404
|
+
'--ngxsmk-spreadsheet-header-height': '2.5rem',
|
|
405
|
+
'--ngxsmk-spreadsheet-cell-padding': '6px 10px',
|
|
406
|
+
'--ngxsmk-spreadsheet-cell-font-size': 'var(--ngxsmk-text-body-sm-size)',
|
|
407
|
+
'--ngxsmk-spreadsheet-scrollbar-size': '8px',
|
|
408
|
+
'--ngxsmk-spreadsheet-shadow': 'var(--ngxsmk-shadow-sm)',
|
|
409
|
+
'--ngxsmk-spreadsheet-radius': 'var(--ngxsmk-radius-lg)',
|
|
410
|
+
'--ngxsmk-spreadsheet-edit-bg': 'var(--ngxsmk-color-surface)',
|
|
411
|
+
'--ngxsmk-spreadsheet-edit-border': 'var(--ngxsmk-color-primary)',
|
|
412
|
+
'--ngxsmk-spreadsheet-row-hover-bg': 'var(--ngxsmk-color-surface-hover)',
|
|
413
|
+
'--ngxsmk-spreadsheet-col-hover-bg': 'color-mix(in srgb, var(--ngxsmk-color-primary) 6%, transparent)',
|
|
414
|
+
'--ngxsmk-spreadsheet-frozen-shadow': '2px 0 4px rgba(0,0,0,0.08)',
|
|
415
|
+
'--ngxsmk-spreadsheet-selection-range': 'color-mix(in srgb, var(--ngxsmk-color-primary) 8%, transparent)',
|
|
416
|
+
},
|
|
417
|
+
inputGroup: {
|
|
418
|
+
'--ngxsmk-input-group-bg': 'var(--ngxsmk-color-surface)',
|
|
419
|
+
'--ngxsmk-input-group-border': 'var(--ngxsmk-color-outline-strong)',
|
|
420
|
+
'--ngxsmk-input-group-radius': 'var(--ngxsmk-radius-md)',
|
|
421
|
+
'--ngxsmk-input-group-shadow': 'none',
|
|
422
|
+
'--ngxsmk-input-group-focus-border': 'var(--ngxsmk-color-primary)',
|
|
423
|
+
'--ngxsmk-input-group-focus-shadow': '0 0 0 3px color-mix(in srgb, var(--ngxsmk-color-primary) 15%, transparent)',
|
|
424
|
+
'--ngxsmk-input-group-error-border': 'var(--ngxsmk-color-error)',
|
|
425
|
+
'--ngxsmk-input-group-error-focus-shadow': '0 0 0 3px color-mix(in srgb, var(--ngxsmk-color-error) 15%, transparent)',
|
|
426
|
+
'--ngxsmk-input-group-success-border': 'var(--ngxsmk-color-success)',
|
|
427
|
+
'--ngxsmk-input-group-warning-border': 'var(--ngxsmk-color-warning)',
|
|
428
|
+
'--ngxsmk-input-group-height': '2.5rem',
|
|
429
|
+
'--ngxsmk-input-group-padding': '0.75rem',
|
|
430
|
+
'--ngxsmk-input-group-gap': 'var(--ngxsmk-space-2)',
|
|
431
|
+
'--ngxsmk-input-group-font': 'var(--ngxsmk-font-sans)',
|
|
432
|
+
},
|
|
433
|
+
};
|
|
434
|
+
/** Tokens that do not change between light and dark mode. */
|
|
435
|
+
function staticVars(theme) {
|
|
436
|
+
const vars = {
|
|
437
|
+
...scaleVars('brand', theme.brand),
|
|
438
|
+
...scaleVars('secondary', theme.secondary),
|
|
439
|
+
...scaleVars('neutral', theme.neutral),
|
|
440
|
+
'--ngxsmk-font-sans': theme.fontFamily.sans,
|
|
441
|
+
'--ngxsmk-font-mono': theme.fontFamily.mono,
|
|
442
|
+
'--ngxsmk-radius-base': theme.radius[theme.radiusBase],
|
|
443
|
+
'--ngxsmk-control-height': CONTROL_HEIGHT,
|
|
444
|
+
// Motion Tokens
|
|
445
|
+
'--ngxsmk-motion-duration': 'var(--ngxsmk-duration-normal)',
|
|
446
|
+
'--ngxsmk-motion-ease': 'var(--ngxsmk-ease-in-out)',
|
|
447
|
+
// Interactive-state motion: shared lift/press transforms. Neutralized to
|
|
448
|
+
// `none` under prefers-reduced-motion (see buildThemeCss).
|
|
449
|
+
'--ngxsmk-hover-lift': 'translateY(-1px)',
|
|
450
|
+
'--ngxsmk-press-scale': 'scale(0.98)',
|
|
451
|
+
// Focus ring: THE focus indicator for every component. Aliases the
|
|
452
|
+
// mode-aware --ngxsmk-shadow-focus; the error variant derives from the
|
|
453
|
+
// semantic error role so invalid controls ring red in both modes.
|
|
454
|
+
'--ngxsmk-focus-ring': 'var(--ngxsmk-shadow-focus)',
|
|
455
|
+
'--ngxsmk-focus-ring-error': '0 0 0 2px color-mix(in srgb, var(--ngxsmk-color-error) 15%, transparent), ' +
|
|
456
|
+
'0 0 0 4px color-mix(in srgb, var(--ngxsmk-color-error) 45%, transparent)',
|
|
457
|
+
// Component-specific token defaults
|
|
458
|
+
...Object.values(COMPONENT_TOKEN_DEFAULTS).reduce((merged, tokens) => ({ ...merged, ...tokens }), {}),
|
|
397
459
|
};
|
|
398
460
|
for (const [key, value] of Object.entries(CONTROL_HEIGHTS)) {
|
|
399
461
|
vars[`--ngxsmk-control-height-${key}`] = value;
|
|
@@ -407,6 +469,12 @@ function staticVars(theme) {
|
|
|
407
469
|
for (const [key, value] of Object.entries(TRACKING)) {
|
|
408
470
|
vars[`--ngxsmk-tracking-${key}`] = value;
|
|
409
471
|
}
|
|
472
|
+
for (const [key, value] of Object.entries(FONT_WEIGHT)) {
|
|
473
|
+
vars[`--ngxsmk-font-weight-${key}`] = value;
|
|
474
|
+
}
|
|
475
|
+
for (const [key, value] of Object.entries(LEADING)) {
|
|
476
|
+
vars[`--ngxsmk-leading-${key}`] = value;
|
|
477
|
+
}
|
|
410
478
|
for (const [key, value] of Object.entries(theme.spacing)) {
|
|
411
479
|
vars[`--ngxsmk-space-${key}`] = value;
|
|
412
480
|
}
|
|
@@ -479,6 +547,19 @@ function lightVars(theme) {
|
|
|
479
547
|
'--ngxsmk-color-ring': theme.brandBase,
|
|
480
548
|
'--ngxsmk-color-backdrop': 'rgb(1 18 40 / 0.4)',
|
|
481
549
|
'--ngxsmk-shadow-focus': `0 0 0 2px ${theme.brand[100]}, 0 0 0 4px color-mix(in srgb, ${theme.brandBase} 45%, transparent)`,
|
|
550
|
+
// Categorical chart palette — a fixed 8-hue qualitative set (NOT derived
|
|
551
|
+
// from the brand, so adjacent series stay distinguishable across themes),
|
|
552
|
+
// stepped for the light surface. Validated for the light chart surface:
|
|
553
|
+
// CVD worst-adjacent ΔE 24.2, chroma/lightness in-band. Charts read these
|
|
554
|
+
// via getComputedStyle; consumers can override any slot to rebrand series.
|
|
555
|
+
'--ngxsmk-chart-1': '#2A78D6',
|
|
556
|
+
'--ngxsmk-chart-2': '#1BAF7A',
|
|
557
|
+
'--ngxsmk-chart-3': '#EDA100',
|
|
558
|
+
'--ngxsmk-chart-4': '#008300',
|
|
559
|
+
'--ngxsmk-chart-5': '#4A3AA7',
|
|
560
|
+
'--ngxsmk-chart-6': '#E34948',
|
|
561
|
+
'--ngxsmk-chart-7': '#E87BA4',
|
|
562
|
+
'--ngxsmk-chart-8': '#EB6834',
|
|
482
563
|
};
|
|
483
564
|
const tertiary = deriveScale(rotateHue(theme.brandBase, 64));
|
|
484
565
|
vars['--ngxsmk-color-tertiary'] = tertiary[500];
|
|
@@ -533,6 +614,18 @@ function darkVars(theme) {
|
|
|
533
614
|
'--ngxsmk-color-ring': theme.brand[400],
|
|
534
615
|
'--ngxsmk-color-backdrop': 'rgb(17 17 18 / 0.6)',
|
|
535
616
|
'--ngxsmk-shadow-focus': `0 0 0 2px color-mix(in srgb, ${theme.brand[400]} 25%, transparent), 0 0 0 4px color-mix(in srgb, ${theme.brand[400]} 45%, transparent)`,
|
|
617
|
+
// Categorical chart palette — the same eight hues as light, re-stepped for
|
|
618
|
+
// the dark surface (not a different palette). Validated for the dark chart
|
|
619
|
+
// surface: all slots ≥ 3:1 contrast, CVD worst-adjacent ΔE 10.3 (floor band,
|
|
620
|
+
// relies on the legend/labels charts already render for secondary encoding).
|
|
621
|
+
'--ngxsmk-chart-1': '#3987E5',
|
|
622
|
+
'--ngxsmk-chart-2': '#199E70',
|
|
623
|
+
'--ngxsmk-chart-3': '#C98500',
|
|
624
|
+
'--ngxsmk-chart-4': '#008300',
|
|
625
|
+
'--ngxsmk-chart-5': '#9085E9',
|
|
626
|
+
'--ngxsmk-chart-6': '#E66767',
|
|
627
|
+
'--ngxsmk-chart-7': '#D55181',
|
|
628
|
+
'--ngxsmk-chart-8': '#D95926',
|
|
536
629
|
};
|
|
537
630
|
const tertiary = deriveScale(rotateHue(theme.brandBase, 64));
|
|
538
631
|
vars['--ngxsmk-color-tertiary'] = tertiary[400];
|
|
@@ -661,19 +754,174 @@ const presets = {
|
|
|
661
754
|
rose: rosePreset,
|
|
662
755
|
};
|
|
663
756
|
|
|
664
|
-
|
|
757
|
+
/**
|
|
758
|
+
* Maps NGXSMK semantic color roles to Ionic's color variable namespace.
|
|
759
|
+
*
|
|
760
|
+
* Ionic expects:
|
|
761
|
+
* --ion-color-primary, --ion-color-primary-rgb, --ion-color-primary-shade,
|
|
762
|
+
* --ion-color-primary-tint, --ion-color-secondary, etc.
|
|
763
|
+
*
|
|
764
|
+
* This adapter produces the subset Ionic needs for theming.
|
|
765
|
+
*/
|
|
766
|
+
const ionicVarsAdapter = {
|
|
767
|
+
name: 'ionic',
|
|
768
|
+
vars(theme) {
|
|
769
|
+
const vars = {};
|
|
770
|
+
// Primary
|
|
771
|
+
vars['--ion-color-primary'] = theme.brandBase;
|
|
772
|
+
vars['--ion-color-primary-rgb'] = hexToRgbStr(theme.brandBase);
|
|
773
|
+
vars['--ion-color-primary-shade'] = shiftLightnessHex(theme.brandBase, -14);
|
|
774
|
+
vars['--ion-color-primary-tint'] = shiftLightnessHex(theme.brandBase, 8);
|
|
775
|
+
// Secondary
|
|
776
|
+
vars['--ion-color-secondary'] = theme.secondaryBase;
|
|
777
|
+
vars['--ion-color-secondary-rgb'] = hexToRgbStr(theme.secondaryBase);
|
|
778
|
+
vars['--ion-color-secondary-shade'] = shiftLightnessHex(theme.secondaryBase, -14);
|
|
779
|
+
vars['--ion-color-secondary-tint'] = shiftLightnessHex(theme.secondaryBase, 8);
|
|
780
|
+
// Tertiary (derived from brand hue + 64)
|
|
781
|
+
const tertiary = deriveScale(rotateHue(theme.brandBase, 64));
|
|
782
|
+
vars['--ion-color-tertiary'] = tertiary[500];
|
|
783
|
+
vars['--ion-color-tertiary-rgb'] = hexToRgbStr(tertiary[500]);
|
|
784
|
+
vars['--ion-color-tertiary-shade'] = tertiary[600];
|
|
785
|
+
vars['--ion-color-tertiary-tint'] = tertiary[400];
|
|
786
|
+
// Semantic status colors
|
|
787
|
+
for (const [name, hex] of Object.entries(theme.semantic)) {
|
|
788
|
+
vars[`--ion-color-${name}`] = hex;
|
|
789
|
+
vars[`--ion-color-${name}-rgb`] = hexToRgbStr(hex);
|
|
790
|
+
}
|
|
791
|
+
// Background / surface
|
|
792
|
+
vars['--ion-background-color'] = theme.neutral[50];
|
|
793
|
+
vars['--ion-background-color-rgb'] = hexToRgbStr(theme.neutral[50]);
|
|
794
|
+
vars['--ion-text-color'] = '#0A1317';
|
|
795
|
+
vars['--ion-text-color-rgb'] = '10, 19, 23';
|
|
796
|
+
// Typography
|
|
797
|
+
vars['--ion-font-family'] = theme.fontFamily.sans;
|
|
798
|
+
// Border radius (Ionic uses --ion-border-radius)
|
|
799
|
+
vars['--ion-border-radius'] = theme.radius[theme.radiusBase];
|
|
800
|
+
return vars;
|
|
801
|
+
},
|
|
802
|
+
};
|
|
803
|
+
/** Default NGXSMK adapter — emits `--ngxsmk-*` variables. */
|
|
804
|
+
const cssVarsAdapter = {
|
|
805
|
+
name: 'ngxsmk',
|
|
806
|
+
vars: () => ({}), // Not used directly — buildThemeCss handles this
|
|
807
|
+
};
|
|
808
|
+
// ─── Helpers ──────────────────────────────────────────────
|
|
809
|
+
function hexToRgbStr(hex) {
|
|
810
|
+
const h = hex.replace('#', '');
|
|
811
|
+
const r = parseInt(h.substring(0, 2), 16);
|
|
812
|
+
const g = parseInt(h.substring(2, 4), 16);
|
|
813
|
+
const b = parseInt(h.substring(4, 6), 16);
|
|
814
|
+
return `${r}, ${g}, ${b}`;
|
|
815
|
+
}
|
|
816
|
+
function shiftLightnessHex(hex, amount) {
|
|
817
|
+
// Simple hex lightness shift — import from color.ts would be circular
|
|
818
|
+
const h = hex.replace('#', '');
|
|
819
|
+
let r = parseInt(h.substring(0, 2), 16);
|
|
820
|
+
let g = parseInt(h.substring(2, 4), 16);
|
|
821
|
+
let b = parseInt(h.substring(4, 6), 16);
|
|
822
|
+
if (amount > 0) {
|
|
823
|
+
r = Math.min(255, r + Math.round((255 - r) * (amount / 100)));
|
|
824
|
+
g = Math.min(255, g + Math.round((255 - g) * (amount / 100)));
|
|
825
|
+
b = Math.min(255, b + Math.round((255 - b) * (amount / 100)));
|
|
826
|
+
}
|
|
827
|
+
else {
|
|
828
|
+
const factor = 1 + amount / 100;
|
|
829
|
+
r = Math.max(0, Math.round(r * factor));
|
|
830
|
+
g = Math.max(0, Math.round(g * factor));
|
|
831
|
+
b = Math.max(0, Math.round(b * factor));
|
|
832
|
+
}
|
|
833
|
+
return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
|
|
834
|
+
}
|
|
835
|
+
|
|
665
836
|
const STYLE_ID = 'ngxsmk-theme';
|
|
837
|
+
const IONIC_STYLE_ID = 'ngxsmk-ionic-theme';
|
|
838
|
+
/**
|
|
839
|
+
* Injects generated theme CSS into the document at runtime.
|
|
840
|
+
*
|
|
841
|
+
* Decoupled from dark-mode switching so consumers who only need
|
|
842
|
+
* token injection (e.g. the CLI theme-preview) don't pull in
|
|
843
|
+
* class-toggle / localStorage / matchMedia logic.
|
|
844
|
+
*
|
|
845
|
+
* SSR-safe: all DOM access goes through the injected document.
|
|
846
|
+
*/
|
|
847
|
+
class NgxThemeInjectorService {
|
|
848
|
+
constructor() {
|
|
849
|
+
this.document = inject(DOCUMENT);
|
|
850
|
+
}
|
|
851
|
+
/**
|
|
852
|
+
* Apply a theme at runtime. Generates token CSS from the config and swaps
|
|
853
|
+
* it into a dedicated `<style>` element, overriding the build-time theme.
|
|
854
|
+
*
|
|
855
|
+
* @param adapter Optional output adapter. Defaults to the standard NGXSMK
|
|
856
|
+
* `--ngxsmk-*` CSS variables. Pass `ionicVarsAdapter` to also generate
|
|
857
|
+
* `--ion-*` variables for Ionic components.
|
|
858
|
+
*/
|
|
859
|
+
applyTheme(config, adapter) {
|
|
860
|
+
const head = this.document.head;
|
|
861
|
+
let style = this.document.getElementById(STYLE_ID);
|
|
862
|
+
if (!style) {
|
|
863
|
+
style = this.document.createElement('style');
|
|
864
|
+
style.id = STYLE_ID;
|
|
865
|
+
head.appendChild(style);
|
|
866
|
+
}
|
|
867
|
+
let css = buildThemeCss(config);
|
|
868
|
+
if (adapter) {
|
|
869
|
+
const resolved = resolveTheme(config);
|
|
870
|
+
const platformVars = adapter.vars(resolved);
|
|
871
|
+
css += '\n' + this.buildAdapterCss(adapter.name, platformVars);
|
|
872
|
+
}
|
|
873
|
+
style.textContent = css;
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* Apply Ionic-specific token variables alongside the standard NGXSMK theme.
|
|
877
|
+
* Generates `--ion-*` variables derived from the same `ThemeConfig`.
|
|
878
|
+
*/
|
|
879
|
+
applyIonicTheme(config) {
|
|
880
|
+
const head = this.document.head;
|
|
881
|
+
let style = this.document.getElementById(IONIC_STYLE_ID);
|
|
882
|
+
if (!style) {
|
|
883
|
+
style = this.document.createElement('style');
|
|
884
|
+
style.id = IONIC_STYLE_ID;
|
|
885
|
+
head.appendChild(style);
|
|
886
|
+
}
|
|
887
|
+
const resolved = resolveTheme(config);
|
|
888
|
+
const vars = ionicVarsAdapter.vars(resolved);
|
|
889
|
+
style.textContent = this.buildAdapterCss('ionic', vars);
|
|
890
|
+
}
|
|
891
|
+
/** Remove a runtime-applied theme, falling back to the build-time CSS. */
|
|
892
|
+
clearTheme() {
|
|
893
|
+
this.document.getElementById(STYLE_ID)?.remove();
|
|
894
|
+
}
|
|
895
|
+
/** Remove the Ionic adapter stylesheet. */
|
|
896
|
+
clearIonicTheme() {
|
|
897
|
+
this.document.getElementById(IONIC_STYLE_ID)?.remove();
|
|
898
|
+
}
|
|
899
|
+
buildAdapterCss(adapterName, vars) {
|
|
900
|
+
const lines = Object.entries(vars).map(([k, v]) => ` ${k}: ${v};`);
|
|
901
|
+
return `/* @ngxsmk/theme — ${adapterName} adapter */\n:root {\n${lines.join('\n')}\n}`;
|
|
902
|
+
}
|
|
903
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.0", ngImport: i0, type: NgxThemeInjectorService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
904
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.0", ngImport: i0, type: NgxThemeInjectorService, providedIn: 'root' }); }
|
|
905
|
+
}
|
|
906
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.0", ngImport: i0, type: NgxThemeInjectorService, decorators: [{
|
|
907
|
+
type: Injectable,
|
|
908
|
+
args: [{ providedIn: 'root' }]
|
|
909
|
+
}] });
|
|
910
|
+
|
|
911
|
+
const STORAGE_KEY = 'ngxsmk-theme-mode';
|
|
666
912
|
/**
|
|
667
913
|
* Runtime theme control: dark-mode switching (class strategy) and dynamic
|
|
668
914
|
* theme application by injecting generated token CSS into the document.
|
|
669
915
|
*
|
|
916
|
+
* CSS injection is delegated to {@link NgxThemeInjectorService} for
|
|
917
|
+
* consumers who only need token injection without dark-mode logic.
|
|
918
|
+
*
|
|
670
919
|
* SSR-safe: all DOM access goes through the injected document and degrades
|
|
671
920
|
* gracefully when `window`/`matchMedia` are unavailable.
|
|
672
921
|
*/
|
|
673
922
|
class NgxsmkThemeService {
|
|
674
923
|
constructor() {
|
|
675
|
-
|
|
676
|
-
// can safely access it when the signals are initialised below.
|
|
924
|
+
this.injector = inject(NgxThemeInjectorService);
|
|
677
925
|
this.document = inject(DOCUMENT);
|
|
678
926
|
this.mode = signal(this.restoreMode());
|
|
679
927
|
this.systemDark = signal(this.matchSystemDark());
|
|
@@ -696,23 +944,21 @@ class NgxsmkThemeService {
|
|
|
696
944
|
toggle() {
|
|
697
945
|
this.setMode(this.isDark() ? 'light' : 'dark');
|
|
698
946
|
}
|
|
699
|
-
/**
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
*/
|
|
703
|
-
applyTheme(config) {
|
|
704
|
-
const head = this.document.head;
|
|
705
|
-
let style = this.document.getElementById(STYLE_ID);
|
|
706
|
-
if (!style) {
|
|
707
|
-
style = this.document.createElement('style');
|
|
708
|
-
style.id = STYLE_ID;
|
|
709
|
-
head.appendChild(style);
|
|
710
|
-
}
|
|
711
|
-
style.textContent = buildThemeCss(config);
|
|
947
|
+
/** @see NgxThemeInjectorService.applyTheme */
|
|
948
|
+
applyTheme(config, adapter) {
|
|
949
|
+
this.injector.applyTheme(config, adapter);
|
|
712
950
|
}
|
|
713
|
-
/**
|
|
951
|
+
/** @see NgxThemeInjectorService.applyIonicTheme */
|
|
952
|
+
applyIonicTheme(config) {
|
|
953
|
+
this.injector.applyIonicTheme(config);
|
|
954
|
+
}
|
|
955
|
+
/** @see NgxThemeInjectorService.clearTheme */
|
|
714
956
|
clearTheme() {
|
|
715
|
-
this.
|
|
957
|
+
this.injector.clearTheme();
|
|
958
|
+
}
|
|
959
|
+
/** @see NgxThemeInjectorService.clearIonicTheme */
|
|
960
|
+
clearIonicTheme() {
|
|
961
|
+
this.injector.clearIonicTheme();
|
|
716
962
|
}
|
|
717
963
|
restoreMode() {
|
|
718
964
|
try {
|
|
@@ -745,5 +991,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.0", ngImpor
|
|
|
745
991
|
* Generated bundle index. Do not edit.
|
|
746
992
|
*/
|
|
747
993
|
|
|
748
|
-
export { CONTROL_HEIGHT, CONTROL_HEIGHTS, DEFAULT_FONT_FAMILY, DEFAULT_NEUTRAL, DEFAULT_SEMANTIC, DEFAULT_TYPOGRAPHY, DURATION, EASING, ICON_SIZE, NgxsmkThemeService, OPACITY, RADIUS, SHADOW, SPACING, TRACKING, Z_INDEX, buildThemeCss, defineConfig, deriveScale, emeraldPreset,
|
|
994
|
+
export { CONTROL_HEIGHT, CONTROL_HEIGHTS, DEFAULT_FONT_FAMILY, DEFAULT_NEUTRAL, DEFAULT_SEMANTIC, DEFAULT_TYPOGRAPHY, DURATION, EASING, FONT_WEIGHT, ICON_SIZE, LEADING, NgxThemeInjectorService, NgxsmkThemeService, OPACITY, RADIUS, SHADOW, SPACING, TRACKING, Z_INDEX, buildThemeCss, cssVarsAdapter, defineConfig, deriveScale, emeraldPreset, hexToOklch, ionicVarsAdapter, neutralPreset, oklchToHex, presets, resolveTheme, rosePreset, rotateHue, shiftLightness, violetPreset };
|
|
749
995
|
//# sourceMappingURL=ngxsmk-theme.mjs.map
|