@ngxsmk/theme 1.3.2 → 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 +12 -0
- 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 +298 -49
- package/fesm2022/ngxsmk-theme.mjs.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 +29 -13
- package/public-api.d.ts +2 -0
- package/public-api.d.ts.map +1 -1
- package/styles/ngxsmk.css +62 -1
- package/styles/ngxsmk.emerald.css +62 -1
- package/styles/ngxsmk.neutral.css +62 -1
- package/styles/ngxsmk.rose.css +62 -1
- package/styles/ngxsmk.violet.css +62 -1
- 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.
|
|
@@ -148,7 +148,7 @@ const DEFAULT_SEMANTIC = {
|
|
|
148
148
|
};
|
|
149
149
|
const DEFAULT_FONT_FAMILY = {
|
|
150
150
|
sans: "'Inter', system-ui, -apple-system, 'Segoe UI', sans-serif",
|
|
151
|
-
mono: "'
|
|
151
|
+
mono: "'Geist Mono', 'Geist Mono Fallback', monospace",
|
|
152
152
|
};
|
|
153
153
|
const DEFAULT_TYPOGRAPHY = {
|
|
154
154
|
display: {
|
|
@@ -213,6 +213,20 @@ const TRACKING = {
|
|
|
213
213
|
normal: '0',
|
|
214
214
|
wide: '0.08em',
|
|
215
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
|
+
};
|
|
216
230
|
const SPACING = {
|
|
217
231
|
'0': '0px',
|
|
218
232
|
'0-5': '0.125rem',
|
|
@@ -339,51 +353,34 @@ function scaleVars(prefix, scale) {
|
|
|
339
353
|
}
|
|
340
354
|
return vars;
|
|
341
355
|
}
|
|
342
|
-
/**
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
...scaleVars('brand', theme.brand),
|
|
346
|
-
...scaleVars('secondary', theme.secondary),
|
|
347
|
-
...scaleVars('neutral', theme.neutral),
|
|
348
|
-
'--ngxsmk-font-sans': theme.fontFamily.sans,
|
|
349
|
-
'--ngxsmk-font-mono': theme.fontFamily.mono,
|
|
350
|
-
'--ngxsmk-radius-base': theme.radius[theme.radiusBase],
|
|
351
|
-
'--ngxsmk-control-height': CONTROL_HEIGHT,
|
|
352
|
-
// Motion Tokens
|
|
353
|
-
'--ngxsmk-motion-duration': 'var(--ngxsmk-duration-normal)',
|
|
354
|
-
'--ngxsmk-motion-ease': 'var(--ngxsmk-ease-in-out)',
|
|
355
|
-
// Interactive-state motion: shared lift/press transforms. Neutralized to
|
|
356
|
-
// `none` under prefers-reduced-motion (see buildThemeCss).
|
|
357
|
-
'--ngxsmk-hover-lift': 'translateY(-1px)',
|
|
358
|
-
'--ngxsmk-press-scale': 'scale(0.98)',
|
|
359
|
-
// Focus ring: THE focus indicator for every component. Aliases the
|
|
360
|
-
// mode-aware --ngxsmk-shadow-focus; the error variant derives from the
|
|
361
|
-
// semantic error role so invalid controls ring red in both modes.
|
|
362
|
-
'--ngxsmk-focus-ring': 'var(--ngxsmk-shadow-focus)',
|
|
363
|
-
'--ngxsmk-focus-ring-error': '0 0 0 2px color-mix(in srgb, var(--ngxsmk-color-error) 15%, transparent), ' +
|
|
364
|
-
'0 0 0 4px color-mix(in srgb, var(--ngxsmk-color-error) 45%, transparent)',
|
|
365
|
-
// Component-specific Tokens (Buttons)
|
|
356
|
+
/** Default CSS custom properties for each component, emitted once in :root. */
|
|
357
|
+
const COMPONENT_TOKEN_DEFAULTS = {
|
|
358
|
+
button: {
|
|
366
359
|
'--ngxsmk-button-bg': 'var(--ngxsmk-color-primary)',
|
|
367
360
|
'--ngxsmk-button-color': 'var(--ngxsmk-color-on-primary)',
|
|
368
361
|
'--ngxsmk-button-font-family': 'var(--ngxsmk-font-sans)',
|
|
369
362
|
'--ngxsmk-button-font-weight': '500',
|
|
370
363
|
'--ngxsmk-button-radius': 'var(--ngxsmk-radius-base)',
|
|
371
364
|
'--ngxsmk-button-disabled-opacity': '0.5',
|
|
372
|
-
|
|
365
|
+
},
|
|
366
|
+
card: {
|
|
373
367
|
'--ngxsmk-card-bg': 'var(--ngxsmk-color-surface)',
|
|
374
368
|
'--ngxsmk-card-radius': 'var(--ngxsmk-radius-xl)',
|
|
375
369
|
'--ngxsmk-card-shadow': 'var(--ngxsmk-shadow-sm)',
|
|
376
370
|
'--ngxsmk-card-border-color': 'var(--ngxsmk-color-outline)',
|
|
377
|
-
|
|
371
|
+
},
|
|
372
|
+
accordion: {
|
|
378
373
|
'--ngxsmk-accordion-bg': 'var(--ngxsmk-color-surface)',
|
|
379
374
|
'--ngxsmk-accordion-radius': 'var(--ngxsmk-radius-lg)',
|
|
380
375
|
'--ngxsmk-accordion-border-color': 'var(--ngxsmk-color-outline)',
|
|
381
|
-
|
|
376
|
+
},
|
|
377
|
+
progress: {
|
|
382
378
|
'--ngxsmk-progress-height': '0.5rem',
|
|
383
379
|
'--ngxsmk-progress-bg': 'var(--ngxsmk-color-surface-variant)',
|
|
384
380
|
'--ngxsmk-progress-color': 'var(--ngxsmk-color-primary)',
|
|
385
381
|
'--ngxsmk-progress-radius': 'var(--ngxsmk-radius-full)',
|
|
386
|
-
|
|
382
|
+
},
|
|
383
|
+
switch: {
|
|
387
384
|
'--ngxsmk-switch-width': '2.25rem',
|
|
388
385
|
'--ngxsmk-switch-height': '1.25rem',
|
|
389
386
|
'--ngxsmk-switch-radius': 'var(--ngxsmk-radius-full)',
|
|
@@ -391,6 +388,74 @@ function staticVars(theme) {
|
|
|
391
388
|
'--ngxsmk-switch-thumb-size': '0.875rem',
|
|
392
389
|
'--ngxsmk-switch-thumb-bg': 'var(--ngxsmk-color-surface)',
|
|
393
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 }), {}),
|
|
394
459
|
};
|
|
395
460
|
for (const [key, value] of Object.entries(CONTROL_HEIGHTS)) {
|
|
396
461
|
vars[`--ngxsmk-control-height-${key}`] = value;
|
|
@@ -404,6 +469,12 @@ function staticVars(theme) {
|
|
|
404
469
|
for (const [key, value] of Object.entries(TRACKING)) {
|
|
405
470
|
vars[`--ngxsmk-tracking-${key}`] = value;
|
|
406
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
|
+
}
|
|
407
478
|
for (const [key, value] of Object.entries(theme.spacing)) {
|
|
408
479
|
vars[`--ngxsmk-space-${key}`] = value;
|
|
409
480
|
}
|
|
@@ -476,6 +547,19 @@ function lightVars(theme) {
|
|
|
476
547
|
'--ngxsmk-color-ring': theme.brandBase,
|
|
477
548
|
'--ngxsmk-color-backdrop': 'rgb(1 18 40 / 0.4)',
|
|
478
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',
|
|
479
563
|
};
|
|
480
564
|
const tertiary = deriveScale(rotateHue(theme.brandBase, 64));
|
|
481
565
|
vars['--ngxsmk-color-tertiary'] = tertiary[500];
|
|
@@ -530,6 +614,18 @@ function darkVars(theme) {
|
|
|
530
614
|
'--ngxsmk-color-ring': theme.brand[400],
|
|
531
615
|
'--ngxsmk-color-backdrop': 'rgb(17 17 18 / 0.6)',
|
|
532
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',
|
|
533
629
|
};
|
|
534
630
|
const tertiary = deriveScale(rotateHue(theme.brandBase, 64));
|
|
535
631
|
vars['--ngxsmk-color-tertiary'] = tertiary[400];
|
|
@@ -658,19 +754,174 @@ const presets = {
|
|
|
658
754
|
rose: rosePreset,
|
|
659
755
|
};
|
|
660
756
|
|
|
661
|
-
|
|
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
|
+
|
|
662
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';
|
|
663
912
|
/**
|
|
664
913
|
* Runtime theme control: dark-mode switching (class strategy) and dynamic
|
|
665
914
|
* theme application by injecting generated token CSS into the document.
|
|
666
915
|
*
|
|
916
|
+
* CSS injection is delegated to {@link NgxThemeInjectorService} for
|
|
917
|
+
* consumers who only need token injection without dark-mode logic.
|
|
918
|
+
*
|
|
667
919
|
* SSR-safe: all DOM access goes through the injected document and degrades
|
|
668
920
|
* gracefully when `window`/`matchMedia` are unavailable.
|
|
669
921
|
*/
|
|
670
922
|
class NgxsmkThemeService {
|
|
671
923
|
constructor() {
|
|
672
|
-
|
|
673
|
-
// can safely access it when the signals are initialised below.
|
|
924
|
+
this.injector = inject(NgxThemeInjectorService);
|
|
674
925
|
this.document = inject(DOCUMENT);
|
|
675
926
|
this.mode = signal(this.restoreMode());
|
|
676
927
|
this.systemDark = signal(this.matchSystemDark());
|
|
@@ -693,23 +944,21 @@ class NgxsmkThemeService {
|
|
|
693
944
|
toggle() {
|
|
694
945
|
this.setMode(this.isDark() ? 'light' : 'dark');
|
|
695
946
|
}
|
|
696
|
-
/**
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
*/
|
|
700
|
-
applyTheme(config) {
|
|
701
|
-
const head = this.document.head;
|
|
702
|
-
let style = this.document.getElementById(STYLE_ID);
|
|
703
|
-
if (!style) {
|
|
704
|
-
style = this.document.createElement('style');
|
|
705
|
-
style.id = STYLE_ID;
|
|
706
|
-
head.appendChild(style);
|
|
707
|
-
}
|
|
708
|
-
style.textContent = buildThemeCss(config);
|
|
947
|
+
/** @see NgxThemeInjectorService.applyTheme */
|
|
948
|
+
applyTheme(config, adapter) {
|
|
949
|
+
this.injector.applyTheme(config, adapter);
|
|
709
950
|
}
|
|
710
|
-
/**
|
|
951
|
+
/** @see NgxThemeInjectorService.applyIonicTheme */
|
|
952
|
+
applyIonicTheme(config) {
|
|
953
|
+
this.injector.applyIonicTheme(config);
|
|
954
|
+
}
|
|
955
|
+
/** @see NgxThemeInjectorService.clearTheme */
|
|
711
956
|
clearTheme() {
|
|
712
|
-
this.
|
|
957
|
+
this.injector.clearTheme();
|
|
958
|
+
}
|
|
959
|
+
/** @see NgxThemeInjectorService.clearIonicTheme */
|
|
960
|
+
clearIonicTheme() {
|
|
961
|
+
this.injector.clearIonicTheme();
|
|
713
962
|
}
|
|
714
963
|
restoreMode() {
|
|
715
964
|
try {
|
|
@@ -742,5 +991,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.0", ngImpor
|
|
|
742
991
|
* Generated bundle index. Do not edit.
|
|
743
992
|
*/
|
|
744
993
|
|
|
745
|
-
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, hexToOklch, neutralPreset, oklchToHex, presets, resolveTheme, rosePreset, rotateHue, shiftLightness, violetPreset };
|
|
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 };
|
|
746
995
|
//# sourceMappingURL=ngxsmk-theme.mjs.map
|