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:
@@ -31,22 +31,38 @@
|
|||||||
<div class="flex h-full items-center justify-center px-6 text-sm text-muted-foreground">Loading Theme Studio…</div>
|
<div class="flex h-full items-center justify-center px-6 text-sm text-muted-foreground">Loading Theme Studio…</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
} @else { @if (desktopUpdateState().restartRequired) {
|
} @else { @if (showDesktopUpdateNotice()) {
|
||||||
<div class="pointer-events-none absolute inset-x-0 top-0 z-20 px-4 pt-4">
|
<div class="pointer-events-none absolute inset-x-0 top-0 z-20 px-4 pt-4">
|
||||||
<div class="pointer-events-auto mx-auto max-w-4xl rounded-md border border-border bg-card p-4 shadow-sm">
|
<div class="pointer-events-auto mx-auto w-full max-w-xl">
|
||||||
<div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
<div class="relative rounded-xl border border-border/80 bg-card/95 px-4 py-3 shadow-lg backdrop-blur">
|
||||||
<div>
|
<button
|
||||||
<p class="text-sm font-semibold text-foreground">Update ready to install</p>
|
type="button"
|
||||||
<p class="mt-1 text-sm text-muted-foreground">
|
(click)="dismissDesktopUpdateNotice()"
|
||||||
MetoYou {{ desktopUpdateState().targetVersion || 'update' }} has been downloaded. Restart the app to finish applying it.
|
class="absolute right-2 top-2 grid h-8 w-8 place-items-center rounded-lg text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground"
|
||||||
|
aria-label="Dismiss update notice"
|
||||||
|
>
|
||||||
|
<ng-icon
|
||||||
|
name="lucideX"
|
||||||
|
class="h-4 w-4"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="pr-10">
|
||||||
|
<p class="text-[11px] font-semibold uppercase tracking-[0.18em] text-primary">Update Ready</p>
|
||||||
|
<p class="mt-1 text-sm font-semibold text-foreground">
|
||||||
|
Restart to install {{ desktopUpdateState().targetVersion || 'the latest update' }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap gap-2">
|
<p class="mt-1 pr-10 text-xs leading-5 text-muted-foreground">
|
||||||
|
The update has already been downloaded. Restart the app when you're ready to finish applying it.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="mt-3 flex flex-wrap gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
(click)="openUpdatesSettings()"
|
(click)="openUpdatesSettings()"
|
||||||
class="inline-flex items-center rounded-md border border-border bg-secondary px-4 py-2 text-sm font-medium text-foreground transition-colors hover:bg-secondary/80"
|
class="inline-flex items-center rounded-lg border border-border bg-secondary px-3 py-1.5 text-xs font-medium text-foreground transition-colors hover:bg-secondary/80"
|
||||||
>
|
>
|
||||||
Update settings
|
Update settings
|
||||||
</button>
|
</button>
|
||||||
@@ -54,7 +70,7 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
(click)="restartToApplyUpdate()"
|
(click)="restartToApplyUpdate()"
|
||||||
class="inline-flex items-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
|
class="inline-flex items-center rounded-lg bg-primary px-3 py-1.5 text-xs font-semibold text-primary-foreground transition-colors hover:bg-primary/90"
|
||||||
>
|
>
|
||||||
Restart now
|
Restart now
|
||||||
</button>
|
</button>
|
||||||
@@ -78,9 +94,9 @@
|
|||||||
class="pointer-events-none absolute z-[80]"
|
class="pointer-events-none absolute z-[80]"
|
||||||
[ngStyle]="themeStudioControlsPositionStyles()"
|
[ngStyle]="themeStudioControlsPositionStyles()"
|
||||||
>
|
>
|
||||||
<div class="pointer-events-auto flex items-center gap-2 rounded-full border border-border/80 bg-card/95 px-2 py-2 shadow-2xl backdrop-blur">
|
<div class="pointer-events-auto flex items-center gap-2 rounded-lg border border-border bg-card px-2 py-2 shadow-lg backdrop-blur">
|
||||||
<div
|
<div
|
||||||
class="rounded-full border border-border bg-background px-3 py-1.5 text-[11px] font-semibold uppercase tracking-[0.18em] text-muted-foreground select-none cursor-grab"
|
class="rounded-md border border-border bg-secondary px-3 py-1.5 text-[11px] font-semibold uppercase tracking-[0.18em] text-muted-foreground select-none cursor-grab"
|
||||||
[class.cursor-grabbing]="isDraggingThemeStudioControls()"
|
[class.cursor-grabbing]="isDraggingThemeStudioControls()"
|
||||||
(pointerdown)="startThemeStudioControlsDrag($event, themeStudioControlsRef)"
|
(pointerdown)="startThemeStudioControlsDrag($event, themeStudioControlsRef)"
|
||||||
>
|
>
|
||||||
@@ -90,7 +106,7 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
(click)="minimizeThemeStudio()"
|
(click)="minimizeThemeStudio()"
|
||||||
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"
|
||||||
>
|
>
|
||||||
Minimize
|
Minimize
|
||||||
</button>
|
</button>
|
||||||
@@ -98,7 +114,7 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
(click)="closeThemeStudio()"
|
(click)="closeThemeStudio()"
|
||||||
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"
|
||||||
>
|
>
|
||||||
Close
|
Close
|
||||||
</button>
|
</button>
|
||||||
@@ -106,7 +122,7 @@
|
|||||||
</div>
|
</div>
|
||||||
} @if (isThemeStudioMinimized()) {
|
} @if (isThemeStudioMinimized()) {
|
||||||
<div class="pointer-events-none absolute bottom-4 right-4 z-[80]">
|
<div class="pointer-events-none absolute bottom-4 right-4 z-[80]">
|
||||||
<div class="pointer-events-auto flex items-center gap-3 rounded-2xl border border-border/80 bg-card/95 px-3 py-3 shadow-2xl backdrop-blur">
|
<div class="pointer-events-auto flex items-center gap-3 rounded-lg border border-border bg-card px-3 py-3 shadow-lg backdrop-blur">
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<p class="text-[11px] font-semibold uppercase tracking-[0.18em] text-primary">Theme Studio</p>
|
<p class="text-[11px] font-semibold uppercase tracking-[0.18em] text-primary">Theme Studio</p>
|
||||||
<p class="mt-1 text-sm font-medium text-foreground">Minimized</p>
|
<p class="mt-1 text-sm font-medium text-foreground">Minimized</p>
|
||||||
@@ -115,7 +131,7 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
(click)="reopenThemeStudio()"
|
(click)="reopenThemeStudio()"
|
||||||
class="rounded-full bg-primary px-3 py-1.5 text-xs font-semibold text-primary-foreground transition-colors hover:bg-primary/90"
|
class="rounded-md bg-primary px-3 py-1.5 text-xs font-semibold text-primary-foreground transition-colors hover:bg-primary/90"
|
||||||
>
|
>
|
||||||
Re-open
|
Re-open
|
||||||
</button>
|
</button>
|
||||||
@@ -123,7 +139,7 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
(click)="dismissMinimizedThemeStudio()"
|
(click)="dismissMinimizedThemeStudio()"
|
||||||
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"
|
||||||
>
|
>
|
||||||
Dismiss
|
Dismiss
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import {
|
|||||||
} from '@angular/router';
|
} from '@angular/router';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
|
import { NgIcon, provideIcons } from '@ng-icons/core';
|
||||||
|
import { lucideX } from '@ng-icons/lucide';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DatabaseService,
|
DatabaseService,
|
||||||
@@ -51,6 +53,7 @@ import {
|
|||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
|
NgIcon,
|
||||||
RouterOutlet,
|
RouterOutlet,
|
||||||
ServersRailComponent,
|
ServersRailComponent,
|
||||||
TitleBarComponent,
|
TitleBarComponent,
|
||||||
@@ -61,6 +64,11 @@ import {
|
|||||||
ThemeNodeDirective,
|
ThemeNodeDirective,
|
||||||
ThemePickerOverlayComponent
|
ThemePickerOverlayComponent
|
||||||
],
|
],
|
||||||
|
viewProviders: [
|
||||||
|
provideIcons({
|
||||||
|
lucideX
|
||||||
|
})
|
||||||
|
],
|
||||||
templateUrl: './app.html',
|
templateUrl: './app.html',
|
||||||
styleUrl: './app.scss'
|
styleUrl: './app.scss'
|
||||||
})
|
})
|
||||||
@@ -72,20 +80,17 @@ export class App implements OnInit, OnDestroy {
|
|||||||
currentRoom = this.store.selectSignal(selectCurrentRoom);
|
currentRoom = this.store.selectSignal(selectCurrentRoom);
|
||||||
desktopUpdates = inject(DesktopAppUpdateService);
|
desktopUpdates = inject(DesktopAppUpdateService);
|
||||||
desktopUpdateState = this.desktopUpdates.state;
|
desktopUpdateState = this.desktopUpdates.state;
|
||||||
|
readonly databaseService = inject(DatabaseService);
|
||||||
private databaseService = inject(DatabaseService);
|
readonly router = inject(Router);
|
||||||
private router = inject(Router);
|
readonly servers = inject(ServerDirectoryFacade);
|
||||||
private servers = inject(ServerDirectoryFacade);
|
readonly notifications = inject(NotificationsFacade);
|
||||||
private notifications = inject(NotificationsFacade);
|
readonly settingsModal = inject(SettingsModalService);
|
||||||
private settingsModal = inject(SettingsModalService);
|
readonly timeSync = inject(TimeSyncService);
|
||||||
private timeSync = inject(TimeSyncService);
|
readonly theme = inject(ThemeService);
|
||||||
private theme = inject(ThemeService);
|
readonly voiceSession = inject(VoiceSessionFacade);
|
||||||
private voiceSession = inject(VoiceSessionFacade);
|
readonly externalLinks = inject(ExternalLinkService);
|
||||||
private externalLinks = inject(ExternalLinkService);
|
readonly electronBridge = inject(ElectronBridgeService);
|
||||||
private electronBridge = inject(ElectronBridgeService);
|
readonly dismissedDesktopUpdateNoticeKey = signal<string | null>(null);
|
||||||
private deepLinkCleanup: (() => void) | null = null;
|
|
||||||
private themeStudioControlsDragOffset: { x: number; y: number } | null = null;
|
|
||||||
private themeStudioControlsBounds: { width: number; height: number } | null = null;
|
|
||||||
readonly themeStudioFullscreenComponent = signal<Type<unknown> | null>(null);
|
readonly themeStudioFullscreenComponent = signal<Type<unknown> | null>(null);
|
||||||
readonly themeStudioControlsPosition = signal<{ x: number; y: number } | null>(null);
|
readonly themeStudioControlsPosition = signal<{ x: number; y: number } | null>(null);
|
||||||
readonly isDraggingThemeStudioControls = signal(false);
|
readonly isDraggingThemeStudioControls = signal(false);
|
||||||
@@ -102,6 +107,17 @@ export class App implements OnInit, OnDestroy {
|
|||||||
return this.settingsModal.activePage() === 'theme'
|
return this.settingsModal.activePage() === 'theme'
|
||||||
&& this.settingsModal.themeStudioMinimized();
|
&& this.settingsModal.themeStudioMinimized();
|
||||||
});
|
});
|
||||||
|
readonly desktopUpdateNoticeKey = computed(() => {
|
||||||
|
const updateState = this.desktopUpdateState();
|
||||||
|
|
||||||
|
return updateState.targetVersion?.trim()
|
||||||
|
|| updateState.latestVersion?.trim()
|
||||||
|
|| `restart:${updateState.currentVersion}`;
|
||||||
|
});
|
||||||
|
readonly showDesktopUpdateNotice = computed(() => {
|
||||||
|
return this.desktopUpdateState().restartRequired
|
||||||
|
&& this.dismissedDesktopUpdateNoticeKey() !== this.desktopUpdateNoticeKey();
|
||||||
|
});
|
||||||
readonly appWorkspaceShellStyles = computed(() => {
|
readonly appWorkspaceShellStyles = computed(() => {
|
||||||
const workspaceStyles = this.appWorkspaceLayoutStyles();
|
const workspaceStyles = this.appWorkspaceLayoutStyles();
|
||||||
|
|
||||||
@@ -131,6 +147,10 @@ export class App implements OnInit, OnDestroy {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
private deepLinkCleanup: (() => void) | null = null;
|
||||||
|
private themeStudioControlsDragOffset: { x: number; y: number } | null = null;
|
||||||
|
private themeStudioControlsBounds: { width: number; height: number } | null = null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
effect(() => {
|
effect(() => {
|
||||||
if (!this.isThemeStudioFullscreen() || this.themeStudioFullscreenComponent()) {
|
if (!this.isThemeStudioFullscreen() || this.themeStudioFullscreenComponent()) {
|
||||||
@@ -259,6 +279,14 @@ export class App implements OnInit, OnDestroy {
|
|||||||
this.settingsModal.open('updates');
|
this.settingsModal.open('updates');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dismissDesktopUpdateNotice(): void {
|
||||||
|
if (!this.desktopUpdateState().restartRequired) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dismissedDesktopUpdateNoticeKey.set(this.desktopUpdateNoticeKey());
|
||||||
|
}
|
||||||
|
|
||||||
startThemeStudioControlsDrag(event: PointerEvent, controlsElement: HTMLElement): void {
|
startThemeStudioControlsDrag(event: PointerEvent, controlsElement: HTMLElement): void {
|
||||||
if (event.button !== 0) {
|
if (event.button !== 0) {
|
||||||
return;
|
return;
|
||||||
@@ -309,15 +337,15 @@ export class App implements OnInit, OnDestroy {
|
|||||||
await this.desktopUpdates.restartToApplyUpdate();
|
await this.desktopUpdates.restartToApplyUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
private clampThemeStudioControlsPosition(x: number, y: number, width: number, height: number): { x: number; y: number } {
|
private clampThemeStudioControlsPosition(left: number, top: number, width: number, height: number): { x: number; y: number } {
|
||||||
const minX = App.THEME_STUDIO_CONTROLS_MARGIN;
|
const minX = App.THEME_STUDIO_CONTROLS_MARGIN;
|
||||||
const minY = App.TITLE_BAR_HEIGHT + App.THEME_STUDIO_CONTROLS_MARGIN;
|
const minY = App.TITLE_BAR_HEIGHT + App.THEME_STUDIO_CONTROLS_MARGIN;
|
||||||
const maxX = Math.max(minX, window.innerWidth - width - App.THEME_STUDIO_CONTROLS_MARGIN);
|
const maxX = Math.max(minX, window.innerWidth - width - App.THEME_STUDIO_CONTROLS_MARGIN);
|
||||||
const maxY = Math.max(minY, window.innerHeight - height - App.THEME_STUDIO_CONTROLS_MARGIN);
|
const maxY = Math.max(minY, window.innerHeight - height - App.THEME_STUDIO_CONTROLS_MARGIN);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
x: Math.min(Math.max(minX, x), maxX),
|
x: Math.min(Math.max(minX, left), maxX),
|
||||||
y: Math.min(Math.max(minY, y), maxY)
|
y: Math.min(Math.max(minY, top), maxY)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
(click)="clearReply()"
|
(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
|
<ng-icon
|
||||||
name="lucideX"
|
name="lucideX"
|
||||||
|
|||||||
@@ -55,7 +55,7 @@
|
|||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<button
|
<button
|
||||||
(click)="saveEdit()"
|
(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
|
<ng-icon
|
||||||
name="lucideCheck"
|
name="lucideCheck"
|
||||||
@@ -64,7 +64,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
(click)="cancelEdit()"
|
(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
|
<ng-icon
|
||||||
name="lucideX"
|
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">
|
<div class="absolute right-2 top-2 flex gap-1 opacity-0 transition-opacity group-hover/img:opacity-100">
|
||||||
<button
|
<button
|
||||||
(click)="openLightbox(att); $event.stopPropagation()"
|
(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"
|
title="View full size"
|
||||||
>
|
>
|
||||||
<ng-icon
|
<ng-icon
|
||||||
@@ -118,7 +118,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
(click)="downloadAttachment(att); $event.stopPropagation()"
|
(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"
|
title="Download"
|
||||||
>
|
>
|
||||||
<ng-icon
|
<ng-icon
|
||||||
@@ -338,7 +338,7 @@
|
|||||||
<div class="relative">
|
<div class="relative">
|
||||||
<button
|
<button
|
||||||
(click)="toggleEmojiPicker()"
|
(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
|
<ng-icon
|
||||||
name="lucideSmile"
|
name="lucideSmile"
|
||||||
@@ -362,7 +362,7 @@
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
(click)="requestReply()"
|
(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
|
<ng-icon
|
||||||
name="lucideReply"
|
name="lucideReply"
|
||||||
@@ -373,7 +373,7 @@
|
|||||||
@if (isOwnMessage()) {
|
@if (isOwnMessage()) {
|
||||||
<button
|
<button
|
||||||
(click)="startEdit()"
|
(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
|
<ng-icon
|
||||||
name="lucideEdit"
|
name="lucideEdit"
|
||||||
@@ -385,7 +385,7 @@
|
|||||||
@if (isOwnMessage() || isAdmin()) {
|
@if (isOwnMessage() || isAdmin()) {
|
||||||
<button
|
<button
|
||||||
(click)="requestDelete()"
|
(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
|
<ng-icon
|
||||||
name="lucideTrash2"
|
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 {
|
interface ChatMessageAttachmentViewModel extends Attachment {
|
||||||
isAudio: boolean;
|
isAudio: boolean;
|
||||||
@@ -292,7 +300,7 @@ export class ChatMessageItemComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
requiresRichMarkdown(content: string): boolean {
|
requiresRichMarkdown(content: string): boolean {
|
||||||
return RICH_MARKDOWN_PATTERN.test(content);
|
return RICH_MARKDOWN_PATTERNS.some((pattern) => pattern.test(content));
|
||||||
}
|
}
|
||||||
|
|
||||||
formatBytes(bytes: number): string {
|
formatBytes(bytes: number): string {
|
||||||
|
|||||||
@@ -73,4 +73,4 @@ export class ChatMessageMarkdownComponent {
|
|||||||
|
|
||||||
return PRISM_LANGUAGE_ALIASES[normalized] ?? normalized;
|
return PRISM_LANGUAGE_ALIASES[normalized] ?? normalized;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
<div class="absolute right-3 top-3 flex gap-2">
|
<div class="absolute right-3 top-3 flex gap-2">
|
||||||
<button
|
<button
|
||||||
(click)="downloadAttachment(lightboxAttachment()!)"
|
(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"
|
title="Download"
|
||||||
>
|
>
|
||||||
<ng-icon
|
<ng-icon
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
(click)="closeLightbox()"
|
(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"
|
title="Close"
|
||||||
>
|
>
|
||||||
<ng-icon
|
<ng-icon
|
||||||
|
|||||||
@@ -80,21 +80,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
} @else {
|
} @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) {
|
@for (gif of results(); track gif.id) {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
(click)="selectGif(gif)"
|
(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
|
<div
|
||||||
class="relative overflow-hidden bg-secondary/30"
|
class="relative flex items-center justify-center overflow-hidden bg-secondary/30"
|
||||||
[style.aspect-ratio]="gifAspectRatio(gif)"
|
[style.height.px]="gifCardHeight(gif)"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
[appChatImageProxyFallback]="gif.previewUrl || gif.url"
|
[appChatImageProxyFallback]="gif.previewUrl || gif.url"
|
||||||
[alt]="gif.title || 'KLIPY GIF'"
|
[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"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
|
|||||||
@@ -23,6 +23,12 @@ import {
|
|||||||
import { KlipyGif, KlipyService } from '../../application/klipy.service';
|
import { KlipyGif, KlipyService } from '../../application/klipy.service';
|
||||||
import { ChatImageProxyFallbackDirective } from '../chat-image-proxy-fallback.directive';
|
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({
|
@Component({
|
||||||
selector: 'app-klipy-gif-picker',
|
selector: 'app-klipy-gif-picker',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
@@ -106,12 +112,8 @@ export class KlipyGifPickerComponent implements OnInit, AfterViewInit, OnDestroy
|
|||||||
this.closed.emit(undefined);
|
this.closed.emit(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
gifAspectRatio(gif: KlipyGif): string {
|
gifCardHeight(gif: KlipyGif): number {
|
||||||
if (gif.width > 0 && gif.height > 0) {
|
return this.getGifCardSize(gif).height;
|
||||||
return `${gif.width} / ${gif.height}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return '1 / 1';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loadResults(reset: boolean): Promise<void> {
|
private async loadResults(reset: boolean): Promise<void> {
|
||||||
@@ -182,4 +184,32 @@ export class KlipyGifPickerComponent implements OnInit, AfterViewInit, OnDestroy
|
|||||||
this.searchTimer = null;
|
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">
|
<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="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
|
<ng-icon
|
||||||
name="lucideBell"
|
name="lucideBell"
|
||||||
class="h-5 w-5"
|
class="h-5 w-5"
|
||||||
@@ -91,9 +91,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</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="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
|
<ng-icon
|
||||||
[name]="enabled() ? 'lucideBell' : 'lucideBellOff'"
|
[name]="enabled() ? 'lucideBell' : 'lucideBellOff'"
|
||||||
class="h-5 w-5"
|
class="h-5 w-5"
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
<button
|
<button
|
||||||
(click)="toggleFullscreen()"
|
(click)="toggleFullscreen()"
|
||||||
type="button"
|
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()) {
|
@if (isFullscreen()) {
|
||||||
<ng-icon
|
<ng-icon
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
<button
|
<button
|
||||||
(click)="stopSharing()"
|
(click)="stopSharing()"
|
||||||
type="button"
|
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"
|
title="Stop sharing"
|
||||||
>
|
>
|
||||||
<ng-icon
|
<ng-icon
|
||||||
@@ -74,7 +74,7 @@
|
|||||||
<button
|
<button
|
||||||
(click)="stopWatching()"
|
(click)="stopWatching()"
|
||||||
type="button"
|
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"
|
title="Stop watching"
|
||||||
>
|
>
|
||||||
<ng-icon
|
<ng-icon
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
<button
|
<button
|
||||||
(click)="openSettings()"
|
(click)="openSettings()"
|
||||||
type="button"
|
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"
|
title="Settings"
|
||||||
>
|
>
|
||||||
<ng-icon
|
<ng-icon
|
||||||
|
|||||||
@@ -10,18 +10,18 @@ import { ThemeRegistryService } from './theme-registry.service';
|
|||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class ElementPickerService {
|
export class ElementPickerService {
|
||||||
private readonly documentRef = inject(DOCUMENT);
|
readonly isPicking = signal(false);
|
||||||
private readonly modal = inject(SettingsModalService);
|
readonly hoveredKey = signal<string | null>(null);
|
||||||
private readonly registry = inject(ThemeRegistryService);
|
readonly selectedKey = signal<string | null>(null);
|
||||||
|
|
||||||
|
readonly documentRef = inject(DOCUMENT);
|
||||||
|
readonly modal = inject(SettingsModalService);
|
||||||
|
readonly registry = inject(ThemeRegistryService);
|
||||||
|
|
||||||
private removeListeners: (() => void)[] = [];
|
private removeListeners: (() => void)[] = [];
|
||||||
private resumePage: SettingsPage | null = null;
|
private resumePage: SettingsPage | null = null;
|
||||||
private shouldRestoreModalOnCancel = true;
|
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 {
|
start(page: SettingsPage = 'theme', restoreModalOnCancel = true): void {
|
||||||
if (this.isPicking()) {
|
if (this.isPicking()) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -15,11 +15,11 @@ import { ThemeService } from './theme.service';
|
|||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class LayoutSyncService {
|
export class LayoutSyncService {
|
||||||
private readonly registry = inject(ThemeRegistryService);
|
|
||||||
private readonly theme = inject(ThemeService);
|
|
||||||
|
|
||||||
readonly draftLayout = computed(() => this.theme.draftTheme().layout);
|
readonly draftLayout = computed(() => this.theme.draftTheme().layout);
|
||||||
|
|
||||||
|
readonly registry = inject(ThemeRegistryService);
|
||||||
|
readonly theme = inject(ThemeService);
|
||||||
|
|
||||||
containers() {
|
containers() {
|
||||||
return this.registry.layoutContainers();
|
return this.registry.layoutContainers();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import { ThemeService } from './theme.service';
|
|||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class ThemeLibraryService {
|
export class ThemeLibraryService {
|
||||||
private readonly storage = inject(ThemeLibraryStorageService);
|
readonly storage = inject(ThemeLibraryStorageService);
|
||||||
private readonly theme = inject(ThemeService);
|
readonly theme = inject(ThemeService);
|
||||||
|
|
||||||
readonly isAvailable = signal(this.storage.isAvailable);
|
readonly isAvailable = signal(this.storage.isAvailable);
|
||||||
readonly entries = signal<SavedThemeSummary[]>([]);
|
readonly entries = signal<SavedThemeSummary[]>([]);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { DOCUMENT } from '@angular/common';
|
import { DOCUMENT } from '@angular/common';
|
||||||
import {
|
import {
|
||||||
Injectable,
|
Injectable,
|
||||||
|
Signal,
|
||||||
computed,
|
computed,
|
||||||
inject,
|
inject,
|
||||||
signal
|
signal
|
||||||
@@ -44,8 +45,44 @@ function resolveBuiltInDefaultMigration(document: ThemeDocument): ThemeDocument
|
|||||||
: document;
|
: 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' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class ThemeService {
|
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 documentRef = inject(DOCUMENT);
|
||||||
|
|
||||||
private readonly activeThemeInternal = signal<ThemeDocument>(createDefaultThemeDocument());
|
private readonly activeThemeInternal = signal<ThemeDocument>(createDefaultThemeDocument());
|
||||||
@@ -60,18 +97,20 @@ export class ThemeService {
|
|||||||
private statusTimeoutId: ReturnType<typeof setTimeout> | null = null;
|
private statusTimeoutId: ReturnType<typeof setTimeout> | null = null;
|
||||||
private animationStyleElement: HTMLStyleElement | null = null;
|
private animationStyleElement: HTMLStyleElement | null = null;
|
||||||
|
|
||||||
readonly activeTheme = this.activeThemeInternal.asReadonly();
|
constructor() {
|
||||||
readonly activeThemeText = this.activeThemeTextInternal.asReadonly();
|
this.activeTheme = this.activeThemeInternal.asReadonly();
|
||||||
readonly draftTheme = this.draftThemeInternal.asReadonly();
|
this.activeThemeText = this.activeThemeTextInternal.asReadonly();
|
||||||
readonly draftText = this.draftTextInternal.asReadonly();
|
this.draftTheme = this.draftThemeInternal.asReadonly();
|
||||||
readonly draftIsValid = this.draftIsValidInternal.asReadonly();
|
this.draftText = this.draftTextInternal.asReadonly();
|
||||||
readonly draftErrors = this.draftErrorsInternal.asReadonly();
|
this.draftIsValid = this.draftIsValidInternal.asReadonly();
|
||||||
readonly statusMessage = this.statusMessageInternal.asReadonly();
|
this.draftErrors = this.draftErrorsInternal.asReadonly();
|
||||||
readonly activeThemeName = computed(() => this.activeThemeInternal().meta.name);
|
this.statusMessage = this.statusMessageInternal.asReadonly();
|
||||||
readonly knownAnimationClasses = computed(() => Object.keys(this.draftThemeInternal().animations));
|
this.activeThemeName = computed(() => this.activeThemeInternal().meta.name);
|
||||||
readonly isDraftDirty = computed(() => {
|
this.knownAnimationClasses = computed(() => Object.keys(this.draftThemeInternal().animations));
|
||||||
return this.draftTextInternal().trim() !== this.activeThemeTextInternal().trim();
|
this.isDraftDirty = computed(() => {
|
||||||
});
|
return this.draftTextInternal().trim() !== this.activeThemeTextInternal().trim();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
initialize(): void {
|
initialize(): void {
|
||||||
if (this.initialized) {
|
if (this.initialized) {
|
||||||
@@ -281,71 +320,13 @@ export class ThemeService {
|
|||||||
styles['backgroundImage'] = backgroundLayers.join(', ');
|
styles['backgroundImage'] = backgroundLayers.join(', ');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (elementTheme.width)
|
for (const property of hostStylePropertyKeys) {
|
||||||
styles['width'] = elementTheme.width;
|
const value = elementTheme[property];
|
||||||
|
|
||||||
if (elementTheme.height)
|
if (value) {
|
||||||
styles['height'] = elementTheme.height;
|
styles[property] = value;
|
||||||
|
}
|
||||||
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') {
|
if (typeof elementTheme.opacity === 'number') {
|
||||||
styles['opacity'] = `${elementTheme.opacity}`;
|
styles['opacity'] = `${elementTheme.opacity}`;
|
||||||
|
|||||||
@@ -31,6 +31,28 @@ function describeCapabilities(entry: (typeof THEME_REGISTRY)[number]): string {
|
|||||||
: 'visual style overrides only';
|
: '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 colorTokenKeys = Object.keys(DEFAULT_THEME_DOCUMENT.tokens.colors);
|
||||||
const radiusTokenKeys = Object.keys(DEFAULT_THEME_DOCUMENT.tokens.radii);
|
const radiusTokenKeys = Object.keys(DEFAULT_THEME_DOCUMENT.tokens.radii);
|
||||||
const effectTokenKeys = Object.keys(DEFAULT_THEME_DOCUMENT.tokens.effects);
|
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? }>',
|
'- animations: Record<string, { duration?, easing?, delay?, iterationCount?, fillMode?, direction?, keyframes? }>',
|
||||||
'',
|
'',
|
||||||
'Layout containers',
|
'Layout containers',
|
||||||
...THEME_LAYOUT_CONTAINERS.map((container) => {
|
...THEME_LAYOUT_CONTAINERS.map((container) => describeLayoutContainer(container)),
|
||||||
const layoutKeys = getLayoutKeysForContainer(container.key);
|
|
||||||
|
|
||||||
return `- ${container.key}: ${container.columns} columns x ${container.rows} rows. ${container.description} Layout keys: ${layoutKeys.join(', ')}.`;
|
|
||||||
}),
|
|
||||||
`- Only these keys should normally appear in layout: ${layoutEditableKeys.join(', ')}.`,
|
`- Only these keys should normally appear in layout: ${layoutEditableKeys.join(', ')}.`,
|
||||||
'',
|
'',
|
||||||
'Registered theme element keys',
|
'Registered theme element keys',
|
||||||
...THEME_REGISTRY.map((entry) => {
|
...THEME_REGISTRY.map((entry) => describeThemeEntry(entry)),
|
||||||
const container = entry.container ?? 'none';
|
|
||||||
|
|
||||||
return `- ${entry.key}: ${entry.label}. Category=${entry.category}. Container=${container}. ${entry.description} Supported extras: ${describeCapabilities(entry)}.`;
|
|
||||||
}),
|
|
||||||
'',
|
'',
|
||||||
'Supported element style fields',
|
'Supported element style fields',
|
||||||
...THEME_ELEMENT_STYLE_FIELDS.map((field) => {
|
...THEME_ELEMENT_STYLE_FIELDS.map((field) => {
|
||||||
|
|||||||
@@ -9,6 +9,27 @@ import {
|
|||||||
getLayoutEditableThemeKeys
|
getLayoutEditableThemeKeys
|
||||||
} from './theme.registry';
|
} 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> {
|
function createDefaultElements(): Record<string, ThemeElementStyles> {
|
||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
THEME_REGISTRY.map((entry) => [entry.key, {}])
|
THEME_REGISTRY.map((entry) => [entry.key, {}])
|
||||||
@@ -89,7 +110,7 @@ function createDarkDefaultElements(): Record<string, ThemeElementStyles> {
|
|||||||
elements['appRoot'] = {
|
elements['appRoot'] = {
|
||||||
backgroundColor: 'hsl(var(--background))',
|
backgroundColor: 'hsl(var(--background))',
|
||||||
color: 'hsl(var(--foreground))',
|
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'] = {
|
elements['serversRail'] = {
|
||||||
@@ -101,14 +122,14 @@ function createDarkDefaultElements(): Record<string, ThemeElementStyles> {
|
|||||||
|
|
||||||
elements['appWorkspace'] = {
|
elements['appWorkspace'] = {
|
||||||
backgroundColor: 'hsl(var(--workspace-background))',
|
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'] = {
|
elements['titleBar'] = {
|
||||||
backgroundColor: 'hsl(var(--title-bar-background) / 0.82)',
|
backgroundColor: 'hsl(var(--title-bar-background) / 0.82)',
|
||||||
color: 'hsl(var(--foreground))',
|
color: 'hsl(var(--foreground))',
|
||||||
gradient: 'linear-gradient(180deg, rgba(20, 26, 41, 0.52), rgba(7, 10, 18, 0.18))',
|
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)'
|
backdropFilter: 'var(--theme-effect-glass-blur)'
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -127,7 +148,7 @@ function createDarkDefaultElements(): Record<string, ThemeElementStyles> {
|
|||||||
color: 'hsl(var(--foreground))',
|
color: 'hsl(var(--foreground))',
|
||||||
border: '1px solid hsl(var(--border) / 0.62)',
|
border: '1px solid hsl(var(--border) / 0.62)',
|
||||||
borderRadius: 'var(--theme-radius-surface)',
|
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)',
|
boxShadow: 'var(--theme-effect-panel-shadow)',
|
||||||
backdropFilter: 'var(--theme-effect-glass-blur)'
|
backdropFilter: 'var(--theme-effect-glass-blur)'
|
||||||
};
|
};
|
||||||
@@ -156,7 +177,7 @@ function createDarkDefaultElements(): Record<string, ThemeElementStyles> {
|
|||||||
color: 'hsl(var(--foreground))',
|
color: 'hsl(var(--foreground))',
|
||||||
border: '1px solid hsl(var(--border) / 0.62)',
|
border: '1px solid hsl(var(--border) / 0.62)',
|
||||||
borderRadius: 'calc(var(--theme-radius-surface) + 0.2rem)',
|
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)',
|
boxShadow: 'var(--theme-effect-panel-shadow)',
|
||||||
backdropFilter: 'var(--theme-effect-glass-blur)'
|
backdropFilter: 'var(--theme-effect-glass-blur)'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ import {
|
|||||||
ThemeSchemaField
|
ThemeSchemaField
|
||||||
} from './theme.models';
|
} 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[] = [
|
export const THEME_TOP_LEVEL_FIELDS: readonly ThemeSchemaField[] = [
|
||||||
{
|
{
|
||||||
key: 'meta',
|
key: 'meta',
|
||||||
@@ -423,7 +427,7 @@ export const THEME_ELEMENT_STYLE_FIELDS: readonly ThemeSchemaField<ThemeElementS
|
|||||||
description: 'CSS gradient layered above any background image.',
|
description: 'CSS gradient layered above any background image.',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
example: 'linear-gradient(180deg, rgba(0,0,0,0.12), rgba(0,0,0,0.72))',
|
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',
|
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 class="mb-3 flex items-center justify-between gap-3 px-1">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-sm font-semibold text-foreground">{{ container().label }}</p>
|
<p class="text-sm font-semibold text-foreground">{{ container().label }}</p>
|
||||||
<p class="text-xs text-muted-foreground">{{ container().description }}</p>
|
<p class="text-xs text-muted-foreground">{{ container().description }}</p>
|
||||||
</div>
|
</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
|
{{ container().columns }} cols x {{ container().rows }} rows
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
#canvasRef
|
#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()"
|
[ngStyle]="frameStyle()"
|
||||||
>
|
>
|
||||||
<div class="theme-grid-editor__grid"></div>
|
<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>
|
<p class="mt-1 line-clamp-2 text-[11px] leading-4 text-muted-foreground">{{ item.description }}</p>
|
||||||
</div>
|
</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 }}
|
{{ item.grid.x }},{{ item.grid.y }} · {{ item.grid.w }}x{{ item.grid.h }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
|
|
||||||
@if (disabled()) {
|
@if (disabled()) {
|
||||||
<div
|
<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.
|
Fix JSON validation errors to re-enable the grid editor.
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
.theme-grid-editor__frame {
|
.theme-grid-editor__frame {
|
||||||
aspect-ratio: 16 / 9;
|
aspect-ratio: 16 / 9;
|
||||||
background:
|
background:
|
||||||
radial-gradient(circle at top, hsl(var(--primary) / 0.08), transparent 45%),
|
radial-gradient(circle at top, hsl(var(--primary) / 0.05), transparent 45%),
|
||||||
linear-gradient(180deg, hsl(var(--background) / 0.96), hsl(var(--card) / 0.98));
|
linear-gradient(180deg, hsl(var(--background) / 0.98), hsl(var(--card) / 0.98));
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-grid-editor__grid {
|
.theme-grid-editor__grid {
|
||||||
@@ -27,11 +27,9 @@
|
|||||||
.theme-grid-editor__item-body {
|
.theme-grid-editor__item-body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border: 1px solid hsl(var(--border) / 0.8);
|
border: 1px solid hsl(var(--border) / 0.8);
|
||||||
border-radius: 1rem;
|
border-radius: 0.5rem;
|
||||||
background:
|
background: linear-gradient(180deg, hsl(var(--card) / 0.98), hsl(var(--background) / 0.98));
|
||||||
linear-gradient(180deg, hsl(var(--card) / 0.96), hsl(var(--background) / 0.96)),
|
box-shadow: 0 1px 2px rgb(0 0 0 / 8%);
|
||||||
radial-gradient(circle at top right, hsl(var(--primary) / 0.1), transparent 45%);
|
|
||||||
box-shadow: 0 12px 30px rgb(0 0 0 / 10%);
|
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
padding: 0.9rem;
|
padding: 0.9rem;
|
||||||
}
|
}
|
||||||
@@ -42,9 +40,7 @@
|
|||||||
|
|
||||||
.theme-grid-editor__item--selected .theme-grid-editor__item-body {
|
.theme-grid-editor__item--selected .theme-grid-editor__item-body {
|
||||||
border-color: hsl(var(--primary));
|
border-color: hsl(var(--primary));
|
||||||
box-shadow:
|
box-shadow: 0 0 0 1px hsl(var(--primary));
|
||||||
0 0 0 1px hsl(var(--primary)),
|
|
||||||
0 14px 34px hsl(var(--primary) / 0.18);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-grid-editor__item--disabled .theme-grid-editor__item-body {
|
.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 {
|
.theme-grid-editor__item:focus-visible .theme-grid-editor__item-body {
|
||||||
box-shadow:
|
box-shadow: 0 0 0 2px hsl(var(--primary) / 0.25);
|
||||||
0 0 0 2px hsl(var(--primary)),
|
|
||||||
0 14px 34px hsl(var(--primary) / 0.18);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-grid-editor__handle {
|
.theme-grid-editor__handle {
|
||||||
@@ -68,12 +62,12 @@
|
|||||||
height: 0.95rem;
|
height: 0.95rem;
|
||||||
width: 0.95rem;
|
width: 0.95rem;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 999px;
|
border-radius: 0.25rem;
|
||||||
background: hsl(var(--primary));
|
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;
|
cursor: nwse-resize;
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-grid-editor__disabled {
|
.theme-grid-editor__disabled {
|
||||||
border: 1px dashed hsl(var(--border));
|
border: 1px dashed hsl(var(--border));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,37 +42,14 @@ export class ThemeGridEditorComponent {
|
|||||||
readonly itemChanged = output<{ key: string; grid: ThemeGridRect }>();
|
readonly itemChanged = output<{ key: string; grid: ThemeGridRect }>();
|
||||||
readonly itemSelected = output<string>();
|
readonly itemSelected = output<string>();
|
||||||
|
|
||||||
private readonly host = inject<ElementRef<HTMLElement>>(ElementRef);
|
|
||||||
private dragState: DragState | null = null;
|
|
||||||
|
|
||||||
readonly canvasRef = viewChild.required<ElementRef<HTMLElement>>('canvasRef');
|
readonly canvasRef = viewChild.required<ElementRef<HTMLElement>>('canvasRef');
|
||||||
readonly frameStyle = computed(() => ({
|
readonly frameStyle = computed(() => ({
|
||||||
'--theme-grid-columns': `${this.container().columns}`,
|
'--theme-grid-columns': `${this.container().columns}`,
|
||||||
'--theme-grid-rows': `${this.container().rows}`
|
'--theme-grid-rows': `${this.container().rows}`
|
||||||
}));
|
}));
|
||||||
|
|
||||||
itemStyle(item: ThemeGridEditorItem): Record<string, string> {
|
private readonly host = inject<ElementRef<HTMLElement>>(ElementRef);
|
||||||
const { columns, rows } = this.container();
|
private dragState: DragState | null = null;
|
||||||
|
|
||||||
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');
|
|
||||||
}
|
|
||||||
|
|
||||||
@HostListener('document:pointermove', ['$event'])
|
@HostListener('document:pointermove', ['$event'])
|
||||||
onPointerMove(event: PointerEvent): void {
|
onPointerMove(event: PointerEvent): void {
|
||||||
@@ -112,6 +89,29 @@ export class ThemeGridEditorComponent {
|
|||||||
this.dragState = null;
|
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 {
|
private startDrag(event: PointerEvent, item: ThemeGridEditorItem, mode: DragMode): void {
|
||||||
if (this.disabled()) {
|
if (this.disabled()) {
|
||||||
return;
|
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({
|
@Component({
|
||||||
selector: 'app-theme-json-code-editor',
|
selector: 'app-theme-json-code-editor',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
template: `
|
templateUrl: './theme-json-code-editor.component.html',
|
||||||
<div
|
styleUrl: './theme-json-code-editor.component.scss'
|
||||||
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;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
})
|
})
|
||||||
export class ThemeJsonCodeEditorComponent implements OnDestroy {
|
export class ThemeJsonCodeEditorComponent implements OnDestroy {
|
||||||
private readonly zone = inject(NgZone);
|
|
||||||
|
|
||||||
readonly editorHostRef = viewChild<ElementRef<HTMLDivElement>>('editorHostRef');
|
readonly editorHostRef = viewChild<ElementRef<HTMLDivElement>>('editorHostRef');
|
||||||
readonly value = input.required<string>();
|
readonly value = input.required<string>();
|
||||||
readonly fullscreen = input(false);
|
readonly fullscreen = input(false);
|
||||||
@@ -134,6 +95,8 @@ export class ThemeJsonCodeEditorComponent implements OnDestroy {
|
|||||||
|
|
||||||
readonly editorMinHeight = computed(() => this.fullscreen() ? 'max(34rem, calc(100vh - 15rem))' : '28rem');
|
readonly editorMinHeight = computed(() => this.fullscreen() ? 'max(34rem, calc(100vh - 15rem))' : '28rem');
|
||||||
|
|
||||||
|
private readonly zone = inject(NgZone);
|
||||||
|
|
||||||
private editorView: EditorView | null = null;
|
private editorView: EditorView | null = null;
|
||||||
private isApplyingExternalValue = false;
|
private isApplyingExternalValue = false;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<div
|
<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.min-h-full]="isFullscreen()"
|
||||||
[class.p-4]="isFullscreen()"
|
[class.p-4]="isFullscreen()"
|
||||||
[class.theme-settings--fullscreen]="isFullscreen()"
|
[class.theme-settings--fullscreen]="isFullscreen()"
|
||||||
@@ -15,21 +15,21 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
(click)="startPicker()"
|
(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
|
Pick UI Element
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
(click)="formatDraft()"
|
(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
|
Format JSON
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
(click)="copyLlmThemeGuide()"
|
(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
|
Copy LLM Guide
|
||||||
</button>
|
</button>
|
||||||
@@ -37,14 +37,14 @@
|
|||||||
type="button"
|
type="button"
|
||||||
(click)="applyDraft()"
|
(click)="applyDraft()"
|
||||||
[disabled]="!draftIsValid()"
|
[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
|
Apply Draft
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
(click)="restoreDefaultTheme()"
|
(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
|
Restore Default
|
||||||
</button>
|
</button>
|
||||||
@@ -60,8 +60,14 @@
|
|||||||
<div class="theme-settings__hero-grid mt-4">
|
<div class="theme-settings__hero-grid mt-4">
|
||||||
<div class="theme-settings__hero-stat">
|
<div class="theme-settings__hero-stat">
|
||||||
<div class="theme-settings__workspace-selector theme-settings__workspace-selector--compact">
|
<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
|
<select
|
||||||
|
id="theme-studio-workspace-select"
|
||||||
class="theme-settings__workspace-select"
|
class="theme-settings__workspace-select"
|
||||||
[value]="activeWorkspace()"
|
[value]="activeWorkspace()"
|
||||||
(change)="onWorkspaceSelect($event)"
|
(change)="onWorkspaceSelect($event)"
|
||||||
@@ -89,13 +95,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (statusMessage()) {
|
@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() }}
|
{{ statusMessage() }}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (!draftIsValid()) {
|
@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>
|
<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">
|
<ul class="mt-2 space-y-1 text-sm text-destructive/90">
|
||||||
@for (error of draftErrors(); track error) {
|
@for (error of draftErrors(); track error) {
|
||||||
@@ -122,7 +128,7 @@
|
|||||||
type="button"
|
type="button"
|
||||||
(click)="saveDraftAsNewTheme()"
|
(click)="saveDraftAsNewTheme()"
|
||||||
[disabled]="!draftIsValid() || savedThemesBusy()"
|
[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
|
Save New
|
||||||
</button>
|
</button>
|
||||||
@@ -130,7 +136,7 @@
|
|||||||
type="button"
|
type="button"
|
||||||
(click)="saveDraftToSelectedTheme()"
|
(click)="saveDraftToSelectedTheme()"
|
||||||
[disabled]="!draftIsValid() || !selectedSavedTheme() || savedThemesBusy()"
|
[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
|
Save Selected
|
||||||
</button>
|
</button>
|
||||||
@@ -138,7 +144,7 @@
|
|||||||
type="button"
|
type="button"
|
||||||
(click)="useSelectedSavedTheme()"
|
(click)="useSelectedSavedTheme()"
|
||||||
[disabled]="!selectedSavedTheme()?.isValid || (savedThemesBusy() && savedThemes().length === 0)"
|
[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
|
Use
|
||||||
</button>
|
</button>
|
||||||
@@ -146,7 +152,7 @@
|
|||||||
type="button"
|
type="button"
|
||||||
(click)="editSelectedSavedTheme()"
|
(click)="editSelectedSavedTheme()"
|
||||||
[disabled]="!selectedSavedTheme()?.isValid || (savedThemesBusy() && savedThemes().length === 0)"
|
[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
|
Edit
|
||||||
</button>
|
</button>
|
||||||
@@ -154,7 +160,7 @@
|
|||||||
type="button"
|
type="button"
|
||||||
(click)="removeSelectedSavedTheme()"
|
(click)="removeSelectedSavedTheme()"
|
||||||
[disabled]="!selectedSavedTheme() || (savedThemesBusy() && savedThemes().length === 0)"
|
[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
|
Remove
|
||||||
</button>
|
</button>
|
||||||
@@ -162,7 +168,7 @@
|
|||||||
type="button"
|
type="button"
|
||||||
(click)="refreshSavedThemes()"
|
(click)="refreshSavedThemes()"
|
||||||
[disabled]="savedThemesBusy() && savedThemes().length === 0"
|
[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
|
Refresh
|
||||||
</button>
|
</button>
|
||||||
@@ -205,7 +211,7 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
} @else {
|
} @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.
|
Save the current draft to create your first reusable Electron theme.
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -252,7 +258,7 @@
|
|||||||
<span class="mt-2 block text-xs leading-5 text-muted-foreground">{{ entry.description }}</span>
|
<span class="mt-2 block text-xs leading-5 text-muted-foreground">{{ entry.description }}</span>
|
||||||
</button>
|
</button>
|
||||||
} @empty {
|
} @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.
|
No registered theme keys match this filter.
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -283,7 +289,7 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
(click)="jumpToStyles()"
|
(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
|
Open styles in JSON
|
||||||
</button>
|
</button>
|
||||||
@@ -291,7 +297,7 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
(click)="jumpToLayout()"
|
(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
|
Open layout in JSON
|
||||||
</button>
|
</button>
|
||||||
@@ -315,10 +321,10 @@
|
|||||||
<p class="text-sm font-semibold text-foreground">Theme JSON</p>
|
<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">
|
<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-md 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-md 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-md 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-slate-900 px-2.5 py-1 text-slate-200">IDE editor</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -334,7 +340,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@if (activeWorkspace() === 'inspector') {
|
@if (activeWorkspace() === 'inspector') {
|
||||||
<div class="space-y-6">
|
<div class="space-y-4">
|
||||||
<section class="theme-studio-card p-4">
|
<section class="theme-studio-card p-4">
|
||||||
<div class="flex items-center justify-between gap-3">
|
<div class="flex items-center justify-between gap-3">
|
||||||
<p class="text-sm font-semibold text-foreground">Selection</p>
|
<p class="text-sm font-semibold text-foreground">Selection</p>
|
||||||
@@ -342,14 +348,14 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
(click)="startPicker()"
|
(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
|
Pick live element
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (selectedElement()) {
|
@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">
|
<div class="flex items-center gap-2">
|
||||||
<p class="text-base font-semibold text-foreground">{{ selectedElement()!.label }}</p>
|
<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>
|
<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"
|
type="button"
|
||||||
(click)="addStarterAnimation()"
|
(click)="addStarterAnimation()"
|
||||||
[disabled]="!draftIsValid()"
|
[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
|
Add fade animation
|
||||||
</button>
|
</button>
|
||||||
@@ -382,7 +388,7 @@
|
|||||||
type="button"
|
type="button"
|
||||||
(click)="applySuggestedProperty(field.key)"
|
(click)="applySuggestedProperty(field.key)"
|
||||||
[disabled]="!draftIsValid()"
|
[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 class="flex items-start justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
@@ -391,7 +397,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<span class="rounded-full bg-secondary px-2 py-0.5 text-[10px] font-medium text-muted-foreground">{{ field.type }}</span>
|
<span class="rounded-full bg-secondary px-2 py-0.5 text-[10px] font-medium text-muted-foreground">{{ field.type }}</span>
|
||||||
</div>
|
</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 }}
|
{{ field.example }}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
@@ -399,7 +405,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</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>
|
<p class="text-sm font-semibold text-foreground">Animation Keys</p>
|
||||||
|
|
||||||
@if (animationKeys().length > 0) {
|
@if (animationKeys().length > 0) {
|
||||||
@@ -408,22 +414,22 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
(click)="jumpToAnimation(animationKey)"
|
(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 }}
|
{{ animationKey }}
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
} @else {
|
} @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.
|
No custom animation keys yet.
|
||||||
</div>
|
</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">
|
<div class="flex flex-wrap gap-2">
|
||||||
@for (field of THEME_ANIMATION_FIELDS; track field.key) {
|
@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>
|
||||||
</div>
|
</div>
|
||||||
@@ -441,7 +447,7 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
(click)="selectContainer(container.key)"
|
(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.bg-primary/10]="selectedContainer() === container.key"
|
||||||
[class.border-primary/40]="selectedContainer() === container.key"
|
[class.border-primary/40]="selectedContainer() === container.key"
|
||||||
>
|
>
|
||||||
@@ -453,7 +459,7 @@
|
|||||||
type="button"
|
type="button"
|
||||||
(click)="resetSelectedContainer()"
|
(click)="resetSelectedContainer()"
|
||||||
[disabled]="!draftIsValid()"
|
[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
|
Reset Container
|
||||||
</button>
|
</button>
|
||||||
@@ -473,19 +479,19 @@
|
|||||||
|
|
||||||
@if (selectedElementGrid()) {
|
@if (selectedElementGrid()) {
|
||||||
<div class="mt-4 grid gap-3 sm:grid-cols-4">
|
<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="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>
|
<p class="mt-2 font-mono text-lg font-semibold text-foreground">{{ selectedElementGrid()!.grid.x }}</p>
|
||||||
</div>
|
</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="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>
|
<p class="mt-2 font-mono text-lg font-semibold text-foreground">{{ selectedElementGrid()!.grid.y }}</p>
|
||||||
</div>
|
</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="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>
|
<p class="mt-2 font-mono text-lg font-semibold text-foreground">{{ selectedElementGrid()!.grid.w }}</p>
|
||||||
</div>
|
</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="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>
|
<p class="mt-2 font-mono text-lg font-semibold text-foreground">{{ selectedElementGrid()!.grid.h }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -29,11 +29,11 @@
|
|||||||
.theme-settings__workspace-select {
|
.theme-settings__workspace-select {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: 1px solid hsl(var(--border));
|
border: 1px solid hsl(var(--border));
|
||||||
border-radius: 0.85rem;
|
border-radius: 0.5rem;
|
||||||
background: hsl(var(--background) / 0.82);
|
background: hsl(var(--secondary));
|
||||||
padding: 0.65rem 0.8rem;
|
padding: 0.6rem 0.75rem;
|
||||||
font-size: 0.88rem;
|
font-size: 0.875rem;
|
||||||
font-weight: 600;
|
font-weight: 500;
|
||||||
color: hsl(var(--foreground));
|
color: hsl(var(--foreground));
|
||||||
outline: none;
|
outline: none;
|
||||||
transition:
|
transition:
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
|
|
||||||
.theme-settings__workspace-select:focus {
|
.theme-settings__workspace-select:focus {
|
||||||
border-color: hsl(var(--primary) / 0.4);
|
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 {
|
.theme-settings__workspace-selector--compact .theme-settings__workspace-select {
|
||||||
@@ -68,20 +68,19 @@
|
|||||||
.theme-settings__saved-theme-list {
|
.theme-settings__saved-theme-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.7rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-settings__saved-theme-button {
|
.theme-settings__saved-theme-button {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: 1px solid hsl(var(--border) / 0.8);
|
border: 1px solid hsl(var(--border) / 0.8);
|
||||||
border-radius: 1rem;
|
border-radius: 0.5rem;
|
||||||
background: hsl(var(--background) / 0.65);
|
background: hsl(var(--secondary) / 0.25);
|
||||||
padding: 0.85rem;
|
padding: 0.75rem;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
transition:
|
transition:
|
||||||
border-color 160ms ease,
|
border-color 160ms ease,
|
||||||
background-color 160ms ease,
|
background-color 160ms ease;
|
||||||
transform 160ms ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-settings__saved-theme-button:hover {
|
.theme-settings__saved-theme-button:hover {
|
||||||
@@ -89,7 +88,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.theme-settings__saved-theme-button--active {
|
.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);
|
background: hsl(var(--primary) / 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,4 +113,4 @@
|
|||||||
display: block;
|
display: block;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,12 +44,12 @@ type ThemeStudioWorkspace = 'editor' | 'inspector' | 'layout';
|
|||||||
styleUrl: './theme-settings.component.scss'
|
styleUrl: './theme-settings.component.scss'
|
||||||
})
|
})
|
||||||
export class ThemeSettingsComponent {
|
export class ThemeSettingsComponent {
|
||||||
private readonly modal = inject(SettingsModalService);
|
readonly modal = inject(SettingsModalService);
|
||||||
private readonly theme = inject(ThemeService);
|
readonly theme = inject(ThemeService);
|
||||||
private readonly themeLibrary = inject(ThemeLibraryService);
|
readonly themeLibrary = inject(ThemeLibraryService);
|
||||||
private readonly registry = inject(ThemeRegistryService);
|
readonly registry = inject(ThemeRegistryService);
|
||||||
private readonly picker = inject(ElementPickerService);
|
readonly picker = inject(ElementPickerService);
|
||||||
private readonly layoutSync = inject(LayoutSyncService);
|
readonly layoutSync = inject(LayoutSyncService);
|
||||||
|
|
||||||
readonly editorRef = viewChild<ThemeJsonCodeEditorComponent>('jsonEditorRef');
|
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',
|
selector: 'app-theme-picker-overlay',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule],
|
imports: [CommonModule],
|
||||||
template: `
|
templateUrl: './theme-picker-overlay.component.html'
|
||||||
@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>
|
|
||||||
}
|
|
||||||
`
|
|
||||||
})
|
})
|
||||||
export class ThemePickerOverlayComponent {
|
export class ThemePickerOverlayComponent {
|
||||||
readonly picker = inject(ElementPickerService);
|
readonly picker = inject(ElementPickerService);
|
||||||
private readonly registry = inject(ThemeRegistryService);
|
readonly registry = inject(ThemeRegistryService);
|
||||||
|
|
||||||
readonly hoveredEntry = computed(() => {
|
readonly hoveredEntry = computed(() => {
|
||||||
return this.registry.getDefinition(this.picker.hoveredKey());
|
return this.registry.getDefinition(this.picker.hoveredKey());
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
(click)="toggleSettings()"
|
(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
|
<ng-icon
|
||||||
name="lucideSettings"
|
name="lucideSettings"
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
@if (canManageChannels()) {
|
@if (canManageChannels()) {
|
||||||
<button
|
<button
|
||||||
(click)="createChannel('text')"
|
(click)="createChannel('text')"
|
||||||
class="rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground"
|
class="grid h-7 w-7 place-items-center rounded-md text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground"
|
||||||
title="Create Text Channel"
|
title="Create Text Channel"
|
||||||
>
|
>
|
||||||
<ng-icon
|
<ng-icon
|
||||||
@@ -98,7 +98,7 @@
|
|||||||
@if (canManageChannels()) {
|
@if (canManageChannels()) {
|
||||||
<button
|
<button
|
||||||
(click)="createChannel('voice')"
|
(click)="createChannel('voice')"
|
||||||
class="rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground"
|
class="grid h-7 w-7 place-items-center rounded-md text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground"
|
||||||
title="Create Voice Channel"
|
title="Create Voice Channel"
|
||||||
>
|
>
|
||||||
<ng-icon
|
<ng-icon
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
@if (server()) {
|
@if (server()) {
|
||||||
<div class="space-y-3 max-w-xl">
|
<div class="max-w-2xl space-y-3">
|
||||||
@if (bannedUsers().length === 0) {
|
@if (bannedUsers().length === 0) {
|
||||||
<p class="text-sm text-muted-foreground text-center py-8">No banned users</p>
|
<p class="text-sm text-muted-foreground text-center py-8">No banned users</p>
|
||||||
} @else {
|
} @else {
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
(click)="unbanUser(ban)"
|
(click)="unbanUser(ban)"
|
||||||
class="p-2 hover:bg-secondary rounded-lg transition-colors text-muted-foreground hover:text-foreground"
|
class="grid h-8 w-8 place-items-center rounded-lg text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground"
|
||||||
>
|
>
|
||||||
<ng-icon
|
<ng-icon
|
||||||
name="lucideX"
|
name="lucideX"
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<div class="max-w-3xl space-y-6">
|
<div class="max-w-4xl space-y-5">
|
||||||
<section class="rounded-xl border border-border bg-card/40 p-5">
|
<section class="rounded-lg border border-border bg-card/40 p-5">
|
||||||
<div class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
<div class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
||||||
<div class="flex items-start gap-3">
|
<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
|
<ng-icon
|
||||||
name="lucideBug"
|
name="lucideBug"
|
||||||
class="h-5 w-5"
|
class="h-5 w-5"
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="grid gap-3 sm:grid-cols-3">
|
<section class="grid gap-3 sm:grid-cols-3">
|
||||||
<div class="rounded-xl border border-border bg-secondary/20 p-4">
|
<div class="rounded-lg border border-border bg-secondary/20 p-4">
|
||||||
<div class="flex items-center gap-2 text-muted-foreground">
|
<div class="flex items-center gap-2 text-muted-foreground">
|
||||||
<ng-icon
|
<ng-icon
|
||||||
name="lucideClock3"
|
name="lucideClock3"
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
<p class="mt-1 text-xs text-muted-foreground">Last update: {{ lastUpdatedLabel() }}</p>
|
<p class="mt-1 text-xs text-muted-foreground">Last update: {{ lastUpdatedLabel() }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-xl border border-border bg-secondary/20 p-4">
|
<div class="rounded-lg border border-border bg-secondary/20 p-4">
|
||||||
<div class="flex items-center gap-2 text-muted-foreground">
|
<div class="flex items-center gap-2 text-muted-foreground">
|
||||||
<ng-icon
|
<ng-icon
|
||||||
name="lucideCircleAlert"
|
name="lucideCircleAlert"
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
<p class="mt-1 text-xs text-muted-foreground">Unhandled runtime failures and rejected promises.</p>
|
<p class="mt-1 text-xs text-muted-foreground">Unhandled runtime failures and rejected promises.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-xl border border-border bg-secondary/20 p-4">
|
<div class="rounded-lg border border-border bg-secondary/20 p-4">
|
||||||
<div class="flex items-center gap-2 text-muted-foreground">
|
<div class="flex items-center gap-2 text-muted-foreground">
|
||||||
<ng-icon
|
<ng-icon
|
||||||
name="lucideTriangleAlert"
|
name="lucideTriangleAlert"
|
||||||
@@ -69,7 +69,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="rounded-xl border border-border bg-card/40 p-5">
|
<section class="rounded-lg border border-border bg-card/40 p-5">
|
||||||
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h5 class="text-sm font-semibold text-foreground">Floating debug console</h5>
|
<h5 class="text-sm font-semibold text-foreground">Floating debug console</h5>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<div class="space-y-6 max-w-xl">
|
<div class="max-w-3xl space-y-5">
|
||||||
<section>
|
<section>
|
||||||
<div class="flex items-center gap-2 mb-3">
|
<div class="flex items-center gap-2 mb-3">
|
||||||
<ng-icon
|
<ng-icon
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
@if (canKickMembers(member)) {
|
@if (canKickMembers(member)) {
|
||||||
<button
|
<button
|
||||||
(click)="kickMember(member)"
|
(click)="kickMember(member)"
|
||||||
class="rounded p-1 text-muted-foreground transition-colors hover:bg-destructive/20 hover:text-destructive"
|
class="grid h-8 w-8 place-items-center rounded-md text-muted-foreground transition-colors hover:bg-destructive/20 hover:text-destructive"
|
||||||
title="Kick"
|
title="Kick"
|
||||||
>
|
>
|
||||||
<ng-icon
|
<ng-icon
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
@if (canBanMembers(member)) {
|
@if (canBanMembers(member)) {
|
||||||
<button
|
<button
|
||||||
(click)="banMember(member)"
|
(click)="banMember(member)"
|
||||||
class="rounded p-1 text-muted-foreground transition-colors hover:bg-destructive/20 hover:text-destructive"
|
class="grid h-8 w-8 place-items-center rounded-md text-muted-foreground transition-colors hover:bg-destructive/20 hover:text-destructive"
|
||||||
title="Ban"
|
title="Ban"
|
||||||
>
|
>
|
||||||
<ng-icon
|
<ng-icon
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<div class="space-y-6 max-w-xl">
|
<div class="max-w-3xl space-y-5">
|
||||||
<!-- Server Endpoints -->
|
<!-- Server Endpoints -->
|
||||||
<section>
|
<section>
|
||||||
<div class="flex items-center justify-between mb-3">
|
<div class="flex items-center justify-between mb-3">
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
(click)="setActiveServer(server.id)"
|
(click)="setActiveServer(server.id)"
|
||||||
class="p-1.5 hover:bg-secondary rounded-lg transition-colors"
|
class="grid h-8 w-8 place-items-center rounded-lg transition-colors hover:bg-secondary"
|
||||||
title="Activate"
|
title="Activate"
|
||||||
>
|
>
|
||||||
<ng-icon
|
<ng-icon
|
||||||
@@ -90,7 +90,7 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
(click)="deactivateServer(server.id)"
|
(click)="deactivateServer(server.id)"
|
||||||
class="p-1.5 hover:bg-secondary rounded-lg transition-colors"
|
class="grid h-8 w-8 place-items-center rounded-lg transition-colors hover:bg-secondary"
|
||||||
title="Deactivate"
|
title="Deactivate"
|
||||||
>
|
>
|
||||||
<ng-icon
|
<ng-icon
|
||||||
@@ -103,7 +103,7 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
(click)="removeServer(server.id)"
|
(click)="removeServer(server.id)"
|
||||||
class="p-1.5 hover:bg-destructive/10 rounded-lg transition-colors"
|
class="grid h-8 w-8 place-items-center rounded-lg transition-colors hover:bg-destructive/10"
|
||||||
title="Remove"
|
title="Remove"
|
||||||
>
|
>
|
||||||
<ng-icon
|
<ng-icon
|
||||||
@@ -139,7 +139,7 @@
|
|||||||
type="button"
|
type="button"
|
||||||
(click)="addServer()"
|
(click)="addServer()"
|
||||||
[disabled]="!newServerName || !newServerUrl"
|
[disabled]="!newServerName || !newServerUrl"
|
||||||
class="px-3 py-1.5 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed self-end"
|
class="grid h-9 w-9 place-items-center self-end rounded-lg bg-primary text-primary-foreground transition-colors hover:bg-primary/90 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
>
|
>
|
||||||
<ng-icon
|
<ng-icon
|
||||||
name="lucidePlus"
|
name="lucidePlus"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
@if (serverData()) {
|
@if (serverData()) {
|
||||||
<div class="space-y-5 max-w-xl">
|
<div class="max-w-2xl space-y-5">
|
||||||
<section>
|
<section>
|
||||||
<h4 class="text-sm font-semibold text-foreground mb-3">Room Settings</h4>
|
<h4 class="text-sm font-semibold text-foreground mb-3">Room Settings</h4>
|
||||||
@if (!isAdmin()) {
|
@if (!isAdmin()) {
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
<button
|
<button
|
||||||
(click)="togglePrivate()"
|
(click)="togglePrivate()"
|
||||||
type="button"
|
type="button"
|
||||||
class="p-2 rounded-lg transition-colors"
|
class="grid h-9 w-9 place-items-center rounded-lg transition-colors"
|
||||||
[class.bg-primary]="isPrivate()"
|
[class.bg-primary]="isPrivate()"
|
||||||
[class.text-primary-foreground]="isPrivate()"
|
[class.text-primary-foreground]="isPrivate()"
|
||||||
[class.bg-secondary]="!isPrivate()"
|
[class.bg-secondary]="!isPrivate()"
|
||||||
|
|||||||
@@ -16,8 +16,8 @@
|
|||||||
<!-- Modal -->
|
<!-- Modal -->
|
||||||
<div class="fixed inset-0 z-[91] flex items-center justify-center p-4 pointer-events-none">
|
<div class="fixed inset-0 z-[91] flex items-center justify-center p-4 pointer-events-none">
|
||||||
<div
|
<div
|
||||||
class="pointer-events-auto relative flex w-full max-w-4xl overflow-hidden rounded-xl border border-border bg-card shadow-2xl transition-all duration-200"
|
class="pointer-events-auto relative flex w-full max-w-5xl overflow-hidden rounded-lg border border-border bg-card shadow-lg transition-all duration-200"
|
||||||
style="height: min(680px, 85vh)"
|
style="height: min(720px, 88vh)"
|
||||||
[class.scale-100]="animating()"
|
[class.scale-100]="animating()"
|
||||||
[class.opacity-100]="animating()"
|
[class.opacity-100]="animating()"
|
||||||
[class.scale-95]="!animating()"
|
[class.scale-95]="!animating()"
|
||||||
@@ -30,24 +30,25 @@
|
|||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
>
|
>
|
||||||
<!-- Side Navigation -->
|
<!-- Side Navigation -->
|
||||||
<nav class="flex w-52 flex-shrink-0 flex-col border-r border-border bg-secondary/40">
|
<nav class="flex w-56 flex-shrink-0 flex-col border-r border-border bg-card">
|
||||||
<div class="p-4 border-b border-border">
|
<div class="border-b border-border px-3 py-3">
|
||||||
<h2 class="text-lg font-semibold text-foreground">Settings</h2>
|
<h2 class="text-lg font-semibold text-foreground">Settings</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-1 overflow-y-auto py-2">
|
<div class="flex-1 overflow-y-auto py-2">
|
||||||
<!-- Global section -->
|
<!-- Global section -->
|
||||||
<p class="px-4 py-1.5 text-[11px] font-semibold text-muted-foreground/70 uppercase tracking-wider">General</p>
|
<p class="px-3 py-1.5 text-[11px] font-semibold uppercase tracking-[0.18em] text-muted-foreground">General</p>
|
||||||
@for (page of globalPages; track page.id) {
|
@for (page of globalPages; track page.id) {
|
||||||
<button
|
<button
|
||||||
(click)="navigate(page.id)"
|
(click)="navigate(page.id)"
|
||||||
type="button"
|
type="button"
|
||||||
class="w-full flex items-center gap-2.5 px-4 py-2 text-sm transition-colors"
|
class="mx-2 flex w-[calc(100%-1rem)] items-center gap-2.5 rounded-md px-2.5 py-1.5 text-sm transition-colors"
|
||||||
[class.bg-primary/10]="activePage() === page.id"
|
[class.bg-secondary]="activePage() === page.id"
|
||||||
[class.text-primary]="activePage() === page.id"
|
[class.text-foreground]="activePage() === page.id"
|
||||||
[class.font-medium]="activePage() === page.id"
|
[class.font-medium]="activePage() === page.id"
|
||||||
[class.text-foreground]="activePage() !== page.id"
|
[class.text-muted-foreground]="activePage() !== page.id"
|
||||||
[class.hover:bg-secondary]="activePage() !== page.id"
|
[class.hover:bg-secondary/70]="activePage() !== page.id"
|
||||||
|
[class.hover:text-foreground]="activePage() !== page.id"
|
||||||
>
|
>
|
||||||
<ng-icon
|
<ng-icon
|
||||||
[name]="page.icon"
|
[name]="page.icon"
|
||||||
@@ -60,12 +61,12 @@
|
|||||||
<!-- Server section -->
|
<!-- Server section -->
|
||||||
@if (manageableRooms().length > 0) {
|
@if (manageableRooms().length > 0) {
|
||||||
<div class="mt-3 pt-3 border-t border-border">
|
<div class="mt-3 pt-3 border-t border-border">
|
||||||
<p class="px-4 py-1.5 text-[11px] font-semibold text-muted-foreground/70 uppercase tracking-wider">Server</p>
|
<p class="px-3 py-1.5 text-[11px] font-semibold uppercase tracking-[0.18em] text-muted-foreground">Server</p>
|
||||||
|
|
||||||
<!-- Server selector -->
|
<!-- Server selector -->
|
||||||
<div class="px-3 pb-2">
|
<div class="px-3 pb-2">
|
||||||
<select
|
<select
|
||||||
class="w-full px-2 py-1.5 bg-secondary rounded-lg border border-border text-foreground text-xs focus:outline-none focus:ring-1 focus:ring-primary"
|
class="w-full rounded-lg border border-border bg-secondary px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-primary"
|
||||||
[value]="selectedServerId() || ''"
|
[value]="selectedServerId() || ''"
|
||||||
(change)="onServerSelect($event)"
|
(change)="onServerSelect($event)"
|
||||||
>
|
>
|
||||||
@@ -81,12 +82,13 @@
|
|||||||
<button
|
<button
|
||||||
(click)="navigate(page.id)"
|
(click)="navigate(page.id)"
|
||||||
type="button"
|
type="button"
|
||||||
class="w-full flex items-center gap-2.5 px-4 py-2 text-sm transition-colors"
|
class="mx-2 flex w-[calc(100%-1rem)] items-center gap-2.5 rounded-md px-2.5 py-1.5 text-sm transition-colors"
|
||||||
[class.bg-primary/10]="activePage() === page.id"
|
[class.bg-secondary]="activePage() === page.id"
|
||||||
[class.text-primary]="activePage() === page.id"
|
[class.text-foreground]="activePage() === page.id"
|
||||||
[class.font-medium]="activePage() === page.id"
|
[class.font-medium]="activePage() === page.id"
|
||||||
[class.text-foreground]="activePage() !== page.id"
|
[class.text-muted-foreground]="activePage() !== page.id"
|
||||||
[class.hover:bg-secondary]="activePage() !== page.id"
|
[class.hover:bg-secondary/70]="activePage() !== page.id"
|
||||||
|
[class.hover:text-foreground]="activePage() !== page.id"
|
||||||
>
|
>
|
||||||
<ng-icon
|
<ng-icon
|
||||||
[name]="page.icon"
|
[name]="page.icon"
|
||||||
@@ -100,15 +102,7 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-auto space-y-3 border-t border-border px-4 py-3">
|
<div class="mt-auto border-t border-border px-3 py-3">
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
(click)="restoreDefaultTheme()"
|
|
||||||
class="w-full rounded-lg border border-destructive/25 bg-destructive/10 px-3 py-2 text-left text-xs font-medium text-destructive transition-colors hover:bg-destructive/15"
|
|
||||||
>
|
|
||||||
Restore default theme
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
(click)="openThirdPartyLicenses()"
|
(click)="openThirdPartyLicenses()"
|
||||||
@@ -122,7 +116,7 @@
|
|||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
<div class="flex-1 flex flex-col min-w-0">
|
<div class="flex-1 flex flex-col min-w-0">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="flex items-center justify-between px-6 py-4 border-b border-border flex-shrink-0">
|
<div class="flex items-center justify-between border-b border-border px-5 py-3 flex-shrink-0">
|
||||||
<h3 class="text-lg font-semibold text-foreground">
|
<h3 class="text-lg font-semibold text-foreground">
|
||||||
@switch (activePage()) {
|
@switch (activePage()) {
|
||||||
@case ('general') {
|
@case ('general') {
|
||||||
@@ -164,7 +158,7 @@
|
|||||||
<button
|
<button
|
||||||
(click)="close()"
|
(click)="close()"
|
||||||
type="button"
|
type="button"
|
||||||
class="p-2 hover:bg-secondary rounded-lg transition-colors text-muted-foreground hover:text-foreground"
|
class="grid h-9 w-9 place-items-center rounded-lg text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground"
|
||||||
>
|
>
|
||||||
<ng-icon
|
<ng-icon
|
||||||
name="lucideX"
|
name="lucideX"
|
||||||
@@ -175,7 +169,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Scrollable Content Area -->
|
<!-- Scrollable Content Area -->
|
||||||
<div class="flex-1 overflow-y-auto p-6">
|
<div class="flex-1 overflow-y-auto bg-background px-4 py-4 sm:px-5 sm:py-4">
|
||||||
@switch (activePage()) {
|
@switch (activePage()) {
|
||||||
@case ('general') {
|
@case ('general') {
|
||||||
<app-general-settings />
|
<app-general-settings />
|
||||||
@@ -184,70 +178,79 @@
|
|||||||
<app-network-settings />
|
<app-network-settings />
|
||||||
}
|
}
|
||||||
@case ('theme') {
|
@case ('theme') {
|
||||||
<div class="mx-auto flex h-full max-w-3xl items-center justify-center">
|
<div class="max-w-3xl space-y-5">
|
||||||
<div class="w-full rounded-[1.5rem] border border-border bg-card/90 p-6 shadow-sm">
|
<div class="flex flex-wrap items-start justify-between gap-4">
|
||||||
<div class="flex flex-wrap items-start justify-between gap-4">
|
<div>
|
||||||
<div>
|
<p class="text-xs font-semibold uppercase tracking-[0.18em] text-primary">Active Theme</p>
|
||||||
<p class="text-xs font-semibold uppercase tracking-[0.18em] text-primary">Active Theme</p>
|
<h4 class="mt-2 text-lg font-semibold text-foreground">{{ activeThemeName() }}</h4>
|
||||||
<h4 class="mt-2 text-xl font-semibold text-foreground">{{ activeThemeName() }}</h4>
|
<p class="mt-2 max-w-2xl text-sm text-muted-foreground">
|
||||||
</div>
|
Launch Theme Studio to edit the live draft, inspect themeable regions, or switch to a saved theme.
|
||||||
|
</p>
|
||||||
@if (themeStudioMinimized()) {
|
|
||||||
<span class="rounded-full border border-primary/25 bg-primary/10 px-3 py-1 text-xs font-semibold text-primary">Minimized</span>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (savedThemesAvailable()) {
|
@if (themeStudioMinimized()) {
|
||||||
<div class="mt-5">
|
<span class="rounded-full bg-primary/10 px-2.5 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-primary"
|
||||||
<label class="text-[11px] font-semibold uppercase tracking-[0.18em] text-muted-foreground">Saved Theme</label>
|
>Minimized</span
|
||||||
|
>
|
||||||
<div class="mt-2 flex flex-wrap gap-2">
|
|
||||||
<select
|
|
||||||
class="min-w-[16rem] flex-1 rounded-full border border-border bg-background px-3 py-2 text-sm font-medium text-foreground outline-none transition-colors focus:border-primary/40 focus:ring-2 focus:ring-primary/15"
|
|
||||||
[value]="selectedSavedTheme()?.fileName || ''"
|
|
||||||
[disabled]="savedThemesBusy() && savedThemes().length === 0"
|
|
||||||
(change)="onSavedThemeSelect($event)"
|
|
||||||
>
|
|
||||||
<option value="">{{ savedThemes().length > 0 ? 'Choose saved theme' : 'No saved themes' }}</option>
|
|
||||||
@for (savedTheme of savedThemes(); track savedTheme.fileName) {
|
|
||||||
<option
|
|
||||||
[value]="savedTheme.fileName"
|
|
||||||
[disabled]="!savedTheme.isValid"
|
|
||||||
>
|
|
||||||
{{ savedTheme.themeName }}
|
|
||||||
</option>
|
|
||||||
}
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
(click)="editSelectedSavedTheme()"
|
|
||||||
[disabled]="!selectedSavedTheme()?.isValid || (savedThemesBusy() && savedThemes().length === 0)"
|
|
||||||
class="inline-flex items-center rounded-full border border-border bg-background px-3 py-2 text-sm font-medium text-foreground transition-colors hover:bg-secondary disabled:cursor-not-allowed disabled:opacity-60"
|
|
||||||
>
|
|
||||||
Edit In Studio
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mt-5 flex flex-wrap gap-2">
|
@if (savedThemesAvailable()) {
|
||||||
<button
|
<div class="space-y-2">
|
||||||
type="button"
|
<label
|
||||||
(click)="openThemeStudio()"
|
for="settings-saved-theme-select"
|
||||||
class="inline-flex items-center rounded-full bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground transition-colors hover:bg-primary/90"
|
class="text-[11px] font-semibold uppercase tracking-[0.18em] text-muted-foreground"
|
||||||
>
|
>
|
||||||
{{ themeStudioMinimized() ? 'Re-open Theme Studio' : 'Open Theme Studio' }}
|
Saved Theme
|
||||||
</button>
|
</label>
|
||||||
|
|
||||||
<button
|
<div class="flex flex-wrap gap-2">
|
||||||
type="button"
|
<select
|
||||||
(click)="restoreDefaultTheme()"
|
id="settings-saved-theme-select"
|
||||||
class="inline-flex items-center rounded-full border border-destructive/25 bg-destructive/10 px-3 py-2 text-sm font-medium text-destructive transition-colors hover:bg-destructive/15"
|
class="min-w-[16rem] flex-1 rounded-lg border border-border bg-secondary px-3 py-2 text-sm text-foreground outline-none transition-colors focus:border-primary/40 focus:ring-1 focus:ring-primary"
|
||||||
>
|
[value]="selectedSavedTheme()?.fileName || ''"
|
||||||
Restore Default
|
[disabled]="savedThemesBusy() && savedThemes().length === 0"
|
||||||
</button>
|
(change)="onSavedThemeSelect($event)"
|
||||||
|
>
|
||||||
|
<option value="">{{ savedThemes().length > 0 ? 'Choose saved theme' : 'No saved themes' }}</option>
|
||||||
|
@for (savedTheme of savedThemes(); track savedTheme.fileName) {
|
||||||
|
<option
|
||||||
|
[value]="savedTheme.fileName"
|
||||||
|
[disabled]="!savedTheme.isValid"
|
||||||
|
>
|
||||||
|
{{ savedTheme.themeName }}
|
||||||
|
</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
(click)="editSelectedSavedTheme()"
|
||||||
|
[disabled]="!selectedSavedTheme()?.isValid || (savedThemesBusy() && savedThemes().length === 0)"
|
||||||
|
class="inline-flex items-center rounded-lg border border-border bg-secondary px-3 py-2 text-sm font-medium text-foreground transition-colors hover:bg-secondary/80 disabled:cursor-not-allowed disabled:opacity-60"
|
||||||
|
>
|
||||||
|
Edit In Studio
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
(click)="openThemeStudio()"
|
||||||
|
class="inline-flex items-center rounded-lg bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground transition-colors hover:bg-primary/90"
|
||||||
|
>
|
||||||
|
{{ themeStudioMinimized() ? 'Re-open Theme Studio' : 'Open Theme Studio' }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
(click)="restoreDefaultTheme()"
|
||||||
|
class="inline-flex items-center rounded-lg border border-destructive/25 bg-destructive/10 px-3 py-2 text-sm font-medium text-destructive transition-colors hover:bg-destructive/15"
|
||||||
|
>
|
||||||
|
Restore Default
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -304,18 +307,20 @@
|
|||||||
aria-label="Close third-party licenses"
|
aria-label="Close third-party licenses"
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
<div class="pointer-events-none absolute inset-0 z-[11] flex items-center justify-center p-4 sm:p-6">
|
<div class="pointer-events-none absolute inset-0 z-[11] flex justify-center p-4 sm:p-6">
|
||||||
<div class="pointer-events-auto w-full max-w-2xl max-h-full overflow-hidden rounded-xl border border-border bg-card shadow-2xl">
|
<div
|
||||||
|
class="pointer-events-auto flex min-h-0 w-full max-w-2xl self-stretch flex-col overflow-hidden rounded-lg border border-border bg-card shadow-lg"
|
||||||
|
>
|
||||||
<div class="flex items-start justify-between gap-4 border-b border-border px-5 py-4">
|
<div class="flex items-start justify-between gap-4 border-b border-border px-5 py-4">
|
||||||
<div>
|
<div>
|
||||||
<h4 class="text-base font-semibold text-foreground">Third-party licenses</h4>
|
<h4 class="text-base font-semibold text-foreground">Third-party licenses</h4>
|
||||||
<p class="mt-1 text-sm text-muted-foreground">License notices for bundled third-party libraries used by the app.</p>
|
<p class="mt-1 text-sm text-muted-foreground">License information for bundled third-party libraries used by the app.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
(click)="closeThirdPartyLicenses()"
|
(click)="closeThirdPartyLicenses()"
|
||||||
class="p-2 hover:bg-secondary rounded-lg transition-colors text-muted-foreground hover:text-foreground"
|
class="grid h-9 w-9 place-items-center rounded-lg text-muted-foreground transition-colors hover:bg-secondary hover:text-foreground"
|
||||||
aria-label="Close third-party licenses"
|
aria-label="Close third-party licenses"
|
||||||
>
|
>
|
||||||
<ng-icon
|
<ng-icon
|
||||||
@@ -325,7 +330,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="max-h-[min(70vh,42rem)] overflow-y-auto px-5 py-4 space-y-4">
|
<div class="min-h-0 flex-1 overflow-y-auto px-5 py-4 space-y-4">
|
||||||
@for (license of thirdPartyLicenses; track license.id) {
|
@for (license of thirdPartyLicenses; track license.id) {
|
||||||
<section class="rounded-lg border border-border bg-secondary/20 p-4">
|
<section class="rounded-lg border border-border bg-secondary/20 p-4">
|
||||||
<div class="flex items-start justify-between gap-4">
|
<div class="flex items-start justify-between gap-4">
|
||||||
@@ -340,14 +345,32 @@
|
|||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
class="text-xs font-medium text-primary hover:underline underline-offset-4"
|
class="text-xs font-medium text-primary hover:underline underline-offset-4"
|
||||||
>
|
>
|
||||||
Source
|
View license
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<pre
|
<div class="mt-4 rounded-md bg-background/80 px-3 py-3">
|
||||||
class="mt-4 whitespace-pre-wrap break-words rounded-md bg-background/80 px-3 py-2 text-[11px] leading-5 text-muted-foreground"
|
<p class="text-[11px] font-semibold uppercase tracking-[0.18em] text-muted-foreground">Packages</p>
|
||||||
>{{ license.text }}</pre
|
<div class="mt-3 flex flex-wrap gap-2">
|
||||||
>
|
@for (packageName of license.packages; track packageName) {
|
||||||
|
<span class="rounded-full border border-border bg-card px-2.5 py-1 text-[11px] font-medium leading-4 text-foreground">
|
||||||
|
{{ packageName }}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (license.note) {
|
||||||
|
<p class="mt-3 text-xs leading-5 text-muted-foreground">{{ license.note }}</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<p class="text-[11px] font-semibold uppercase tracking-[0.18em] text-muted-foreground">License text</p>
|
||||||
|
<pre
|
||||||
|
class="mt-2 whitespace-pre-wrap break-words rounded-md border border-border/70 bg-card px-3 py-3 text-[11px] leading-5 text-muted-foreground"
|
||||||
|
>{{ license.text }}</pre
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,42 +3,345 @@ export interface ThirdPartyLicense {
|
|||||||
name: string;
|
name: string;
|
||||||
licenseName: string;
|
licenseName: string;
|
||||||
sourceUrl: string;
|
sourceUrl: string;
|
||||||
|
packages: readonly string[];
|
||||||
text: string;
|
text: string;
|
||||||
|
note?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const toLicenseText = (lines: readonly string[]): string => lines.join('\n');
|
||||||
|
|
||||||
|
const GROUPED_LICENSE_NOTE = 'Grouped by the license declared in the installed package metadata for the packages below. Some upstream packages include their own copyright notices in addition to this standard license text.';
|
||||||
|
|
||||||
|
const MIT_LICENSE_TEXT = toLicenseText([
|
||||||
|
'MIT License',
|
||||||
|
'',
|
||||||
|
'Copyright <YEAR> <COPYRIGHT HOLDER>',
|
||||||
|
'',
|
||||||
|
'Permission is hereby granted, free of charge, to any person obtaining a copy',
|
||||||
|
'of this software and associated documentation files (the "Software"), to deal',
|
||||||
|
'in the Software without restriction, including without limitation the rights',
|
||||||
|
'to use, copy, modify, merge, publish, distribute, sublicense, and/or sell',
|
||||||
|
'copies of the Software, and to permit persons to whom the Software is',
|
||||||
|
'furnished to do so, subject to the following conditions:',
|
||||||
|
'',
|
||||||
|
'The above copyright notice and this permission notice shall be included in all',
|
||||||
|
'copies or substantial portions of the Software.',
|
||||||
|
'',
|
||||||
|
'THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR',
|
||||||
|
'IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,',
|
||||||
|
'FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE',
|
||||||
|
'AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER',
|
||||||
|
'LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,',
|
||||||
|
'OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE',
|
||||||
|
'SOFTWARE.'
|
||||||
|
]);
|
||||||
|
|
||||||
|
const APACHE_LICENSE_TEXT = toLicenseText([
|
||||||
|
'Apache License',
|
||||||
|
'Version 2.0, January 2004',
|
||||||
|
'http://www.apache.org/licenses/',
|
||||||
|
'',
|
||||||
|
'TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION',
|
||||||
|
'',
|
||||||
|
'1. Definitions.',
|
||||||
|
'',
|
||||||
|
'"License" shall mean the terms and conditions for use, reproduction, and distribution as',
|
||||||
|
'defined by Sections 1 through 9 of this document.',
|
||||||
|
'',
|
||||||
|
'"Licensor" shall mean the copyright owner or entity authorized by the copyright owner',
|
||||||
|
'that is granting the License.',
|
||||||
|
'',
|
||||||
|
'"Legal Entity" shall mean the union of the acting entity and all other entities that',
|
||||||
|
'control, are controlled by, or are under common control with that entity. For the',
|
||||||
|
'purposes of this definition, "control" means (i) the power, direct or indirect, to cause',
|
||||||
|
'the direction or management of such entity, whether by contract or otherwise, or (ii)',
|
||||||
|
'ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial',
|
||||||
|
'ownership of such entity.',
|
||||||
|
'',
|
||||||
|
'"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted',
|
||||||
|
'by this License.',
|
||||||
|
'',
|
||||||
|
'"Source" form shall mean the preferred form for making modifications, including but',
|
||||||
|
'not limited to software source code, documentation source, and configuration files.',
|
||||||
|
'',
|
||||||
|
'"Object" form shall mean any form resulting from mechanical transformation or',
|
||||||
|
'translation of a Source form, including but not limited to compiled object code,',
|
||||||
|
'generated documentation, and conversions to other media types.',
|
||||||
|
'',
|
||||||
|
'"Work" shall mean the work of authorship, whether in Source or Object form, made',
|
||||||
|
'available under the License, as indicated by a copyright notice that is included in or',
|
||||||
|
'attached to the Work (an example is provided in the Appendix below).',
|
||||||
|
'',
|
||||||
|
'"Derivative Works" shall mean any work, whether in Source or Object form, that is based',
|
||||||
|
'on (or derived from) the Work and for which the editorial revisions, annotations,',
|
||||||
|
'elaborations, or other modifications represent, as a whole, an original work of',
|
||||||
|
'authorship. For the purposes of this License, Derivative Works shall not include works',
|
||||||
|
'that remain separable from, or merely link (or bind by name) to the interfaces of,',
|
||||||
|
'the Work and Derivative Works thereof.',
|
||||||
|
'',
|
||||||
|
'"Contribution" shall mean any work of authorship, including the original version of the',
|
||||||
|
'Work and any modifications or additions to that Work or Derivative Works thereof, that',
|
||||||
|
'is intentionally submitted to Licensor for inclusion in the Work by the copyright owner',
|
||||||
|
'or by an individual or Legal Entity authorized to submit on behalf of the copyright',
|
||||||
|
'owner. For the purposes of this definition, "submitted" means any form of electronic,',
|
||||||
|
'verbal, or written communication sent to the Licensor or its representatives, including',
|
||||||
|
'but not limited to communication on electronic mailing lists, source code control',
|
||||||
|
'systems, and issue tracking systems that are managed by, or on behalf of, the Licensor',
|
||||||
|
'for the purpose of discussing and improving the Work, but excluding communication that',
|
||||||
|
'is conspicuously marked or otherwise designated in writing by the copyright owner as',
|
||||||
|
'"Not a Contribution."',
|
||||||
|
'',
|
||||||
|
'"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a',
|
||||||
|
'Contribution has been received by Licensor and subsequently incorporated within the',
|
||||||
|
'Work.',
|
||||||
|
'',
|
||||||
|
'2. Grant of Copyright License. Subject to the terms and conditions of this License, each',
|
||||||
|
'Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge,',
|
||||||
|
'royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of,',
|
||||||
|
'publicly display, publicly perform, sublicense, and distribute the Work and such',
|
||||||
|
'Derivative Works in Source or Object form.',
|
||||||
|
'',
|
||||||
|
'3. Grant of Patent License. Subject to the terms and conditions of this License, each',
|
||||||
|
'Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge,',
|
||||||
|
'royalty-free, irrevocable (except as stated in this section) patent license to make,',
|
||||||
|
'have made, use, offer to sell, sell, import, and otherwise transfer the Work, where',
|
||||||
|
'such license applies only to those patent claims licensable by such Contributor that',
|
||||||
|
'are necessarily infringed by their Contribution(s) alone or by combination of their',
|
||||||
|
'Contribution(s) with the Work to which such Contribution(s) was submitted. If You',
|
||||||
|
'institute patent litigation against any entity (including a cross-claim or counterclaim',
|
||||||
|
'in a lawsuit) alleging that the Work or a Contribution incorporated within the Work',
|
||||||
|
'constitutes direct or contributory patent infringement, then any patent licenses',
|
||||||
|
'granted to You under this License for that Work shall terminate as of the date such',
|
||||||
|
'litigation is filed.',
|
||||||
|
'',
|
||||||
|
'4. Redistribution. You may reproduce and distribute copies of the Work or Derivative',
|
||||||
|
'Works thereof in any medium, with or without modifications, and in Source or Object',
|
||||||
|
'form, provided that You meet the following conditions:',
|
||||||
|
'',
|
||||||
|
' a. You must give any other recipients of the Work or Derivative Works a copy of',
|
||||||
|
' this License; and',
|
||||||
|
'',
|
||||||
|
' b. You must cause any modified files to carry prominent notices stating that You',
|
||||||
|
' changed the files; and',
|
||||||
|
'',
|
||||||
|
' c. You must retain, in the Source form of any Derivative Works that You distribute,',
|
||||||
|
' all copyright, patent, trademark, and attribution notices from the Source form',
|
||||||
|
' of the Work, excluding those notices that do not pertain to any part of the',
|
||||||
|
' Derivative Works; and',
|
||||||
|
'',
|
||||||
|
' d. If the Work includes a "NOTICE" text file as part of its distribution, then any',
|
||||||
|
' Derivative Works that You distribute must include a readable copy of the',
|
||||||
|
' attribution notices contained within such NOTICE file, excluding those notices',
|
||||||
|
' that do not pertain to any part of the Derivative Works, in at least one of',
|
||||||
|
' the following places: within a NOTICE text file distributed as part of the',
|
||||||
|
' Derivative Works; within the Source form or documentation, if provided along',
|
||||||
|
' with the Derivative Works; or, within a display generated by the Derivative',
|
||||||
|
' Works, if and wherever such third-party notices normally appear. The contents',
|
||||||
|
' of the NOTICE file are for informational purposes only and do not modify the',
|
||||||
|
' License. You may add Your own attribution notices within Derivative Works',
|
||||||
|
' that You distribute, alongside or as an addendum to the NOTICE text from the',
|
||||||
|
' Work, provided that such additional attribution notices cannot be construed',
|
||||||
|
' as modifying the License.',
|
||||||
|
'',
|
||||||
|
'You may add Your own copyright statement to Your modifications and may provide',
|
||||||
|
'additional or different license terms and conditions for use, reproduction, or',
|
||||||
|
'distribution of Your modifications, or for any such Derivative Works as a whole,',
|
||||||
|
'provided Your use, reproduction, and distribution of the Work otherwise complies with',
|
||||||
|
'the conditions stated in this License.',
|
||||||
|
'',
|
||||||
|
'5. Submission of Contributions. Unless You explicitly state otherwise, any',
|
||||||
|
'Contribution intentionally submitted for inclusion in the Work by You to the Licensor',
|
||||||
|
'shall be under the terms and conditions of this License, without any additional terms',
|
||||||
|
'or conditions. Notwithstanding the above, nothing herein shall supersede or modify',
|
||||||
|
'the terms of any separate license agreement you may have executed with Licensor',
|
||||||
|
'regarding such Contributions.',
|
||||||
|
'',
|
||||||
|
'6. Trademarks. This License does not grant permission to use the trade names,',
|
||||||
|
'trademarks, service marks, or product names of the Licensor, except as required for',
|
||||||
|
'reasonable and customary use in describing the origin of the Work and reproducing the',
|
||||||
|
'content of the NOTICE file.',
|
||||||
|
'',
|
||||||
|
'7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing,',
|
||||||
|
'Licensor provides the Work (and each Contributor provides its Contributions) on an',
|
||||||
|
'"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or',
|
||||||
|
'implied, including, without limitation, any warranties or conditions of TITLE,',
|
||||||
|
'NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are',
|
||||||
|
'solely responsible for determining the appropriateness of using or redistributing the',
|
||||||
|
'Work and assume any risks associated with Your exercise of permissions under this',
|
||||||
|
'License.',
|
||||||
|
'',
|
||||||
|
'8. Limitation of Liability. In no event and under no legal theory, whether in tort',
|
||||||
|
'(including negligence), contract, or otherwise, unless required by applicable law',
|
||||||
|
'(such as deliberate and grossly negligent acts) or agreed to in writing, shall any',
|
||||||
|
'Contributor be liable to You for damages, including any direct, indirect, special,',
|
||||||
|
'incidental, or consequential damages of any character arising as a result of this',
|
||||||
|
'License or out of the use or inability to use the Work (including but not limited to',
|
||||||
|
'damages for loss of goodwill, work stoppage, computer failure or malfunction, or any',
|
||||||
|
'and all other commercial damages or losses), even if such Contributor has been',
|
||||||
|
'advised of the possibility of such damages.',
|
||||||
|
'',
|
||||||
|
'9. Accepting Warranty or Additional Liability. While redistributing the Work or',
|
||||||
|
'Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance',
|
||||||
|
'of support, warranty, indemnity, or other liability obligations and/or rights',
|
||||||
|
'consistent with this License. However, in accepting such obligations, You may act',
|
||||||
|
'only on Your own behalf and on Your sole responsibility, not on behalf of any other',
|
||||||
|
'Contributor, and only if You agree to indemnify, defend, and hold each Contributor',
|
||||||
|
'harmless for any liability incurred by, or claims asserted against, such Contributor',
|
||||||
|
'by reason of your accepting any such warranty or additional liability.',
|
||||||
|
'',
|
||||||
|
'END OF TERMS AND CONDITIONS'
|
||||||
|
]);
|
||||||
|
|
||||||
|
const WAVESURFER_BSD_LICENSE_TEXT = toLicenseText([
|
||||||
|
'BSD 3-Clause License',
|
||||||
|
'',
|
||||||
|
'Copyright (c) 2012-2023, katspaugh and contributors',
|
||||||
|
'All rights reserved.',
|
||||||
|
'',
|
||||||
|
'Redistribution and use in source and binary forms, with or without modification, are permitted provided',
|
||||||
|
'that the following conditions are met:',
|
||||||
|
'',
|
||||||
|
'* Redistributions of source code must retain the above copyright notice, this list of conditions and',
|
||||||
|
' the following disclaimer.',
|
||||||
|
'',
|
||||||
|
'* Redistributions in binary form must reproduce the above copyright notice, this list of conditions',
|
||||||
|
' and the following disclaimer in the documentation and/or other materials provided with the',
|
||||||
|
' distribution.',
|
||||||
|
'',
|
||||||
|
'* Neither the name of the copyright holder nor the names of its contributors may be used to endorse',
|
||||||
|
' or promote products derived from this software without specific prior written permission.',
|
||||||
|
'',
|
||||||
|
'THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR',
|
||||||
|
'IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND',
|
||||||
|
'FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR',
|
||||||
|
'CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL',
|
||||||
|
'DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,',
|
||||||
|
'DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER',
|
||||||
|
'IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT',
|
||||||
|
'OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.'
|
||||||
|
]);
|
||||||
|
|
||||||
|
const ISC_LICENSE_TEXT = toLicenseText([
|
||||||
|
'ISC License',
|
||||||
|
'',
|
||||||
|
'Copyright <YEAR> <OWNER>',
|
||||||
|
'',
|
||||||
|
'Permission to use, copy, modify, and/or distribute this software for any purpose',
|
||||||
|
'with or without fee is hereby granted, provided that the above copyright notice',
|
||||||
|
'and this permission notice appear in all copies.',
|
||||||
|
'',
|
||||||
|
'THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH',
|
||||||
|
'REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND',
|
||||||
|
'FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT,',
|
||||||
|
'OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE,',
|
||||||
|
'DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS',
|
||||||
|
'ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS',
|
||||||
|
'SOFTWARE.'
|
||||||
|
]);
|
||||||
|
|
||||||
|
const ZERO_BSD_LICENSE_TEXT = toLicenseText([
|
||||||
|
'Zero-Clause BSD',
|
||||||
|
'',
|
||||||
|
'Permission to use, copy, modify, and/or distribute this software for any purpose',
|
||||||
|
'with or without fee is hereby granted.',
|
||||||
|
'',
|
||||||
|
'THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH',
|
||||||
|
'REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND',
|
||||||
|
'FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT,',
|
||||||
|
'OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE,',
|
||||||
|
'DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS',
|
||||||
|
'ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS',
|
||||||
|
'SOFTWARE.'
|
||||||
|
]);
|
||||||
|
|
||||||
export const THIRD_PARTY_LICENSES: ThirdPartyLicense[] = [
|
export const THIRD_PARTY_LICENSES: ThirdPartyLicense[] = [
|
||||||
{
|
{
|
||||||
id: 'wavesurfer-js',
|
id: 'mit',
|
||||||
name: 'wavesurfer.js',
|
name: 'MIT-licensed packages',
|
||||||
|
licenseName: 'MIT License',
|
||||||
|
sourceUrl: 'https://opensource.org/licenses/MIT',
|
||||||
|
packages: [
|
||||||
|
'@angular/common',
|
||||||
|
'@angular/compiler',
|
||||||
|
'@angular/core',
|
||||||
|
'@angular/forms',
|
||||||
|
'@angular/platform-browser',
|
||||||
|
'@angular/router',
|
||||||
|
'@codemirror/commands',
|
||||||
|
'@codemirror/lang-json',
|
||||||
|
'@codemirror/language',
|
||||||
|
'@codemirror/state',
|
||||||
|
'@codemirror/theme-one-dark',
|
||||||
|
'@codemirror/view',
|
||||||
|
'codemirror',
|
||||||
|
'@ng-icons/core',
|
||||||
|
'@ngrx/effects',
|
||||||
|
'@ngrx/entity',
|
||||||
|
'@ngrx/store',
|
||||||
|
'@ngrx/store-devtools',
|
||||||
|
'auto-launch',
|
||||||
|
'clsx',
|
||||||
|
'cytoscape',
|
||||||
|
'electron-updater',
|
||||||
|
'mermaid',
|
||||||
|
'ngx-remark',
|
||||||
|
'prismjs',
|
||||||
|
'remark',
|
||||||
|
'remark-breaks',
|
||||||
|
'remark-gfm',
|
||||||
|
'simple-peer',
|
||||||
|
'sql.js',
|
||||||
|
'typeorm',
|
||||||
|
'uuid'
|
||||||
|
],
|
||||||
|
text: MIT_LICENSE_TEXT,
|
||||||
|
note: GROUPED_LICENSE_NOTE
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'apache-2-0',
|
||||||
|
name: 'Apache-licensed packages',
|
||||||
|
licenseName: 'Apache License 2.0',
|
||||||
|
sourceUrl: 'https://www.apache.org/licenses/LICENSE-2.0',
|
||||||
|
packages: [
|
||||||
|
'rxjs',
|
||||||
|
'@timephy/rnnoise-wasm',
|
||||||
|
'class-variance-authority',
|
||||||
|
'reflect-metadata'
|
||||||
|
],
|
||||||
|
text: APACHE_LICENSE_TEXT,
|
||||||
|
note: GROUPED_LICENSE_NOTE
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'bsd-3-clause',
|
||||||
|
name: 'BSD-licensed packages',
|
||||||
licenseName: 'BSD 3-Clause License',
|
licenseName: 'BSD 3-Clause License',
|
||||||
sourceUrl: 'https://github.com/katspaugh/wavesurfer.js/blob/main/LICENSE',
|
sourceUrl: 'https://opensource.org/licenses/BSD-3-Clause',
|
||||||
text: [
|
packages: [
|
||||||
'BSD 3-Clause License',
|
'wavesurfer.js'
|
||||||
'',
|
],
|
||||||
'Copyright (c) 2012-2023, katspaugh and contributors',
|
text: WAVESURFER_BSD_LICENSE_TEXT,
|
||||||
'All rights reserved.',
|
note: 'License text reproduced from the bundled wavesurfer.js package license.'
|
||||||
'',
|
},
|
||||||
'Redistribution and use in source and binary forms, with or without modification, are permitted provided',
|
{
|
||||||
'that the following conditions are met:',
|
id: 'isc',
|
||||||
'',
|
name: 'ISC-licensed packages',
|
||||||
'* Redistributions of source code must retain the above copyright notice, this list of conditions and',
|
licenseName: 'ISC License',
|
||||||
' the following disclaimer.',
|
sourceUrl: 'https://opensource.org/license/isc-license-txt',
|
||||||
'',
|
packages: [
|
||||||
'* Redistributions in binary form must reproduce the above copyright notice, this list of conditions',
|
'@ng-icons/lucide'
|
||||||
' and the following disclaimer in the documentation and/or other materials provided with the',
|
],
|
||||||
' distribution.',
|
text: ISC_LICENSE_TEXT,
|
||||||
'',
|
note: GROUPED_LICENSE_NOTE
|
||||||
'* Neither the name of the copyright holder nor the names of its contributors may be used to endorse',
|
},
|
||||||
' or promote products derived from this software without specific prior written permission.',
|
{
|
||||||
'',
|
id: '0bsd',
|
||||||
'THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR',
|
name: '0BSD-licensed packages',
|
||||||
'IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND',
|
licenseName: '0BSD License',
|
||||||
'FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR',
|
sourceUrl: 'https://opensource.org/license/0bsd',
|
||||||
'CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL',
|
packages: [
|
||||||
'DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,',
|
'tslib'
|
||||||
'DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER',
|
],
|
||||||
'IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT',
|
text: ZERO_BSD_LICENSE_TEXT,
|
||||||
'OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.'
|
note: GROUPED_LICENSE_NOTE
|
||||||
].join('\n')
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<section class="rounded-xl border border-border bg-card/60 p-5">
|
<section class="rounded-lg border border-border bg-card/60 p-5">
|
||||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h4 class="text-base font-semibold text-foreground">Desktop app updates</h4>
|
<h4 class="text-base font-semibold text-foreground">Desktop app updates</h4>
|
||||||
@@ -15,27 +15,27 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
@if (!isElectron) {
|
@if (!isElectron) {
|
||||||
<section class="rounded-xl border border-border bg-secondary/30 p-5">
|
<section class="rounded-lg border border-border bg-secondary/30 p-5">
|
||||||
<p class="text-sm text-muted-foreground">Automatic updates are only available in the packaged Electron desktop app.</p>
|
<p class="text-sm text-muted-foreground">Automatic updates are only available in the packaged Electron desktop app.</p>
|
||||||
</section>
|
</section>
|
||||||
} @else {
|
} @else {
|
||||||
<section class="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
<section class="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||||
<div class="rounded-xl border border-border bg-secondary/20 p-4">
|
<div class="rounded-lg border border-border bg-secondary/20 p-4">
|
||||||
<p class="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/70">Installed</p>
|
<p class="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/70">Installed</p>
|
||||||
<p class="mt-2 text-lg font-semibold text-foreground">{{ state().currentVersion }}</p>
|
<p class="mt-2 text-lg font-semibold text-foreground">{{ state().currentVersion }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-xl border border-border bg-secondary/20 p-4">
|
<div class="rounded-lg border border-border bg-secondary/20 p-4">
|
||||||
<p class="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/70">Latest in manifest</p>
|
<p class="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/70">Latest in manifest</p>
|
||||||
<p class="mt-2 text-lg font-semibold text-foreground">{{ state().latestVersion || 'Unknown' }}</p>
|
<p class="mt-2 text-lg font-semibold text-foreground">{{ state().latestVersion || 'Unknown' }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-xl border border-border bg-secondary/20 p-4">
|
<div class="rounded-lg border border-border bg-secondary/20 p-4">
|
||||||
<p class="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/70">Target version</p>
|
<p class="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/70">Target version</p>
|
||||||
<p class="mt-2 text-lg font-semibold text-foreground">{{ state().targetVersion || 'Automatic' }}</p>
|
<p class="mt-2 text-lg font-semibold text-foreground">{{ state().targetVersion || 'Automatic' }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-xl border border-border bg-secondary/20 p-4">
|
<div class="rounded-lg border border-border bg-secondary/20 p-4">
|
||||||
<p class="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/70">Last checked</p>
|
<p class="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/70">Last checked</p>
|
||||||
<p class="mt-2 text-sm font-medium text-foreground">
|
<p class="mt-2 text-sm font-medium text-foreground">
|
||||||
{{ state().lastCheckedAt ? (state().lastCheckedAt | date: 'medium') : 'Not checked yet' }}
|
{{ state().lastCheckedAt ? (state().lastCheckedAt | date: 'medium') : 'Not checked yet' }}
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="space-y-4 rounded-xl border border-border bg-card/60 p-5">
|
<section class="space-y-4 rounded-lg border border-border bg-card/60 p-5">
|
||||||
<div>
|
<div>
|
||||||
<h5 class="text-sm font-semibold text-foreground">Update policy</h5>
|
<h5 class="text-sm font-semibold text-foreground">Update policy</h5>
|
||||||
<p class="mt-1 text-sm text-muted-foreground">
|
<p class="mt-1 text-sm text-muted-foreground">
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-xl border border-border bg-secondary/20 p-4">
|
<div class="rounded-lg border border-border bg-secondary/20 p-4">
|
||||||
<p class="text-sm font-medium text-foreground">Status</p>
|
<p class="text-sm font-medium text-foreground">Status</p>
|
||||||
<p class="mt-1 text-sm text-muted-foreground">
|
<p class="mt-1 text-sm text-muted-foreground">
|
||||||
{{ state().statusMessage || 'Waiting for release information from the active server.' }}
|
{{ state().statusMessage || 'Waiting for release information from the active server.' }}
|
||||||
@@ -109,7 +109,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="space-y-4 rounded-xl border border-border bg-card/60 p-5">
|
<section class="space-y-4 rounded-lg border border-border bg-card/60 p-5">
|
||||||
<div>
|
<div>
|
||||||
<h5 class="text-sm font-semibold text-foreground">Manifest URL priority</h5>
|
<h5 class="text-sm font-semibold text-foreground">Manifest URL priority</h5>
|
||||||
<p class="mt-1 text-sm text-muted-foreground">
|
<p class="mt-1 text-sm text-muted-foreground">
|
||||||
@@ -118,7 +118,7 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-xl border border-border bg-secondary/20 p-4 text-sm text-muted-foreground">
|
<div class="rounded-lg border border-border bg-secondary/20 p-4 text-sm text-muted-foreground">
|
||||||
<p class="font-medium text-foreground">
|
<p class="font-medium text-foreground">
|
||||||
{{ isUsingConnectedServerDefaults() ? 'Using connected server defaults' : 'Using saved manifest URLs' }}
|
{{ isUsingConnectedServerDefaults() ? 'Using connected server defaults' : 'Using saved manifest URLs' }}
|
||||||
</p>
|
</p>
|
||||||
@@ -160,7 +160,7 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
@if (state().serverBlocked) {
|
@if (state().serverBlocked) {
|
||||||
<section class="rounded-xl border border-red-500/30 bg-red-500/10 p-5">
|
<section class="rounded-lg border border-red-500/30 bg-red-500/10 p-5">
|
||||||
<h5 class="text-sm font-semibold text-foreground">Server update required</h5>
|
<h5 class="text-sm font-semibold text-foreground">Server update required</h5>
|
||||||
<p class="mt-1 text-sm text-muted-foreground">{{ state().serverBlockMessage }}</p>
|
<p class="mt-1 text-sm text-muted-foreground">{{ state().serverBlockMessage }}</p>
|
||||||
<div class="mt-3 grid gap-2 text-xs text-muted-foreground sm:grid-cols-2">
|
<div class="mt-3 grid gap-2 text-xs text-muted-foreground sm:grid-cols-2">
|
||||||
@@ -177,7 +177,7 @@
|
|||||||
</section>
|
</section>
|
||||||
}
|
}
|
||||||
|
|
||||||
<section class="rounded-xl border border-border bg-secondary/20 p-4">
|
<section class="rounded-lg border border-border bg-secondary/20 p-4">
|
||||||
<p class="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/70">Resolved manifest URL</p>
|
<p class="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/70">Resolved manifest URL</p>
|
||||||
<p class="mt-2 break-all text-sm text-muted-foreground">{{ state().manifestUrl || 'No working manifest URL has been resolved yet.' }}</p>
|
<p class="mt-2 break-all text-sm text-muted-foreground">{{ state().manifestUrl || 'No working manifest URL has been resolved yet.' }}</p>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<div class="space-y-6 max-w-xl">
|
<div class="max-w-3xl space-y-5">
|
||||||
<!-- Devices -->
|
<!-- Devices -->
|
||||||
<section>
|
<section>
|
||||||
<div class="flex items-center gap-2 mb-3">
|
<div class="flex items-center gap-2 mb-3">
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
(click)="goBack()"
|
(click)="goBack()"
|
||||||
class="p-2 hover:bg-secondary rounded-lg transition-colors"
|
class="grid h-9 w-9 place-items-center rounded-lg transition-colors hover:bg-secondary"
|
||||||
title="Go back"
|
title="Go back"
|
||||||
>
|
>
|
||||||
<ng-icon
|
<ng-icon
|
||||||
@@ -102,7 +102,7 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
(click)="setActiveServer(server.id)"
|
(click)="setActiveServer(server.id)"
|
||||||
class="p-2 hover:bg-secondary rounded-lg transition-colors"
|
class="grid h-9 w-9 place-items-center rounded-lg transition-colors hover:bg-secondary"
|
||||||
title="Activate"
|
title="Activate"
|
||||||
>
|
>
|
||||||
<ng-icon
|
<ng-icon
|
||||||
@@ -115,7 +115,7 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
(click)="deactivateServer(server.id)"
|
(click)="deactivateServer(server.id)"
|
||||||
class="p-2 hover:bg-secondary rounded-lg transition-colors"
|
class="grid h-9 w-9 place-items-center rounded-lg transition-colors hover:bg-secondary"
|
||||||
title="Deactivate"
|
title="Deactivate"
|
||||||
>
|
>
|
||||||
<ng-icon
|
<ng-icon
|
||||||
@@ -128,7 +128,7 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
(click)="removeServer(server.id)"
|
(click)="removeServer(server.id)"
|
||||||
class="p-2 hover:bg-destructive/10 rounded-lg transition-colors"
|
class="grid h-9 w-9 place-items-center rounded-lg transition-colors hover:bg-destructive/10"
|
||||||
title="Remove server"
|
title="Remove server"
|
||||||
>
|
>
|
||||||
<ng-icon
|
<ng-icon
|
||||||
@@ -164,7 +164,7 @@
|
|||||||
type="button"
|
type="button"
|
||||||
(click)="addServer()"
|
(click)="addServer()"
|
||||||
[disabled]="!newServerName || !newServerUrl"
|
[disabled]="!newServerName || !newServerUrl"
|
||||||
class="px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed self-end"
|
class="grid h-10 w-10 place-items-center self-end rounded-lg bg-primary text-primary-foreground transition-colors hover:bg-primary/90 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
>
|
>
|
||||||
<ng-icon
|
<ng-icon
|
||||||
name="lucidePlus"
|
name="lucidePlus"
|
||||||
|
|||||||
@@ -21,7 +21,11 @@ function normalizeRoleIds(roleIds: readonly string[] | undefined): string[] | un
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalized = Array.from(new Set(roleIds.filter((roleId): roleId is string => typeof roleId === 'string' && roleId.trim().length > 0).map((roleId) => roleId.trim())));
|
const normalized = Array.from(new Set(
|
||||||
|
roleIds
|
||||||
|
.filter((roleId): roleId is string => typeof roleId === 'string' && roleId.trim().length > 0)
|
||||||
|
.map((roleId) => roleId.trim())
|
||||||
|
));
|
||||||
|
|
||||||
return normalized.length > 0 ? normalized : undefined;
|
return normalized.length > 0 ? normalized : undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,9 @@
|
|||||||
|
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
font-feature-settings: "rlig" 1, "calt" 1;
|
font-feature-settings:
|
||||||
|
'rlig' 1,
|
||||||
|
'calt' 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,9 +100,9 @@
|
|||||||
|
|
||||||
.theme-settings__workspace {
|
.theme-settings__workspace {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 1.5rem;
|
gap: 1rem;
|
||||||
grid-template-columns: minmax(18rem, 22rem) minmax(0, 1fr);
|
grid-template-columns: minmax(16rem, 19rem) minmax(0, 1fr);
|
||||||
align-items: start;
|
align-items: stretch;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,7 +112,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-settings__main {
|
.theme-settings__main {
|
||||||
@@ -118,12 +120,12 @@
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1.5rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-settings--fullscreen .theme-settings__workspace {
|
.theme-settings--fullscreen .theme-settings__workspace {
|
||||||
gap: 0.875rem;
|
gap: 0.75rem;
|
||||||
grid-template-columns: minmax(15.5rem, 17rem) minmax(0, 1fr);
|
grid-template-columns: minmax(15rem, 16.5rem) minmax(0, 1fr);
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,17 +173,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.theme-settings--fullscreen .theme-studio-card {
|
.theme-settings--fullscreen .theme-studio-card {
|
||||||
border-radius: 1rem;
|
border-radius: 0.5rem;
|
||||||
box-shadow: 0 10px 24px rgb(15 23 42 / 0.08);
|
box-shadow: 0 1px 2px rgb(15 23 42 / 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-settings--fullscreen .theme-settings__hero-grid {
|
.theme-settings--fullscreen .theme-settings__hero-grid {
|
||||||
gap: 0.55rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-settings--fullscreen .theme-settings__hero-stat {
|
.theme-settings--fullscreen .theme-settings__hero-stat {
|
||||||
border-radius: 0.85rem;
|
border-radius: 0.5rem;
|
||||||
padding: 0.7rem 0.85rem;
|
padding: 0.65rem 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-settings--fullscreen .theme-settings__hero-label {
|
.theme-settings--fullscreen .theme-settings__hero-label {
|
||||||
@@ -190,17 +192,17 @@
|
|||||||
|
|
||||||
.theme-settings--fullscreen .theme-settings__hero-value {
|
.theme-settings--fullscreen .theme-settings__hero-value {
|
||||||
margin-top: 0.3rem;
|
margin-top: 0.3rem;
|
||||||
font-size: 0.92rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-settings--fullscreen .theme-settings__workspace-tab,
|
.theme-settings--fullscreen .theme-settings__workspace-tab,
|
||||||
.theme-settings--fullscreen .theme-settings__entry-button {
|
.theme-settings--fullscreen .theme-settings__entry-button {
|
||||||
border-radius: 0.85rem;
|
border-radius: 0.5rem;
|
||||||
padding: 0.72rem 0.8rem;
|
padding: 0.72rem 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-settings--fullscreen .theme-settings__search-input {
|
.theme-settings--fullscreen .theme-settings__search-input {
|
||||||
border-radius: 0.85rem;
|
border-radius: 0.5rem;
|
||||||
padding: 0.72rem 0.85rem;
|
padding: 0.72rem 0.85rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,24 +214,22 @@
|
|||||||
|
|
||||||
.theme-studio-card {
|
.theme-studio-card {
|
||||||
border: 1px solid hsl(var(--border));
|
border: 1px solid hsl(var(--border));
|
||||||
border-radius: 1.5rem;
|
border-radius: 0.5rem;
|
||||||
background:
|
background: hsl(var(--card));
|
||||||
linear-gradient(180deg, hsl(var(--card) / 0.96), hsl(var(--background) / 0.96)),
|
box-shadow: 0 1px 2px rgb(15 23 42 / 0.05);
|
||||||
radial-gradient(circle at top right, hsl(var(--primary) / 0.08), transparent 45%);
|
|
||||||
box-shadow: 0 14px 34px rgb(15 23 42 / 0.08);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-settings__hero-grid {
|
.theme-settings__hero-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.75rem;
|
gap: 0.5rem;
|
||||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-settings__hero-stat {
|
.theme-settings__hero-stat {
|
||||||
border: 1px solid hsl(var(--border));
|
border: 1px solid hsl(var(--border));
|
||||||
border-radius: 1rem;
|
border-radius: 0.5rem;
|
||||||
background: hsl(var(--background) / 0.72);
|
background: hsl(var(--secondary) / 0.3);
|
||||||
padding: 0.9rem 1rem;
|
padding: 0.75rem 0.875rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-settings__hero-label {
|
.theme-settings__hero-label {
|
||||||
@@ -243,8 +243,8 @@
|
|||||||
|
|
||||||
.theme-settings__hero-value {
|
.theme-settings__hero-value {
|
||||||
display: block;
|
display: block;
|
||||||
margin-top: 0.45rem;
|
margin-top: 0.35rem;
|
||||||
font-size: 0.98rem;
|
font-size: 0.95rem;
|
||||||
color: hsl(var(--foreground));
|
color: hsl(var(--foreground));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,26 +252,22 @@
|
|||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: 1px solid hsl(var(--border));
|
border: 1px solid hsl(var(--border));
|
||||||
border-radius: 1rem;
|
border-radius: 0.5rem;
|
||||||
background: hsl(var(--background) / 0.72);
|
background: hsl(var(--secondary) / 0.25);
|
||||||
padding: 0.85rem 0.95rem;
|
padding: 0.75rem 0.875rem;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
transition:
|
transition:
|
||||||
background-color 160ms ease,
|
background-color 160ms ease,
|
||||||
border-color 160ms ease,
|
border-color 160ms ease;
|
||||||
transform 160ms ease,
|
|
||||||
box-shadow 160ms ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-settings__workspace-tab:hover {
|
.theme-settings__workspace-tab:hover {
|
||||||
background: hsl(var(--secondary) / 0.78);
|
background: hsl(var(--secondary) / 0.45);
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-settings__workspace-tab--active {
|
.theme-settings__workspace-tab--active {
|
||||||
border-color: hsl(var(--primary) / 0.4);
|
border-color: hsl(var(--primary) / 0.35);
|
||||||
background: hsl(var(--primary) / 0.08);
|
background: hsl(var(--primary) / 0.08);
|
||||||
box-shadow: 0 8px 22px hsl(var(--primary) / 0.12);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-settings__workspace-tab-title {
|
.theme-settings__workspace-tab-title {
|
||||||
@@ -291,10 +287,10 @@
|
|||||||
|
|
||||||
.theme-settings__search-input {
|
.theme-settings__search-input {
|
||||||
border: 1px solid hsl(var(--border));
|
border: 1px solid hsl(var(--border));
|
||||||
border-radius: 0.95rem;
|
border-radius: 0.5rem;
|
||||||
background: hsl(var(--background) / 0.82);
|
background: hsl(var(--secondary));
|
||||||
padding: 0.8rem 0.95rem;
|
padding: 0.65rem 0.8rem;
|
||||||
font-size: 0.9rem;
|
font-size: 0.875rem;
|
||||||
color: hsl(var(--foreground));
|
color: hsl(var(--foreground));
|
||||||
outline: none;
|
outline: none;
|
||||||
transition:
|
transition:
|
||||||
@@ -305,14 +301,14 @@
|
|||||||
|
|
||||||
.theme-settings__search-input:focus {
|
.theme-settings__search-input:focus {
|
||||||
border-color: hsl(var(--primary) / 0.4);
|
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__entry-list {
|
.theme-settings__entry-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
max-height: min(60vh, 34rem);
|
max-height: min(60vh, 34rem);
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.7rem;
|
gap: 0.5rem;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding-right: 0.15rem;
|
padding-right: 0.15rem;
|
||||||
}
|
}
|
||||||
@@ -321,26 +317,22 @@
|
|||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: 1px solid hsl(var(--border));
|
border: 1px solid hsl(var(--border));
|
||||||
border-radius: 1rem;
|
border-radius: 0.5rem;
|
||||||
background: hsl(var(--background) / 0.72);
|
background: hsl(var(--secondary) / 0.25);
|
||||||
padding: 0.9rem 0.95rem;
|
padding: 0.75rem 0.875rem;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
transition:
|
transition:
|
||||||
background-color 160ms ease,
|
background-color 160ms ease,
|
||||||
border-color 160ms ease,
|
border-color 160ms ease;
|
||||||
transform 160ms ease,
|
|
||||||
box-shadow 160ms ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-settings__entry-button:hover {
|
.theme-settings__entry-button:hover {
|
||||||
background: hsl(var(--secondary) / 0.72);
|
background: hsl(var(--secondary) / 0.45);
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-settings__entry-button--active {
|
.theme-settings__entry-button--active {
|
||||||
border-color: hsl(var(--primary) / 0.42);
|
border-color: hsl(var(--primary) / 0.35);
|
||||||
background: hsl(var(--primary) / 0.08);
|
background: hsl(var(--primary) / 0.08);
|
||||||
box-shadow: 0 8px 24px hsl(var(--primary) / 0.12);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1279px) {
|
@media (max-width: 1279px) {
|
||||||
|
|||||||
Reference in New Issue
Block a user