@memberjunction/ng-shared-generic 5.4.0 → 5.5.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 CHANGED
@@ -111,6 +111,55 @@ RecentAccessService.Instance.TrackAccess({
111
111
  const recentItems = RecentAccessService.Instance.RecentItems;
112
112
  ```
113
113
 
114
+ ### ThemeService
115
+
116
+ Manages application-wide theming with built-in light/dark modes and pluggable custom theme support. Preferences are persisted per-user via `UserInfoEngine`.
117
+
118
+ ```typescript
119
+ import { ThemeService, ThemeDefinition } from '@memberjunction/ng-shared-generic';
120
+
121
+ const themeService = inject(ThemeService);
122
+
123
+ // Register a custom theme before Initialize()
124
+ themeService.RegisterTheme({
125
+ Id: 'my-dark',
126
+ Name: 'My Dark',
127
+ BaseTheme: 'dark',
128
+ CssUrl: 'assets/themes/my-dark.css',
129
+ IsBuiltIn: false
130
+ });
131
+
132
+ // Switch themes at runtime
133
+ await themeService.SetTheme('my-dark');
134
+
135
+ // React to theme changes
136
+ themeService.AppliedTheme$.subscribe(themeId => {
137
+ console.log('Active theme:', themeId);
138
+ });
139
+ ```
140
+
141
+ Key properties: `Preference$`, `AppliedTheme$`, `AvailableThemes`, `IsInitialized`.
142
+
143
+ Key methods: `RegisterTheme()`, `RegisterThemes()`, `Initialize()`, `SetTheme()`, `Reset()`.
144
+
145
+ ### Design Tokens
146
+
147
+ This package ships `_tokens.scss`, the CSS custom property foundation for the entire MJ design system. Tokens cover colors, typography, spacing, effects, and z-index.
148
+
149
+ Use semantic tokens in component SCSS:
150
+
151
+ ```scss
152
+ .my-panel {
153
+ background: var(--mj-bg-surface);
154
+ color: var(--mj-text-primary);
155
+ border: 1px solid var(--mj-border-default);
156
+ padding: var(--mj-space-4);
157
+ border-radius: var(--mj-radius-md);
158
+ }
159
+ ```
160
+
161
+ For the full token catalog, architecture details, custom theme creation guide, and best practices, see **[THEMING.md](./THEMING.md)**.
162
+
114
163
  ## Dependencies
115
164
 
116
165
  - [@memberjunction/core](../../MJCore/README.md) -- Core MJ framework
@@ -0,0 +1,434 @@
1
+ /**
2
+ * MemberJunction Design Tokens
3
+ * ============================
4
+ *
5
+ * This file defines all design tokens for the MJ design system.
6
+ * These tokens provide consistent styling across all components.
7
+ *
8
+ * Token Categories:
9
+ * - Primitives: Raw color values (not used directly in components)
10
+ * - Semantic: Purpose-based tokens (what components should use)
11
+ * - Component: Specific component overrides (if needed)
12
+ */
13
+
14
+ :root {
15
+ /* ========================================
16
+ PRIMITIVE TOKENS
17
+ Raw values - DO NOT use directly in components
18
+ ======================================== */
19
+
20
+ /* Brand Colors - Primary Blue */
21
+ --mj-color-brand-50: #e6f3f9;
22
+ --mj-color-brand-100: #b3dbed;
23
+ --mj-color-brand-200: #80c3e1;
24
+ --mj-color-brand-300: #4dabd5;
25
+ --mj-color-brand-350: #3395c8; /* Primary hover (lighter blue) */
26
+ --mj-color-brand-400: #2699cc;
27
+ --mj-color-brand-450: #1a88bf; /* Between 400 and 500 */
28
+ --mj-color-brand-500: #0076b6; /* MJ Primary Blue */
29
+ --mj-color-brand-600: #006aa3;
30
+ --mj-color-brand-700: #005a8a;
31
+ --mj-color-brand-800: #004a71;
32
+ --mj-color-brand-900: #092340; /* MJ Navy */
33
+
34
+ /* Accent Colors - Light Blue (from MJ website) */
35
+ --mj-color-accent-50: #f0faff;
36
+ --mj-color-accent-100: #e0f4fe;
37
+ --mj-color-accent-200: #bae8fd;
38
+ --mj-color-accent-300: #aae7fd; /* MJ Light Blue */
39
+ --mj-color-accent-400: #5cc0ed; /* MJ Skip Agent Blue */
40
+ --mj-color-accent-500: #38a9d9; /* MJ Accent */
41
+ --mj-color-accent-600: #2490c0;
42
+ --mj-color-accent-700: #1d7299;
43
+ --mj-color-accent-800: #1c5d7d;
44
+ --mj-color-accent-900: #1b4d68;
45
+
46
+ /* Tertiary Colors - Cyan/Teal (secondary actions, info) */
47
+ --mj-color-tertiary-50: #ecfeff;
48
+ --mj-color-tertiary-100: #cffafe;
49
+ --mj-color-tertiary-200: #a5f3fc;
50
+ --mj-color-tertiary-300: #67e8f9;
51
+ --mj-color-tertiary-400: #22d3ee;
52
+ --mj-color-tertiary-500: #06b6d4; /* MJ Tertiary Cyan */
53
+ --mj-color-tertiary-600: #0891b2;
54
+ --mj-color-tertiary-700: #0e7490;
55
+ --mj-color-tertiary-800: #155e75;
56
+ --mj-color-tertiary-900: #164e63;
57
+
58
+ /* Neutral Colors (Slate palette) */
59
+ --mj-color-neutral-0: #ffffff;
60
+ --mj-color-neutral-50: #f8fafc;
61
+ --mj-color-neutral-100: #f1f5f9;
62
+ --mj-color-neutral-200: #e2e8f0;
63
+ --mj-color-neutral-300: #cbd5e1;
64
+ --mj-color-neutral-400: #94a3b8;
65
+ --mj-color-neutral-500: #64748b;
66
+ --mj-color-neutral-600: #475569;
67
+ --mj-color-neutral-700: #334155;
68
+ --mj-color-neutral-800: #1e293b;
69
+ --mj-color-neutral-900: #0f172a;
70
+ --mj-color-neutral-950: #020617;
71
+
72
+ /* Status Colors */
73
+ --mj-color-success-50: #f0fdf4;
74
+ --mj-color-success-100: #dcfce7;
75
+ --mj-color-success-200: #bbf7d0;
76
+ --mj-color-success-300: #86efac;
77
+ --mj-color-success-400: #4ade80;
78
+ --mj-color-success-500: #22c55e;
79
+ --mj-color-success-600: #16a34a;
80
+ --mj-color-success-700: #15803d;
81
+ --mj-color-success-800: #166534;
82
+
83
+ --mj-color-warning-50: #fffbeb;
84
+ --mj-color-warning-100: #fef3c7;
85
+ --mj-color-warning-200: #fde68a;
86
+ --mj-color-warning-300: #fcd34d;
87
+ --mj-color-warning-400: #fbbf24;
88
+ --mj-color-warning-500: #f59e0b;
89
+ --mj-color-warning-600: #d97706;
90
+ --mj-color-warning-700: #b45309;
91
+ --mj-color-warning-800: #92400e;
92
+ --mj-color-warning-900: #78350f;
93
+
94
+ --mj-color-error-50: #fef2f2;
95
+ --mj-color-error-100: #fee2e2;
96
+ --mj-color-error-200: #fecaca;
97
+ --mj-color-error-300: #fca5a5;
98
+ --mj-color-error-400: #f87171;
99
+ --mj-color-error-500: #ef4444;
100
+ --mj-color-error-600: #dc2626;
101
+ --mj-color-error-700: #b91c1c;
102
+
103
+ --mj-color-info-50: #eff6ff;
104
+ --mj-color-info-100: #dbeafe;
105
+ --mj-color-info-500: #3b82f6;
106
+ --mj-color-info-600: #2563eb;
107
+ --mj-color-info-700: #1d4ed8;
108
+
109
+ /* Violet Colors (applications, special features) */
110
+ --mj-color-violet-50: #f5f3ff;
111
+ --mj-color-violet-100: #ede9fe;
112
+ --mj-color-violet-200: #ddd6fe;
113
+ --mj-color-violet-300: #c4b5fd;
114
+ --mj-color-violet-400: #a78bfa;
115
+ --mj-color-violet-500: #8b5cf6;
116
+ --mj-color-violet-600: #7c3aed;
117
+ --mj-color-violet-700: #6d28d9;
118
+
119
+ /* Indigo Colors (updated actions, indicators) */
120
+ --mj-color-indigo-50: #eef2ff;
121
+ --mj-color-indigo-100: #e0e7ff;
122
+ --mj-color-indigo-500: #6366f1;
123
+ --mj-color-indigo-600: #4f46e5;
124
+
125
+ /* ========================================
126
+ SEMANTIC TOKENS
127
+ USE THESE in components
128
+ ======================================== */
129
+
130
+ /* Background Colors */
131
+ --mj-bg-page: var(--mj-color-neutral-50);
132
+ --mj-bg-surface: var(--mj-color-neutral-0);
133
+ --mj-bg-surface-elevated: var(--mj-color-neutral-0);
134
+ --mj-bg-surface-card: var(--mj-color-neutral-50);
135
+ --mj-bg-surface-sunken: var(--mj-color-neutral-100);
136
+ --mj-bg-surface-hover: var(--mj-color-neutral-100);
137
+ --mj-bg-surface-active: var(--mj-color-neutral-200);
138
+ --mj-bg-overlay: rgba(15, 23, 42, 0.5);
139
+
140
+ /* Text Colors */
141
+ --mj-text-primary: var(--mj-color-neutral-800);
142
+ --mj-text-secondary: var(--mj-color-neutral-600);
143
+ --mj-text-muted: var(--mj-color-neutral-500);
144
+ --mj-text-disabled: var(--mj-color-neutral-400);
145
+ --mj-text-inverse: var(--mj-color-neutral-0);
146
+ --mj-text-link: var(--mj-color-brand-500);
147
+ --mj-text-link-hover: var(--mj-color-brand-600);
148
+
149
+ /* Border Colors */
150
+ --mj-border-default: var(--mj-color-neutral-200);
151
+ --mj-border-subtle: var(--mj-color-neutral-100);
152
+ --mj-border-strong: var(--mj-color-neutral-300);
153
+ --mj-border-focus: var(--mj-color-brand-500);
154
+ --mj-border-error: var(--mj-color-error-500);
155
+
156
+ /* Brand Colors (semantic) */
157
+ --mj-brand-primary: var(--mj-color-brand-500);
158
+ --mj-brand-primary-hover: var(--mj-color-brand-600);
159
+ --mj-brand-primary-active: var(--mj-color-brand-700);
160
+ --mj-brand-primary-light: var(--mj-color-brand-350);
161
+ --mj-brand-secondary: var(--mj-color-brand-900);
162
+ --mj-brand-on-primary: var(--mj-color-neutral-0);
163
+ --mj-brand-on-secondary: var(--mj-color-neutral-0);
164
+
165
+ /* Accent Colors (semantic) - light blue highlights, emphasis */
166
+ --mj-brand-accent: var(--mj-color-accent-400);
167
+ --mj-brand-accent-hover: var(--mj-color-accent-500);
168
+ --mj-brand-accent-active: var(--mj-color-accent-600);
169
+ --mj-brand-accent-subtle: var(--mj-color-accent-50);
170
+ --mj-brand-on-accent: var(--mj-color-neutral-900);
171
+
172
+ /* Tertiary Colors (semantic) - secondary actions, complementary highlights */
173
+ --mj-brand-tertiary: var(--mj-color-tertiary-500);
174
+ --mj-brand-tertiary-hover: var(--mj-color-tertiary-600);
175
+ --mj-brand-tertiary-active: var(--mj-color-tertiary-700);
176
+ --mj-brand-tertiary-subtle: var(--mj-color-tertiary-50);
177
+ --mj-brand-on-tertiary: var(--mj-color-neutral-0);
178
+
179
+ /* Status Colors (semantic) */
180
+ --mj-status-success: var(--mj-color-success-500);
181
+ --mj-status-success-bg: var(--mj-color-success-50);
182
+ --mj-status-success-text: var(--mj-color-success-700);
183
+ --mj-status-success-border: var(--mj-color-success-500);
184
+
185
+ --mj-status-warning: var(--mj-color-warning-500);
186
+ --mj-status-warning-bg: var(--mj-color-warning-50);
187
+ --mj-status-warning-text: var(--mj-color-warning-700);
188
+ --mj-status-warning-border: var(--mj-color-warning-500);
189
+
190
+ --mj-status-error: var(--mj-color-error-500);
191
+ --mj-status-error-bg: var(--mj-color-error-50);
192
+ --mj-status-error-text: var(--mj-color-error-700);
193
+ --mj-status-error-border: var(--mj-color-error-500);
194
+
195
+ --mj-status-info: var(--mj-color-info-500);
196
+ --mj-status-info-bg: var(--mj-color-info-50);
197
+ --mj-status-info-text: var(--mj-color-info-700);
198
+ --mj-status-info-border: var(--mj-color-info-500);
199
+
200
+ /* Application Accent (set per-application from metadata)
201
+ Default uses amber accent, but apps can override to use primary/tertiary/custom */
202
+ --mj-app-accent: var(--mj-brand-accent);
203
+ --mj-app-accent-hover: var(--mj-brand-accent-hover);
204
+ --mj-app-accent-subtle: var(--mj-brand-accent-subtle);
205
+
206
+ /* Highlight Color - for important callouts, notifications, badges */
207
+ --mj-highlight: var(--mj-brand-accent);
208
+ --mj-highlight-hover: var(--mj-brand-accent-hover);
209
+ --mj-highlight-subtle: var(--mj-brand-accent-subtle);
210
+ --mj-on-highlight: var(--mj-brand-on-accent);
211
+
212
+ /* Login Banner Gradient & Waves */
213
+ --mj-login-grad-start: var(--mj-color-brand-900);
214
+ --mj-login-grad-mid: var(--mj-color-brand-700);
215
+ --mj-login-grad-end: var(--mj-color-brand-300);
216
+ --mj-login-wave-1: var(--mj-color-brand-500);
217
+ --mj-login-wave-2: var(--mj-color-accent-400);
218
+ --mj-login-wave-3: var(--mj-color-brand-300);
219
+
220
+ /* ========================================
221
+ TYPOGRAPHY
222
+ ======================================== */
223
+
224
+ /* Font Family */
225
+ --mj-font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
226
+ --mj-font-family-mono: 'JetBrains Mono', 'SF Mono', Consolas, 'Liberation Mono', Menlo, monospace;
227
+
228
+ /* Font Sizes */
229
+ --mj-text-xs: 0.75rem; /* 12px */
230
+ --mj-text-sm: 0.875rem; /* 14px */
231
+ --mj-text-base: 1rem; /* 16px */
232
+ --mj-text-lg: 1.125rem; /* 18px */
233
+ --mj-text-xl: 1.25rem; /* 20px */
234
+ --mj-text-2xl: 1.5rem; /* 24px */
235
+ --mj-text-3xl: 1.875rem; /* 30px */
236
+ --mj-text-4xl: 2.25rem; /* 36px */
237
+ --mj-text-5xl: 3rem; /* 48px */
238
+ --mj-text-6xl: 3.75rem; /* 60px */
239
+ --mj-text-display: 5rem; /* 80px - hero/display headings */
240
+
241
+ /* Font Weights */
242
+ --mj-font-normal: 400;
243
+ --mj-font-medium: 500;
244
+ --mj-font-semibold: 600;
245
+ --mj-font-bold: 700;
246
+
247
+ /* Line Heights */
248
+ --mj-leading-none: 1;
249
+ --mj-leading-tight: 1.25;
250
+ --mj-leading-snug: 1.375;
251
+ --mj-leading-normal: 1.5;
252
+ --mj-leading-relaxed: 1.625;
253
+ --mj-leading-loose: 2;
254
+
255
+ /* Letter Spacing */
256
+ --mj-tracking-tighter: -0.05em;
257
+ --mj-tracking-tight: -0.025em;
258
+ --mj-tracking-normal: 0;
259
+ --mj-tracking-wide: 0.025em;
260
+ --mj-tracking-wider: 0.05em;
261
+
262
+ /* ========================================
263
+ SPACING (4px base scale)
264
+ ======================================== */
265
+
266
+ --mj-space-0: 0;
267
+ --mj-space-px: 1px;
268
+ --mj-space-0-5: 0.125rem; /* 2px */
269
+ --mj-space-1: 0.25rem; /* 4px */
270
+ --mj-space-1-5: 0.375rem; /* 6px */
271
+ --mj-space-2: 0.5rem; /* 8px */
272
+ --mj-space-2-5: 0.625rem; /* 10px */
273
+ --mj-space-3: 0.75rem; /* 12px */
274
+ --mj-space-3-5: 0.875rem; /* 14px */
275
+ --mj-space-4: 1rem; /* 16px */
276
+ --mj-space-5: 1.25rem; /* 20px */
277
+ --mj-space-6: 1.5rem; /* 24px */
278
+ --mj-space-7: 1.75rem; /* 28px */
279
+ --mj-space-8: 2rem; /* 32px */
280
+ --mj-space-9: 2.25rem; /* 36px */
281
+ --mj-space-10: 2.5rem; /* 40px */
282
+ --mj-space-11: 2.75rem; /* 44px */
283
+ --mj-space-12: 3rem; /* 48px */
284
+ --mj-space-14: 3.5rem; /* 56px */
285
+ --mj-space-16: 4rem; /* 64px */
286
+ --mj-space-20: 5rem; /* 80px */
287
+ --mj-space-24: 6rem; /* 96px */
288
+
289
+ /* ========================================
290
+ EFFECTS
291
+ ======================================== */
292
+
293
+ /* Border Radius */
294
+ --mj-radius-none: 0;
295
+ --mj-radius-sm: 4px;
296
+ --mj-radius-md: 8px;
297
+ --mj-radius-lg: 12px;
298
+ --mj-radius-xl: 16px;
299
+ --mj-radius-2xl: 24px;
300
+ --mj-radius-full: 9999px;
301
+
302
+ /* Shadows */
303
+ --mj-shadow-none: none;
304
+ --mj-shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
305
+ --mj-shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
306
+ --mj-shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);
307
+ --mj-shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
308
+ --mj-shadow-2xl: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
309
+ --mj-shadow-inner: inset 0 2px 4px 0 rgba(0, 0, 0, 0.05);
310
+
311
+ /* Brand Shadows */
312
+ --mj-shadow-brand-sm: 0 2px 8px rgba(0, 118, 182, 0.3);
313
+ --mj-shadow-brand-md: 0 4px 12px rgba(0, 118, 182, 0.4);
314
+
315
+ /* Focus Ring */
316
+ --mj-ring-width: 2px;
317
+ --mj-ring-offset: 2px;
318
+ --mj-ring-color: var(--mj-brand-primary);
319
+ --mj-focus-ring: 0 0 0 var(--mj-ring-offset) var(--mj-bg-surface), 0 0 0 calc(var(--mj-ring-offset) + var(--mj-ring-width)) var(--mj-ring-color);
320
+
321
+ /* Transitions */
322
+ --mj-transition-fast: 150ms ease;
323
+ --mj-transition-base: 200ms ease;
324
+ --mj-transition-slow: 300ms ease;
325
+ --mj-transition-colors: color var(--mj-transition-base), background-color var(--mj-transition-base), border-color var(--mj-transition-base);
326
+
327
+ /* ========================================
328
+ Z-INDEX SCALE
329
+ ======================================== */
330
+
331
+ --mj-z-base: 0;
332
+ --mj-z-dropdown: 100;
333
+ --mj-z-sticky: 200;
334
+ --mj-z-fixed: 300;
335
+ --mj-z-modal-backdrop: 400;
336
+ --mj-z-modal: 500;
337
+ --mj-z-popover: 600;
338
+ --mj-z-tooltip: 700;
339
+ --mj-z-toast: 800;
340
+ }
341
+
342
+ /* ========================================
343
+ DARK THEME
344
+ ======================================== */
345
+
346
+ [data-theme="dark"] {
347
+
348
+ /* Background Colors */
349
+
350
+ --mj-bg-page: var(--mj-color-neutral-900);
351
+ --mj-bg-surface: var(--mj-color-neutral-800);
352
+ --mj-bg-surface-elevated: var(--mj-color-neutral-700);
353
+ --mj-bg-surface-card: var(--mj-color-neutral-800);
354
+ --mj-bg-surface-sunken: var(--mj-color-neutral-950);
355
+ --mj-bg-surface-hover: var(--mj-color-neutral-700);
356
+ --mj-bg-surface-active: var(--mj-color-neutral-600);
357
+ --mj-bg-overlay: rgba(0, 0, 0, 0.7);
358
+
359
+ /* Text Colors */
360
+ --mj-text-primary: var(--mj-color-neutral-100);
361
+ --mj-text-secondary: var(--mj-color-neutral-300);
362
+ --mj-text-muted: var(--mj-color-neutral-400);
363
+ --mj-text-disabled: var(--mj-color-neutral-600);
364
+ --mj-text-inverse: var(--mj-color-neutral-900);
365
+ --mj-text-link: var(--mj-color-brand-300);
366
+ --mj-text-link-hover: var(--mj-color-brand-200);
367
+
368
+ /* Border Colors */
369
+ --mj-border-default: var(--mj-color-neutral-700);
370
+ --mj-border-subtle: var(--mj-color-neutral-800);
371
+ --mj-border-strong: var(--mj-color-neutral-600);
372
+ --mj-border-focus: var(--mj-color-brand-400);
373
+ --mj-border-error: var(--mj-color-error-400);
374
+
375
+ /* Brand Colors (adjusted for dark) */
376
+ --mj-brand-primary: var(--mj-color-brand-400);
377
+ --mj-brand-primary-hover: var(--mj-color-brand-300);
378
+ --mj-brand-primary-active: var(--mj-color-brand-200);
379
+
380
+ /* Accent Colors (adjusted for dark) */
381
+ --mj-brand-accent: var(--mj-color-accent-300);
382
+ --mj-brand-accent-hover: var(--mj-color-accent-200);
383
+ --mj-brand-accent-active: var(--mj-color-accent-100);
384
+ --mj-brand-accent-subtle: color-mix(in srgb, var(--mj-color-accent-400) 15%, transparent);
385
+ --mj-brand-on-accent: var(--mj-color-neutral-900);
386
+
387
+ /* Tertiary Colors (adjusted for dark) */
388
+ --mj-brand-tertiary: var(--mj-color-tertiary-400);
389
+ --mj-brand-tertiary-hover: var(--mj-color-tertiary-300);
390
+ --mj-brand-tertiary-active: var(--mj-color-tertiary-200);
391
+ --mj-brand-tertiary-subtle: color-mix(in srgb, var(--mj-color-tertiary-500) 15%, transparent);
392
+
393
+ /* Highlight (adjusted for dark) */
394
+ --mj-highlight: var(--mj-color-accent-300);
395
+ --mj-highlight-hover: var(--mj-color-accent-200);
396
+ --mj-highlight-subtle: var(--mj-brand-accent-subtle);
397
+
398
+ /* Status Colors (adjusted for dark backgrounds) */
399
+ --mj-status-success-bg: rgba(34, 197, 94, 0.15);
400
+ --mj-status-success-text: var(--mj-color-success-100);
401
+ --mj-status-success-border: var(--mj-color-success-600);
402
+
403
+ --mj-status-warning-bg: rgba(245, 158, 11, 0.15);
404
+ --mj-status-warning-text: var(--mj-color-warning-100);
405
+ --mj-status-warning-border: var(--mj-color-warning-600);
406
+
407
+ --mj-status-error-bg: rgba(239, 68, 68, 0.15);
408
+ --mj-status-error-text: var(--mj-color-error-100);
409
+ --mj-status-error-border: var(--mj-color-error-600);
410
+
411
+ --mj-status-info-bg: rgba(59, 130, 246, 0.15);
412
+ --mj-status-info-text: var(--mj-color-info-100);
413
+ --mj-status-info-border: var(--mj-color-info-600);
414
+
415
+ /* Application Accent (dark mode adjustment) */
416
+ --mj-app-accent: var(--mj-brand-accent);
417
+ --mj-app-accent-hover: var(--mj-brand-accent-hover);
418
+ --mj-app-accent-subtle: var(--mj-brand-accent-subtle);
419
+
420
+ /* Shadows (darker for dark mode) */
421
+ --mj-shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.3);
422
+ --mj-shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4), 0 2px 4px -2px rgba(0, 0, 0, 0.3);
423
+ --mj-shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.4), 0 4px 6px -4px rgba(0, 0, 0, 0.3);
424
+ --mj-shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.5), 0 8px 10px -6px rgba(0, 0, 0, 0.4);
425
+ --mj-shadow-2xl: 0 25px 50px -12px rgba(0, 0, 0, 0.6);
426
+
427
+ /* Login Banner Gradient & Waves */
428
+ --mj-login-grad-start: var(--mj-color-neutral-950);
429
+ --mj-login-grad-mid: var(--mj-color-brand-900);
430
+ --mj-login-grad-end: var(--mj-color-brand-700);
431
+ --mj-login-wave-1: var(--mj-color-accent-500);
432
+ --mj-login-wave-2: var(--mj-color-tertiary-400);
433
+ --mj-login-wave-3: var(--mj-color-brand-400);
434
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"recent-access.service.d.ts","sourceRoot":"","sources":["../../src/lib/recent-access.service.ts"],"names":[],"mappings":"AACA,OAAO,EAAqB,YAAY,EAAmC,MAAM,sBAAsB,CAAC;AAExG,OAAO,EAAmB,UAAU,EAAE,MAAM,MAAM,CAAC;;AAEnD;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,IAAI,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,6EAA6E;IAC7E,YAAY,EAAE,QAAQ,GAAG,MAAM,GAAG,WAAW,GAAG,UAAU,GAAG,QAAQ,CAAC;CACvE;AAED;;;GAGG;AACH,qBAGa,mBAAmB;IAC9B,OAAO,CAAC,MAAM,CAAC,SAAS,CAAsB;IAC9C,OAAO,CAAC,aAAa,CAA+C;IACpE,OAAO,CAAC,WAAW,CAAuC;IAC1D,OAAO,CAAC,SAAS,CAAS;;IAS1B,WAAkB,QAAQ,IAAI,mBAAmB,CAEhD;IAED;;OAEG;IACH,IAAW,WAAW,IAAI,UAAU,CAAC,gBAAgB,EAAE,CAAC,CAEvD;IAED;;OAEG;IACH,IAAW,gBAAgB,IAAI,gBAAgB,EAAE,CAEhD;IAED;;OAEG;IACH,IAAW,SAAS,IAAI,UAAU,CAAC,OAAO,CAAC,CAE1C;IAED;;;;;;;OAOG;IACU,SAAS,CACpB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,GAAG,YAAY,EAC/B,YAAY,GAAE,QAAQ,GAAG,MAAM,GAAG,WAAW,GAAG,UAAU,GAAG,QAAmB,GAC/E,OAAO,CAAC,IAAI,CAAC;IA8DhB;;;;OAIG;IACU,eAAe,CAAC,QAAQ,GAAE,MAAW,EAAE,YAAY,GAAE,OAAe,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC;IAgD/G;;OAEG;IACU,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC;IAIhD;;;OAGG;YACW,kBAAkB;IA+BhC;;;;;OAKG;IACH,OAAO,CAAC,0BAA0B;IA0BlC;;OAEG;IACH,OAAO,CAAC,qBAAqB;IAmB7B;;OAEG;IACI,UAAU,IAAI,IAAI;yCAxQd,mBAAmB;6CAAnB,mBAAmB;CA4Q/B"}
1
+ {"version":3,"file":"recent-access.service.d.ts","sourceRoot":"","sources":["../../src/lib/recent-access.service.ts"],"names":[],"mappings":"AACA,OAAO,EAAqB,YAAY,EAAmC,MAAM,sBAAsB,CAAC;AAGxG,OAAO,EAAmB,UAAU,EAAE,MAAM,MAAM,CAAC;;AAEnD;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,IAAI,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,6EAA6E;IAC7E,YAAY,EAAE,QAAQ,GAAG,MAAM,GAAG,WAAW,GAAG,UAAU,GAAG,QAAQ,CAAC;CACvE;AAED;;;GAGG;AACH,qBAGa,mBAAmB;IAC9B,OAAO,CAAC,MAAM,CAAC,SAAS,CAAsB;IAC9C,OAAO,CAAC,aAAa,CAA+C;IACpE,OAAO,CAAC,WAAW,CAAuC;IAC1D,OAAO,CAAC,SAAS,CAAS;;IAS1B,WAAkB,QAAQ,IAAI,mBAAmB,CAEhD;IAED;;OAEG;IACH,IAAW,WAAW,IAAI,UAAU,CAAC,gBAAgB,EAAE,CAAC,CAEvD;IAED;;OAEG;IACH,IAAW,gBAAgB,IAAI,gBAAgB,EAAE,CAEhD;IAED;;OAEG;IACH,IAAW,SAAS,IAAI,UAAU,CAAC,OAAO,CAAC,CAE1C;IAED;;;;;;;OAOG;IACU,SAAS,CACpB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,GAAG,YAAY,EAC/B,YAAY,GAAE,QAAQ,GAAG,MAAM,GAAG,WAAW,GAAG,UAAU,GAAG,QAAmB,GAC/E,OAAO,CAAC,IAAI,CAAC;IA8DhB;;;;OAIG;IACU,eAAe,CAAC,QAAQ,GAAE,MAAW,EAAE,YAAY,GAAE,OAAe,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC;IAgD/G;;OAEG;IACU,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC;IAIhD;;;OAGG;YACW,kBAAkB;IA+BhC;;;;;OAKG;IACH,OAAO,CAAC,0BAA0B;IA0BlC;;OAEG;IACH,OAAO,CAAC,qBAAqB;IAmB7B;;OAEG;IACI,UAAU,IAAI,IAAI;yCAxQd,mBAAmB;6CAAnB,mBAAmB;CA4Q/B"}
@@ -1,6 +1,7 @@
1
1
  import { Injectable } from '@angular/core';
2
2
  import { Metadata, RunView, CompositeKey, LogError } from '@memberjunction/core';
3
3
  import { UserInfoEngine } from '@memberjunction/core-entities';
4
+ import { UUIDsEqual } from '@memberjunction/global';
4
5
  import { BehaviorSubject } from 'rxjs';
5
6
  import * as i0 from "@angular/core";
6
7
  /**
@@ -119,7 +120,7 @@ export class RecentAccessService {
119
120
  const userRecordLogs = UserInfoEngine.Instance.UserRecordLogs.slice(0, maxItems);
120
121
  const items = [];
121
122
  for (const log of userRecordLogs) {
122
- const entityInfo = md.Entities.find(e => e.ID === log.EntityID);
123
+ const entityInfo = md.Entities.find(e => UUIDsEqual(e.ID, log.EntityID));
123
124
  if (!entityInfo)
124
125
  continue;
125
126
  // Determine resource type based on entity name
@@ -1 +1 @@
1
- {"version":3,"file":"recent-access.service.js","sourceRoot":"","sources":["../../src/lib/recent-access.service.ts"],"names":[],"mappings":"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","sourcesContent":["import { Injectable } from '@angular/core';\nimport { Metadata, RunView, CompositeKey, LogError, EntityRecordNameInput } from '@memberjunction/core';\nimport { MJUserRecordLogEntity, UserInfoEngine } from '@memberjunction/core-entities';\nimport { BehaviorSubject, Observable } from 'rxjs';\n\n/**\n * Represents a recently accessed resource\n */\nexport interface RecentAccessItem {\n id: string;\n entityId: string;\n entityName: string;\n recordId: string;\n recordName?: string;\n latestAt: Date;\n totalCount: number;\n /** Resource type for special handling (record, view, dashboard, artifact) */\n resourceType: 'record' | 'view' | 'dashboard' | 'artifact' | 'report';\n}\n\n/**\n * Service for tracking and retrieving recently accessed resources.\n * Uses the User Record Logs entity to persist access history.\n */\n@Injectable({\n providedIn: 'root'\n})\nexport class RecentAccessService {\n private static _instance: RecentAccessService;\n private _recentItems$ = new BehaviorSubject<RecentAccessItem[]>([]);\n private _isLoading$ = new BehaviorSubject<boolean>(false);\n private _isLoaded = false;\n\n constructor() {\n if (RecentAccessService._instance) {\n return RecentAccessService._instance;\n }\n RecentAccessService._instance = this;\n }\n\n public static get Instance(): RecentAccessService {\n return RecentAccessService._instance;\n }\n\n /**\n * Observable of recent access items, sorted by most recent first\n */\n public get RecentItems(): Observable<RecentAccessItem[]> {\n return this._recentItems$.asObservable();\n }\n\n /**\n * Current value of recent items\n */\n public get RecentItemsValue(): RecentAccessItem[] {\n return this._recentItems$.value;\n }\n\n /**\n * Observable loading state\n */\n public get IsLoading(): Observable<boolean> {\n return this._isLoading$.asObservable();\n }\n\n /**\n * Logs access to a record. Creates or updates the User Record Log entry.\n * This is a fire-and-forget operation - errors are logged but don't interrupt the user.\n *\n * @param entityName - The name of the entity being accessed\n * @param recordId - The record ID (single value or CompositeKey string)\n * @param resourceType - The type of resource being accessed\n */\n public async logAccess(\n entityName: string,\n recordId: string | CompositeKey,\n resourceType: 'record' | 'view' | 'dashboard' | 'artifact' | 'report' = 'record'\n ): Promise<void> {\n try {\n const md = new Metadata();\n const entityInfo = md.Entities.find(e => e.Name === entityName);\n if (!entityInfo) {\n console.warn(`RecentAccessService: Entity \"${entityName}\" not found in metadata`);\n return;\n }\n\n // Convert CompositeKey to string if needed\n const recordIdString = recordId instanceof CompositeKey\n ? recordId.Values(',') // Values() returns joined string with specified delimiter\n : recordId;\n\n // Check if we already have a log entry for this user/entity/record combination\n const rv = new RunView();\n const existingResult = await rv.RunView<MJUserRecordLogEntity>({\n EntityName: 'MJ: User Record Logs',\n ExtraFilter: `UserID='${md.CurrentUser.ID}' AND EntityID='${entityInfo.ID}' AND RecordID='${recordIdString}'`,\n ResultType: 'entity_object',\n MaxRows: 1\n });\n\n if (!existingResult.Success) {\n console.error('RecentAccessService: Failed to check existing log', existingResult.ErrorMessage);\n return;\n }\n\n if (existingResult.Results && existingResult.Results.length > 0) {\n // Update existing entry\n const existing = existingResult.Results[0];\n existing.LatestAt = new Date();\n existing.TotalCount = (existing.TotalCount || 0) + 1;\n\n const saveResult = await existing.Save();\n if (!saveResult) {\n console.error('RecentAccessService: Failed to update log entry');\n }\n } else {\n // Create new entry\n const newLog = await md.GetEntityObject<MJUserRecordLogEntity>('MJ: User Record Logs');\n newLog.UserID = md.CurrentUser.ID;\n newLog.EntityID = entityInfo.ID;\n newLog.RecordID = recordIdString;\n // EarliestAt and LatestAt have default values of getdate() in the DB\n // TotalCount defaults to 0, so we set it to 1\n newLog.TotalCount = 1;\n\n const saveResult = await newLog.Save();\n if (!saveResult) {\n console.error('RecentAccessService: Failed to create log entry');\n }\n }\n\n // Refresh the recent items list in background\n this.refreshRecentItems();\n } catch (error) {\n // Don't throw - this is non-critical functionality\n console.error('RecentAccessService: Error logging access', error);\n }\n }\n\n /**\n * Loads recent access items for the current user using UserInfoEngine (cached).\n * @param maxItems - Maximum number of items to return (default 15)\n * @param forceRefresh - Force refresh even if already loaded\n */\n public async loadRecentItems(maxItems: number = 15, forceRefresh: boolean = false): Promise<RecentAccessItem[]> {\n if (this._isLoaded && !forceRefresh) {\n return this._recentItems$.value;\n }\n\n try {\n this._isLoading$.next(true);\n\n const md = new Metadata();\n\n // Get recent records, limited to maxItems (already ordered by LatestAt DESC in engine)\n const userRecordLogs = UserInfoEngine.Instance.UserRecordLogs.slice(0, maxItems);\n\n const items: RecentAccessItem[] = [];\n\n for (const log of userRecordLogs) {\n const entityInfo = md.Entities.find(e => e.ID === log.EntityID);\n if (!entityInfo) continue;\n\n // Determine resource type based on entity name\n const resourceType = this.determineResourceType(entityInfo.Name);\n\n items.push({\n id: log.ID,\n entityId: log.EntityID,\n entityName: entityInfo.Name,\n recordId: log.RecordID,\n latestAt: log.LatestAt,\n totalCount: log.TotalCount,\n resourceType\n });\n }\n\n // Batch-resolve record names for all items\n await this.resolveRecordNames(items, md);\n\n this._recentItems$.next(items);\n this._isLoaded = true;\n\n return items;\n } catch (error) {\n LogError('RecentAccessService: Error loading recent items', undefined, error);\n return [];\n } finally {\n this._isLoading$.next(false);\n }\n }\n\n /**\n * Refresh recent items in background\n */\n public async refreshRecentItems(): Promise<void> {\n await this.loadRecentItems(15, true);\n }\n\n /**\n * Batch-resolve record names for a list of recent access items using GetEntityRecordNames().\n * Mutates the items in-place to set recordName where resolved.\n */\n private async resolveRecordNames(items: RecentAccessItem[], md: Metadata): Promise<void> {\n const nameInputs: EntityRecordNameInput[] = [];\n const indexMap = new Map<string, number>();\n\n for (let i = 0; i < items.length; i++) {\n const item = items[i];\n const compositeKey = this.buildCompositeKeyForRecord(item.entityName, item.recordId, md);\n if (!compositeKey) continue;\n\n nameInputs.push({ EntityName: item.entityName, CompositeKey: compositeKey });\n indexMap.set(`${item.entityName}||${compositeKey.ToConcatenatedString()}`, i);\n }\n\n if (nameInputs.length === 0) return;\n\n try {\n const nameResults = await md.GetEntityRecordNames(nameInputs);\n for (const result of nameResults) {\n if (result.Success && result.RecordName) {\n const key = `${result.EntityName}||${result.CompositeKey.ToConcatenatedString()}`;\n const index = indexMap.get(key);\n if (index !== undefined) {\n items[index].recordName = result.RecordName;\n }\n }\n }\n } catch (error) {\n console.warn('RecentAccessService: Failed to resolve record names', error);\n }\n }\n\n /**\n * Build a CompositeKey for a record. RecordID may be stored as either:\n * - Concatenated format: \"FieldName|Value\" or \"Field1|Val1||Field2|Val2\"\n * - Plain value: just the raw value (e.g. a GUID)\n * Detects the format and constructs the key accordingly.\n */\n private buildCompositeKeyForRecord(entityName: string, recordId: string, md: Metadata): CompositeKey | null {\n if (!recordId) return null;\n\n // If recordId contains '|', it's in concatenated format\n if (recordId.includes('|')) {\n try {\n const compositeKey = new CompositeKey();\n compositeKey.LoadFromConcatenatedString(recordId);\n if (compositeKey.KeyValuePairs.length > 0) return compositeKey;\n } catch {\n // Fall through to entity-based lookup\n }\n }\n\n // Plain value — look up entity primary key field(s) to construct the key\n const entityInfo = md.Entities.find(e => e.Name === entityName);\n if (!entityInfo) return null;\n\n const pkField = entityInfo.FirstPrimaryKey;\n if (!pkField) return null;\n\n const compositeKey = new CompositeKey();\n compositeKey.KeyValuePairs = [{ FieldName: pkField.Name, Value: recordId }];\n return compositeKey;\n }\n\n /**\n * Determines the resource type based on entity name\n */\n private determineResourceType(entityName: string): 'record' | 'view' | 'dashboard' | 'artifact' | 'report' {\n const normalizedName = entityName.toLowerCase();\n\n if (normalizedName === 'user views') {\n return 'view';\n }\n if (normalizedName === 'dashboards') {\n return 'dashboard';\n }\n if (normalizedName === 'mj: conversation artifacts' || normalizedName === 'conversation artifacts') {\n return 'artifact';\n }\n if (normalizedName === 'reports') {\n return 'report';\n }\n\n return 'record';\n }\n\n /**\n * Clears the cached recent items (useful for logout)\n */\n public clearCache(): void {\n this._recentItems$.next([]);\n this._isLoaded = false;\n }\n}\n"]}
1
+ {"version":3,"file":"recent-access.service.js","sourceRoot":"","sources":["../../src/lib/recent-access.service.ts"],"names":[],"mappings":"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","sourcesContent":["import { Injectable } from '@angular/core';\nimport { Metadata, RunView, CompositeKey, LogError, EntityRecordNameInput } from '@memberjunction/core';\nimport { MJUserRecordLogEntity, UserInfoEngine } from '@memberjunction/core-entities';\nimport { UUIDsEqual } from '@memberjunction/global';\nimport { BehaviorSubject, Observable } from 'rxjs';\n\n/**\n * Represents a recently accessed resource\n */\nexport interface RecentAccessItem {\n id: string;\n entityId: string;\n entityName: string;\n recordId: string;\n recordName?: string;\n latestAt: Date;\n totalCount: number;\n /** Resource type for special handling (record, view, dashboard, artifact) */\n resourceType: 'record' | 'view' | 'dashboard' | 'artifact' | 'report';\n}\n\n/**\n * Service for tracking and retrieving recently accessed resources.\n * Uses the User Record Logs entity to persist access history.\n */\n@Injectable({\n providedIn: 'root'\n})\nexport class RecentAccessService {\n private static _instance: RecentAccessService;\n private _recentItems$ = new BehaviorSubject<RecentAccessItem[]>([]);\n private _isLoading$ = new BehaviorSubject<boolean>(false);\n private _isLoaded = false;\n\n constructor() {\n if (RecentAccessService._instance) {\n return RecentAccessService._instance;\n }\n RecentAccessService._instance = this;\n }\n\n public static get Instance(): RecentAccessService {\n return RecentAccessService._instance;\n }\n\n /**\n * Observable of recent access items, sorted by most recent first\n */\n public get RecentItems(): Observable<RecentAccessItem[]> {\n return this._recentItems$.asObservable();\n }\n\n /**\n * Current value of recent items\n */\n public get RecentItemsValue(): RecentAccessItem[] {\n return this._recentItems$.value;\n }\n\n /**\n * Observable loading state\n */\n public get IsLoading(): Observable<boolean> {\n return this._isLoading$.asObservable();\n }\n\n /**\n * Logs access to a record. Creates or updates the User Record Log entry.\n * This is a fire-and-forget operation - errors are logged but don't interrupt the user.\n *\n * @param entityName - The name of the entity being accessed\n * @param recordId - The record ID (single value or CompositeKey string)\n * @param resourceType - The type of resource being accessed\n */\n public async logAccess(\n entityName: string,\n recordId: string | CompositeKey,\n resourceType: 'record' | 'view' | 'dashboard' | 'artifact' | 'report' = 'record'\n ): Promise<void> {\n try {\n const md = new Metadata();\n const entityInfo = md.Entities.find(e => e.Name === entityName);\n if (!entityInfo) {\n console.warn(`RecentAccessService: Entity \"${entityName}\" not found in metadata`);\n return;\n }\n\n // Convert CompositeKey to string if needed\n const recordIdString = recordId instanceof CompositeKey\n ? recordId.Values(',') // Values() returns joined string with specified delimiter\n : recordId;\n\n // Check if we already have a log entry for this user/entity/record combination\n const rv = new RunView();\n const existingResult = await rv.RunView<MJUserRecordLogEntity>({\n EntityName: 'MJ: User Record Logs',\n ExtraFilter: `UserID='${md.CurrentUser.ID}' AND EntityID='${entityInfo.ID}' AND RecordID='${recordIdString}'`,\n ResultType: 'entity_object',\n MaxRows: 1\n });\n\n if (!existingResult.Success) {\n console.error('RecentAccessService: Failed to check existing log', existingResult.ErrorMessage);\n return;\n }\n\n if (existingResult.Results && existingResult.Results.length > 0) {\n // Update existing entry\n const existing = existingResult.Results[0];\n existing.LatestAt = new Date();\n existing.TotalCount = (existing.TotalCount || 0) + 1;\n\n const saveResult = await existing.Save();\n if (!saveResult) {\n console.error('RecentAccessService: Failed to update log entry');\n }\n } else {\n // Create new entry\n const newLog = await md.GetEntityObject<MJUserRecordLogEntity>('MJ: User Record Logs');\n newLog.UserID = md.CurrentUser.ID;\n newLog.EntityID = entityInfo.ID;\n newLog.RecordID = recordIdString;\n // EarliestAt and LatestAt have default values of getdate() in the DB\n // TotalCount defaults to 0, so we set it to 1\n newLog.TotalCount = 1;\n\n const saveResult = await newLog.Save();\n if (!saveResult) {\n console.error('RecentAccessService: Failed to create log entry');\n }\n }\n\n // Refresh the recent items list in background\n this.refreshRecentItems();\n } catch (error) {\n // Don't throw - this is non-critical functionality\n console.error('RecentAccessService: Error logging access', error);\n }\n }\n\n /**\n * Loads recent access items for the current user using UserInfoEngine (cached).\n * @param maxItems - Maximum number of items to return (default 15)\n * @param forceRefresh - Force refresh even if already loaded\n */\n public async loadRecentItems(maxItems: number = 15, forceRefresh: boolean = false): Promise<RecentAccessItem[]> {\n if (this._isLoaded && !forceRefresh) {\n return this._recentItems$.value;\n }\n\n try {\n this._isLoading$.next(true);\n\n const md = new Metadata();\n\n // Get recent records, limited to maxItems (already ordered by LatestAt DESC in engine)\n const userRecordLogs = UserInfoEngine.Instance.UserRecordLogs.slice(0, maxItems);\n\n const items: RecentAccessItem[] = [];\n\n for (const log of userRecordLogs) {\n const entityInfo = md.Entities.find(e => UUIDsEqual(e.ID, log.EntityID));\n if (!entityInfo) continue;\n\n // Determine resource type based on entity name\n const resourceType = this.determineResourceType(entityInfo.Name);\n\n items.push({\n id: log.ID,\n entityId: log.EntityID,\n entityName: entityInfo.Name,\n recordId: log.RecordID,\n latestAt: log.LatestAt,\n totalCount: log.TotalCount,\n resourceType\n });\n }\n\n // Batch-resolve record names for all items\n await this.resolveRecordNames(items, md);\n\n this._recentItems$.next(items);\n this._isLoaded = true;\n\n return items;\n } catch (error) {\n LogError('RecentAccessService: Error loading recent items', undefined, error);\n return [];\n } finally {\n this._isLoading$.next(false);\n }\n }\n\n /**\n * Refresh recent items in background\n */\n public async refreshRecentItems(): Promise<void> {\n await this.loadRecentItems(15, true);\n }\n\n /**\n * Batch-resolve record names for a list of recent access items using GetEntityRecordNames().\n * Mutates the items in-place to set recordName where resolved.\n */\n private async resolveRecordNames(items: RecentAccessItem[], md: Metadata): Promise<void> {\n const nameInputs: EntityRecordNameInput[] = [];\n const indexMap = new Map<string, number>();\n\n for (let i = 0; i < items.length; i++) {\n const item = items[i];\n const compositeKey = this.buildCompositeKeyForRecord(item.entityName, item.recordId, md);\n if (!compositeKey) continue;\n\n nameInputs.push({ EntityName: item.entityName, CompositeKey: compositeKey });\n indexMap.set(`${item.entityName}||${compositeKey.ToConcatenatedString()}`, i);\n }\n\n if (nameInputs.length === 0) return;\n\n try {\n const nameResults = await md.GetEntityRecordNames(nameInputs);\n for (const result of nameResults) {\n if (result.Success && result.RecordName) {\n const key = `${result.EntityName}||${result.CompositeKey.ToConcatenatedString()}`;\n const index = indexMap.get(key);\n if (index !== undefined) {\n items[index].recordName = result.RecordName;\n }\n }\n }\n } catch (error) {\n console.warn('RecentAccessService: Failed to resolve record names', error);\n }\n }\n\n /**\n * Build a CompositeKey for a record. RecordID may be stored as either:\n * - Concatenated format: \"FieldName|Value\" or \"Field1|Val1||Field2|Val2\"\n * - Plain value: just the raw value (e.g. a GUID)\n * Detects the format and constructs the key accordingly.\n */\n private buildCompositeKeyForRecord(entityName: string, recordId: string, md: Metadata): CompositeKey | null {\n if (!recordId) return null;\n\n // If recordId contains '|', it's in concatenated format\n if (recordId.includes('|')) {\n try {\n const compositeKey = new CompositeKey();\n compositeKey.LoadFromConcatenatedString(recordId);\n if (compositeKey.KeyValuePairs.length > 0) return compositeKey;\n } catch {\n // Fall through to entity-based lookup\n }\n }\n\n // Plain value — look up entity primary key field(s) to construct the key\n const entityInfo = md.Entities.find(e => e.Name === entityName);\n if (!entityInfo) return null;\n\n const pkField = entityInfo.FirstPrimaryKey;\n if (!pkField) return null;\n\n const compositeKey = new CompositeKey();\n compositeKey.KeyValuePairs = [{ FieldName: pkField.Name, Value: recordId }];\n return compositeKey;\n }\n\n /**\n * Determines the resource type based on entity name\n */\n private determineResourceType(entityName: string): 'record' | 'view' | 'dashboard' | 'artifact' | 'report' {\n const normalizedName = entityName.toLowerCase();\n\n if (normalizedName === 'user views') {\n return 'view';\n }\n if (normalizedName === 'dashboards') {\n return 'dashboard';\n }\n if (normalizedName === 'mj: conversation artifacts' || normalizedName === 'conversation artifacts') {\n return 'artifact';\n }\n if (normalizedName === 'reports') {\n return 'report';\n }\n\n return 'record';\n }\n\n /**\n * Clears the cached recent items (useful for logout)\n */\n public clearCache(): void {\n this._recentItems$.next([]);\n this._isLoaded = false;\n }\n}\n"]}
@@ -29,8 +29,8 @@ export interface ThemeDefinition {
29
29
  * a dynamically loaded stylesheet.
30
30
  *
31
31
  * CSS resolution for custom themes (e.g. "Izzy Dark" extending dark):
32
- * 1. `:root` light defaults (from _tokens.scss)
33
- * 2. `[data-theme="dark"]` dark overrides (from _tokens.scss)
32
+ * 1. `:root` light defaults (from _tokens.scss in this package)
33
+ * 2. `[data-theme="dark"]` dark overrides (from _tokens.scss in this package)
34
34
  * 3. `[data-theme-overlay="izzy-dark"]` custom overrides (loaded dynamically)
35
35
  *
36
36
  * Follows the DeveloperModeService pattern:
@@ -34,8 +34,8 @@ const DARK_THEME = {
34
34
  * a dynamically loaded stylesheet.
35
35
  *
36
36
  * CSS resolution for custom themes (e.g. "Izzy Dark" extending dark):
37
- * 1. `:root` light defaults (from _tokens.scss)
38
- * 2. `[data-theme="dark"]` dark overrides (from _tokens.scss)
37
+ * 1. `:root` light defaults (from _tokens.scss in this package)
38
+ * 2. `[data-theme="dark"]` dark overrides (from _tokens.scss in this package)
39
39
  * 3. `[data-theme-overlay="izzy-dark"]` custom overrides (loaded dynamically)
40
40
  *
41
41
  * Follows the DeveloperModeService pattern:
@@ -1 +1 @@
1
- {"version":3,"file":"theme.service.js","sourceRoot":"","sources":["../../src/lib/theme.service.ts"],"names":[],"mappings":"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","sourcesContent":["import { Injectable } from '@angular/core';\nimport { BehaviorSubject, Observable } from 'rxjs';\nimport { UserInfoEngine } from '@memberjunction/core-entities';\n\n/**\n * Defines a theme available in the application.\n * Built-in themes (light/dark) have no CssUrl.\n * Custom themes specify a BaseTheme to inherit from and a CssUrl with overrides.\n */\nexport interface ThemeDefinition {\n /** Unique identifier (e.g. 'light', 'dark', 'izzy-dark') */\n Id: string;\n /** Human-readable display name (e.g. 'Light', 'Dark', 'Izzy Dark') */\n Name: string;\n /** Which built-in theme this inherits from */\n BaseTheme: 'light' | 'dark';\n /** URL to the CSS file with token overrides (omit for built-in themes) */\n CssUrl?: string;\n /** Whether this is a built-in theme (light/dark) */\n IsBuiltIn: boolean;\n /** Optional description shown in theme picker */\n Description?: string;\n /** Optional preview swatch colors for a future theme picker UI */\n PreviewColors?: string[];\n}\n\n/**\n * Setting key for theme preference in MJ: User Settings entity\n */\nconst THEME_SETTING_KEY = 'Explorer.Theme';\n\n/**\n * Built-in light theme definition\n */\nconst LIGHT_THEME: ThemeDefinition = {\n Id: 'light',\n Name: 'Light',\n BaseTheme: 'light',\n IsBuiltIn: true,\n Description: 'Default light theme'\n};\n\n/**\n * Built-in dark theme definition\n */\nconst DARK_THEME: ThemeDefinition = {\n Id: 'dark',\n Name: 'Dark',\n BaseTheme: 'dark',\n IsBuiltIn: true,\n Description: 'Default dark theme'\n};\n\n/**\n * Service to manage application themes with pluggable custom theme support.\n *\n * Built-in themes (light/dark) work identically to before. Custom themes\n * inherit from a base theme and overlay additional CSS token overrides via\n * a dynamically loaded stylesheet.\n *\n * CSS resolution for custom themes (e.g. \"Izzy Dark\" extending dark):\n * 1. `:root` light defaults (from _tokens.scss)\n * 2. `[data-theme=\"dark\"]` dark overrides (from _tokens.scss)\n * 3. `[data-theme-overlay=\"izzy-dark\"]` custom overrides (loaded dynamically)\n *\n * Follows the DeveloperModeService pattern:\n * - Settings persisted via UserInfoEngine\n * - BehaviorSubject for reactive state\n * - Initialize after login, Reset on logout\n */\n@Injectable({ providedIn: 'root' })\nexport class ThemeService {\n private _preference$ = new BehaviorSubject<string>('system');\n private _appliedTheme$ = new BehaviorSubject<string>('light');\n private _initialized = false;\n private systemMediaQuery: MediaQueryList | null = null;\n private boundSystemThemeHandler: (() => void) | null = null;\n\n /** Registry of available themes, seeded with built-in light and dark */\n private themeRegistry = new Map<string, ThemeDefinition>([\n ['light', LIGHT_THEME],\n ['dark', DARK_THEME]\n ]);\n\n /** Cache of loaded <link> elements by theme ID to avoid re-downloading */\n private loadedCssLinks = new Map<string, HTMLLinkElement>();\n\n /**\n * Observable for user's theme preference (theme ID or 'system')\n */\n public get Preference$(): Observable<string> {\n return this._preference$.asObservable();\n }\n\n /**\n * Observable for the actually applied theme (resolved theme ID)\n */\n public get AppliedTheme$(): Observable<string> {\n return this._appliedTheme$.asObservable();\n }\n\n /**\n * Current theme preference (synchronous access)\n */\n public get Preference(): string {\n return this._preference$.value;\n }\n\n /**\n * Currently applied theme ID (synchronous access)\n */\n public get AppliedTheme(): string {\n return this._appliedTheme$.value;\n }\n\n /**\n * Whether the service has been initialized\n */\n public get IsInitialized(): boolean {\n return this._initialized;\n }\n\n /**\n * All registered themes, for UI consumption (e.g. theme picker menus)\n */\n public get AvailableThemes(): ThemeDefinition[] {\n return Array.from(this.themeRegistry.values());\n }\n\n /**\n * Look up a theme definition by ID.\n * Returns undefined if the theme ID is not registered.\n */\n public GetThemeDefinition(id: string): ThemeDefinition | undefined {\n return this.themeRegistry.get(id);\n }\n\n // ========================================\n // THEME REGISTRATION\n // ========================================\n\n /**\n * Register a custom theme. If a theme with the same ID already exists,\n * it is replaced (allowing override of built-in themes if desired).\n */\n public RegisterTheme(theme: ThemeDefinition): void {\n this.themeRegistry.set(theme.Id, theme);\n }\n\n /**\n * Register multiple custom themes at once.\n */\n public RegisterThemes(themes: ThemeDefinition[]): void {\n for (const theme of themes) {\n this.RegisterTheme(theme);\n }\n }\n\n // ========================================\n // LIFECYCLE\n // ========================================\n\n /**\n * Initialize the theme service.\n * Call after login when UserInfoEngine is available.\n */\n public async Initialize(): Promise<void> {\n if (this._initialized) {\n return;\n }\n\n this.setupSystemThemeListener();\n\n const savedPreference = await this.loadSetting();\n this._preference$.next(savedPreference);\n\n const resolvedThemeId = this.resolveTheme(savedPreference);\n await this.applyTheme(resolvedThemeId);\n\n this._initialized = true;\n }\n\n /**\n * Set the theme preference and apply it.\n * @param preference - A registered theme ID or 'system'\n */\n public async SetTheme(preference: string): Promise<void> {\n if (preference === this._preference$.value) {\n return;\n }\n\n this._preference$.next(preference);\n\n const resolvedThemeId = this.resolveTheme(preference);\n await this.applyTheme(resolvedThemeId);\n\n await this.saveSetting(preference);\n }\n\n /**\n * Reset the service (call on logout)\n */\n public Reset(): void {\n if (this.systemMediaQuery && this.boundSystemThemeHandler) {\n this.systemMediaQuery.removeEventListener('change', this.boundSystemThemeHandler);\n }\n this.systemMediaQuery = null;\n this.boundSystemThemeHandler = null;\n\n this._preference$.next('system');\n this._appliedTheme$.next('light');\n this._initialized = false;\n\n // Remove theme attributes\n document.documentElement.removeAttribute('data-theme');\n document.documentElement.removeAttribute('data-theme-overlay');\n\n // Disable all custom CSS links\n this.disableAllCustomCss();\n }\n\n // ========================================\n // THEME APPLICATION\n // ========================================\n\n /**\n * Apply a resolved theme ID to the DOM.\n * Sets `data-theme` based on BaseTheme and `data-theme-overlay` for custom themes.\n * Loads custom CSS if needed, disables previous custom CSS.\n */\n private async applyTheme(themeId: string): Promise<void> {\n const themeDef = this.themeRegistry.get(themeId);\n\n // Fall back to 'light' if the theme ID isn't recognized\n if (!themeDef) {\n this.applyBuiltInTheme('light');\n this._appliedTheme$.next('light');\n return;\n }\n\n // Set the base theme attribute (drives existing [data-theme=\"dark\"] selectors)\n this.applyBaseThemeAttribute(themeDef.BaseTheme);\n\n if (themeDef.IsBuiltIn) {\n // Built-in theme: remove overlay, disable custom CSS\n document.documentElement.removeAttribute('data-theme-overlay');\n this.disableAllCustomCss();\n } else if (themeDef.CssUrl) {\n // Custom theme: load/enable CSS and set overlay attribute\n await this.loadThemeCss(themeDef);\n document.documentElement.setAttribute('data-theme-overlay', themeDef.Id);\n }\n\n this._appliedTheme$.next(themeId);\n }\n\n /**\n * Apply the base theme attribute to <html>.\n * 'dark' sets data-theme=\"dark\"; 'light' removes it (matching existing convention).\n */\n private applyBaseThemeAttribute(baseTheme: 'light' | 'dark'): void {\n if (baseTheme === 'dark') {\n document.documentElement.setAttribute('data-theme', 'dark');\n } else {\n document.documentElement.removeAttribute('data-theme');\n }\n }\n\n /**\n * Shorthand for built-in theme application (no custom CSS)\n */\n private applyBuiltInTheme(baseTheme: 'light' | 'dark'): void {\n this.applyBaseThemeAttribute(baseTheme);\n document.documentElement.removeAttribute('data-theme-overlay');\n this.disableAllCustomCss();\n }\n\n // ========================================\n // DYNAMIC CSS LOADING\n // ========================================\n\n /**\n * Load (or re-enable) a custom theme's CSS file.\n * Injects a <link> element into <head> with a data-mj-theme attribute.\n * Caches the link element to avoid re-downloading on theme switches.\n * Returns a Promise that resolves once the stylesheet is loaded.\n */\n private loadThemeCss(theme: ThemeDefinition): Promise<void> {\n if (!theme.CssUrl) {\n return Promise.resolve();\n }\n\n // Disable all other custom CSS first\n this.disableAllCustomCss();\n\n // Check cache — if already loaded, just re-enable\n const existingLink = this.loadedCssLinks.get(theme.Id);\n if (existingLink) {\n existingLink.disabled = false;\n return Promise.resolve();\n }\n\n // Create and inject new <link> element\n return new Promise<void>((resolve, reject) => {\n const link = document.createElement('link');\n link.rel = 'stylesheet';\n link.href = theme.CssUrl!;\n link.setAttribute('data-mj-theme', theme.Id);\n\n link.onload = () => resolve();\n link.onerror = () => {\n console.warn(`Failed to load theme CSS: ${theme.CssUrl}`);\n // Still resolve so the theme switch isn't blocked\n resolve();\n };\n\n document.head.appendChild(link);\n this.loadedCssLinks.set(theme.Id, link);\n });\n }\n\n /**\n * Disable (not remove) all custom theme CSS <link> elements.\n * Disabling rather than removing avoids re-downloading when switching back.\n */\n private disableAllCustomCss(): void {\n for (const link of this.loadedCssLinks.values()) {\n link.disabled = true;\n }\n }\n\n // ========================================\n // THEME RESOLUTION\n // ========================================\n\n /**\n * Resolve a preference string to an actual theme ID.\n * 'system' resolves to 'light' or 'dark' based on OS preference.\n * Unrecognized IDs fall back to 'light'.\n */\n private resolveTheme(preference: string): string {\n if (preference === 'system') {\n return this.getSystemTheme();\n }\n\n // If the preference is a registered theme, use it directly\n if (this.themeRegistry.has(preference)) {\n return preference;\n }\n\n // Unrecognized theme ID — fall back to light\n return 'light';\n }\n\n /**\n * Get system theme preference from OS\n */\n private getSystemTheme(): 'light' | 'dark' {\n if (typeof window === 'undefined') {\n return 'light';\n }\n return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';\n }\n\n /**\n * Setup listener for system theme changes\n */\n private setupSystemThemeListener(): void {\n if (typeof window === 'undefined') {\n return;\n }\n\n this.systemMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');\n this.boundSystemThemeHandler = () => this.onSystemThemeChange();\n this.systemMediaQuery.addEventListener('change', this.boundSystemThemeHandler);\n }\n\n /**\n * Handle system theme change (only applies if in 'system' mode)\n */\n private async onSystemThemeChange(): Promise<void> {\n if (this._preference$.value === 'system') {\n const newThemeId = this.getSystemTheme();\n await this.applyTheme(newThemeId);\n }\n }\n\n // ========================================\n // PERSISTENCE\n // ========================================\n\n /**\n * Load theme preference from User Settings.\n * Accepts any registered theme ID or 'system'.\n * Falls back to 'system' if saved value is not recognized.\n */\n private async loadSetting(): Promise<string> {\n try {\n const engine = UserInfoEngine.Instance;\n const settingValue = engine.GetSetting(THEME_SETTING_KEY);\n\n if (!settingValue) {\n return 'system';\n }\n\n // Accept 'system' or any registered theme ID\n if (settingValue === 'system' || this.themeRegistry.has(settingValue)) {\n return settingValue;\n }\n\n // Saved theme no longer registered — fall back\n return 'system';\n } catch (error) {\n console.warn('Failed to load theme setting:', error);\n return 'system';\n }\n }\n\n /**\n * Save theme preference to User Settings\n */\n private async saveSetting(preference: string): Promise<void> {\n try {\n const engine = UserInfoEngine.Instance;\n await engine.SetSetting(THEME_SETTING_KEY, preference);\n } catch (error) {\n console.warn('Failed to save theme setting:', error);\n }\n }\n}\n"]}
1
+ {"version":3,"file":"theme.service.js","sourceRoot":"","sources":["../../src/lib/theme.service.ts"],"names":[],"mappings":"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","sourcesContent":["import { Injectable } from '@angular/core';\nimport { BehaviorSubject, Observable } from 'rxjs';\nimport { UserInfoEngine } from '@memberjunction/core-entities';\n\n/**\n * Defines a theme available in the application.\n * Built-in themes (light/dark) have no CssUrl.\n * Custom themes specify a BaseTheme to inherit from and a CssUrl with overrides.\n */\nexport interface ThemeDefinition {\n /** Unique identifier (e.g. 'light', 'dark', 'izzy-dark') */\n Id: string;\n /** Human-readable display name (e.g. 'Light', 'Dark', 'Izzy Dark') */\n Name: string;\n /** Which built-in theme this inherits from */\n BaseTheme: 'light' | 'dark';\n /** URL to the CSS file with token overrides (omit for built-in themes) */\n CssUrl?: string;\n /** Whether this is a built-in theme (light/dark) */\n IsBuiltIn: boolean;\n /** Optional description shown in theme picker */\n Description?: string;\n /** Optional preview swatch colors for a future theme picker UI */\n PreviewColors?: string[];\n}\n\n/**\n * Setting key for theme preference in MJ: User Settings entity\n */\nconst THEME_SETTING_KEY = 'Explorer.Theme';\n\n/**\n * Built-in light theme definition\n */\nconst LIGHT_THEME: ThemeDefinition = {\n Id: 'light',\n Name: 'Light',\n BaseTheme: 'light',\n IsBuiltIn: true,\n Description: 'Default light theme'\n};\n\n/**\n * Built-in dark theme definition\n */\nconst DARK_THEME: ThemeDefinition = {\n Id: 'dark',\n Name: 'Dark',\n BaseTheme: 'dark',\n IsBuiltIn: true,\n Description: 'Default dark theme'\n};\n\n/**\n * Service to manage application themes with pluggable custom theme support.\n *\n * Built-in themes (light/dark) work identically to before. Custom themes\n * inherit from a base theme and overlay additional CSS token overrides via\n * a dynamically loaded stylesheet.\n *\n * CSS resolution for custom themes (e.g. \"Izzy Dark\" extending dark):\n * 1. `:root` light defaults (from _tokens.scss in this package)\n * 2. `[data-theme=\"dark\"]` dark overrides (from _tokens.scss in this package)\n * 3. `[data-theme-overlay=\"izzy-dark\"]` custom overrides (loaded dynamically)\n *\n * Follows the DeveloperModeService pattern:\n * - Settings persisted via UserInfoEngine\n * - BehaviorSubject for reactive state\n * - Initialize after login, Reset on logout\n */\n@Injectable({ providedIn: 'root' })\nexport class ThemeService {\n private _preference$ = new BehaviorSubject<string>('system');\n private _appliedTheme$ = new BehaviorSubject<string>('light');\n private _initialized = false;\n private systemMediaQuery: MediaQueryList | null = null;\n private boundSystemThemeHandler: (() => void) | null = null;\n\n /** Registry of available themes, seeded with built-in light and dark */\n private themeRegistry = new Map<string, ThemeDefinition>([\n ['light', LIGHT_THEME],\n ['dark', DARK_THEME]\n ]);\n\n /** Cache of loaded <link> elements by theme ID to avoid re-downloading */\n private loadedCssLinks = new Map<string, HTMLLinkElement>();\n\n /**\n * Observable for user's theme preference (theme ID or 'system')\n */\n public get Preference$(): Observable<string> {\n return this._preference$.asObservable();\n }\n\n /**\n * Observable for the actually applied theme (resolved theme ID)\n */\n public get AppliedTheme$(): Observable<string> {\n return this._appliedTheme$.asObservable();\n }\n\n /**\n * Current theme preference (synchronous access)\n */\n public get Preference(): string {\n return this._preference$.value;\n }\n\n /**\n * Currently applied theme ID (synchronous access)\n */\n public get AppliedTheme(): string {\n return this._appliedTheme$.value;\n }\n\n /**\n * Whether the service has been initialized\n */\n public get IsInitialized(): boolean {\n return this._initialized;\n }\n\n /**\n * All registered themes, for UI consumption (e.g. theme picker menus)\n */\n public get AvailableThemes(): ThemeDefinition[] {\n return Array.from(this.themeRegistry.values());\n }\n\n /**\n * Look up a theme definition by ID.\n * Returns undefined if the theme ID is not registered.\n */\n public GetThemeDefinition(id: string): ThemeDefinition | undefined {\n return this.themeRegistry.get(id);\n }\n\n // ========================================\n // THEME REGISTRATION\n // ========================================\n\n /**\n * Register a custom theme. If a theme with the same ID already exists,\n * it is replaced (allowing override of built-in themes if desired).\n */\n public RegisterTheme(theme: ThemeDefinition): void {\n this.themeRegistry.set(theme.Id, theme);\n }\n\n /**\n * Register multiple custom themes at once.\n */\n public RegisterThemes(themes: ThemeDefinition[]): void {\n for (const theme of themes) {\n this.RegisterTheme(theme);\n }\n }\n\n // ========================================\n // LIFECYCLE\n // ========================================\n\n /**\n * Initialize the theme service.\n * Call after login when UserInfoEngine is available.\n */\n public async Initialize(): Promise<void> {\n if (this._initialized) {\n return;\n }\n\n this.setupSystemThemeListener();\n\n const savedPreference = await this.loadSetting();\n this._preference$.next(savedPreference);\n\n const resolvedThemeId = this.resolveTheme(savedPreference);\n await this.applyTheme(resolvedThemeId);\n\n this._initialized = true;\n }\n\n /**\n * Set the theme preference and apply it.\n * @param preference - A registered theme ID or 'system'\n */\n public async SetTheme(preference: string): Promise<void> {\n if (preference === this._preference$.value) {\n return;\n }\n\n this._preference$.next(preference);\n\n const resolvedThemeId = this.resolveTheme(preference);\n await this.applyTheme(resolvedThemeId);\n\n await this.saveSetting(preference);\n }\n\n /**\n * Reset the service (call on logout)\n */\n public Reset(): void {\n if (this.systemMediaQuery && this.boundSystemThemeHandler) {\n this.systemMediaQuery.removeEventListener('change', this.boundSystemThemeHandler);\n }\n this.systemMediaQuery = null;\n this.boundSystemThemeHandler = null;\n\n this._preference$.next('system');\n this._appliedTheme$.next('light');\n this._initialized = false;\n\n // Remove theme attributes\n document.documentElement.removeAttribute('data-theme');\n document.documentElement.removeAttribute('data-theme-overlay');\n\n // Disable all custom CSS links\n this.disableAllCustomCss();\n }\n\n // ========================================\n // THEME APPLICATION\n // ========================================\n\n /**\n * Apply a resolved theme ID to the DOM.\n * Sets `data-theme` based on BaseTheme and `data-theme-overlay` for custom themes.\n * Loads custom CSS if needed, disables previous custom CSS.\n */\n private async applyTheme(themeId: string): Promise<void> {\n const themeDef = this.themeRegistry.get(themeId);\n\n // Fall back to 'light' if the theme ID isn't recognized\n if (!themeDef) {\n this.applyBuiltInTheme('light');\n this._appliedTheme$.next('light');\n return;\n }\n\n // Set the base theme attribute (drives existing [data-theme=\"dark\"] selectors)\n this.applyBaseThemeAttribute(themeDef.BaseTheme);\n\n if (themeDef.IsBuiltIn) {\n // Built-in theme: remove overlay, disable custom CSS\n document.documentElement.removeAttribute('data-theme-overlay');\n this.disableAllCustomCss();\n } else if (themeDef.CssUrl) {\n // Custom theme: load/enable CSS and set overlay attribute\n await this.loadThemeCss(themeDef);\n document.documentElement.setAttribute('data-theme-overlay', themeDef.Id);\n }\n\n this._appliedTheme$.next(themeId);\n }\n\n /**\n * Apply the base theme attribute to <html>.\n * 'dark' sets data-theme=\"dark\"; 'light' removes it (matching existing convention).\n */\n private applyBaseThemeAttribute(baseTheme: 'light' | 'dark'): void {\n if (baseTheme === 'dark') {\n document.documentElement.setAttribute('data-theme', 'dark');\n } else {\n document.documentElement.removeAttribute('data-theme');\n }\n }\n\n /**\n * Shorthand for built-in theme application (no custom CSS)\n */\n private applyBuiltInTheme(baseTheme: 'light' | 'dark'): void {\n this.applyBaseThemeAttribute(baseTheme);\n document.documentElement.removeAttribute('data-theme-overlay');\n this.disableAllCustomCss();\n }\n\n // ========================================\n // DYNAMIC CSS LOADING\n // ========================================\n\n /**\n * Load (or re-enable) a custom theme's CSS file.\n * Injects a <link> element into <head> with a data-mj-theme attribute.\n * Caches the link element to avoid re-downloading on theme switches.\n * Returns a Promise that resolves once the stylesheet is loaded.\n */\n private loadThemeCss(theme: ThemeDefinition): Promise<void> {\n if (!theme.CssUrl) {\n return Promise.resolve();\n }\n\n // Disable all other custom CSS first\n this.disableAllCustomCss();\n\n // Check cache — if already loaded, just re-enable\n const existingLink = this.loadedCssLinks.get(theme.Id);\n if (existingLink) {\n existingLink.disabled = false;\n return Promise.resolve();\n }\n\n // Create and inject new <link> element\n return new Promise<void>((resolve, reject) => {\n const link = document.createElement('link');\n link.rel = 'stylesheet';\n link.href = theme.CssUrl!;\n link.setAttribute('data-mj-theme', theme.Id);\n\n link.onload = () => resolve();\n link.onerror = () => {\n console.warn(`Failed to load theme CSS: ${theme.CssUrl}`);\n // Still resolve so the theme switch isn't blocked\n resolve();\n };\n\n document.head.appendChild(link);\n this.loadedCssLinks.set(theme.Id, link);\n });\n }\n\n /**\n * Disable (not remove) all custom theme CSS <link> elements.\n * Disabling rather than removing avoids re-downloading when switching back.\n */\n private disableAllCustomCss(): void {\n for (const link of this.loadedCssLinks.values()) {\n link.disabled = true;\n }\n }\n\n // ========================================\n // THEME RESOLUTION\n // ========================================\n\n /**\n * Resolve a preference string to an actual theme ID.\n * 'system' resolves to 'light' or 'dark' based on OS preference.\n * Unrecognized IDs fall back to 'light'.\n */\n private resolveTheme(preference: string): string {\n if (preference === 'system') {\n return this.getSystemTheme();\n }\n\n // If the preference is a registered theme, use it directly\n if (this.themeRegistry.has(preference)) {\n return preference;\n }\n\n // Unrecognized theme ID — fall back to light\n return 'light';\n }\n\n /**\n * Get system theme preference from OS\n */\n private getSystemTheme(): 'light' | 'dark' {\n if (typeof window === 'undefined') {\n return 'light';\n }\n return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';\n }\n\n /**\n * Setup listener for system theme changes\n */\n private setupSystemThemeListener(): void {\n if (typeof window === 'undefined') {\n return;\n }\n\n this.systemMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');\n this.boundSystemThemeHandler = () => this.onSystemThemeChange();\n this.systemMediaQuery.addEventListener('change', this.boundSystemThemeHandler);\n }\n\n /**\n * Handle system theme change (only applies if in 'system' mode)\n */\n private async onSystemThemeChange(): Promise<void> {\n if (this._preference$.value === 'system') {\n const newThemeId = this.getSystemTheme();\n await this.applyTheme(newThemeId);\n }\n }\n\n // ========================================\n // PERSISTENCE\n // ========================================\n\n /**\n * Load theme preference from User Settings.\n * Accepts any registered theme ID or 'system'.\n * Falls back to 'system' if saved value is not recognized.\n */\n private async loadSetting(): Promise<string> {\n try {\n const engine = UserInfoEngine.Instance;\n const settingValue = engine.GetSetting(THEME_SETTING_KEY);\n\n if (!settingValue) {\n return 'system';\n }\n\n // Accept 'system' or any registered theme ID\n if (settingValue === 'system' || this.themeRegistry.has(settingValue)) {\n return settingValue;\n }\n\n // Saved theme no longer registered — fall back\n return 'system';\n } catch (error) {\n console.warn('Failed to load theme setting:', error);\n return 'system';\n }\n }\n\n /**\n * Save theme preference to User Settings\n */\n private async saveSetting(preference: string): Promise<void> {\n try {\n const engine = UserInfoEngine.Instance;\n await engine.SetSetting(THEME_SETTING_KEY, preference);\n } catch (error) {\n console.warn('Failed to save theme setting:', error);\n }\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memberjunction/ng-shared-generic",
3
- "version": "5.4.0",
3
+ "version": "5.5.0",
4
4
  "description": "MemberJunction: Generic Angular Shared Package - utility services and reusable elements used in any Angular application.",
5
5
  "main": "./dist/public-api.js",
6
6
  "typings": "./dist/public-api.d.ts",
@@ -9,7 +9,9 @@
9
9
  ],
10
10
  "scripts": {
11
11
  "test": "echo \"No tests configured yet\"",
12
- "build": "ngc",
12
+ "build": "npm run clean && ngc && npm run copy-assets",
13
+ "copy-assets": "cpy 'src/lib/_tokens.scss' dist/lib --flat",
14
+ "clean": "rimraf dist",
13
15
  "test:watch": "vitest"
14
16
  },
15
17
  "keywords": [
@@ -29,8 +31,8 @@
29
31
  "rxjs": "^7.8.2"
30
32
  },
31
33
  "dependencies": {
32
- "@memberjunction/core": "5.4.0",
33
- "@memberjunction/core-entities": "5.4.0",
34
+ "@memberjunction/core": "5.5.0",
35
+ "@memberjunction/core-entities": "5.5.0",
34
36
  "tslib": "^2.8.1"
35
37
  },
36
38
  "sideEffects": false,