Add access control rework

This commit is contained in:
2026-04-02 03:18:37 +02:00
parent 314a26325f
commit 37cac95b38
111 changed files with 5355 additions and 1892 deletions

View File

@@ -1,10 +1,11 @@
import { DOCUMENT } from '@angular/common';
import { Injectable, inject, signal } from '@angular/core';
import {
SettingsModalService,
type SettingsPage
} from '../../../core/services/settings-modal.service';
Injectable,
inject,
signal
} from '@angular/core';
import { SettingsModalService, type SettingsPage } from '../../../core/services/settings-modal.service';
import { ThemeRegistryService } from './theme-registry.service';
@Injectable({ providedIn: 'root' })
@@ -13,7 +14,7 @@ export class ElementPickerService {
private readonly modal = inject(SettingsModalService);
private readonly registry = inject(ThemeRegistryService);
private removeListeners: Array<() => void> = [];
private removeListeners: (() => void)[] = [];
private resumePage: SettingsPage | null = null;
private shouldRestoreModalOnCancel = true;
@@ -69,7 +70,6 @@ export class ElementPickerService {
this.hoveredKey.set(key);
};
const onClick = (event: Event) => {
const key = this.resolveThemeKeyFromTarget(event.target);
@@ -86,7 +86,6 @@ export class ElementPickerService {
this.completePick(key);
};
const onKeyDown = (event: Event) => {
const keyboardEvent = event as KeyboardEvent;
@@ -129,4 +128,4 @@ export class ElementPickerService {
? key
: null;
}
}
}

View File

@@ -1,4 +1,8 @@
import { Injectable, computed, inject } from '@angular/core';
import {
Injectable,
computed,
inject
} from '@angular/core';
import {
ThemeContainerKey,
@@ -55,4 +59,4 @@ export class LayoutSyncService {
}
}, true, `${containerKey} restored to its default layout.`);
}
}
}

View File

@@ -1,4 +1,9 @@
import { Injectable, computed, inject, signal } from '@angular/core';
import {
Injectable,
computed,
inject,
signal
} from '@angular/core';
import type { SavedThemeSummary } from '../domain/theme.models';
import { ThemeLibraryStorageService } from '../infrastructure/theme-library.storage';
import { ThemeService } from './theme.service';
@@ -173,4 +178,4 @@ export class ThemeLibraryService {
this.selectedFileName.set(entries[0]?.fileName ?? null);
}
}
}

View File

@@ -1,9 +1,6 @@
import { Injectable, signal } from '@angular/core';
import {
ThemeLayoutContainerDefinition,
ThemeRegistryEntry
} from '../domain/theme.models';
import { ThemeLayoutContainerDefinition, ThemeRegistryEntry } from '../domain/theme.models';
import {
THEME_LAYOUT_CONTAINERS,
THEME_REGISTRY,
@@ -85,4 +82,4 @@ export class ThemeRegistryService {
this.mountedCounts.set(nextCounts);
}
}
}

View File

@@ -1,5 +1,10 @@
import { DOCUMENT } from '@angular/common';
import { Injectable, computed, inject, signal } from '@angular/core';
import {
Injectable,
computed,
inject,
signal
} from '@angular/core';
import {
ThemeAnimationDefinition,
@@ -13,12 +18,8 @@ import {
createDefaultThemeDocument,
isLegacyDefaultThemeDocument
} from '../domain/theme.defaults';
import {
createAnimationStarterDefinition
} from '../domain/theme.schema';
import {
findThemeLayoutContainer
} from '../domain/theme.registry';
import { createAnimationStarterDefinition } from '../domain/theme.schema';
import { findThemeLayoutContainer } from '../domain/theme.registry';
import { validateThemeDocument } from '../domain/theme.validation';
import {
loadThemeStorageSnapshot,
@@ -280,28 +281,71 @@ export class ThemeService {
styles['backgroundImage'] = backgroundLayers.join(', ');
}
if (elementTheme.width) styles['width'] = elementTheme.width;
if (elementTheme.height) styles['height'] = elementTheme.height;
if (elementTheme.minWidth) styles['minWidth'] = elementTheme.minWidth;
if (elementTheme.minHeight) styles['minHeight'] = elementTheme.minHeight;
if (elementTheme.maxWidth) styles['maxWidth'] = elementTheme.maxWidth;
if (elementTheme.maxHeight) styles['maxHeight'] = elementTheme.maxHeight;
if (elementTheme.position) styles['position'] = elementTheme.position;
if (elementTheme.top) styles['top'] = elementTheme.top;
if (elementTheme.right) styles['right'] = elementTheme.right;
if (elementTheme.bottom) styles['bottom'] = elementTheme.bottom;
if (elementTheme.left) styles['left'] = elementTheme.left;
if (elementTheme.padding) styles['padding'] = elementTheme.padding;
if (elementTheme.margin) styles['margin'] = elementTheme.margin;
if (elementTheme.border) styles['border'] = elementTheme.border;
if (elementTheme.borderRadius) styles['borderRadius'] = elementTheme.borderRadius;
if (elementTheme.backgroundColor) styles['backgroundColor'] = elementTheme.backgroundColor;
if (elementTheme.color) styles['color'] = elementTheme.color;
if (elementTheme.backgroundSize) styles['backgroundSize'] = elementTheme.backgroundSize;
if (elementTheme.backgroundPosition) styles['backgroundPosition'] = elementTheme.backgroundPosition;
if (elementTheme.backgroundRepeat) styles['backgroundRepeat'] = elementTheme.backgroundRepeat;
if (elementTheme.boxShadow) styles['boxShadow'] = elementTheme.boxShadow;
if (elementTheme.backdropFilter) styles['backdropFilter'] = elementTheme.backdropFilter;
if (elementTheme.width)
styles['width'] = elementTheme.width;
if (elementTheme.height)
styles['height'] = elementTheme.height;
if (elementTheme.minWidth)
styles['minWidth'] = elementTheme.minWidth;
if (elementTheme.minHeight)
styles['minHeight'] = elementTheme.minHeight;
if (elementTheme.maxWidth)
styles['maxWidth'] = elementTheme.maxWidth;
if (elementTheme.maxHeight)
styles['maxHeight'] = elementTheme.maxHeight;
if (elementTheme.position)
styles['position'] = elementTheme.position;
if (elementTheme.top)
styles['top'] = elementTheme.top;
if (elementTheme.right)
styles['right'] = elementTheme.right;
if (elementTheme.bottom)
styles['bottom'] = elementTheme.bottom;
if (elementTheme.left)
styles['left'] = elementTheme.left;
if (elementTheme.padding)
styles['padding'] = elementTheme.padding;
if (elementTheme.margin)
styles['margin'] = elementTheme.margin;
if (elementTheme.border)
styles['border'] = elementTheme.border;
if (elementTheme.borderRadius)
styles['borderRadius'] = elementTheme.borderRadius;
if (elementTheme.backgroundColor)
styles['backgroundColor'] = elementTheme.backgroundColor;
if (elementTheme.color)
styles['color'] = elementTheme.color;
if (elementTheme.backgroundSize)
styles['backgroundSize'] = elementTheme.backgroundSize;
if (elementTheme.backgroundPosition)
styles['backgroundPosition'] = elementTheme.backgroundPosition;
if (elementTheme.backgroundRepeat)
styles['backgroundRepeat'] = elementTheme.backgroundRepeat;
if (elementTheme.boxShadow)
styles['boxShadow'] = elementTheme.boxShadow;
if (elementTheme.backdropFilter)
styles['backdropFilter'] = elementTheme.backdropFilter;
if (typeof elementTheme.opacity === 'number') {
styles['opacity'] = `${elementTheme.opacity}`;
@@ -512,4 +556,4 @@ export class ThemeService {
this.statusTimeoutId = null;
}, 5000);
}
}
}

View File

@@ -1,8 +1,5 @@
import { DEFAULT_THEME_DOCUMENT } from './theme.defaults';
import {
THEME_LAYOUT_CONTAINERS,
THEME_REGISTRY
} from './theme.registry';
import { THEME_LAYOUT_CONTAINERS, THEME_REGISTRY } from './theme.registry';
import {
THEME_ANIMATION_FIELDS,
THEME_ELEMENT_STYLE_FIELDS,
@@ -168,4 +165,4 @@ export const THEME_LLM_GUIDE = [
'- Keep layout edits plausible for the declared container grid size.',
'- If a field is unsupported, omit it instead of guessing.',
'- If a section does not need changes, leave it empty rather than filling it with noise.'
].join('\n');
].join('\n');

View File

@@ -27,6 +27,7 @@ function createDefaultLayout(): Record<string, ThemeLayoutEntry> {
w: 1,
h: 1 }
};
continue;
}
@@ -40,6 +41,7 @@ function createDefaultLayout(): Record<string, ThemeLayoutEntry> {
w: (appShell?.columns ?? 20) - 1,
h: 1 }
};
continue;
}
@@ -51,6 +53,7 @@ function createDefaultLayout(): Record<string, ThemeLayoutEntry> {
w: 4,
h: 12 }
};
continue;
}
@@ -62,6 +65,7 @@ function createDefaultLayout(): Record<string, ThemeLayoutEntry> {
w: 12,
h: 12 }
};
continue;
}
@@ -87,16 +91,19 @@ function createDarkDefaultElements(): Record<string, ThemeElementStyles> {
color: 'hsl(var(--foreground))',
gradient: 'radial-gradient(circle at top, hsl(var(--surface-highlight) / 0.18), transparent 34%), linear-gradient(180deg, rgba(5, 8, 15, 0.98), rgba(9, 12, 20, 1))'
};
elements['serversRail'] = {
backgroundColor: 'hsl(var(--rail-background) / 0.96)',
gradient: 'linear-gradient(180deg, rgba(10, 14, 25, 0.92), rgba(6, 9, 16, 0.98))',
boxShadow: 'inset -1px 0 0 hsl(var(--border) / 0.82), 18px 0 38px rgba(0, 0, 0, 0.22)',
backdropFilter: 'var(--theme-effect-glass-blur)'
};
elements['appWorkspace'] = {
backgroundColor: 'hsl(var(--workspace-background))',
gradient: 'radial-gradient(circle at top right, hsl(var(--surface-highlight-alt) / 0.14), transparent 30%), linear-gradient(180deg, rgba(9, 12, 21, 0.96), rgba(7, 10, 18, 1))'
};
elements['titleBar'] = {
backgroundColor: 'hsl(var(--title-bar-background) / 0.82)',
color: 'hsl(var(--foreground))',
@@ -104,6 +111,7 @@ function createDarkDefaultElements(): Record<string, ThemeElementStyles> {
boxShadow: 'inset 0 -1px 0 hsl(var(--border) / 0.78), 0 12px 28px rgba(0, 0, 0, 0.18)',
backdropFilter: 'var(--theme-effect-glass-blur)'
};
elements['chatRoomChannelsPanel'] = {
backgroundColor: 'hsl(var(--panel-background) / 0.9)',
color: 'hsl(var(--foreground))',
@@ -113,6 +121,7 @@ function createDarkDefaultElements(): Record<string, ThemeElementStyles> {
boxShadow: 'var(--theme-effect-soft-shadow)',
backdropFilter: 'var(--theme-effect-glass-blur)'
};
elements['chatRoomMainPanel'] = {
backgroundColor: 'hsl(var(--panel-background) / 0.82)',
color: 'hsl(var(--foreground))',
@@ -122,6 +131,7 @@ function createDarkDefaultElements(): Record<string, ThemeElementStyles> {
boxShadow: 'var(--theme-effect-panel-shadow)',
backdropFilter: 'var(--theme-effect-glass-blur)'
};
elements['chatRoomMembersPanel'] = {
backgroundColor: 'hsl(var(--panel-background-alt) / 0.92)',
color: 'hsl(var(--foreground))',
@@ -131,6 +141,7 @@ function createDarkDefaultElements(): Record<string, ThemeElementStyles> {
boxShadow: 'var(--theme-effect-soft-shadow)',
backdropFilter: 'var(--theme-effect-glass-blur)'
};
elements['chatRoomEmptyState'] = {
backgroundColor: 'hsl(var(--panel-background-alt) / 0.88)',
color: 'hsl(var(--muted-foreground))',
@@ -139,6 +150,7 @@ function createDarkDefaultElements(): Record<string, ThemeElementStyles> {
gradient: 'radial-gradient(circle at top, hsl(var(--surface-highlight) / 0.08), transparent 45%)',
boxShadow: 'var(--theme-effect-soft-shadow)'
};
elements['voiceWorkspace'] = {
backgroundColor: 'hsl(var(--panel-background) / 0.74)',
color: 'hsl(var(--foreground))',
@@ -148,6 +160,7 @@ function createDarkDefaultElements(): Record<string, ThemeElementStyles> {
boxShadow: 'var(--theme-effect-panel-shadow)',
backdropFilter: 'var(--theme-effect-glass-blur)'
};
elements['floatingVoiceControls'] = {
backgroundColor: 'hsl(var(--panel-background-alt) / 0.94)',
color: 'hsl(var(--foreground))',
@@ -238,4 +251,4 @@ export function isLegacyDefaultThemeDocument(document: ThemeDocument): boolean {
}
export const DEFAULT_THEME_DOCUMENT: ThemeDocument = createDefaultThemeDocument();
export const DEFAULT_THEME_JSON = JSON.stringify(DEFAULT_THEME_DOCUMENT, null, 2);
export const DEFAULT_THEME_JSON = JSON.stringify(DEFAULT_THEME_DOCUMENT, null, 2);

View File

@@ -130,4 +130,4 @@ export interface ThemeSchemaField<T extends string = string> {
type: 'string' | 'number' | 'object';
example: string | number;
examples: readonly (string | number)[];
}
}

View File

@@ -1,7 +1,4 @@
import {
ThemeLayoutContainerDefinition,
ThemeRegistryEntry
} from './theme.models';
import { ThemeLayoutContainerDefinition, ThemeRegistryEntry } from './theme.models';
export const THEME_LAYOUT_CONTAINERS: readonly ThemeLayoutContainerDefinition[] = [
{
@@ -158,4 +155,4 @@ export function getPickerVisibleThemeKeys(): string[] {
return THEME_REGISTRY
.filter((entry) => entry.pickerVisible)
.map((entry) => entry.key);
}
}

View File

@@ -96,28 +96,44 @@ export const THEME_GRID_FIELDS: readonly ThemeSchemaField[] = [
description: 'Horizontal grid start column, zero-based.',
type: 'number',
example: 4,
examples: [0, 1, 4]
examples: [
0,
1,
4
]
},
{
key: 'y',
description: 'Vertical grid start row, zero-based.',
type: 'number',
example: 0,
examples: [0, 1, 6]
examples: [
0,
1,
6
]
},
{
key: 'w',
description: 'Grid width in columns.',
type: 'number',
example: 12,
examples: [1, 4, 12]
examples: [
1,
4,
12
]
},
{
key: 'h',
description: 'Grid height in rows.',
type: 'number',
example: 12,
examples: [1, 4, 12]
examples: [
1,
4,
12
]
}
];
@@ -127,14 +143,22 @@ export const THEME_ANIMATION_FIELDS: readonly ThemeSchemaField[] = [
description: 'Animation duration.',
type: 'string',
example: '240ms',
examples: ['200ms', '240ms', '600ms']
examples: [
'200ms',
'240ms',
'600ms'
]
},
{
key: 'easing',
description: 'Animation easing function.',
type: 'string',
example: 'ease-out',
examples: ['ease', 'ease-out', 'cubic-bezier(0.16, 1, 0.3, 1)']
examples: [
'ease',
'ease-out',
'cubic-bezier(0.16, 1, 0.3, 1)'
]
},
{
key: 'delay',
@@ -155,14 +179,23 @@ export const THEME_ANIMATION_FIELDS: readonly ThemeSchemaField[] = [
description: 'Animation fill behavior after running.',
type: 'string',
example: 'both',
examples: ['none', 'forwards', 'backwards', 'both']
examples: [
'none',
'forwards',
'backwards',
'both'
]
},
{
key: 'direction',
description: 'Animation direction.',
type: 'string',
example: 'normal',
examples: ['normal', 'reverse', 'alternate']
examples: [
'normal',
'reverse',
'alternate'
]
},
{
key: 'keyframes',
@@ -179,14 +212,22 @@ export const THEME_ELEMENT_STYLE_FIELDS: readonly ThemeSchemaField<ThemeElementS
description: 'CSS width applied to the selected element host.',
type: 'string',
example: '280px',
examples: ['280px', '20rem', 'min(24rem, 30vw)']
examples: [
'280px',
'20rem',
'min(24rem, 30vw)'
]
},
{
key: 'height',
description: 'CSS height applied to the selected element host.',
type: 'string',
example: '100%',
examples: ['100%', '22rem', 'calc(100vh - 4rem)']
examples: [
'100%',
'22rem',
'calc(100vh - 4rem)'
]
},
{
key: 'minWidth',
@@ -221,56 +262,89 @@ export const THEME_ELEMENT_STYLE_FIELDS: readonly ThemeSchemaField<ThemeElementS
description: 'CSS positioning mode for the host element.',
type: 'string',
example: 'relative',
examples: ['static', 'relative', 'absolute', 'sticky']
examples: [
'static',
'relative',
'absolute',
'sticky'
]
},
{
key: 'top',
description: 'CSS top inset used with positioned elements.',
type: 'string',
example: '12px',
examples: ['0', '12px', '2rem']
examples: [
'0',
'12px',
'2rem'
]
},
{
key: 'right',
description: 'CSS right inset used with positioned elements.',
type: 'string',
example: '12px',
examples: ['0', '12px', '2rem']
examples: [
'0',
'12px',
'2rem'
]
},
{
key: 'bottom',
description: 'CSS bottom inset used with positioned elements.',
type: 'string',
example: '12px',
examples: ['0', '12px', '2rem']
examples: [
'0',
'12px',
'2rem'
]
},
{
key: 'left',
description: 'CSS left inset used with positioned elements.',
type: 'string',
example: '12px',
examples: ['0', '12px', '2rem']
examples: [
'0',
'12px',
'2rem'
]
},
{
key: 'opacity',
description: 'Element opacity between 0 and 1.',
type: 'number',
example: 0.96,
examples: [0.72, 0.88, 1]
examples: [
0.72,
0.88,
1
]
},
{
key: 'padding',
description: 'CSS padding shorthand for internal spacing.',
type: 'string',
example: '12px',
examples: ['12px', '12px 16px', '1rem 1.25rem']
examples: [
'12px',
'12px 16px',
'1rem 1.25rem'
]
},
{
key: 'margin',
description: 'CSS margin shorthand for external spacing.',
type: 'string',
example: '0',
examples: ['0', '12px', '0 0 12px']
examples: [
'0',
'12px',
'0 0 12px'
]
},
{
key: 'border',
@@ -284,7 +358,11 @@ export const THEME_ELEMENT_STYLE_FIELDS: readonly ThemeSchemaField<ThemeElementS
description: 'CSS border radius shorthand.',
type: 'string',
example: '16px',
examples: ['12px', '16px', '999px']
examples: [
'12px',
'16px',
'999px'
]
},
{
key: 'backgroundColor',
@@ -312,21 +390,33 @@ export const THEME_ELEMENT_STYLE_FIELDS: readonly ThemeSchemaField<ThemeElementS
description: 'CSS background-size value.',
type: 'string',
example: 'cover',
examples: ['cover', 'contain', 'auto 100%']
examples: [
'cover',
'contain',
'auto 100%'
]
},
{
key: 'backgroundPosition',
description: 'CSS background-position value.',
type: 'string',
example: 'center',
examples: ['center', 'top left', '50% 20%']
examples: [
'center',
'top left',
'50% 20%'
]
},
{
key: 'backgroundRepeat',
description: 'CSS background-repeat value.',
type: 'string',
example: 'no-repeat',
examples: ['no-repeat', 'repeat', 'repeat-x']
examples: [
'no-repeat',
'repeat',
'repeat-x'
]
},
{
key: 'gradient',
@@ -429,4 +519,4 @@ export function createAnimationStarterDefinition(): ThemeDocument['animations'][
}
}
};
}
}

View File

@@ -12,15 +12,58 @@ import {
getLayoutEditableThemeKeys
} from './theme.registry';
const TOP_LEVEL_KEYS = ['meta', 'tokens', 'layout', 'elements', 'animations'] as const;
const META_KEYS = ['name', 'version', 'description'] as const;
const TOKEN_GROUP_KEYS = ['colors', 'spacing', 'radii', 'effects'] as const;
const TOP_LEVEL_KEYS = [
'meta',
'tokens',
'layout',
'elements',
'animations'
] as const;
const META_KEYS = [
'name',
'version',
'description'
] as const;
const TOKEN_GROUP_KEYS = [
'colors',
'spacing',
'radii',
'effects'
] as const;
const LAYOUT_ENTRY_KEYS = ['container', 'grid'] as const;
const GRID_KEYS = ['x', 'y', 'w', 'h'] as const;
const ANIMATION_KEYS = ['duration', 'easing', 'delay', 'iterationCount', 'fillMode', 'direction', 'keyframes'] as const;
const POSITION_VALUES = ['static', 'relative', 'absolute', 'sticky'] as const;
const FILL_MODE_VALUES = ['none', 'forwards', 'backwards', 'both'] as const;
const DIRECTION_VALUES = ['normal', 'reverse', 'alternate', 'alternate-reverse'] as const;
const GRID_KEYS = [
'x',
'y',
'w',
'h'
] as const;
const ANIMATION_KEYS = [
'duration',
'easing',
'delay',
'iterationCount',
'fillMode',
'direction',
'keyframes'
] as const;
const POSITION_VALUES = [
'static',
'relative',
'absolute',
'sticky'
] as const;
const FILL_MODE_VALUES = [
'none',
'forwards',
'backwards',
'both'
] as const;
const DIRECTION_VALUES = [
'normal',
'reverse',
'alternate',
'alternate-reverse'
] as const;
const SAFE_LINK_PROTOCOLS = ['http:', 'https:'] as const;
const SAFE_CLASS_PATTERN = /^[A-Za-z_][A-Za-z0-9_-]*$/;
const KEYFRAME_STEP_PATTERN = /^(from|to|(?:\d|[1-9]\d|100)%)$/;
@@ -185,6 +228,7 @@ function validateElementStyles(value: unknown, path: string, errors: string[]):
errors.push(`${path}.${key} must be a valid absolute URL.`);
}
}
continue;
}
@@ -192,6 +236,7 @@ function validateElementStyles(value: unknown, path: string, errors: string[]):
if (validateString(fieldValue, `${path}.${key}`, errors) && !SAFE_CLASS_PATTERN.test(fieldValue)) {
errors.push(`${path}.${key} must be a safe CSS class token.`);
}
continue;
}
@@ -449,4 +494,4 @@ export function validateThemeDocument(input: unknown): ThemeValidationResult {
errors: [],
value: normaliseThemeDocument(input as Partial<ThemeDocument>)
};
}
}

View File

@@ -132,4 +132,4 @@ export class ThemeGridEditorComponent {
private clamp(value: number, minimum: number, maximum: number): number {
return Math.min(Math.max(value, minimum), maximum);
}
}
}

View File

@@ -169,6 +169,7 @@ export class ThemeJsonCodeEditorComponent implements OnDestroy {
insert: nextValue
}
});
this.isApplyingExternalValue = false;
});
@@ -203,6 +204,7 @@ export class ThemeJsonCodeEditorComponent implements OnDestroy {
selection: EditorSelection.range(selectionStart, selectionEnd),
effects: EditorView.scrollIntoView(selectionStart, { y: 'center' })
});
this.editorView.focus();
}
@@ -242,4 +244,4 @@ export class ThemeJsonCodeEditorComponent implements OnDestroy {
});
});
}
}
}

View File

@@ -64,7 +64,7 @@ export class ThemeSettingsComponent {
readonly animationKeys = this.theme.knownAnimationClasses;
readonly layoutContainers = this.layoutSync.containers();
readonly themeEntries = this.registry.entries();
readonly workspaceTabs: ReadonlyArray<{ key: ThemeStudioWorkspace; label: string; description: string }> = [
readonly workspaceTabs: readonly { key: ThemeStudioWorkspace; label: string; description: string }[] = [
{
key: 'editor',
label: 'JSON Editor',
@@ -129,7 +129,8 @@ export class ThemeSettingsComponent {
return this.themeEntries.filter((entry) => entry.pickerVisible || entry.layoutEditable);
});
readonly filteredEntries = computed(() => {
const query = this.explorerQuery().trim().toLowerCase();
const query = this.explorerQuery().trim()
.toLowerCase();
if (!query) {
return this.mountedEntries();
@@ -470,4 +471,4 @@ export class ThemeSettingsComponent {
return text.indexOf(`"${key}"`, sectionIndex);
}
}
}

View File

@@ -4,7 +4,8 @@ import {
HostListener,
effect,
inject,
input
input,
OnDestroy
} from '@angular/core';
import { ExternalLinkService } from '../../../core/platform';
@@ -24,7 +25,7 @@ function looksLikeImageReference(value: string): boolean {
selector: '[appThemeNode]',
standalone: true
})
export class ThemeNodeDirective {
export class ThemeNodeDirective implements OnDestroy {
readonly themeKey = input.required<string>({ alias: 'appThemeNode' });
private readonly host = inject<ElementRef<HTMLElement>>(ElementRef);
@@ -245,4 +246,4 @@ export class ThemeNodeDirective {
iconTarget.style.backgroundImage = 'none';
iconTarget.textContent = '';
}
}
}

View File

@@ -55,4 +55,4 @@ export class ThemePickerOverlayComponent {
cancel(): void {
this.picker.cancel();
}
}
}

View File

@@ -10,4 +10,4 @@ export * from './domain/theme.schema';
export * from './domain/theme.validation';
export { ThemeNodeDirective } from './feature/theme-node.directive';
export { ThemePickerOverlayComponent } from './feature/theme-picker-overlay.component';
export { ThemePickerOverlayComponent } from './feature/theme-picker-overlay.component';

View File

@@ -203,7 +203,7 @@ export class ThemeLibraryStorageService {
isValid: true,
modifiedAt: file.modifiedAt,
themeName: result.value.meta.name,
version: result.value.meta.version,
version: result.value.meta.version
};
} catch (error) {
return {
@@ -218,4 +218,4 @@ export class ThemeLibraryStorageService {
};
}
}
}
}

View File

@@ -1,7 +1,4 @@
import {
STORAGE_KEY_THEME_ACTIVE,
STORAGE_KEY_THEME_DRAFT
} from '../../../core/constants';
import { STORAGE_KEY_THEME_ACTIVE, STORAGE_KEY_THEME_DRAFT } from '../../../core/constants';
export interface ThemeStorageSnapshot {
activeText: string | null;
@@ -41,4 +38,4 @@ export function saveActiveThemeText(text: string): void {
export function saveDraftThemeText(text: string): void {
writeStoredThemeText(STORAGE_KEY_THEME_DRAFT, text);
}
}