@ngxsmk/theme 2.0.0 → 3.1.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.
@@ -1,6 +1,6 @@
1
1
  import { DOCUMENT } from '@angular/common';
2
2
  import * as i0 from '@angular/core';
3
- import { inject, Injectable, signal, computed, effect } from '@angular/core';
3
+ import { inject, Injectable, signal, computed, effect, makeEnvironmentProviders, ENVIRONMENT_INITIALIZER } from '@angular/core';
4
4
 
5
5
  /**
6
6
  * Design token model for the NGXSMK universal theme engine.
@@ -303,6 +303,15 @@ const EASING = {
303
303
  'in-out': 'cubic-bezier(0.4, 0, 0.2, 1)',
304
304
  emphasized: 'cubic-bezier(0.2, 0, 0, 1)',
305
305
  };
306
+ /** Safe-area inset defaults (fallback to CSS env() variables for iOS/Android/Ionic). */
307
+ const SAFE_AREA = {
308
+ top: 'env(safe-area-inset-top, 0px)',
309
+ bottom: 'env(safe-area-inset-bottom, 0px)',
310
+ left: 'env(safe-area-inset-left, 0px)',
311
+ right: 'env(safe-area-inset-right, 0px)',
312
+ };
313
+ /** Minimum touch target size for mobile accessibility (Apple HIG & Android Material). */
314
+ const TOUCH_TARGET_MIN = '44px';
306
315
 
307
316
  /**
308
317
  * Identity helper that gives `ngxsmk.config.ts` files full type inference:
@@ -329,9 +338,9 @@ function resolveTheme(config) {
329
338
  fontFamily: { ...DEFAULT_FONT_FAMILY, ...config.typography?.fontFamily },
330
339
  typography: DEFAULT_TYPOGRAPHY,
331
340
  spacing: SPACING,
332
- radius: RADIUS,
341
+ radius: { ...RADIUS, ...(config.radiusScale ?? {}) },
333
342
  radiusBase: config.borderRadius ?? 'md',
334
- shadow: SHADOW,
343
+ shadow: { ...SHADOW, ...(config.shadowScale ?? {}) },
335
344
  duration: DURATION,
336
345
  easing: EASING,
337
346
  zIndex: Z_INDEX,
@@ -362,12 +371,25 @@ const COMPONENT_TOKEN_DEFAULTS = {
362
371
  '--ngxsmk-button-font-weight': '500',
363
372
  '--ngxsmk-button-radius': 'var(--ngxsmk-radius-base)',
364
373
  '--ngxsmk-button-disabled-opacity': '0.5',
374
+ '--ngxsmk-button-border-width': '1px',
375
+ '--ngxsmk-button-gap': 'var(--ngxsmk-space-2)',
376
+ },
377
+ input: {
378
+ '--ngxsmk-input-bg': 'var(--ngxsmk-color-surface)',
379
+ '--ngxsmk-input-color': 'var(--ngxsmk-color-on-surface)',
380
+ '--ngxsmk-input-border': 'var(--ngxsmk-color-outline-strong)',
381
+ '--ngxsmk-input-radius': 'var(--ngxsmk-radius-base)',
382
+ '--ngxsmk-input-height': 'var(--ngxsmk-control-height)',
383
+ '--ngxsmk-input-padding-inline': 'var(--ngxsmk-space-3)',
384
+ '--ngxsmk-input-placeholder': 'var(--ngxsmk-color-on-surface-variant)',
365
385
  },
366
386
  card: {
367
387
  '--ngxsmk-card-bg': 'var(--ngxsmk-color-surface)',
368
388
  '--ngxsmk-card-radius': 'var(--ngxsmk-radius-xl)',
369
389
  '--ngxsmk-card-shadow': 'var(--ngxsmk-shadow-sm)',
370
390
  '--ngxsmk-card-border-color': 'var(--ngxsmk-color-outline)',
391
+ '--ngxsmk-card-border-width': '1px',
392
+ '--ngxsmk-card-padding': 'var(--ngxsmk-space-5)',
371
393
  },
372
394
  accordion: {
373
395
  '--ngxsmk-accordion-bg': 'var(--ngxsmk-color-surface)',
@@ -389,6 +411,122 @@ const COMPONENT_TOKEN_DEFAULTS = {
389
411
  '--ngxsmk-switch-thumb-bg': 'var(--ngxsmk-color-surface)',
390
412
  '--ngxsmk-switch-checked-bg': 'var(--ngxsmk-color-primary)',
391
413
  },
414
+ checkbox: {
415
+ '--ngxsmk-checkbox-size': '1.125rem',
416
+ '--ngxsmk-checkbox-radius': 'var(--ngxsmk-radius-sm)',
417
+ '--ngxsmk-checkbox-border': 'var(--ngxsmk-color-outline-strong)',
418
+ '--ngxsmk-checkbox-checked-bg': 'var(--ngxsmk-color-primary)',
419
+ },
420
+ badge: {
421
+ '--ngxsmk-badge-radius': 'var(--ngxsmk-radius-full)',
422
+ '--ngxsmk-badge-font-weight': '600',
423
+ '--ngxsmk-badge-padding-inline': '0.5rem',
424
+ '--ngxsmk-badge-padding-block': '0.125rem',
425
+ '--ngxsmk-badge-border-mix': '20%',
426
+ },
427
+ alert: {
428
+ '--ngxsmk-alert-radius': 'var(--ngxsmk-radius-lg)',
429
+ '--ngxsmk-alert-border-mix': '22%',
430
+ '--ngxsmk-alert-padding': 'var(--ngxsmk-space-4)',
431
+ },
432
+ dialog: {
433
+ '--ngxsmk-dialog-radius': 'var(--ngxsmk-radius-2xl)',
434
+ '--ngxsmk-dialog-shadow': 'var(--ngxsmk-shadow-xl)',
435
+ '--ngxsmk-dialog-bg': 'var(--ngxsmk-color-surface-elevated)',
436
+ '--ngxsmk-dialog-backdrop': 'var(--ngxsmk-color-backdrop)',
437
+ '--ngxsmk-dialog-blur': '4px',
438
+ '--ngxsmk-dialog-max-width': '28rem',
439
+ },
440
+ sheet: {
441
+ '--ngxsmk-sheet-radius': 'var(--ngxsmk-radius-xl)',
442
+ '--ngxsmk-sheet-shadow': 'var(--ngxsmk-shadow-xl)',
443
+ '--ngxsmk-sheet-bg': 'var(--ngxsmk-color-surface-elevated)',
444
+ '--ngxsmk-sheet-backdrop': 'var(--ngxsmk-color-backdrop)',
445
+ '--ngxsmk-sheet-width': '24rem',
446
+ },
447
+ lightbox: {
448
+ '--ngxsmk-lightbox-backdrop': 'rgb(0 0 0 / 0.9)',
449
+ '--ngxsmk-lightbox-fg': '#ffffff',
450
+ '--ngxsmk-lightbox-control-bg': 'rgb(255 255 255 / 0.12)',
451
+ '--ngxsmk-lightbox-control-bg-hover': 'rgb(255 255 255 / 0.22)',
452
+ },
453
+ toast: {
454
+ '--ngxsmk-toast-radius': 'var(--ngxsmk-radius-lg)',
455
+ '--ngxsmk-toast-shadow': 'var(--ngxsmk-shadow-lg)',
456
+ '--ngxsmk-toast-bg': 'var(--ngxsmk-color-surface-elevated)',
457
+ '--ngxsmk-toast-accent-width': '3px',
458
+ },
459
+ table: {
460
+ '--ngxsmk-table-border': 'var(--ngxsmk-color-outline)',
461
+ '--ngxsmk-table-header-bg': 'color-mix(in srgb, var(--ngxsmk-color-surface-variant) 55%, transparent)',
462
+ '--ngxsmk-table-row-hover': 'var(--ngxsmk-color-surface-hover)',
463
+ '--ngxsmk-table-stripe': 'color-mix(in srgb, var(--ngxsmk-color-surface-variant) 35%, transparent)',
464
+ '--ngxsmk-table-drop-indicator': 'inset 2px 0 0 var(--ngxsmk-color-primary)',
465
+ '--ngxsmk-table-cell-padding': '0.625rem 0.75rem',
466
+ },
467
+ tabs: {
468
+ '--ngxsmk-tabs-indicator': '2px',
469
+ '--ngxsmk-tabs-radius': 'var(--ngxsmk-radius-md)',
470
+ '--ngxsmk-tabs-gap': 'var(--ngxsmk-space-1)',
471
+ },
472
+ select: {
473
+ '--ngxsmk-select-bg': 'var(--ngxsmk-color-surface)',
474
+ '--ngxsmk-select-border': 'var(--ngxsmk-color-outline-strong)',
475
+ '--ngxsmk-select-radius': 'var(--ngxsmk-radius-base)',
476
+ '--ngxsmk-select-list-shadow': 'var(--ngxsmk-shadow-lg)',
477
+ '--ngxsmk-select-list-radius': 'var(--ngxsmk-radius-lg)',
478
+ },
479
+ menu: {
480
+ '--ngxsmk-menu-bg': 'var(--ngxsmk-color-surface-elevated)',
481
+ '--ngxsmk-menu-radius': 'var(--ngxsmk-radius-lg)',
482
+ '--ngxsmk-menu-shadow': 'var(--ngxsmk-shadow-lg)',
483
+ '--ngxsmk-menu-border': 'var(--ngxsmk-color-outline)',
484
+ '--ngxsmk-menu-item-radius': 'var(--ngxsmk-radius-md)',
485
+ },
486
+ pagination: {
487
+ '--ngxsmk-pagination-size': '2.25rem',
488
+ '--ngxsmk-pagination-radius': 'var(--ngxsmk-radius-md)',
489
+ },
490
+ formField: {
491
+ '--ngxsmk-form-field-gap': 'var(--ngxsmk-space-1-5)',
492
+ '--ngxsmk-form-field-label-weight': '500',
493
+ },
494
+ radio: {
495
+ '--ngxsmk-radio-size': '1.125rem',
496
+ '--ngxsmk-radio-dot-size': '0.5rem',
497
+ '--ngxsmk-radio-border': 'var(--ngxsmk-color-outline-strong)',
498
+ },
499
+ dataTable: {
500
+ '--ngxsmk-data-table-border': 'var(--ngxsmk-color-outline)',
501
+ '--ngxsmk-data-table-radius': 'var(--ngxsmk-radius-md)',
502
+ '--ngxsmk-data-table-filter-bg': 'color-mix(in srgb, var(--ngxsmk-color-surface-variant) 40%, transparent)',
503
+ '--ngxsmk-data-table-filter-label-size': '0.7rem',
504
+ '--ngxsmk-data-table-page-size': '2rem',
505
+ '--ngxsmk-data-table-loading-height': '3px',
506
+ },
507
+ aiChat: {
508
+ '--ngxsmk-ai-chat-radius': 'var(--ngxsmk-radius-xl)',
509
+ '--ngxsmk-ai-chat-border': 'var(--ngxsmk-color-outline)',
510
+ '--ngxsmk-ai-chat-bg': 'var(--ngxsmk-color-surface)',
511
+ '--ngxsmk-ai-chat-header-bg': 'var(--ngxsmk-color-surface-variant)',
512
+ '--ngxsmk-ai-chat-bubble-bg': 'var(--ngxsmk-color-surface-variant)',
513
+ '--ngxsmk-ai-chat-bubble-user-bg': 'var(--ngxsmk-color-primary)',
514
+ '--ngxsmk-ai-chat-bubble-radius': 'var(--ngxsmk-radius-lg)',
515
+ '--ngxsmk-ai-chat-composer-radius': 'var(--ngxsmk-radius-full)',
516
+ },
517
+ promptInput: {
518
+ '--ngxsmk-prompt-radius': 'var(--ngxsmk-radius-xl)',
519
+ '--ngxsmk-prompt-border': 'var(--ngxsmk-color-outline-strong)',
520
+ '--ngxsmk-prompt-shadow': 'var(--ngxsmk-shadow-sm)',
521
+ '--ngxsmk-prompt-bg': 'var(--ngxsmk-color-surface)',
522
+ '--ngxsmk-prompt-send-radius': 'var(--ngxsmk-radius-full)',
523
+ },
524
+ chatBubble: {
525
+ '--ngxsmk-chat-bubble-radius': 'var(--ngxsmk-radius-lg)',
526
+ '--ngxsmk-chat-bubble-bg': 'var(--ngxsmk-color-surface-container)',
527
+ '--ngxsmk-chat-bubble-user-bg': 'var(--ngxsmk-color-primary-container)',
528
+ '--ngxsmk-chat-bubble-padding': 'var(--ngxsmk-space-2) var(--ngxsmk-space-3)',
529
+ },
392
530
  spreadsheet: {
393
531
  '--ngxsmk-spreadsheet-bg': 'var(--ngxsmk-color-surface)',
394
532
  '--ngxsmk-spreadsheet-header-bg': 'var(--ngxsmk-color-surface-variant)',
@@ -420,16 +558,22 @@ const COMPONENT_TOKEN_DEFAULTS = {
420
558
  '--ngxsmk-input-group-radius': 'var(--ngxsmk-radius-md)',
421
559
  '--ngxsmk-input-group-shadow': 'none',
422
560
  '--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)',
561
+ '--ngxsmk-input-group-focus-shadow': 'var(--ngxsmk-focus-ring)',
424
562
  '--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)',
563
+ '--ngxsmk-input-group-error-focus-shadow': 'var(--ngxsmk-focus-ring-error)',
426
564
  '--ngxsmk-input-group-success-border': 'var(--ngxsmk-color-success)',
427
565
  '--ngxsmk-input-group-warning-border': 'var(--ngxsmk-color-warning)',
428
- '--ngxsmk-input-group-height': '2.5rem',
566
+ '--ngxsmk-input-group-height': 'var(--ngxsmk-control-height)',
429
567
  '--ngxsmk-input-group-padding': '0.75rem',
430
568
  '--ngxsmk-input-group-gap': 'var(--ngxsmk-space-2)',
431
569
  '--ngxsmk-input-group-font': 'var(--ngxsmk-font-sans)',
432
570
  },
571
+ heatmap: {
572
+ '--ngxsmk-heatmap-level-1': 'color-mix(in srgb, var(--ngxsmk-color-success) 28%, var(--ngxsmk-color-surface-variant))',
573
+ '--ngxsmk-heatmap-level-2': 'color-mix(in srgb, var(--ngxsmk-color-success) 52%, var(--ngxsmk-color-surface-variant))',
574
+ '--ngxsmk-heatmap-level-3': 'color-mix(in srgb, var(--ngxsmk-color-success) 76%, var(--ngxsmk-color-surface-variant))',
575
+ '--ngxsmk-heatmap-level-4': 'var(--ngxsmk-color-success)',
576
+ },
433
577
  };
434
578
  /** Tokens that do not change between light and dark mode. */
435
579
  function staticVars(theme) {
@@ -454,6 +598,12 @@ function staticVars(theme) {
454
598
  '--ngxsmk-focus-ring': 'var(--ngxsmk-shadow-focus)',
455
599
  '--ngxsmk-focus-ring-error': '0 0 0 2px color-mix(in srgb, var(--ngxsmk-color-error) 15%, transparent), ' +
456
600
  '0 0 0 4px color-mix(in srgb, var(--ngxsmk-color-error) 45%, transparent)',
601
+ // Mobile viewport & safe-area tokens (iOS / Android / Ionic)
602
+ '--ngxsmk-safe-area-top': 'env(safe-area-inset-top, 0px)',
603
+ '--ngxsmk-safe-area-bottom': 'env(safe-area-inset-bottom, 0px)',
604
+ '--ngxsmk-safe-area-left': 'env(safe-area-inset-left, 0px)',
605
+ '--ngxsmk-safe-area-right': 'env(safe-area-inset-right, 0px)',
606
+ '--ngxsmk-touch-target-min': '44px',
457
607
  // Component-specific token defaults
458
608
  ...Object.values(COMPONENT_TOKEN_DEFAULTS).reduce((merged, tokens) => ({ ...merged, ...tokens }), {}),
459
609
  };
@@ -532,12 +682,17 @@ function lightVars(theme) {
532
682
  '--ngxsmk-color-on-surface': '#0A1317',
533
683
  '--ngxsmk-color-surface-variant': theme.neutral[100],
534
684
  '--ngxsmk-color-on-surface-variant': theme.neutral[500],
685
+ '--ngxsmk-color-surface-muted': theme.neutral[50],
686
+ '--ngxsmk-color-surface-elevated': '#FFFFFF',
687
+ '--ngxsmk-color-surface-overlay': '#FFFFFF',
688
+ '--ngxsmk-color-on-surface-muted': theme.neutral[400],
535
689
  '--ngxsmk-color-surface-hover': 'rgb(5 54 89 / 0.05)',
536
690
  '--ngxsmk-color-surface-active': 'rgb(5 54 89 / 0.1)',
537
691
  '--ngxsmk-color-surface-container': '#FFFFFF',
538
692
  '--ngxsmk-color-outline': 'rgb(5 54 89 / 0.1)',
539
693
  '--ngxsmk-color-outline-strong': theme.neutral[200],
540
694
  '--ngxsmk-color-outline-variant': 'rgb(5 54 89 / 0.06)',
695
+ '--ngxsmk-color-outline-subtle': 'rgb(5 54 89 / 0.05)',
541
696
  // Elevation ladder
542
697
  '--ngxsmk-color-surface-1': '#FFFFFF',
543
698
  '--ngxsmk-color-surface-2': theme.neutral[50],
@@ -599,12 +754,17 @@ function darkVars(theme) {
599
754
  '--ngxsmk-color-on-surface': '#DFE2E5',
600
755
  '--ngxsmk-color-surface-variant': '#28292C',
601
756
  '--ngxsmk-color-on-surface-variant': '#AAAFB5',
757
+ '--ngxsmk-color-surface-muted': '#141416',
758
+ '--ngxsmk-color-surface-elevated': '#28292C',
759
+ '--ngxsmk-color-surface-overlay': '#2F3136',
760
+ '--ngxsmk-color-on-surface-muted': '#777C85',
602
761
  '--ngxsmk-color-surface-hover': 'rgb(255 255 255 / 0.05)',
603
762
  '--ngxsmk-color-surface-active': 'rgb(255 255 255 / 0.1)',
604
763
  '--ngxsmk-color-surface-container': '#28292C',
605
764
  '--ngxsmk-color-outline': 'rgb(242 244 246 / 0.1)',
606
765
  '--ngxsmk-color-outline-strong': '#494D53',
607
766
  '--ngxsmk-color-outline-variant': 'rgb(242 244 246 / 0.06)',
767
+ '--ngxsmk-color-outline-subtle': 'rgb(242 244 246 / 0.05)',
608
768
  // Elevation ladder
609
769
  '--ngxsmk-color-surface-1': theme.neutral[800],
610
770
  '--ngxsmk-color-surface-2': theme.neutral[700],
@@ -738,6 +898,9 @@ const neutralPreset = {
738
898
  /**
739
899
  * Default preset: Emerald green. This preset generates the prebuilt
740
900
  * `ngxsmk.css` stylesheet shipped with @ngxsmk/theme.
901
+ *
902
+ * **Classic contract:** keep this visually stable for existing consumers.
903
+ * Premium Ink lives in `inkPreset` / `ngxsmk.ink.css` as an opt-in.
741
904
  */
742
905
  const emeraldPreset = {
743
906
  name: 'emerald',
@@ -747,59 +910,298 @@ const rosePreset = {
747
910
  name: 'rose',
748
911
  brand: { primary: '#E11D48' },
749
912
  };
913
+ /**
914
+ * Cool graphite neutrals for the Ink & precision look. Independent of the
915
+ * Classic `DEFAULT_NEUTRAL` so emerald/violet/rose stay unchanged.
916
+ */
917
+ const INK_NEUTRAL = {
918
+ 50: '#F6F7F8',
919
+ 100: '#EBEDF0',
920
+ 200: '#D5D8DE',
921
+ 300: '#B0B5BF',
922
+ 400: '#7E8491',
923
+ 500: '#5A606C',
924
+ 600: '#454954',
925
+ 700: '#33363E',
926
+ 800: '#1C1E24',
927
+ 900: '#12141A',
928
+ 950: '#0A0B0E',
929
+ };
930
+ /**
931
+ * Opt-in premium preset: Ink & precision.
932
+ * Generates `ngxsmk.ink.css`. Does not replace the default `ngxsmk.css`.
933
+ *
934
+ * Consumers:
935
+ * ```css
936
+ * @import '@ngxsmk/theme/styles/ngxsmk.ink.css';
937
+ * ```
938
+ * or `theme.applyTheme(inkPreset)`.
939
+ */
940
+ const inkPreset = {
941
+ name: 'ink',
942
+ brand: { primary: '#0F766E' },
943
+ neutral: INK_NEUTRAL,
944
+ typography: {
945
+ fontFamily: {
946
+ sans: "'Geist Sans', 'Geist Sans Fallback', system-ui, -apple-system, sans-serif",
947
+ mono: "'Geist Mono', 'Geist Mono Fallback', monospace",
948
+ },
949
+ },
950
+ borderRadius: 'md',
951
+ radiusScale: {
952
+ sm: '0.1875rem', // 3px
953
+ md: '0.375rem', // 6px — element base
954
+ lg: '0.5rem', // 8px — cards / popovers
955
+ xl: '0.625rem', // 10px
956
+ '2xl': '0.75rem', // 12px
957
+ '3xl': '1rem', // 16px — page surfaces (was 28px in Classic)
958
+ },
959
+ shadowScale: {
960
+ sm: '0 0 0 1px rgb(0 0 0 / 0.04), 0 1px 2px rgb(0 0 0 / 0.04)',
961
+ md: '0 0 0 1px rgb(0 0 0 / 0.04), 0 1px 2px rgb(0 0 0 / 0.05), 0 4px 10px rgb(0 0 0 / 0.05)',
962
+ lg: '0 0 0 1px rgb(0 0 0 / 0.05), 0 2px 4px rgb(0 0 0 / 0.05), 0 10px 28px rgb(0 0 0 / 0.08)',
963
+ xl: '0 0 0 1px rgb(0 0 0 / 0.05), 0 4px 8px rgb(0 0 0 / 0.06), 0 18px 40px rgb(0 0 0 / 0.1)',
964
+ '2xl': '0 0 0 1px rgb(0 0 0 / 0.06), 0 8px 16px rgb(0 0 0 / 0.08), 0 28px 56px rgb(0 0 0 / 0.12)',
965
+ },
966
+ overrides: {
967
+ light: {
968
+ // Graphite canvas — quieter than Classic cool-blue wash.
969
+ '--ngxsmk-color-background': '#F4F5F7',
970
+ '--ngxsmk-color-surface': '#FFFFFF',
971
+ '--ngxsmk-color-surface-elevated': '#FFFFFF',
972
+ '--ngxsmk-color-surface-overlay': '#FFFFFF',
973
+ '--ngxsmk-color-surface-variant': '#EBEDF0',
974
+ '--ngxsmk-color-on-background': '#0A0B0E',
975
+ '--ngxsmk-color-on-surface': '#0A0B0E',
976
+ '--ngxsmk-color-on-surface-variant': '#5A606C',
977
+ '--ngxsmk-color-surface-hover': 'rgb(10 11 14 / 0.04)',
978
+ '--ngxsmk-color-surface-active': 'rgb(10 11 14 / 0.08)',
979
+ '--ngxsmk-color-outline': 'rgb(10 11 14 / 0.1)',
980
+ '--ngxsmk-color-outline-strong': '#D5D8DE',
981
+ '--ngxsmk-color-outline-variant': 'rgb(10 11 14 / 0.06)',
982
+ '--ngxsmk-color-outline-subtle': 'rgb(10 11 14 / 0.05)',
983
+ '--ngxsmk-color-backdrop': 'rgb(10 11 14 / 0.48)',
984
+ '--ngxsmk-color-ring': '#0F766E',
985
+ // Precision focus — tighter than Classic double ring.
986
+ '--ngxsmk-shadow-focus': '0 0 0 1px #FFFFFF, 0 0 0 3px color-mix(in srgb, #0F766E 55%, transparent)',
987
+ // Density.
988
+ '--ngxsmk-control-height': '2.25rem',
989
+ '--ngxsmk-control-height-sm': '1.875rem',
990
+ '--ngxsmk-control-height-md': '2.25rem',
991
+ '--ngxsmk-control-height-lg': '2.75rem',
992
+ // Component chrome — border-first, no float.
993
+ '--ngxsmk-hover-lift': 'none',
994
+ '--ngxsmk-button-font-weight': '600',
995
+ '--ngxsmk-button-radius': 'var(--ngxsmk-radius-md)',
996
+ '--ngxsmk-input-border': 'var(--ngxsmk-color-outline)',
997
+ '--ngxsmk-input-height': 'var(--ngxsmk-control-height)',
998
+ '--ngxsmk-input-group-height': '2.25rem',
999
+ '--ngxsmk-input-group-border': 'var(--ngxsmk-color-outline)',
1000
+ '--ngxsmk-card-shadow': 'none',
1001
+ '--ngxsmk-card-radius': 'var(--ngxsmk-radius-lg)',
1002
+ '--ngxsmk-card-border-width': '1px',
1003
+ '--ngxsmk-dialog-radius': 'var(--ngxsmk-radius-lg)',
1004
+ '--ngxsmk-dialog-blur': '8px',
1005
+ '--ngxsmk-sheet-radius': 'var(--ngxsmk-radius-lg)',
1006
+ '--ngxsmk-toast-radius': 'var(--ngxsmk-radius-md)',
1007
+ '--ngxsmk-toast-accent-width': '2px',
1008
+ '--ngxsmk-select-border': 'var(--ngxsmk-color-outline)',
1009
+ '--ngxsmk-menu-radius': 'var(--ngxsmk-radius-md)',
1010
+ '--ngxsmk-pagination-size': '2rem',
1011
+ '--ngxsmk-pagination-radius': 'var(--ngxsmk-radius-md)',
1012
+ '--ngxsmk-table-header-bg': 'color-mix(in srgb, var(--ngxsmk-color-surface-variant) 70%, transparent)',
1013
+ '--ngxsmk-badge-font-weight': '650',
1014
+ '--ngxsmk-progress-height': '0.375rem',
1015
+ // Forms — denser labels, quieter group chrome.
1016
+ '--ngxsmk-form-field-gap': 'var(--ngxsmk-space-1)',
1017
+ '--ngxsmk-form-field-label-weight': '600',
1018
+ '--ngxsmk-input-group-radius': 'var(--ngxsmk-radius-md)',
1019
+ '--ngxsmk-input-group-padding': '0.625rem',
1020
+ '--ngxsmk-radio-border': 'var(--ngxsmk-color-outline)',
1021
+ // Data table — ops density.
1022
+ '--ngxsmk-data-table-radius': 'var(--ngxsmk-radius-md)',
1023
+ '--ngxsmk-data-table-filter-bg': 'color-mix(in srgb, var(--ngxsmk-color-surface-variant) 55%, transparent)',
1024
+ '--ngxsmk-data-table-page-size': '1.875rem',
1025
+ '--ngxsmk-data-table-loading-height': '2px',
1026
+ // AI surfaces — border-first console, no soft float.
1027
+ '--ngxsmk-ai-chat-radius': 'var(--ngxsmk-radius-lg)',
1028
+ '--ngxsmk-ai-chat-header-bg': 'var(--ngxsmk-color-surface)',
1029
+ '--ngxsmk-ai-chat-bubble-bg': 'var(--ngxsmk-color-surface-variant)',
1030
+ '--ngxsmk-ai-chat-bubble-radius': 'var(--ngxsmk-radius-md)',
1031
+ '--ngxsmk-ai-chat-composer-radius': 'var(--ngxsmk-radius-md)',
1032
+ '--ngxsmk-prompt-radius': 'var(--ngxsmk-radius-lg)',
1033
+ '--ngxsmk-prompt-border': 'var(--ngxsmk-color-outline)',
1034
+ '--ngxsmk-prompt-shadow': 'none',
1035
+ '--ngxsmk-prompt-send-radius': 'var(--ngxsmk-radius-md)',
1036
+ '--ngxsmk-chat-bubble-radius': 'var(--ngxsmk-radius-md)',
1037
+ '--ngxsmk-chat-bubble-padding': 'var(--ngxsmk-space-2) var(--ngxsmk-space-3)',
1038
+ },
1039
+ dark: {
1040
+ '--ngxsmk-color-background': '#0A0B0E',
1041
+ '--ngxsmk-color-surface': '#12141A',
1042
+ '--ngxsmk-color-surface-elevated': '#1C1E24',
1043
+ '--ngxsmk-color-surface-overlay': '#1C1E24',
1044
+ '--ngxsmk-color-surface-container': '#12141A',
1045
+ '--ngxsmk-color-surface-variant': '#1C1E24',
1046
+ '--ngxsmk-color-surface-1': '#12141A',
1047
+ '--ngxsmk-color-surface-2': '#1C1E24',
1048
+ '--ngxsmk-color-outline': 'rgb(246 247 248 / 0.12)',
1049
+ '--ngxsmk-color-outline-strong': 'rgb(246 247 248 / 0.18)',
1050
+ '--ngxsmk-color-outline-variant': 'rgb(246 247 248 / 0.08)',
1051
+ '--ngxsmk-color-backdrop': 'rgb(0 0 0 / 0.62)',
1052
+ '--ngxsmk-shadow-focus': '0 0 0 1px #12141A, 0 0 0 3px color-mix(in srgb, #2DD4BF 50%, transparent)',
1053
+ '--ngxsmk-hover-lift': 'none',
1054
+ '--ngxsmk-control-height': '2.25rem',
1055
+ '--ngxsmk-control-height-sm': '1.875rem',
1056
+ '--ngxsmk-control-height-md': '2.25rem',
1057
+ '--ngxsmk-control-height-lg': '2.75rem',
1058
+ '--ngxsmk-button-font-weight': '600',
1059
+ '--ngxsmk-card-shadow': 'none',
1060
+ '--ngxsmk-card-radius': 'var(--ngxsmk-radius-lg)',
1061
+ '--ngxsmk-dialog-radius': 'var(--ngxsmk-radius-lg)',
1062
+ '--ngxsmk-input-border': 'var(--ngxsmk-color-outline)',
1063
+ '--ngxsmk-select-border': 'var(--ngxsmk-color-outline)',
1064
+ '--ngxsmk-form-field-label-weight': '600',
1065
+ '--ngxsmk-input-group-height': '2.25rem',
1066
+ '--ngxsmk-ai-chat-radius': 'var(--ngxsmk-radius-lg)',
1067
+ '--ngxsmk-ai-chat-header-bg': 'var(--ngxsmk-color-surface)',
1068
+ '--ngxsmk-prompt-shadow': 'none',
1069
+ '--ngxsmk-prompt-border': 'var(--ngxsmk-color-outline)',
1070
+ '--ngxsmk-prompt-send-radius': 'var(--ngxsmk-radius-md)',
1071
+ '--ngxsmk-data-table-filter-bg': 'color-mix(in srgb, var(--ngxsmk-color-surface-variant) 70%, transparent)',
1072
+ },
1073
+ },
1074
+ };
750
1075
  const presets = {
751
1076
  violet: violetPreset,
752
1077
  neutral: neutralPreset,
753
1078
  emerald: emeraldPreset,
754
1079
  rose: rosePreset,
1080
+ ink: inkPreset,
755
1081
  };
756
1082
 
757
1083
  /**
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.
1084
+ * Maps NGXSMK semantic color roles to Ionic 7 & 8 color variable specifications.
763
1085
  *
764
- * This adapter produces the subset Ionic needs for theming.
1086
+ * Emits full color suites (base, rgb, contrast, contrast-rgb, shade, tint),
1087
+ * 19-step contrast ladder (--ion-color-step-50..950), safe area mappings, and
1088
+ * surface variables for seamless Ionic framework interoperability.
765
1089
  */
766
1090
  const ionicVarsAdapter = {
767
1091
  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);
1092
+ vars(theme, options) {
1093
+ if (options?.isDark) {
1094
+ return this.darkVars ? this.darkVars(theme) : buildLightVars(theme);
790
1095
  }
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;
1096
+ return buildLightVars(theme);
1097
+ },
1098
+ darkVars(theme) {
1099
+ return buildDarkVars(theme);
801
1100
  },
802
1101
  };
1102
+ function buildColorSuite(prefix, baseHex, vars) {
1103
+ const contrastHex = getContrastHex(baseHex);
1104
+ vars[`--ion-color-${prefix}`] = baseHex;
1105
+ vars[`--ion-color-${prefix}-rgb`] = hexToRgbStr(baseHex);
1106
+ vars[`--ion-color-${prefix}-contrast`] = contrastHex;
1107
+ vars[`--ion-color-${prefix}-contrast-rgb`] = hexToRgbStr(contrastHex);
1108
+ vars[`--ion-color-${prefix}-shade`] = shiftLightnessHex(baseHex, -12);
1109
+ vars[`--ion-color-${prefix}-tint`] = shiftLightnessHex(baseHex, 10);
1110
+ }
1111
+ function buildLightVars(theme) {
1112
+ const vars = {};
1113
+ // Primary & Secondary
1114
+ buildColorSuite('primary', theme.brandBase, vars);
1115
+ buildColorSuite('secondary', theme.secondaryBase, vars);
1116
+ // Tertiary (derived from brand hue + 64)
1117
+ const tertiary = deriveScale(rotateHue(theme.brandBase, 64));
1118
+ buildColorSuite('tertiary', tertiary[500], vars);
1119
+ // Status & Feedback colors
1120
+ const successHex = theme.semantic['success'] ?? '#0D8626';
1121
+ const warningHex = theme.semantic['warning'] ?? '#E9AF08';
1122
+ const dangerHex = theme.semantic['error'] ?? '#E3193B';
1123
+ const infoHex = theme.semantic['info'] ?? '#2563EB';
1124
+ buildColorSuite('success', successHex, vars);
1125
+ buildColorSuite('warning', warningHex, vars);
1126
+ buildColorSuite('danger', dangerHex, vars);
1127
+ buildColorSuite('info', infoHex, vars);
1128
+ // Medium / Light / Dark baseline scales
1129
+ buildColorSuite('light', theme.neutral[100] ?? '#F1F4F7', vars);
1130
+ buildColorSuite('medium', theme.neutral[500] ?? '#778593', vars);
1131
+ buildColorSuite('dark', theme.neutral[900] ?? '#18181A', vars);
1132
+ // Background / Surfaces
1133
+ const bgHex = theme.neutral[50] ?? '#FFFFFF';
1134
+ const textHex = '#0A1317';
1135
+ vars['--ion-background-color'] = bgHex;
1136
+ vars['--ion-background-color-rgb'] = hexToRgbStr(bgHex);
1137
+ vars['--ion-text-color'] = textHex;
1138
+ vars['--ion-text-color-rgb'] = hexToRgbStr(textHex);
1139
+ vars['--ion-card-background'] = '#FFFFFF';
1140
+ vars['--ion-item-background'] = '#FFFFFF';
1141
+ vars['--ion-item-color'] = textHex;
1142
+ vars['--ion-toolbar-background'] = bgHex;
1143
+ vars['--ion-toolbar-color'] = textHex;
1144
+ vars['--ion-tab-bar-background'] = '#FFFFFF';
1145
+ vars['--ion-border-color'] = theme.neutral[200] ?? '#CCD3DB';
1146
+ // Typography & Radius
1147
+ vars['--ion-font-family'] = theme.fontFamily.sans;
1148
+ vars['--ion-border-radius'] = theme.radius[theme.radiusBase];
1149
+ // Stepped colors for light mode (interpolated between bg and text)
1150
+ interpolateSteppedColors(bgHex, textHex, vars);
1151
+ // Safe area insets
1152
+ vars['--ion-safe-area-top'] = 'var(--ngxsmk-safe-area-top, env(safe-area-inset-top, 0px))';
1153
+ vars['--ion-safe-area-bottom'] =
1154
+ 'var(--ngxsmk-safe-area-bottom, env(safe-area-inset-bottom, 0px))';
1155
+ vars['--ion-safe-area-left'] = 'var(--ngxsmk-safe-area-left, env(safe-area-inset-left, 0px))';
1156
+ vars['--ion-safe-area-right'] = 'var(--ngxsmk-safe-area-right, env(safe-area-inset-right, 0px))';
1157
+ return vars;
1158
+ }
1159
+ function buildDarkVars(theme) {
1160
+ const vars = {};
1161
+ // Primary & Secondary dark adjustments
1162
+ const primaryDark = theme.brand[400] ?? theme.brandBase;
1163
+ const secondaryDark = theme.secondary[400] ?? theme.secondaryBase;
1164
+ buildColorSuite('primary', primaryDark, vars);
1165
+ buildColorSuite('secondary', secondaryDark, vars);
1166
+ // Tertiary
1167
+ const tertiary = deriveScale(rotateHue(theme.brandBase, 64));
1168
+ buildColorSuite('tertiary', tertiary[400] ?? tertiary[500], vars);
1169
+ // Status & Feedback
1170
+ buildColorSuite('success', shiftLightnessHex(theme.semantic['success'] ?? '#0D8626', 10), vars);
1171
+ buildColorSuite('warning', shiftLightnessHex(theme.semantic['warning'] ?? '#E9AF08', 8), vars);
1172
+ buildColorSuite('danger', shiftLightnessHex(theme.semantic['error'] ?? '#E3193B', 10), vars);
1173
+ buildColorSuite('info', shiftLightnessHex(theme.semantic['info'] ?? '#2563EB', 10), vars);
1174
+ buildColorSuite('light', theme.neutral[800] ?? '#1F1F22', vars);
1175
+ buildColorSuite('medium', theme.neutral[400] ?? '#778593', vars);
1176
+ buildColorSuite('dark', theme.neutral[100] ?? '#F1F4F7', vars);
1177
+ // Dark backgrounds & surfaces
1178
+ const darkBgHex = '#111112';
1179
+ const darkTextHex = '#DFE2E5';
1180
+ const darkSurface = '#1F1F22';
1181
+ vars['--ion-background-color'] = darkBgHex;
1182
+ vars['--ion-background-color-rgb'] = hexToRgbStr(darkBgHex);
1183
+ vars['--ion-text-color'] = darkTextHex;
1184
+ vars['--ion-text-color-rgb'] = hexToRgbStr(darkTextHex);
1185
+ vars['--ion-card-background'] = darkSurface;
1186
+ vars['--ion-item-background'] = darkSurface;
1187
+ vars['--ion-item-color'] = darkTextHex;
1188
+ vars['--ion-toolbar-background'] = '#18181A';
1189
+ vars['--ion-toolbar-color'] = darkTextHex;
1190
+ vars['--ion-tab-bar-background'] = '#18181A';
1191
+ vars['--ion-border-color'] = 'rgba(242, 244, 246, 0.1)';
1192
+ // Typography & Radius
1193
+ vars['--ion-font-family'] = theme.fontFamily.sans;
1194
+ vars['--ion-border-radius'] = theme.radius[theme.radiusBase];
1195
+ // Stepped colors for dark mode
1196
+ interpolateSteppedColors(darkBgHex, darkTextHex, vars);
1197
+ // Safe area insets
1198
+ vars['--ion-safe-area-top'] = 'var(--ngxsmk-safe-area-top, env(safe-area-inset-top, 0px))';
1199
+ vars['--ion-safe-area-bottom'] =
1200
+ 'var(--ngxsmk-safe-area-bottom, env(safe-area-inset-bottom, 0px))';
1201
+ vars['--ion-safe-area-left'] = 'var(--ngxsmk-safe-area-left, env(safe-area-inset-left, 0px))';
1202
+ vars['--ion-safe-area-right'] = 'var(--ngxsmk-safe-area-right, env(safe-area-inset-right, 0px))';
1203
+ return vars;
1204
+ }
803
1205
  /** Default NGXSMK adapter — emits `--ngxsmk-*` variables. */
804
1206
  const cssVarsAdapter = {
805
1207
  name: 'ngxsmk',
@@ -807,18 +1209,26 @@ const cssVarsAdapter = {
807
1209
  };
808
1210
  // ─── Helpers ──────────────────────────────────────────────
809
1211
  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);
1212
+ const clean = hex.replace('#', '');
1213
+ const r = parseInt(clean.substring(0, 2), 16) || 0;
1214
+ const g = parseInt(clean.substring(2, 4), 16) || 0;
1215
+ const b = parseInt(clean.substring(4, 6), 16) || 0;
814
1216
  return `${r}, ${g}, ${b}`;
815
1217
  }
1218
+ function getContrastHex(hex) {
1219
+ const clean = hex.replace('#', '');
1220
+ const r = parseInt(clean.substring(0, 2), 16) || 0;
1221
+ const g = parseInt(clean.substring(2, 4), 16) || 0;
1222
+ const b = parseInt(clean.substring(4, 6), 16) || 0;
1223
+ // Standard WCAG relative luminance approximation
1224
+ const yiq = (r * 299 + g * 587 + b * 114) / 1000;
1225
+ return yiq >= 150 ? '#000000' : '#FFFFFF';
1226
+ }
816
1227
  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);
1228
+ const clean = hex.replace('#', '');
1229
+ let r = parseInt(clean.substring(0, 2), 16) || 0;
1230
+ let g = parseInt(clean.substring(2, 4), 16) || 0;
1231
+ let b = parseInt(clean.substring(4, 6), 16) || 0;
822
1232
  if (amount > 0) {
823
1233
  r = Math.min(255, r + Math.round((255 - r) * (amount / 100)));
824
1234
  g = Math.min(255, g + Math.round((255 - g) * (amount / 100)));
@@ -830,7 +1240,28 @@ function shiftLightnessHex(hex, amount) {
830
1240
  g = Math.max(0, Math.round(g * factor));
831
1241
  b = Math.max(0, Math.round(b * factor));
832
1242
  }
833
- return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
1243
+ return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`.toUpperCase();
1244
+ }
1245
+ function interpolateSteppedColors(bgHex, textHex, vars) {
1246
+ const cleanBg = bgHex.replace('#', '');
1247
+ const cleanText = textHex.replace('#', '');
1248
+ const bgR = parseInt(cleanBg.substring(0, 2), 16) || 0;
1249
+ const bgG = parseInt(cleanBg.substring(2, 4), 16) || 0;
1250
+ const bgB = parseInt(cleanBg.substring(4, 6), 16) || 0;
1251
+ const textR = parseInt(cleanText.substring(0, 2), 16) || 0;
1252
+ const textG = parseInt(cleanText.substring(2, 4), 16) || 0;
1253
+ const textB = parseInt(cleanText.substring(4, 6), 16) || 0;
1254
+ const steps = [
1255
+ 50, 100, 150, 200, 250, 300, 350, 400, 450, 500, 550, 600, 650, 700, 750, 800, 850, 900, 950,
1256
+ ];
1257
+ for (const step of steps) {
1258
+ const weight = step / 1000;
1259
+ const r = Math.round(bgR * (1 - weight) + textR * weight);
1260
+ const g = Math.round(bgG * (1 - weight) + textG * weight);
1261
+ const b = Math.round(bgB * (1 - weight) + textB * weight);
1262
+ const hex = `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`.toUpperCase();
1263
+ vars[`--ion-color-step-${step}`] = hex;
1264
+ }
834
1265
  }
835
1266
 
836
1267
  const STYLE_ID = 'ngxsmk-theme';
@@ -862,7 +1293,7 @@ class NgxThemeInjectorService {
862
1293
  if (!style) {
863
1294
  style = this.document.createElement('style');
864
1295
  style.id = STYLE_ID;
865
- head.appendChild(style);
1296
+ head?.appendChild(style);
866
1297
  }
867
1298
  let css = buildThemeCss(config);
868
1299
  if (adapter) {
@@ -870,7 +1301,9 @@ class NgxThemeInjectorService {
870
1301
  const platformVars = adapter.vars(resolved);
871
1302
  css += '\n' + this.buildAdapterCss(adapter.name, platformVars);
872
1303
  }
873
- style.textContent = css;
1304
+ if (style) {
1305
+ style.textContent = css;
1306
+ }
874
1307
  }
875
1308
  /**
876
1309
  * Apply Ionic-specific token variables alongside the standard NGXSMK theme.
@@ -882,11 +1315,12 @@ class NgxThemeInjectorService {
882
1315
  if (!style) {
883
1316
  style = this.document.createElement('style');
884
1317
  style.id = IONIC_STYLE_ID;
885
- head.appendChild(style);
1318
+ head?.appendChild(style);
886
1319
  }
887
1320
  const resolved = resolveTheme(config);
888
- const vars = ionicVarsAdapter.vars(resolved);
889
- style.textContent = this.buildAdapterCss('ionic', vars);
1321
+ const lightVars = ionicVarsAdapter.vars(resolved);
1322
+ const darkVars = ionicVarsAdapter.darkVars ? ionicVarsAdapter.darkVars(resolved) : undefined;
1323
+ style.textContent = this.buildAdapterCss('ionic', lightVars, darkVars);
890
1324
  }
891
1325
  /** Remove a runtime-applied theme, falling back to the build-time CSS. */
892
1326
  clearTheme() {
@@ -896,9 +1330,15 @@ class NgxThemeInjectorService {
896
1330
  clearIonicTheme() {
897
1331
  this.document.getElementById(IONIC_STYLE_ID)?.remove();
898
1332
  }
899
- buildAdapterCss(adapterName, vars) {
1333
+ buildAdapterCss(adapterName, vars, darkVars) {
900
1334
  const lines = Object.entries(vars).map(([k, v]) => ` ${k}: ${v};`);
901
- return `/* @ngxsmk/theme — ${adapterName} adapter */\n:root {\n${lines.join('\n')}\n}`;
1335
+ let result = `/* @ngxsmk/theme — ${adapterName} adapter */\n:root {\n${lines.join('\n')}\n}`;
1336
+ if (darkVars && Object.keys(darkVars).length > 0) {
1337
+ const darkLines = Object.entries(darkVars).map(([k, v]) => ` ${k}: ${v};`);
1338
+ result += `\n\n:root.dark, body.dark, html.dark, [data-theme="dark"] {\n${darkLines.join('\n')}\n}`;
1339
+ result += `\n\n@media (prefers-color-scheme: dark) {\n :root:not(.light) {\n ${darkLines.join('\n ')}\n }\n}`;
1340
+ }
1341
+ return result;
902
1342
  }
903
1343
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.0", ngImport: i0, type: NgxThemeInjectorService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
904
1344
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.0", ngImport: i0, type: NgxThemeInjectorService, providedIn: 'root' }); }
@@ -983,6 +1423,33 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.0", ngImpor
983
1423
  args: [{ providedIn: 'root' }]
984
1424
  }], ctorParameters: () => [] });
985
1425
 
1426
+ /**
1427
+ * Standalone provider for Angular 18-22+ applications that automatically
1428
+ * synchronizes NGXSMK design tokens with Ionic 7/8 CSS variables on startup.
1429
+ *
1430
+ * @example
1431
+ * ```ts
1432
+ * export const appConfig: ApplicationConfig = {
1433
+ * providers: [
1434
+ * provideIonicAngular(),
1435
+ * provideNgxsmkIonicTheme(),
1436
+ * ]
1437
+ * };
1438
+ * ```
1439
+ */
1440
+ function provideNgxsmkIonicTheme(config) {
1441
+ return makeEnvironmentProviders([
1442
+ {
1443
+ provide: ENVIRONMENT_INITIALIZER,
1444
+ multi: true,
1445
+ useValue: () => {
1446
+ const theme = inject(NgxsmkThemeService);
1447
+ theme.applyIonicTheme(config ?? violetPreset);
1448
+ },
1449
+ },
1450
+ ]);
1451
+ }
1452
+
986
1453
  /*
987
1454
  * Public API Surface of @ngxsmk/theme
988
1455
  */
@@ -991,5 +1458,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.0", ngImpor
991
1458
  * Generated bundle index. Do not edit.
992
1459
  */
993
1460
 
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 };
1461
+ 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, SAFE_AREA, SHADOW, SPACING, TOUCH_TARGET_MIN, TRACKING, Z_INDEX, buildThemeCss, cssVarsAdapter, defineConfig, deriveScale, emeraldPreset, hexToOklch, inkPreset, ionicVarsAdapter, neutralPreset, oklchToHex, presets, provideNgxsmkIonicTheme, resolveTheme, rosePreset, rotateHue, shiftLightness, violetPreset };
995
1462
  //# sourceMappingURL=ngxsmk-theme.mjs.map