Add access control rework
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -130,4 +130,4 @@ export interface ThemeSchemaField<T extends string = string> {
|
||||
type: 'string' | 'number' | 'object';
|
||||
example: string | number;
|
||||
examples: readonly (string | number)[];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'][
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,4 +132,4 @@ export class ThemeGridEditorComponent {
|
||||
private clamp(value: number, minimum: number, maximum: number): number {
|
||||
return Math.min(Math.max(value, minimum), maximum);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,4 +55,4 @@ export class ThemePickerOverlayComponent {
|
||||
cancel(): void {
|
||||
this.picker.cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 {
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user