Fix lint, make design more consistent, add license texts,
All checks were successful
Queue Release Build / prepare (push) Successful in 11s
Deploy Web Apps / deploy (push) Successful in 14m0s
Queue Release Build / build-linux (push) Successful in 35m41s
Queue Release Build / build-windows (push) Successful in 28m53s
Queue Release Build / finalize (push) Successful in 2m6s
All checks were successful
Queue Release Build / prepare (push) Successful in 11s
Deploy Web Apps / deploy (push) Successful in 14m0s
Queue Release Build / build-linux (push) Successful in 35m41s
Queue Release Build / build-windows (push) Successful in 28m53s
Queue Release Build / finalize (push) Successful in 2m6s
This commit is contained in:
@@ -11,7 +11,7 @@
|
||||
</span>
|
||||
<button
|
||||
(click)="clearReply()"
|
||||
class="rounded p-1 hover:bg-secondary"
|
||||
class="grid h-6 w-6 place-items-center rounded transition-colors hover:bg-secondary"
|
||||
>
|
||||
<ng-icon
|
||||
name="lucideX"
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
<div class="flex flex-col gap-2">
|
||||
<button
|
||||
(click)="saveEdit()"
|
||||
class="rounded p-1 text-primary hover:bg-primary/10"
|
||||
class="grid h-6 w-6 place-items-center rounded text-primary transition-colors hover:bg-primary/10"
|
||||
>
|
||||
<ng-icon
|
||||
name="lucideCheck"
|
||||
@@ -64,7 +64,7 @@
|
||||
</button>
|
||||
<button
|
||||
(click)="cancelEdit()"
|
||||
class="rounded p-1 text-muted-foreground hover:bg-secondary"
|
||||
class="grid h-6 w-6 place-items-center rounded text-muted-foreground transition-colors hover:bg-secondary"
|
||||
>
|
||||
<ng-icon
|
||||
name="lucideX"
|
||||
@@ -108,7 +108,7 @@
|
||||
<div class="absolute right-2 top-2 flex gap-1 opacity-0 transition-opacity group-hover/img:opacity-100">
|
||||
<button
|
||||
(click)="openLightbox(att); $event.stopPropagation()"
|
||||
class="rounded-md bg-black/60 p-1.5 text-white backdrop-blur-sm transition-colors hover:bg-black/80"
|
||||
class="grid h-7 w-7 place-items-center rounded-md bg-black/60 text-white backdrop-blur-sm transition-colors hover:bg-black/80"
|
||||
title="View full size"
|
||||
>
|
||||
<ng-icon
|
||||
@@ -118,7 +118,7 @@
|
||||
</button>
|
||||
<button
|
||||
(click)="downloadAttachment(att); $event.stopPropagation()"
|
||||
class="rounded-md bg-black/60 p-1.5 text-white backdrop-blur-sm transition-colors hover:bg-black/80"
|
||||
class="grid h-7 w-7 place-items-center rounded-md bg-black/60 text-white backdrop-blur-sm transition-colors hover:bg-black/80"
|
||||
title="Download"
|
||||
>
|
||||
<ng-icon
|
||||
@@ -338,7 +338,7 @@
|
||||
<div class="relative">
|
||||
<button
|
||||
(click)="toggleEmojiPicker()"
|
||||
class="rounded-l-lg p-1.5 transition-colors hover:bg-secondary"
|
||||
class="grid h-8 w-8 place-items-center rounded-l-lg transition-colors hover:bg-secondary"
|
||||
>
|
||||
<ng-icon
|
||||
name="lucideSmile"
|
||||
@@ -362,7 +362,7 @@
|
||||
|
||||
<button
|
||||
(click)="requestReply()"
|
||||
class="p-1.5 transition-colors hover:bg-secondary"
|
||||
class="grid h-8 w-8 place-items-center transition-colors hover:bg-secondary"
|
||||
>
|
||||
<ng-icon
|
||||
name="lucideReply"
|
||||
@@ -373,7 +373,7 @@
|
||||
@if (isOwnMessage()) {
|
||||
<button
|
||||
(click)="startEdit()"
|
||||
class="p-1.5 transition-colors hover:bg-secondary"
|
||||
class="grid h-8 w-8 place-items-center transition-colors hover:bg-secondary"
|
||||
>
|
||||
<ng-icon
|
||||
name="lucideEdit"
|
||||
@@ -385,7 +385,7 @@
|
||||
@if (isOwnMessage() || isAdmin()) {
|
||||
<button
|
||||
(click)="requestDelete()"
|
||||
class="rounded-r-lg p-1.5 transition-colors hover:bg-destructive/10"
|
||||
class="grid h-8 w-8 place-items-center rounded-r-lg transition-colors hover:bg-destructive/10"
|
||||
>
|
||||
<ng-icon
|
||||
name="lucideTrash2"
|
||||
|
||||
@@ -55,7 +55,15 @@ const COMMON_EMOJIS = [
|
||||
'🔥',
|
||||
'👀'
|
||||
];
|
||||
const RICH_MARKDOWN_PATTERN = /(^|\n)(#{1,6}\s|>\s|[-*+]\s|\d+\.\s|```|~~~)|!\[[^\]]*\]\([^)]+\)|\[[^\]]+\]\([^)]+\)|`[^`\n]+`|\*\*[^*\n]+\*\*|__[^_\n]+__|\*[^*\n]+\*|_[^_\n]+_|(?:^|\n)\|.+\|/m;
|
||||
const RICH_MARKDOWN_PATTERNS = [
|
||||
/(^|\n)(#{1,6}\s|>\s|[-*+]\s|\d+\.\s|```|~~~)/m,
|
||||
/!\[[^\]]*\]\([^)]+\)/,
|
||||
/\[[^\]]+\]\([^)]+\)/,
|
||||
/`[^`\n]+`/,
|
||||
/\*\*[^*\n]+\*\*|__[^_\n]+__/,
|
||||
/\*[^*\n]+\*|_[^_\n]+_/,
|
||||
/(?:^|\n)\|.+\|/m
|
||||
];
|
||||
|
||||
interface ChatMessageAttachmentViewModel extends Attachment {
|
||||
isAudio: boolean;
|
||||
@@ -292,7 +300,7 @@ export class ChatMessageItemComponent {
|
||||
}
|
||||
|
||||
requiresRichMarkdown(content: string): boolean {
|
||||
return RICH_MARKDOWN_PATTERN.test(content);
|
||||
return RICH_MARKDOWN_PATTERNS.some((pattern) => pattern.test(content));
|
||||
}
|
||||
|
||||
formatBytes(bytes: number): string {
|
||||
|
||||
@@ -73,4 +73,4 @@ export class ChatMessageMarkdownComponent {
|
||||
|
||||
return PRISM_LANGUAGE_ALIASES[normalized] ?? normalized;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<div class="absolute right-3 top-3 flex gap-2">
|
||||
<button
|
||||
(click)="downloadAttachment(lightboxAttachment()!)"
|
||||
class="rounded-lg bg-black/60 p-2 text-white backdrop-blur-sm transition-colors hover:bg-black/80"
|
||||
class="grid h-9 w-9 place-items-center rounded-lg bg-black/60 text-white backdrop-blur-sm transition-colors hover:bg-black/80"
|
||||
title="Download"
|
||||
>
|
||||
<ng-icon
|
||||
@@ -30,7 +30,7 @@
|
||||
</button>
|
||||
<button
|
||||
(click)="closeLightbox()"
|
||||
class="rounded-lg bg-black/60 p-2 text-white backdrop-blur-sm transition-colors hover:bg-black/80"
|
||||
class="grid h-9 w-9 place-items-center rounded-lg bg-black/60 text-white backdrop-blur-sm transition-colors hover:bg-black/80"
|
||||
title="Close"
|
||||
>
|
||||
<ng-icon
|
||||
|
||||
@@ -80,21 +80,21 @@
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="grid grid-cols-2 gap-4 md:grid-cols-3 xl:grid-cols-4">
|
||||
<div class="columns-[12rem] gap-4">
|
||||
@for (gif of results(); track gif.id) {
|
||||
<button
|
||||
type="button"
|
||||
(click)="selectGif(gif)"
|
||||
class="group overflow-hidden rounded-2xl border border-border/80 bg-secondary/10 text-left shadow-sm transition-transform duration-200 hover:-translate-y-0.5 hover:border-primary/50 hover:bg-secondary/30"
|
||||
class="group mx-auto mb-4 inline-block w-full max-w-[15.5rem] break-inside-avoid align-top overflow-hidden rounded-2xl border border-border/80 bg-secondary/10 text-left shadow-sm transition-transform duration-200 hover:-translate-y-0.5 hover:border-primary/50 hover:bg-secondary/30"
|
||||
>
|
||||
<div
|
||||
class="relative overflow-hidden bg-secondary/30"
|
||||
[style.aspect-ratio]="gifAspectRatio(gif)"
|
||||
class="relative flex items-center justify-center overflow-hidden bg-secondary/30"
|
||||
[style.height.px]="gifCardHeight(gif)"
|
||||
>
|
||||
<img
|
||||
[appChatImageProxyFallback]="gif.previewUrl || gif.url"
|
||||
[alt]="gif.title || 'KLIPY GIF'"
|
||||
class="h-full w-full object-cover transition-transform duration-200 group-hover:scale-[1.03]"
|
||||
class="h-full w-full object-contain p-1.5 transition-transform duration-200 group-hover:scale-[1.03]"
|
||||
loading="lazy"
|
||||
/>
|
||||
<span
|
||||
|
||||
@@ -23,6 +23,12 @@ import {
|
||||
import { KlipyGif, KlipyService } from '../../application/klipy.service';
|
||||
import { ChatImageProxyFallbackDirective } from '../chat-image-proxy-fallback.directive';
|
||||
|
||||
const KLIPY_CARD_MIN_WIDTH = 140;
|
||||
const KLIPY_CARD_MAX_WIDTH = 248;
|
||||
const KLIPY_CARD_MIN_HEIGHT = 104;
|
||||
const KLIPY_CARD_MAX_HEIGHT = 220;
|
||||
const KLIPY_CARD_FALLBACK_SIZE = 160;
|
||||
|
||||
@Component({
|
||||
selector: 'app-klipy-gif-picker',
|
||||
standalone: true,
|
||||
@@ -106,12 +112,8 @@ export class KlipyGifPickerComponent implements OnInit, AfterViewInit, OnDestroy
|
||||
this.closed.emit(undefined);
|
||||
}
|
||||
|
||||
gifAspectRatio(gif: KlipyGif): string {
|
||||
if (gif.width > 0 && gif.height > 0) {
|
||||
return `${gif.width} / ${gif.height}`;
|
||||
}
|
||||
|
||||
return '1 / 1';
|
||||
gifCardHeight(gif: KlipyGif): number {
|
||||
return this.getGifCardSize(gif).height;
|
||||
}
|
||||
|
||||
private async loadResults(reset: boolean): Promise<void> {
|
||||
@@ -182,4 +184,32 @@ export class KlipyGifPickerComponent implements OnInit, AfterViewInit, OnDestroy
|
||||
this.searchTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
private getGifCardSize(gif: KlipyGif): { width: number; height: number } {
|
||||
if (gif.width <= 0 || gif.height <= 0) {
|
||||
return {
|
||||
width: KLIPY_CARD_FALLBACK_SIZE,
|
||||
height: KLIPY_CARD_FALLBACK_SIZE
|
||||
};
|
||||
}
|
||||
|
||||
const maxScale = Math.min(
|
||||
KLIPY_CARD_MAX_WIDTH / gif.width,
|
||||
KLIPY_CARD_MAX_HEIGHT / gif.height
|
||||
);
|
||||
const minScale = Math.max(
|
||||
KLIPY_CARD_MIN_WIDTH / gif.width,
|
||||
KLIPY_CARD_MIN_HEIGHT / gif.height
|
||||
);
|
||||
const scale = minScale <= maxScale
|
||||
? Math.min(maxScale, Math.max(minScale, 1))
|
||||
: maxScale;
|
||||
const scaledWidth = Math.round(gif.width * scale);
|
||||
const scaledHeight = Math.round(gif.height * scale);
|
||||
|
||||
return {
|
||||
width: Math.min(KLIPY_CARD_MAX_WIDTH, Math.max(KLIPY_CARD_MIN_WIDTH, scaledWidth)),
|
||||
height: Math.min(KLIPY_CARD_MAX_HEIGHT, Math.max(KLIPY_CARD_MIN_HEIGHT, scaledHeight))
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="space-y-6">
|
||||
<section class="rounded-xl border border-border bg-secondary/20 p-5">
|
||||
<section class="rounded-lg border border-border bg-secondary/20 p-5">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="rounded-xl bg-primary/10 p-2 text-primary">
|
||||
<div class="grid h-9 w-9 shrink-0 place-items-center rounded-lg bg-primary/10 text-primary">
|
||||
<ng-icon
|
||||
name="lucideBell"
|
||||
class="h-5 w-5"
|
||||
@@ -91,9 +91,9 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="rounded-xl border border-border bg-card p-5">
|
||||
<section class="rounded-lg border border-border bg-card p-5">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="rounded-xl bg-secondary p-2 text-muted-foreground">
|
||||
<div class="grid h-9 w-9 shrink-0 place-items-center rounded-lg bg-secondary text-muted-foreground">
|
||||
<ng-icon
|
||||
[name]="enabled() ? 'lucideBell' : 'lucideBellOff'"
|
||||
class="h-5 w-5"
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
<button
|
||||
(click)="toggleFullscreen()"
|
||||
type="button"
|
||||
class="p-2 bg-white/10 hover:bg-white/20 rounded-lg transition-colors"
|
||||
class="grid h-9 w-9 place-items-center rounded-lg bg-white/10 transition-colors hover:bg-white/20"
|
||||
>
|
||||
@if (isFullscreen()) {
|
||||
<ng-icon
|
||||
@@ -62,7 +62,7 @@
|
||||
<button
|
||||
(click)="stopSharing()"
|
||||
type="button"
|
||||
class="p-2 bg-destructive hover:bg-destructive/90 rounded-lg transition-colors"
|
||||
class="grid h-9 w-9 place-items-center rounded-lg bg-destructive transition-colors hover:bg-destructive/90"
|
||||
title="Stop sharing"
|
||||
>
|
||||
<ng-icon
|
||||
@@ -74,7 +74,7 @@
|
||||
<button
|
||||
(click)="stopWatching()"
|
||||
type="button"
|
||||
class="p-2 bg-destructive hover:bg-destructive/90 rounded-lg transition-colors"
|
||||
class="grid h-9 w-9 place-items-center rounded-lg bg-destructive transition-colors hover:bg-destructive/90"
|
||||
title="Stop watching"
|
||||
>
|
||||
<ng-icon
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
<button
|
||||
(click)="openSettings()"
|
||||
type="button"
|
||||
class="p-2 bg-secondary hover:bg-secondary/80 rounded-lg border border-border transition-colors"
|
||||
class="grid h-9 w-9 place-items-center rounded-lg border border-border bg-secondary transition-colors hover:bg-secondary/80"
|
||||
title="Settings"
|
||||
>
|
||||
<ng-icon
|
||||
|
||||
@@ -10,18 +10,18 @@ import { ThemeRegistryService } from './theme-registry.service';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ElementPickerService {
|
||||
private readonly documentRef = inject(DOCUMENT);
|
||||
private readonly modal = inject(SettingsModalService);
|
||||
private readonly registry = inject(ThemeRegistryService);
|
||||
readonly isPicking = signal(false);
|
||||
readonly hoveredKey = signal<string | null>(null);
|
||||
readonly selectedKey = signal<string | null>(null);
|
||||
|
||||
readonly documentRef = inject(DOCUMENT);
|
||||
readonly modal = inject(SettingsModalService);
|
||||
readonly registry = inject(ThemeRegistryService);
|
||||
|
||||
private removeListeners: (() => void)[] = [];
|
||||
private resumePage: SettingsPage | null = null;
|
||||
private shouldRestoreModalOnCancel = true;
|
||||
|
||||
readonly isPicking = signal(false);
|
||||
readonly hoveredKey = signal<string | null>(null);
|
||||
readonly selectedKey = signal<string | null>(null);
|
||||
|
||||
start(page: SettingsPage = 'theme', restoreModalOnCancel = true): void {
|
||||
if (this.isPicking()) {
|
||||
return;
|
||||
|
||||
@@ -15,11 +15,11 @@ import { ThemeService } from './theme.service';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class LayoutSyncService {
|
||||
private readonly registry = inject(ThemeRegistryService);
|
||||
private readonly theme = inject(ThemeService);
|
||||
|
||||
readonly draftLayout = computed(() => this.theme.draftTheme().layout);
|
||||
|
||||
readonly registry = inject(ThemeRegistryService);
|
||||
readonly theme = inject(ThemeService);
|
||||
|
||||
containers() {
|
||||
return this.registry.layoutContainers();
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@ import { ThemeService } from './theme.service';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ThemeLibraryService {
|
||||
private readonly storage = inject(ThemeLibraryStorageService);
|
||||
private readonly theme = inject(ThemeService);
|
||||
readonly storage = inject(ThemeLibraryStorageService);
|
||||
readonly theme = inject(ThemeService);
|
||||
|
||||
readonly isAvailable = signal(this.storage.isAvailable);
|
||||
readonly entries = signal<SavedThemeSummary[]>([]);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import {
|
||||
Injectable,
|
||||
Signal,
|
||||
computed,
|
||||
inject,
|
||||
signal
|
||||
@@ -44,8 +45,44 @@ function resolveBuiltInDefaultMigration(document: ThemeDocument): ThemeDocument
|
||||
: document;
|
||||
}
|
||||
|
||||
const hostStylePropertyKeys = [
|
||||
'width',
|
||||
'height',
|
||||
'minWidth',
|
||||
'minHeight',
|
||||
'maxWidth',
|
||||
'maxHeight',
|
||||
'position',
|
||||
'top',
|
||||
'right',
|
||||
'bottom',
|
||||
'left',
|
||||
'padding',
|
||||
'margin',
|
||||
'border',
|
||||
'borderRadius',
|
||||
'backgroundColor',
|
||||
'color',
|
||||
'backgroundSize',
|
||||
'backgroundPosition',
|
||||
'backgroundRepeat',
|
||||
'boxShadow',
|
||||
'backdropFilter'
|
||||
] as const satisfies readonly (keyof ThemeElementStyles)[];
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ThemeService {
|
||||
readonly activeTheme: Signal<ThemeDocument>;
|
||||
readonly activeThemeText: Signal<string>;
|
||||
readonly draftTheme: Signal<ThemeDocument>;
|
||||
readonly draftText: Signal<string>;
|
||||
readonly draftIsValid: Signal<boolean>;
|
||||
readonly draftErrors: Signal<string[]>;
|
||||
readonly statusMessage: Signal<string | null>;
|
||||
readonly activeThemeName: Signal<string>;
|
||||
readonly knownAnimationClasses: Signal<string[]>;
|
||||
readonly isDraftDirty: Signal<boolean>;
|
||||
|
||||
private readonly documentRef = inject(DOCUMENT);
|
||||
|
||||
private readonly activeThemeInternal = signal<ThemeDocument>(createDefaultThemeDocument());
|
||||
@@ -60,18 +97,20 @@ export class ThemeService {
|
||||
private statusTimeoutId: ReturnType<typeof setTimeout> | null = null;
|
||||
private animationStyleElement: HTMLStyleElement | null = null;
|
||||
|
||||
readonly activeTheme = this.activeThemeInternal.asReadonly();
|
||||
readonly activeThemeText = this.activeThemeTextInternal.asReadonly();
|
||||
readonly draftTheme = this.draftThemeInternal.asReadonly();
|
||||
readonly draftText = this.draftTextInternal.asReadonly();
|
||||
readonly draftIsValid = this.draftIsValidInternal.asReadonly();
|
||||
readonly draftErrors = this.draftErrorsInternal.asReadonly();
|
||||
readonly statusMessage = this.statusMessageInternal.asReadonly();
|
||||
readonly activeThemeName = computed(() => this.activeThemeInternal().meta.name);
|
||||
readonly knownAnimationClasses = computed(() => Object.keys(this.draftThemeInternal().animations));
|
||||
readonly isDraftDirty = computed(() => {
|
||||
return this.draftTextInternal().trim() !== this.activeThemeTextInternal().trim();
|
||||
});
|
||||
constructor() {
|
||||
this.activeTheme = this.activeThemeInternal.asReadonly();
|
||||
this.activeThemeText = this.activeThemeTextInternal.asReadonly();
|
||||
this.draftTheme = this.draftThemeInternal.asReadonly();
|
||||
this.draftText = this.draftTextInternal.asReadonly();
|
||||
this.draftIsValid = this.draftIsValidInternal.asReadonly();
|
||||
this.draftErrors = this.draftErrorsInternal.asReadonly();
|
||||
this.statusMessage = this.statusMessageInternal.asReadonly();
|
||||
this.activeThemeName = computed(() => this.activeThemeInternal().meta.name);
|
||||
this.knownAnimationClasses = computed(() => Object.keys(this.draftThemeInternal().animations));
|
||||
this.isDraftDirty = computed(() => {
|
||||
return this.draftTextInternal().trim() !== this.activeThemeTextInternal().trim();
|
||||
});
|
||||
}
|
||||
|
||||
initialize(): void {
|
||||
if (this.initialized) {
|
||||
@@ -281,71 +320,13 @@ export class ThemeService {
|
||||
styles['backgroundImage'] = backgroundLayers.join(', ');
|
||||
}
|
||||
|
||||
if (elementTheme.width)
|
||||
styles['width'] = elementTheme.width;
|
||||
for (const property of hostStylePropertyKeys) {
|
||||
const value = elementTheme[property];
|
||||
|
||||
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 (value) {
|
||||
styles[property] = value;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof elementTheme.opacity === 'number') {
|
||||
styles['opacity'] = `${elementTheme.opacity}`;
|
||||
|
||||
@@ -31,6 +31,28 @@ function describeCapabilities(entry: (typeof THEME_REGISTRY)[number]): string {
|
||||
: 'visual style overrides only';
|
||||
}
|
||||
|
||||
function describeLayoutContainer(container: (typeof THEME_LAYOUT_CONTAINERS)[number]): string {
|
||||
const layoutKeys = getLayoutKeysForContainer(container.key);
|
||||
|
||||
return [
|
||||
`- ${container.key}: ${container.columns} columns x ${container.rows} rows.`,
|
||||
container.description,
|
||||
`Layout keys: ${layoutKeys.join(', ')}.`
|
||||
].join(' ');
|
||||
}
|
||||
|
||||
function describeThemeEntry(entry: (typeof THEME_REGISTRY)[number]): string {
|
||||
const container = entry.container ?? 'none';
|
||||
|
||||
return [
|
||||
`- ${entry.key}: ${entry.label}.`,
|
||||
`Category=${entry.category}.`,
|
||||
`Container=${container}.`,
|
||||
entry.description,
|
||||
`Supported extras: ${describeCapabilities(entry)}.`
|
||||
].join(' ');
|
||||
}
|
||||
|
||||
const colorTokenKeys = Object.keys(DEFAULT_THEME_DOCUMENT.tokens.colors);
|
||||
const radiusTokenKeys = Object.keys(DEFAULT_THEME_DOCUMENT.tokens.radii);
|
||||
const effectTokenKeys = Object.keys(DEFAULT_THEME_DOCUMENT.tokens.effects);
|
||||
@@ -132,19 +154,11 @@ export const THEME_LLM_GUIDE = [
|
||||
'- animations: Record<string, { duration?, easing?, delay?, iterationCount?, fillMode?, direction?, keyframes? }>',
|
||||
'',
|
||||
'Layout containers',
|
||||
...THEME_LAYOUT_CONTAINERS.map((container) => {
|
||||
const layoutKeys = getLayoutKeysForContainer(container.key);
|
||||
|
||||
return `- ${container.key}: ${container.columns} columns x ${container.rows} rows. ${container.description} Layout keys: ${layoutKeys.join(', ')}.`;
|
||||
}),
|
||||
...THEME_LAYOUT_CONTAINERS.map((container) => describeLayoutContainer(container)),
|
||||
`- Only these keys should normally appear in layout: ${layoutEditableKeys.join(', ')}.`,
|
||||
'',
|
||||
'Registered theme element keys',
|
||||
...THEME_REGISTRY.map((entry) => {
|
||||
const container = entry.container ?? 'none';
|
||||
|
||||
return `- ${entry.key}: ${entry.label}. Category=${entry.category}. Container=${container}. ${entry.description} Supported extras: ${describeCapabilities(entry)}.`;
|
||||
}),
|
||||
...THEME_REGISTRY.map((entry) => describeThemeEntry(entry)),
|
||||
'',
|
||||
'Supported element style fields',
|
||||
...THEME_ELEMENT_STYLE_FIELDS.map((field) => {
|
||||
|
||||
@@ -9,6 +9,27 @@ import {
|
||||
getLayoutEditableThemeKeys
|
||||
} from './theme.registry';
|
||||
|
||||
const APP_ROOT_BASE_GRADIENT =
|
||||
'radial-gradient(circle at top, '
|
||||
+ 'hsl(var(--surface-highlight) / 0.18), transparent 34%)';
|
||||
const APP_ROOT_OVERLAY_GRADIENT = 'linear-gradient(180deg, rgba(5, 8, 15, 0.98), rgba(9, 12, 20, 1))';
|
||||
const APP_ROOT_GRADIENT_LAYERS = [APP_ROOT_BASE_GRADIENT, APP_ROOT_OVERLAY_GRADIENT] as const;
|
||||
const APP_WORKSPACE_BASE_GRADIENT =
|
||||
'radial-gradient(circle at top right, '
|
||||
+ 'hsl(var(--surface-highlight-alt) / 0.14), transparent 30%)';
|
||||
const APP_WORKSPACE_OVERLAY_GRADIENT = 'linear-gradient(180deg, rgba(9, 12, 21, 0.96), rgba(7, 10, 18, 1))';
|
||||
const APP_WORKSPACE_GRADIENT_LAYERS = [APP_WORKSPACE_BASE_GRADIENT, APP_WORKSPACE_OVERLAY_GRADIENT] as const;
|
||||
const CHAT_ROOM_MAIN_PANEL_BASE_GRADIENT =
|
||||
'radial-gradient(circle at top, '
|
||||
+ 'hsl(var(--surface-highlight) / 0.12), transparent 28%)';
|
||||
const CHAT_ROOM_MAIN_PANEL_OVERLAY_GRADIENT = 'linear-gradient(180deg, rgba(16, 20, 34, 0.82), rgba(8, 11, 19, 0.92))';
|
||||
const CHAT_ROOM_MAIN_PANEL_GRADIENT_LAYERS = [CHAT_ROOM_MAIN_PANEL_BASE_GRADIENT, CHAT_ROOM_MAIN_PANEL_OVERLAY_GRADIENT] as const;
|
||||
const VOICE_WORKSPACE_BASE_GRADIENT =
|
||||
'radial-gradient(circle at top right, '
|
||||
+ 'hsl(var(--surface-highlight) / 0.14), transparent 32%)';
|
||||
const VOICE_WORKSPACE_OVERLAY_GRADIENT = 'linear-gradient(180deg, rgba(18, 23, 37, 0.78), rgba(9, 12, 21, 0.88))';
|
||||
const VOICE_WORKSPACE_GRADIENT_LAYERS = [VOICE_WORKSPACE_BASE_GRADIENT, VOICE_WORKSPACE_OVERLAY_GRADIENT] as const;
|
||||
|
||||
function createDefaultElements(): Record<string, ThemeElementStyles> {
|
||||
return Object.fromEntries(
|
||||
THEME_REGISTRY.map((entry) => [entry.key, {}])
|
||||
@@ -89,7 +110,7 @@ function createDarkDefaultElements(): Record<string, ThemeElementStyles> {
|
||||
elements['appRoot'] = {
|
||||
backgroundColor: 'hsl(var(--background))',
|
||||
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))'
|
||||
gradient: APP_ROOT_GRADIENT_LAYERS.join(', ')
|
||||
};
|
||||
|
||||
elements['serversRail'] = {
|
||||
@@ -101,14 +122,14 @@ function createDarkDefaultElements(): Record<string, ThemeElementStyles> {
|
||||
|
||||
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))'
|
||||
gradient: APP_WORKSPACE_GRADIENT_LAYERS.join(', ')
|
||||
};
|
||||
|
||||
elements['titleBar'] = {
|
||||
backgroundColor: 'hsl(var(--title-bar-background) / 0.82)',
|
||||
color: 'hsl(var(--foreground))',
|
||||
gradient: 'linear-gradient(180deg, rgba(20, 26, 41, 0.52), rgba(7, 10, 18, 0.18))',
|
||||
boxShadow: 'inset 0 -1px 0 hsl(var(--border) / 0.78), 0 12px 28px rgba(0, 0, 0, 0.18)',
|
||||
boxShadow: ['inset 0 -1px 0 hsl(var(--border) / 0.78)', '0 12px 28px rgba(0, 0, 0, 0.18)'].join(', '),
|
||||
backdropFilter: 'var(--theme-effect-glass-blur)'
|
||||
};
|
||||
|
||||
@@ -127,7 +148,7 @@ function createDarkDefaultElements(): Record<string, ThemeElementStyles> {
|
||||
color: 'hsl(var(--foreground))',
|
||||
border: '1px solid hsl(var(--border) / 0.62)',
|
||||
borderRadius: 'var(--theme-radius-surface)',
|
||||
gradient: 'radial-gradient(circle at top, hsl(var(--surface-highlight) / 0.12), transparent 28%), linear-gradient(180deg, rgba(16, 20, 34, 0.82), rgba(8, 11, 19, 0.92))',
|
||||
gradient: CHAT_ROOM_MAIN_PANEL_GRADIENT_LAYERS.join(', '),
|
||||
boxShadow: 'var(--theme-effect-panel-shadow)',
|
||||
backdropFilter: 'var(--theme-effect-glass-blur)'
|
||||
};
|
||||
@@ -156,7 +177,7 @@ function createDarkDefaultElements(): Record<string, ThemeElementStyles> {
|
||||
color: 'hsl(var(--foreground))',
|
||||
border: '1px solid hsl(var(--border) / 0.62)',
|
||||
borderRadius: 'calc(var(--theme-radius-surface) + 0.2rem)',
|
||||
gradient: 'radial-gradient(circle at top right, hsl(var(--surface-highlight) / 0.14), transparent 32%), linear-gradient(180deg, rgba(18, 23, 37, 0.78), rgba(9, 12, 21, 0.88))',
|
||||
gradient: VOICE_WORKSPACE_GRADIENT_LAYERS.join(', '),
|
||||
boxShadow: 'var(--theme-effect-panel-shadow)',
|
||||
backdropFilter: 'var(--theme-effect-glass-blur)'
|
||||
};
|
||||
|
||||
@@ -4,6 +4,10 @@ import {
|
||||
ThemeSchemaField
|
||||
} from './theme.models';
|
||||
|
||||
const RADIAL_GRADIENT_EXAMPLE =
|
||||
'radial-gradient(circle at top, rgba(255,255,255,0.12), '
|
||||
+ 'transparent 60%)';
|
||||
|
||||
export const THEME_TOP_LEVEL_FIELDS: readonly ThemeSchemaField[] = [
|
||||
{
|
||||
key: 'meta',
|
||||
@@ -423,7 +427,7 @@ export const THEME_ELEMENT_STYLE_FIELDS: readonly ThemeSchemaField<ThemeElementS
|
||||
description: 'CSS gradient layered above any background image.',
|
||||
type: 'string',
|
||||
example: 'linear-gradient(180deg, rgba(0,0,0,0.12), rgba(0,0,0,0.72))',
|
||||
examples: ['linear-gradient(180deg, rgba(0,0,0,0.12), rgba(0,0,0,0.72))', 'radial-gradient(circle at top, rgba(255,255,255,0.12), transparent 60%)']
|
||||
examples: ['linear-gradient(180deg, rgba(0,0,0,0.12), rgba(0,0,0,0.72))', RADIAL_GRADIENT_EXAMPLE]
|
||||
},
|
||||
{
|
||||
key: 'boxShadow',
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
<div class="theme-grid-editor rounded-2xl border border-border bg-background/50 p-3">
|
||||
<div class="theme-grid-editor rounded-lg border border-border bg-card/40 p-3">
|
||||
<div class="mb-3 flex items-center justify-between gap-3 px-1">
|
||||
<div>
|
||||
<p class="text-sm font-semibold text-foreground">{{ container().label }}</p>
|
||||
<p class="text-xs text-muted-foreground">{{ container().description }}</p>
|
||||
</div>
|
||||
|
||||
<div class="rounded-full bg-secondary px-2.5 py-1 text-[11px] font-medium text-muted-foreground">
|
||||
<div class="rounded-md bg-secondary px-2.5 py-1 text-[11px] font-medium text-muted-foreground">
|
||||
{{ container().columns }} cols x {{ container().rows }} rows
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
#canvasRef
|
||||
class="theme-grid-editor__frame relative overflow-hidden rounded-xl border border-border/80"
|
||||
class="theme-grid-editor__frame relative overflow-hidden rounded-lg border border-border/80"
|
||||
[ngStyle]="frameStyle()"
|
||||
>
|
||||
<div class="theme-grid-editor__grid"></div>
|
||||
@@ -40,7 +40,7 @@
|
||||
<p class="mt-1 line-clamp-2 text-[11px] leading-4 text-muted-foreground">{{ item.description }}</p>
|
||||
</div>
|
||||
|
||||
<div class="rounded-full bg-background/80 px-2 py-0.5 text-[10px] font-medium text-muted-foreground shadow-sm">
|
||||
<div class="rounded-md bg-background px-2 py-0.5 text-[10px] font-medium text-muted-foreground shadow-sm">
|
||||
{{ item.grid.x }},{{ item.grid.y }} · {{ item.grid.w }}x{{ item.grid.h }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -57,7 +57,7 @@
|
||||
|
||||
@if (disabled()) {
|
||||
<div
|
||||
class="theme-grid-editor__disabled absolute inset-0 flex items-center justify-center rounded-xl bg-background/75 px-6 text-center text-sm text-muted-foreground backdrop-blur-sm"
|
||||
class="theme-grid-editor__disabled absolute inset-0 flex items-center justify-center rounded-lg bg-background/75 px-6 text-center text-sm text-muted-foreground backdrop-blur-sm"
|
||||
>
|
||||
Fix JSON validation errors to re-enable the grid editor.
|
||||
</div>
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
.theme-grid-editor__frame {
|
||||
aspect-ratio: 16 / 9;
|
||||
background:
|
||||
radial-gradient(circle at top, hsl(var(--primary) / 0.08), transparent 45%),
|
||||
linear-gradient(180deg, hsl(var(--background) / 0.96), hsl(var(--card) / 0.98));
|
||||
radial-gradient(circle at top, hsl(var(--primary) / 0.05), transparent 45%),
|
||||
linear-gradient(180deg, hsl(var(--background) / 0.98), hsl(var(--card) / 0.98));
|
||||
}
|
||||
|
||||
.theme-grid-editor__grid {
|
||||
@@ -27,11 +27,9 @@
|
||||
.theme-grid-editor__item-body {
|
||||
height: 100%;
|
||||
border: 1px solid hsl(var(--border) / 0.8);
|
||||
border-radius: 1rem;
|
||||
background:
|
||||
linear-gradient(180deg, hsl(var(--card) / 0.96), hsl(var(--background) / 0.96)),
|
||||
radial-gradient(circle at top right, hsl(var(--primary) / 0.1), transparent 45%);
|
||||
box-shadow: 0 12px 30px rgb(0 0 0 / 10%);
|
||||
border-radius: 0.5rem;
|
||||
background: linear-gradient(180deg, hsl(var(--card) / 0.98), hsl(var(--background) / 0.98));
|
||||
box-shadow: 0 1px 2px rgb(0 0 0 / 8%);
|
||||
cursor: grab;
|
||||
padding: 0.9rem;
|
||||
}
|
||||
@@ -42,9 +40,7 @@
|
||||
|
||||
.theme-grid-editor__item--selected .theme-grid-editor__item-body {
|
||||
border-color: hsl(var(--primary));
|
||||
box-shadow:
|
||||
0 0 0 1px hsl(var(--primary)),
|
||||
0 14px 34px hsl(var(--primary) / 0.18);
|
||||
box-shadow: 0 0 0 1px hsl(var(--primary));
|
||||
}
|
||||
|
||||
.theme-grid-editor__item--disabled .theme-grid-editor__item-body {
|
||||
@@ -56,9 +52,7 @@
|
||||
}
|
||||
|
||||
.theme-grid-editor__item:focus-visible .theme-grid-editor__item-body {
|
||||
box-shadow:
|
||||
0 0 0 2px hsl(var(--primary)),
|
||||
0 14px 34px hsl(var(--primary) / 0.18);
|
||||
box-shadow: 0 0 0 2px hsl(var(--primary) / 0.25);
|
||||
}
|
||||
|
||||
.theme-grid-editor__handle {
|
||||
@@ -68,12 +62,12 @@
|
||||
height: 0.95rem;
|
||||
width: 0.95rem;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
border-radius: 0.25rem;
|
||||
background: hsl(var(--primary));
|
||||
box-shadow: 0 0 0 3px hsl(var(--background));
|
||||
box-shadow: 0 0 0 2px hsl(var(--background));
|
||||
cursor: nwse-resize;
|
||||
}
|
||||
|
||||
.theme-grid-editor__disabled {
|
||||
border: 1px dashed hsl(var(--border));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,37 +42,14 @@ export class ThemeGridEditorComponent {
|
||||
readonly itemChanged = output<{ key: string; grid: ThemeGridRect }>();
|
||||
readonly itemSelected = output<string>();
|
||||
|
||||
private readonly host = inject<ElementRef<HTMLElement>>(ElementRef);
|
||||
private dragState: DragState | null = null;
|
||||
|
||||
readonly canvasRef = viewChild.required<ElementRef<HTMLElement>>('canvasRef');
|
||||
readonly frameStyle = computed(() => ({
|
||||
'--theme-grid-columns': `${this.container().columns}`,
|
||||
'--theme-grid-rows': `${this.container().rows}`
|
||||
}));
|
||||
|
||||
itemStyle(item: ThemeGridEditorItem): Record<string, string> {
|
||||
const { columns, rows } = this.container();
|
||||
|
||||
return {
|
||||
left: `${(item.grid.x / columns) * 100}%`,
|
||||
top: `${(item.grid.y / rows) * 100}%`,
|
||||
width: `${(item.grid.w / columns) * 100}%`,
|
||||
height: `${(item.grid.h / rows) * 100}%`
|
||||
};
|
||||
}
|
||||
|
||||
selectItem(key: string): void {
|
||||
this.itemSelected.emit(key);
|
||||
}
|
||||
|
||||
startMove(event: PointerEvent, item: ThemeGridEditorItem): void {
|
||||
this.startDrag(event, item, 'move');
|
||||
}
|
||||
|
||||
startResize(event: PointerEvent, item: ThemeGridEditorItem): void {
|
||||
this.startDrag(event, item, 'resize');
|
||||
}
|
||||
private readonly host = inject<ElementRef<HTMLElement>>(ElementRef);
|
||||
private dragState: DragState | null = null;
|
||||
|
||||
@HostListener('document:pointermove', ['$event'])
|
||||
onPointerMove(event: PointerEvent): void {
|
||||
@@ -112,6 +89,29 @@ export class ThemeGridEditorComponent {
|
||||
this.dragState = null;
|
||||
}
|
||||
|
||||
itemStyle(item: ThemeGridEditorItem): Record<string, string> {
|
||||
const { columns, rows } = this.container();
|
||||
|
||||
return {
|
||||
left: `${(item.grid.x / columns) * 100}%`,
|
||||
top: `${(item.grid.y / rows) * 100}%`,
|
||||
width: `${(item.grid.w / columns) * 100}%`,
|
||||
height: `${(item.grid.h / rows) * 100}%`
|
||||
};
|
||||
}
|
||||
|
||||
selectItem(key: string): void {
|
||||
this.itemSelected.emit(key);
|
||||
}
|
||||
|
||||
startMove(event: PointerEvent, item: ThemeGridEditorItem): void {
|
||||
this.startDrag(event, item, 'move');
|
||||
}
|
||||
|
||||
startResize(event: PointerEvent, item: ThemeGridEditorItem): void {
|
||||
this.startDrag(event, item, 'resize');
|
||||
}
|
||||
|
||||
private startDrag(event: PointerEvent, item: ThemeGridEditorItem, mode: DragMode): void {
|
||||
if (this.disabled()) {
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<div
|
||||
class="theme-json-code-editor"
|
||||
[style.minHeight]="editorMinHeight()"
|
||||
>
|
||||
<div
|
||||
#editorHostRef
|
||||
class="theme-json-code-editor__host"
|
||||
></div>
|
||||
</div>
|
||||
@@ -0,0 +1,22 @@
|
||||
:host {
|
||||
display: block;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.theme-json-code-editor {
|
||||
overflow: hidden;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 0.5rem;
|
||||
background: linear-gradient(180deg, #111827, #0f172a);
|
||||
box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.03);
|
||||
}
|
||||
|
||||
.theme-json-code-editor:focus-within {
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgb(125 211 252 / 0.35),
|
||||
0 0 0 1px rgb(56 189 248 / 0.25);
|
||||
}
|
||||
|
||||
.theme-json-code-editor__host {
|
||||
min-height: inherit;
|
||||
}
|
||||
@@ -84,49 +84,10 @@ const THEME_JSON_EDITOR_THEME = EditorView.theme({
|
||||
@Component({
|
||||
selector: 'app-theme-json-code-editor',
|
||||
standalone: true,
|
||||
template: `
|
||||
<div
|
||||
class="theme-json-code-editor"
|
||||
[style.minHeight]="editorMinHeight()"
|
||||
>
|
||||
<div
|
||||
#editorHostRef
|
||||
class="theme-json-code-editor__host"
|
||||
></div>
|
||||
</div>
|
||||
`,
|
||||
styles: `
|
||||
:host {
|
||||
display: block;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.theme-json-code-editor {
|
||||
overflow: hidden;
|
||||
border: 1px solid #2f405c;
|
||||
border-radius: 1rem;
|
||||
background:
|
||||
radial-gradient(circle at top right, rgb(96 165 250 / 0.16), transparent 34%),
|
||||
linear-gradient(180deg, #172033, #0e1625);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgb(125 211 252 / 0.08),
|
||||
0 0 0 1px rgb(15 23 42 / 0.16);
|
||||
}
|
||||
|
||||
.theme-json-code-editor:focus-within {
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgb(125 211 252 / 0.5),
|
||||
0 0 0 3px rgb(14 165 233 / 0.18);
|
||||
}
|
||||
|
||||
.theme-json-code-editor__host {
|
||||
min-height: inherit;
|
||||
}
|
||||
`
|
||||
templateUrl: './theme-json-code-editor.component.html',
|
||||
styleUrl: './theme-json-code-editor.component.scss'
|
||||
})
|
||||
export class ThemeJsonCodeEditorComponent implements OnDestroy {
|
||||
private readonly zone = inject(NgZone);
|
||||
|
||||
readonly editorHostRef = viewChild<ElementRef<HTMLDivElement>>('editorHostRef');
|
||||
readonly value = input.required<string>();
|
||||
readonly fullscreen = input(false);
|
||||
@@ -134,6 +95,8 @@ export class ThemeJsonCodeEditorComponent implements OnDestroy {
|
||||
|
||||
readonly editorMinHeight = computed(() => this.fullscreen() ? 'max(34rem, calc(100vh - 15rem))' : '28rem');
|
||||
|
||||
private readonly zone = inject(NgZone);
|
||||
|
||||
private editorView: EditorView | null = null;
|
||||
private isApplyingExternalValue = false;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div
|
||||
class="theme-settings flex min-h-0 w-full flex-col space-y-4"
|
||||
class="theme-settings flex min-h-0 w-full flex-col space-y-3"
|
||||
[class.min-h-full]="isFullscreen()"
|
||||
[class.p-4]="isFullscreen()"
|
||||
[class.theme-settings--fullscreen]="isFullscreen()"
|
||||
@@ -15,21 +15,21 @@
|
||||
<button
|
||||
type="button"
|
||||
(click)="startPicker()"
|
||||
class="inline-flex items-center rounded-full border border-border bg-background px-3 py-1.5 text-sm font-medium text-foreground transition-colors hover:bg-secondary"
|
||||
class="inline-flex items-center rounded-md border border-border bg-secondary px-3 py-1.5 text-sm font-medium text-foreground transition-colors hover:bg-secondary/80"
|
||||
>
|
||||
Pick UI Element
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
(click)="formatDraft()"
|
||||
class="inline-flex items-center rounded-full border border-border bg-background px-3 py-1.5 text-sm font-medium text-foreground transition-colors hover:bg-secondary"
|
||||
class="inline-flex items-center rounded-md border border-border bg-secondary px-3 py-1.5 text-sm font-medium text-foreground transition-colors hover:bg-secondary/80"
|
||||
>
|
||||
Format JSON
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
(click)="copyLlmThemeGuide()"
|
||||
class="inline-flex items-center rounded-full border border-border bg-background px-3 py-1.5 text-sm font-medium text-foreground transition-colors hover:bg-secondary"
|
||||
class="inline-flex items-center rounded-md border border-border bg-secondary px-3 py-1.5 text-sm font-medium text-foreground transition-colors hover:bg-secondary/80"
|
||||
>
|
||||
Copy LLM Guide
|
||||
</button>
|
||||
@@ -37,14 +37,14 @@
|
||||
type="button"
|
||||
(click)="applyDraft()"
|
||||
[disabled]="!draftIsValid()"
|
||||
class="inline-flex items-center rounded-full bg-primary px-4 py-1.5 text-sm font-semibold text-primary-foreground transition-colors hover:bg-primary/90 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
class="inline-flex items-center rounded-md bg-primary px-4 py-1.5 text-sm font-semibold text-primary-foreground transition-colors hover:bg-primary/90 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
Apply Draft
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
(click)="restoreDefaultTheme()"
|
||||
class="inline-flex items-center rounded-full border border-destructive/25 bg-destructive/10 px-3 py-1.5 text-sm font-medium text-destructive transition-colors hover:bg-destructive/15"
|
||||
class="inline-flex items-center rounded-md border border-destructive/25 bg-destructive/10 px-3 py-1.5 text-sm font-medium text-destructive transition-colors hover:bg-destructive/15"
|
||||
>
|
||||
Restore Default
|
||||
</button>
|
||||
@@ -60,8 +60,14 @@
|
||||
<div class="theme-settings__hero-grid mt-4">
|
||||
<div class="theme-settings__hero-stat">
|
||||
<div class="theme-settings__workspace-selector theme-settings__workspace-selector--compact">
|
||||
<label class="theme-settings__workspace-selector-label">Workspace</label>
|
||||
<label
|
||||
for="theme-studio-workspace-select"
|
||||
class="theme-settings__workspace-selector-label"
|
||||
>
|
||||
Workspace
|
||||
</label>
|
||||
<select
|
||||
id="theme-studio-workspace-select"
|
||||
class="theme-settings__workspace-select"
|
||||
[value]="activeWorkspace()"
|
||||
(change)="onWorkspaceSelect($event)"
|
||||
@@ -89,13 +95,13 @@
|
||||
</div>
|
||||
|
||||
@if (statusMessage()) {
|
||||
<div class="mt-3 rounded-2xl border border-primary/20 bg-primary/10 px-4 py-3 text-sm text-primary">
|
||||
<div class="mt-3 rounded-lg border border-primary/20 bg-primary/10 px-4 py-3 text-sm text-primary">
|
||||
{{ statusMessage() }}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!draftIsValid()) {
|
||||
<div class="mt-3 rounded-2xl border border-destructive/30 bg-destructive/8 p-4">
|
||||
<div class="mt-3 rounded-lg border border-destructive/30 bg-destructive/10 p-4">
|
||||
<p class="text-sm font-semibold text-destructive">The draft is invalid. The last working theme is still active.</p>
|
||||
<ul class="mt-2 space-y-1 text-sm text-destructive/90">
|
||||
@for (error of draftErrors(); track error) {
|
||||
@@ -122,7 +128,7 @@
|
||||
type="button"
|
||||
(click)="saveDraftAsNewTheme()"
|
||||
[disabled]="!draftIsValid() || savedThemesBusy()"
|
||||
class="rounded-full border border-border bg-background px-3 py-1.5 text-xs font-medium text-foreground transition-colors hover:bg-secondary disabled:cursor-not-allowed disabled:opacity-60"
|
||||
class="rounded-md border border-border bg-secondary px-3 py-1.5 text-xs font-medium text-foreground transition-colors hover:bg-secondary/80 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
Save New
|
||||
</button>
|
||||
@@ -130,7 +136,7 @@
|
||||
type="button"
|
||||
(click)="saveDraftToSelectedTheme()"
|
||||
[disabled]="!draftIsValid() || !selectedSavedTheme() || savedThemesBusy()"
|
||||
class="rounded-full border border-border bg-background px-3 py-1.5 text-xs font-medium text-foreground transition-colors hover:bg-secondary disabled:cursor-not-allowed disabled:opacity-60"
|
||||
class="rounded-md border border-border bg-secondary px-3 py-1.5 text-xs font-medium text-foreground transition-colors hover:bg-secondary/80 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
Save Selected
|
||||
</button>
|
||||
@@ -138,7 +144,7 @@
|
||||
type="button"
|
||||
(click)="useSelectedSavedTheme()"
|
||||
[disabled]="!selectedSavedTheme()?.isValid || (savedThemesBusy() && savedThemes().length === 0)"
|
||||
class="rounded-full border border-border bg-background px-3 py-1.5 text-xs font-medium text-foreground transition-colors hover:bg-secondary disabled:cursor-not-allowed disabled:opacity-60"
|
||||
class="rounded-md border border-border bg-secondary px-3 py-1.5 text-xs font-medium text-foreground transition-colors hover:bg-secondary/80 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
Use
|
||||
</button>
|
||||
@@ -146,7 +152,7 @@
|
||||
type="button"
|
||||
(click)="editSelectedSavedTheme()"
|
||||
[disabled]="!selectedSavedTheme()?.isValid || (savedThemesBusy() && savedThemes().length === 0)"
|
||||
class="rounded-full border border-border bg-background px-3 py-1.5 text-xs font-medium text-foreground transition-colors hover:bg-secondary disabled:cursor-not-allowed disabled:opacity-60"
|
||||
class="rounded-md border border-border bg-secondary px-3 py-1.5 text-xs font-medium text-foreground transition-colors hover:bg-secondary/80 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
@@ -154,7 +160,7 @@
|
||||
type="button"
|
||||
(click)="removeSelectedSavedTheme()"
|
||||
[disabled]="!selectedSavedTheme() || (savedThemesBusy() && savedThemes().length === 0)"
|
||||
class="rounded-full border border-destructive/25 bg-destructive/10 px-3 py-1.5 text-xs font-medium text-destructive transition-colors hover:bg-destructive/15 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
class="rounded-md border border-destructive/25 bg-destructive/10 px-3 py-1.5 text-xs font-medium text-destructive transition-colors hover:bg-destructive/15 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
@@ -162,7 +168,7 @@
|
||||
type="button"
|
||||
(click)="refreshSavedThemes()"
|
||||
[disabled]="savedThemesBusy() && savedThemes().length === 0"
|
||||
class="rounded-full border border-border bg-background px-3 py-1.5 text-xs font-medium text-foreground transition-colors hover:bg-secondary disabled:cursor-not-allowed disabled:opacity-60"
|
||||
class="rounded-md border border-border bg-secondary px-3 py-1.5 text-xs font-medium text-foreground transition-colors hover:bg-secondary/80 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
Refresh
|
||||
</button>
|
||||
@@ -205,7 +211,7 @@
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
<div class="mt-4 rounded-2xl border border-dashed border-border bg-background/60 px-4 py-5 text-sm text-muted-foreground">
|
||||
<div class="mt-4 rounded-lg border border-dashed border-border bg-secondary/10 px-4 py-5 text-sm text-muted-foreground">
|
||||
Save the current draft to create your first reusable Electron theme.
|
||||
</div>
|
||||
}
|
||||
@@ -252,7 +258,7 @@
|
||||
<span class="mt-2 block text-xs leading-5 text-muted-foreground">{{ entry.description }}</span>
|
||||
</button>
|
||||
} @empty {
|
||||
<div class="rounded-2xl border border-dashed border-border bg-background/60 px-4 py-5 text-sm text-muted-foreground">
|
||||
<div class="rounded-lg border border-dashed border-border bg-secondary/10 px-4 py-5 text-sm text-muted-foreground">
|
||||
No registered theme keys match this filter.
|
||||
</div>
|
||||
}
|
||||
@@ -283,7 +289,7 @@
|
||||
<button
|
||||
type="button"
|
||||
(click)="jumpToStyles()"
|
||||
class="rounded-full border border-border bg-background px-3 py-1.5 text-xs font-medium text-foreground transition-colors hover:bg-secondary"
|
||||
class="rounded-md border border-border bg-secondary px-3 py-1.5 text-xs font-medium text-foreground transition-colors hover:bg-secondary/80"
|
||||
>
|
||||
Open styles in JSON
|
||||
</button>
|
||||
@@ -291,7 +297,7 @@
|
||||
<button
|
||||
type="button"
|
||||
(click)="jumpToLayout()"
|
||||
class="rounded-full border border-border bg-background px-3 py-1.5 text-xs font-medium text-foreground transition-colors hover:bg-secondary"
|
||||
class="rounded-md border border-border bg-secondary px-3 py-1.5 text-xs font-medium text-foreground transition-colors hover:bg-secondary/80"
|
||||
>
|
||||
Open layout in JSON
|
||||
</button>
|
||||
@@ -315,10 +321,10 @@
|
||||
<p class="text-sm font-semibold text-foreground">Theme JSON</p>
|
||||
|
||||
<div class="flex flex-wrap gap-2 text-[11px] font-medium text-muted-foreground">
|
||||
<span class="rounded-full bg-secondary px-2.5 py-1">{{ draftLineCount() }} lines</span>
|
||||
<span class="rounded-full bg-secondary px-2.5 py-1">{{ draftCharacterCount() }} chars</span>
|
||||
<span class="rounded-full bg-secondary px-2.5 py-1">{{ draftErrorCount() }} errors</span>
|
||||
<span class="rounded-full bg-slate-900 px-2.5 py-1 text-slate-200">IDE editor</span>
|
||||
<span class="rounded-md bg-secondary px-2.5 py-1">{{ draftLineCount() }} lines</span>
|
||||
<span class="rounded-md bg-secondary px-2.5 py-1">{{ draftCharacterCount() }} chars</span>
|
||||
<span class="rounded-md bg-secondary px-2.5 py-1">{{ draftErrorCount() }} errors</span>
|
||||
<span class="rounded-md bg-slate-900 px-2.5 py-1 text-slate-200">IDE editor</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -334,7 +340,7 @@
|
||||
}
|
||||
|
||||
@if (activeWorkspace() === 'inspector') {
|
||||
<div class="space-y-6">
|
||||
<div class="space-y-4">
|
||||
<section class="theme-studio-card p-4">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<p class="text-sm font-semibold text-foreground">Selection</p>
|
||||
@@ -342,14 +348,14 @@
|
||||
<button
|
||||
type="button"
|
||||
(click)="startPicker()"
|
||||
class="rounded-full border border-border bg-background px-3 py-1.5 text-xs font-medium text-foreground transition-colors hover:bg-secondary"
|
||||
class="rounded-md border border-border bg-secondary px-3 py-1.5 text-xs font-medium text-foreground transition-colors hover:bg-secondary/80"
|
||||
>
|
||||
Pick live element
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (selectedElement()) {
|
||||
<div class="mt-4 rounded-2xl border border-border/80 bg-background/65 p-4">
|
||||
<div class="mt-4 rounded-lg border border-border/80 bg-secondary/20 p-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<p class="text-base font-semibold text-foreground">{{ selectedElement()!.label }}</p>
|
||||
<span class="rounded-full bg-secondary px-2 py-0.5 font-mono text-[11px] text-muted-foreground">{{ selectedElement()!.key }}</span>
|
||||
@@ -370,7 +376,7 @@
|
||||
type="button"
|
||||
(click)="addStarterAnimation()"
|
||||
[disabled]="!draftIsValid()"
|
||||
class="rounded-full border border-border bg-background px-3 py-1.5 text-xs font-medium text-foreground transition-colors hover:bg-secondary disabled:cursor-not-allowed disabled:opacity-60"
|
||||
class="rounded-md border border-border bg-secondary px-3 py-1.5 text-xs font-medium text-foreground transition-colors hover:bg-secondary/80 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
Add fade animation
|
||||
</button>
|
||||
@@ -382,7 +388,7 @@
|
||||
type="button"
|
||||
(click)="applySuggestedProperty(field.key)"
|
||||
[disabled]="!draftIsValid()"
|
||||
class="rounded-2xl border border-border/80 bg-background/65 p-3 text-left transition-colors hover:bg-secondary/45 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
class="rounded-lg border border-border/80 bg-secondary/20 p-3 text-left transition-colors hover:bg-secondary/40 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
@@ -391,7 +397,7 @@
|
||||
</div>
|
||||
<span class="rounded-full bg-secondary px-2 py-0.5 text-[10px] font-medium text-muted-foreground">{{ field.type }}</span>
|
||||
</div>
|
||||
<div class="mt-3 inline-flex rounded-full bg-primary/10 px-2.5 py-1 font-mono text-[11px] text-primary">
|
||||
<div class="mt-3 inline-flex rounded-md bg-primary/10 px-2.5 py-1 font-mono text-[11px] text-primary">
|
||||
{{ field.example }}
|
||||
</div>
|
||||
</button>
|
||||
@@ -399,7 +405,7 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="theme-studio-card p-5">
|
||||
<section class="theme-studio-card p-4">
|
||||
<p class="text-sm font-semibold text-foreground">Animation Keys</p>
|
||||
|
||||
@if (animationKeys().length > 0) {
|
||||
@@ -408,22 +414,22 @@
|
||||
<button
|
||||
type="button"
|
||||
(click)="jumpToAnimation(animationKey)"
|
||||
class="rounded-full border border-border bg-background px-3 py-1.5 font-mono text-[11px] text-foreground transition-colors hover:bg-secondary"
|
||||
class="rounded-md border border-border bg-secondary px-3 py-1.5 font-mono text-[11px] text-foreground transition-colors hover:bg-secondary/80"
|
||||
>
|
||||
{{ animationKey }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
<div class="mt-4 rounded-2xl border border-dashed border-border bg-background/60 px-4 py-5 text-sm text-muted-foreground">
|
||||
<div class="mt-4 rounded-lg border border-dashed border-border bg-secondary/10 px-4 py-5 text-sm text-muted-foreground">
|
||||
No custom animation keys yet.
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="mt-4 rounded-2xl border border-border/80 bg-background/65 p-4">
|
||||
<div class="mt-4 rounded-lg border border-border/80 bg-secondary/20 p-4">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
@for (field of THEME_ANIMATION_FIELDS; track field.key) {
|
||||
<span class="rounded-full bg-secondary px-2.5 py-1 font-mono text-[11px] text-foreground/80">{{ field.key }}</span>
|
||||
<span class="rounded-md bg-secondary px-2.5 py-1 font-mono text-[11px] text-foreground/80">{{ field.key }}</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -441,7 +447,7 @@
|
||||
<button
|
||||
type="button"
|
||||
(click)="selectContainer(container.key)"
|
||||
class="rounded-full border border-border bg-background px-3 py-1.5 text-xs font-medium text-foreground transition-colors hover:bg-secondary"
|
||||
class="rounded-md border border-border bg-secondary px-3 py-1.5 text-xs font-medium text-foreground transition-colors hover:bg-secondary/80"
|
||||
[class.bg-primary/10]="selectedContainer() === container.key"
|
||||
[class.border-primary/40]="selectedContainer() === container.key"
|
||||
>
|
||||
@@ -453,7 +459,7 @@
|
||||
type="button"
|
||||
(click)="resetSelectedContainer()"
|
||||
[disabled]="!draftIsValid()"
|
||||
class="rounded-full border border-border bg-background px-3 py-1.5 text-xs font-medium text-foreground transition-colors hover:bg-secondary disabled:cursor-not-allowed disabled:opacity-60"
|
||||
class="rounded-md border border-border bg-secondary px-3 py-1.5 text-xs font-medium text-foreground transition-colors hover:bg-secondary/80 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
Reset Container
|
||||
</button>
|
||||
@@ -473,19 +479,19 @@
|
||||
|
||||
@if (selectedElementGrid()) {
|
||||
<div class="mt-4 grid gap-3 sm:grid-cols-4">
|
||||
<div class="rounded-2xl border border-border/80 bg-background/65 p-3">
|
||||
<div class="rounded-lg border border-border/80 bg-secondary/20 p-3">
|
||||
<p class="text-[11px] font-semibold uppercase tracking-[0.18em] text-muted-foreground">x</p>
|
||||
<p class="mt-2 font-mono text-lg font-semibold text-foreground">{{ selectedElementGrid()!.grid.x }}</p>
|
||||
</div>
|
||||
<div class="rounded-2xl border border-border/80 bg-background/65 p-3">
|
||||
<div class="rounded-lg border border-border/80 bg-secondary/20 p-3">
|
||||
<p class="text-[11px] font-semibold uppercase tracking-[0.18em] text-muted-foreground">y</p>
|
||||
<p class="mt-2 font-mono text-lg font-semibold text-foreground">{{ selectedElementGrid()!.grid.y }}</p>
|
||||
</div>
|
||||
<div class="rounded-2xl border border-border/80 bg-background/65 p-3">
|
||||
<div class="rounded-lg border border-border/80 bg-secondary/20 p-3">
|
||||
<p class="text-[11px] font-semibold uppercase tracking-[0.18em] text-muted-foreground">w</p>
|
||||
<p class="mt-2 font-mono text-lg font-semibold text-foreground">{{ selectedElementGrid()!.grid.w }}</p>
|
||||
</div>
|
||||
<div class="rounded-2xl border border-border/80 bg-background/65 p-3">
|
||||
<div class="rounded-lg border border-border/80 bg-secondary/20 p-3">
|
||||
<p class="text-[11px] font-semibold uppercase tracking-[0.18em] text-muted-foreground">h</p>
|
||||
<p class="mt-2 font-mono text-lg font-semibold text-foreground">{{ selectedElementGrid()!.grid.h }}</p>
|
||||
</div>
|
||||
|
||||
@@ -29,11 +29,11 @@
|
||||
.theme-settings__workspace-select {
|
||||
width: 100%;
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 0.85rem;
|
||||
background: hsl(var(--background) / 0.82);
|
||||
padding: 0.65rem 0.8rem;
|
||||
font-size: 0.88rem;
|
||||
font-weight: 600;
|
||||
border-radius: 0.5rem;
|
||||
background: hsl(var(--secondary));
|
||||
padding: 0.6rem 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: hsl(var(--foreground));
|
||||
outline: none;
|
||||
transition:
|
||||
@@ -44,7 +44,7 @@
|
||||
|
||||
.theme-settings__workspace-select:focus {
|
||||
border-color: hsl(var(--primary) / 0.4);
|
||||
box-shadow: 0 0 0 3px hsl(var(--primary) / 0.12);
|
||||
box-shadow: 0 0 0 1px hsl(var(--primary) / 0.2);
|
||||
}
|
||||
|
||||
.theme-settings__workspace-selector--compact .theme-settings__workspace-select {
|
||||
@@ -68,20 +68,19 @@
|
||||
.theme-settings__saved-theme-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.7rem;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.theme-settings__saved-theme-button {
|
||||
width: 100%;
|
||||
border: 1px solid hsl(var(--border) / 0.8);
|
||||
border-radius: 1rem;
|
||||
background: hsl(var(--background) / 0.65);
|
||||
padding: 0.85rem;
|
||||
border-radius: 0.5rem;
|
||||
background: hsl(var(--secondary) / 0.25);
|
||||
padding: 0.75rem;
|
||||
text-align: left;
|
||||
transition:
|
||||
border-color 160ms ease,
|
||||
background-color 160ms ease,
|
||||
transform 160ms ease;
|
||||
background-color 160ms ease;
|
||||
}
|
||||
|
||||
.theme-settings__saved-theme-button:hover {
|
||||
@@ -89,7 +88,7 @@
|
||||
}
|
||||
|
||||
.theme-settings__saved-theme-button--active {
|
||||
border-color: hsl(var(--primary) / 0.38);
|
||||
border-color: hsl(var(--primary) / 0.35);
|
||||
background: hsl(var(--primary) / 0.08);
|
||||
}
|
||||
|
||||
@@ -114,4 +113,4 @@
|
||||
display: block;
|
||||
min-height: 0;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,12 +44,12 @@ type ThemeStudioWorkspace = 'editor' | 'inspector' | 'layout';
|
||||
styleUrl: './theme-settings.component.scss'
|
||||
})
|
||||
export class ThemeSettingsComponent {
|
||||
private readonly modal = inject(SettingsModalService);
|
||||
private readonly theme = inject(ThemeService);
|
||||
private readonly themeLibrary = inject(ThemeLibraryService);
|
||||
private readonly registry = inject(ThemeRegistryService);
|
||||
private readonly picker = inject(ElementPickerService);
|
||||
private readonly layoutSync = inject(LayoutSyncService);
|
||||
readonly modal = inject(SettingsModalService);
|
||||
readonly theme = inject(ThemeService);
|
||||
readonly themeLibrary = inject(ThemeLibraryService);
|
||||
readonly registry = inject(ThemeRegistryService);
|
||||
readonly picker = inject(ElementPickerService);
|
||||
readonly layoutSync = inject(LayoutSyncService);
|
||||
|
||||
readonly editorRef = viewChild<ThemeJsonCodeEditorComponent>('jsonEditorRef');
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
@if (picker.isPicking()) {
|
||||
<div class="pointer-events-none fixed inset-x-0 bottom-4 z-[95] flex justify-center px-4">
|
||||
<div class="pointer-events-auto max-w-xl rounded-lg border border-border bg-card px-4 py-3 shadow-lg backdrop-blur">
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.18em] text-primary">Theme Picker Active</p>
|
||||
<p class="mt-1 text-sm text-foreground">Click a highlighted area to inspect its theme key.</p>
|
||||
<p class="mt-1 text-xs text-muted-foreground">
|
||||
Hovering:
|
||||
<span class="font-medium text-foreground">{{ hoveredEntry()?.label || 'Move over a themeable region' }}</span>
|
||||
@if (hoveredEntry()) {
|
||||
<span class="ml-1 rounded-md bg-secondary px-2 py-0.5 font-mono text-[11px] text-foreground/80">
|
||||
{{ hoveredEntry()!.key }}
|
||||
</span>
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
(click)="cancel()"
|
||||
class="inline-flex items-center rounded-md border border-border bg-secondary px-3 py-2 text-sm font-medium text-foreground transition-colors hover:bg-secondary/80"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -12,41 +12,11 @@ import { ThemeRegistryService } from '../application/theme-registry.service';
|
||||
selector: 'app-theme-picker-overlay',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
template: `
|
||||
@if (picker.isPicking()) {
|
||||
<div class="pointer-events-none fixed inset-x-0 bottom-4 z-[95] flex justify-center px-4">
|
||||
<div class="pointer-events-auto max-w-xl rounded-2xl border border-border bg-card/95 px-4 py-3 shadow-2xl backdrop-blur-xl">
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.18em] text-primary">Theme Picker Active</p>
|
||||
<p class="mt-1 text-sm text-foreground">
|
||||
Click a highlighted area to inspect its theme key.
|
||||
</p>
|
||||
<p class="mt-1 text-xs text-muted-foreground">
|
||||
Hovering:
|
||||
<span class="font-medium text-foreground">{{ hoveredEntry()?.label || 'Move over a themeable region' }}</span>
|
||||
@if (hoveredEntry()) {
|
||||
<span class="ml-1 rounded-full bg-secondary px-2 py-0.5 font-mono text-[11px] text-foreground/80">{{ hoveredEntry()!.key }}</span>
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
(click)="cancel()"
|
||||
class="inline-flex items-center rounded-full border border-border bg-secondary px-3 py-2 text-sm font-medium text-foreground transition-colors hover:bg-secondary/80"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
`
|
||||
templateUrl: './theme-picker-overlay.component.html'
|
||||
})
|
||||
export class ThemePickerOverlayComponent {
|
||||
readonly picker = inject(ElementPickerService);
|
||||
private readonly registry = inject(ThemeRegistryService);
|
||||
readonly registry = inject(ThemeRegistryService);
|
||||
|
||||
readonly hoveredEntry = computed(() => {
|
||||
return this.registry.getDefinition(this.picker.hoveredKey());
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
<button
|
||||
type="button"
|
||||
(click)="toggleSettings()"
|
||||
class="rounded-md p-2 transition-colors hover:bg-secondary"
|
||||
class="grid h-8 w-8 place-items-center rounded-md transition-colors hover:bg-secondary"
|
||||
>
|
||||
<ng-icon
|
||||
name="lucideSettings"
|
||||
|
||||
Reference in New Issue
Block a user