Files
Toju/toju-app/src/app/domains/theme/feature/settings/theme-settings/theme-settings.component.html

505 lines
23 KiB
HTML

<div
class="theme-settings flex min-h-0 w-full flex-col space-y-3"
[class.min-h-full]="isFullscreen()"
[class.p-4]="isFullscreen()"
[class.theme-settings--fullscreen]="isFullscreen()"
>
<section class="theme-studio-card p-4">
<div class="flex flex-wrap items-start justify-between gap-4">
<div>
<p class="text-xs font-semibold uppercase tracking-[0.18em] text-primary">Theme Studio</p>
<h4 class="mt-1 text-xl font-semibold text-foreground">{{ draftTheme().meta.name }}</h4>
</div>
<div class="flex flex-wrap gap-2">
<button
type="button"
(click)="startPicker()"
class="inline-flex items-center rounded-md border border-border bg-secondary px-3 py-1.5 text-sm font-medium text-foreground transition-colors hover:bg-secondary/80"
>
Pick UI Element
</button>
<button
type="button"
(click)="formatDraft()"
class="inline-flex items-center rounded-md border border-border bg-secondary px-3 py-1.5 text-sm font-medium text-foreground transition-colors hover:bg-secondary/80"
>
Format JSON
</button>
<button
type="button"
(click)="copyLlmThemeGuide()"
class="inline-flex items-center rounded-md border border-border bg-secondary px-3 py-1.5 text-sm font-medium text-foreground transition-colors hover:bg-secondary/80"
>
Copy LLM Guide
</button>
<button
type="button"
(click)="applyDraft()"
[disabled]="!draftIsValid()"
class="inline-flex items-center rounded-md bg-primary px-4 py-1.5 text-sm font-semibold text-primary-foreground transition-colors hover:bg-primary/90 disabled:cursor-not-allowed disabled:opacity-60"
>
Apply Draft
</button>
<button
type="button"
(click)="restoreDefaultTheme()"
class="inline-flex items-center rounded-md border border-destructive/25 bg-destructive/10 px-3 py-1.5 text-sm font-medium text-destructive transition-colors hover:bg-destructive/15"
>
Restore Default
</button>
</div>
</div>
@if (llmGuideCopyMessage()) {
<div class="mt-3 inline-flex rounded-full border border-emerald-500/20 bg-emerald-500/10 px-3 py-1 text-xs font-semibold text-emerald-700">
{{ llmGuideCopyMessage() }}
</div>
}
<div class="theme-settings__hero-grid mt-4">
<div class="theme-settings__hero-stat">
<div class="theme-settings__workspace-selector theme-settings__workspace-selector--compact">
<label
for="theme-studio-workspace-select"
class="theme-settings__workspace-selector-label"
>
Workspace
</label>
<select
id="theme-studio-workspace-select"
class="theme-settings__workspace-select"
[value]="activeWorkspace()"
(change)="onWorkspaceSelect($event)"
>
@for (workspace of workspaceTabs; track workspace.key) {
<option [value]="workspace.key">{{ workspace.label }}</option>
}
</select>
</div>
</div>
<div class="theme-settings__hero-stat">
<span class="theme-settings__hero-label">Regions</span>
<strong class="theme-settings__hero-value">{{ mountedEntryCount() }}</strong>
</div>
<div class="theme-settings__hero-stat">
<span class="theme-settings__hero-label">Draft</span>
<strong
class="theme-settings__hero-value"
[class.text-amber-700]="isDraftDirty()"
[class.text-emerald-700]="!isDraftDirty()"
>
{{ isDraftDirty() ? 'Unsaved changes' : 'In sync' }}
</strong>
</div>
</div>
@if (statusMessage()) {
<div class="mt-3 rounded-lg border border-primary/20 bg-primary/10 px-4 py-3 text-sm text-primary">
{{ statusMessage() }}
</div>
}
@if (!draftIsValid()) {
<div class="mt-3 rounded-lg border border-destructive/30 bg-destructive/10 p-4">
<p class="text-sm font-semibold text-destructive">The draft is invalid. The last working theme is still active.</p>
<ul class="mt-2 space-y-1 text-sm text-destructive/90">
@for (error of draftErrors(); track error) {
<li>{{ error }}</li>
}
</ul>
</div>
}
</section>
<div class="theme-settings__workspace min-h-0 flex-1">
<aside class="theme-settings__sidebar">
@if (savedThemesAvailable()) {
<section class="theme-studio-card p-3.5">
<div class="flex items-center justify-between gap-3">
<p class="text-sm font-semibold text-foreground">Saved Themes</p>
<span class="rounded-full bg-secondary px-2.5 py-1 text-[11px] font-medium text-muted-foreground">
{{ savedThemesBusy() ? 'Syncing' : savedThemes().length + ' saved' }}
</span>
</div>
<div class="mt-3 flex flex-wrap gap-2">
<button
type="button"
(click)="saveDraftAsNewTheme()"
[disabled]="!draftIsValid() || savedThemesBusy()"
class="rounded-md border border-border bg-secondary px-3 py-1.5 text-xs font-medium text-foreground transition-colors hover:bg-secondary/80 disabled:cursor-not-allowed disabled:opacity-60"
>
Save New
</button>
<button
type="button"
(click)="saveDraftToSelectedTheme()"
[disabled]="!draftIsValid() || !selectedSavedTheme() || savedThemesBusy()"
class="rounded-md border border-border bg-secondary px-3 py-1.5 text-xs font-medium text-foreground transition-colors hover:bg-secondary/80 disabled:cursor-not-allowed disabled:opacity-60"
>
Save Selected
</button>
<button
type="button"
(click)="useSelectedSavedTheme()"
[disabled]="!selectedSavedTheme()?.isValid || (savedThemesBusy() && savedThemes().length === 0)"
class="rounded-md border border-border bg-secondary px-3 py-1.5 text-xs font-medium text-foreground transition-colors hover:bg-secondary/80 disabled:cursor-not-allowed disabled:opacity-60"
>
Use
</button>
<button
type="button"
(click)="editSelectedSavedTheme()"
[disabled]="!selectedSavedTheme()?.isValid || (savedThemesBusy() && savedThemes().length === 0)"
class="rounded-md border border-border bg-secondary px-3 py-1.5 text-xs font-medium text-foreground transition-colors hover:bg-secondary/80 disabled:cursor-not-allowed disabled:opacity-60"
>
Edit
</button>
<button
type="button"
(click)="removeSelectedSavedTheme()"
[disabled]="!selectedSavedTheme() || (savedThemesBusy() && savedThemes().length === 0)"
class="rounded-md border border-destructive/25 bg-destructive/10 px-3 py-1.5 text-xs font-medium text-destructive transition-colors hover:bg-destructive/15 disabled:cursor-not-allowed disabled:opacity-60"
>
Remove
</button>
<button
type="button"
(click)="refreshSavedThemes()"
[disabled]="savedThemesBusy() && savedThemes().length === 0"
class="rounded-md border border-border bg-secondary px-3 py-1.5 text-xs font-medium text-foreground transition-colors hover:bg-secondary/80 disabled:cursor-not-allowed disabled:opacity-60"
>
Refresh
</button>
</div>
@if (savedThemes().length > 0) {
<div class="theme-settings__saved-theme-list mt-4">
@for (savedTheme of savedThemes(); track savedTheme.fileName) {
<button
type="button"
(click)="selectSavedTheme(savedTheme.fileName)"
class="theme-settings__saved-theme-button"
[class.theme-settings__saved-theme-button--active]="selectedSavedTheme()?.fileName === savedTheme.fileName"
>
<div class="flex items-start justify-between gap-3">
<div>
<p class="text-sm font-semibold text-foreground">{{ savedTheme.themeName }}</p>
<p class="mt-1 font-mono text-[11px] text-muted-foreground">{{ savedTheme.fileName }}</p>
</div>
@if (savedTheme.isValid) {
<span class="rounded-full bg-emerald-500/12 px-2 py-0.5 text-[10px] font-medium text-emerald-700">Ready</span>
} @else {
<span class="rounded-full bg-destructive/10 px-2 py-0.5 text-[10px] font-medium text-destructive">Invalid</span>
}
</div>
@if (savedTheme.version) {
<p class="mt-2 text-[11px] font-medium text-muted-foreground">v{{ savedTheme.version }}</p>
}
@if (savedTheme.description) {
<p class="mt-2 text-xs leading-5 text-muted-foreground">{{ savedTheme.description }}</p>
}
@if (savedTheme.error) {
<p class="mt-2 text-xs leading-5 text-destructive">{{ savedTheme.error }}</p>
}
</button>
}
</div>
} @else {
<div class="mt-4 rounded-lg border border-dashed border-border bg-secondary/10 px-4 py-5 text-sm text-muted-foreground">
Save the current draft to create your first reusable Electron theme.
</div>
}
@if (savedThemesPath()) {
<p class="mt-3 font-mono text-[11px] leading-5 text-muted-foreground">{{ savedThemesPath() }}</p>
}
</section>
}
<section class="theme-studio-card p-3.5">
<div class="flex items-center justify-between gap-3">
<p class="text-sm font-semibold text-foreground">Explorer</p>
<span class="rounded-full bg-secondary px-2.5 py-1 text-[11px] font-medium text-muted-foreground">
{{ filteredEntries().length }} shown
</span>
</div>
<div class="mt-4">
<input
type="text"
[value]="explorerQuery()"
(input)="onExplorerQueryInput($event)"
placeholder="Search theme keys"
class="theme-settings__search-input mt-2 w-full"
/>
</div>
<div class="theme-settings__entry-list mt-4">
@for (entry of filteredEntries(); track entry.key) {
<button
type="button"
(click)="selectThemeEntry(entry.key)"
class="theme-settings__entry-button"
[class.theme-settings__entry-button--active]="selectedElementKey() === entry.key"
>
<div class="flex items-center justify-between gap-2">
<span class="text-sm font-semibold text-foreground">{{ entry.label }}</span>
@if (isMounted(entry)) {
<span class="rounded-full bg-emerald-500/12 px-2 py-0.5 text-[10px] font-medium text-emerald-700">Mounted</span>
}
</div>
<span class="mt-1 block font-mono text-[11px] text-muted-foreground">{{ entry.key }}</span>
<span class="mt-2 block text-xs leading-5 text-muted-foreground">{{ entry.description }}</span>
</button>
} @empty {
<div class="rounded-lg border border-dashed border-border bg-secondary/10 px-4 py-5 text-sm text-muted-foreground">
No registered theme keys match this filter.
</div>
}
</div>
</section>
</aside>
<main class="theme-settings__main">
<section class="theme-studio-card p-3.5">
<div class="flex flex-wrap items-start justify-between gap-4">
<div>
@if (selectedElement()) {
<div class="flex flex-wrap items-center gap-2">
<span class="text-base font-semibold text-foreground">{{ selectedElement()!.label }}</span>
<span class="rounded-full bg-secondary px-2 py-0.5 font-mono text-[11px] text-muted-foreground">{{ selectedElement()!.key }}</span>
@if (selectedElement()!.container) {
<span class="rounded-full bg-secondary px-2 py-0.5 text-[11px] font-medium text-muted-foreground">{{
selectedElement()!.container
}}</span>
}
</div>
<p class="mt-2 text-sm leading-6 text-muted-foreground">{{ selectedElement()!.description }}</p>
}
</div>
@if (selectedElement()) {
<div class="flex flex-wrap gap-2">
<button
type="button"
(click)="jumpToStyles()"
class="rounded-md border border-border bg-secondary px-3 py-1.5 text-xs font-medium text-foreground transition-colors hover:bg-secondary/80"
>
Open styles in JSON
</button>
@if (selectedElement()!.layoutEditable) {
<button
type="button"
(click)="jumpToLayout()"
class="rounded-md border border-border bg-secondary px-3 py-1.5 text-xs font-medium text-foreground transition-colors hover:bg-secondary/80"
>
Open layout in JSON
</button>
}
</div>
}
</div>
@if (selectedElementCapabilities().length > 0) {
<div class="mt-4 flex flex-wrap gap-2">
@for (capability of selectedElementCapabilities(); track capability) {
<span class="rounded-full bg-secondary px-2.5 py-1 text-[11px] font-medium text-muted-foreground">{{ capability }}</span>
}
</div>
}
</section>
@if (activeWorkspace() === 'editor') {
<section class="theme-studio-card theme-settings__editor-card p-4">
<div class="flex flex-wrap items-start justify-between gap-4 border-b border-border pb-3">
<p class="text-sm font-semibold text-foreground">Theme JSON</p>
<div class="flex flex-wrap gap-2 text-[11px] font-medium text-muted-foreground">
<span class="rounded-md bg-secondary px-2.5 py-1">{{ draftLineCount() }} lines</span>
<span class="rounded-md bg-secondary px-2.5 py-1">{{ draftCharacterCount() }} chars</span>
<span class="rounded-md bg-secondary px-2.5 py-1">{{ draftErrorCount() }} errors</span>
<span class="rounded-md bg-slate-900 px-2.5 py-1 text-slate-200">IDE editor</span>
</div>
</div>
<div class="theme-settings__editor-panel pt-3">
<app-theme-json-code-editor
#jsonEditorRef
[value]="draftText()"
[fullscreen]="isFullscreen()"
(valueChange)="onDraftEditorValueChange($event)"
/>
</div>
</section>
}
@if (activeWorkspace() === 'inspector') {
<div class="space-y-4">
<section class="theme-studio-card p-4">
<div class="flex items-center justify-between gap-3">
<p class="text-sm font-semibold text-foreground">Selection</p>
<button
type="button"
(click)="startPicker()"
class="rounded-md border border-border bg-secondary px-3 py-1.5 text-xs font-medium text-foreground transition-colors hover:bg-secondary/80"
>
Pick live element
</button>
</div>
@if (selectedElement()) {
<div class="mt-4 rounded-lg border border-border/80 bg-secondary/20 p-4">
<div class="flex items-center gap-2">
<p class="text-base font-semibold text-foreground">{{ selectedElement()!.label }}</p>
<span class="rounded-full bg-secondary px-2 py-0.5 font-mono text-[11px] text-muted-foreground">{{ selectedElement()!.key }}</span>
@if (isMounted(selectedElement()!)) {
<span class="rounded-full bg-emerald-500/12 px-2 py-0.5 text-[11px] font-medium text-emerald-700">Mounted now</span>
}
</div>
<p class="mt-2 text-sm leading-6 text-muted-foreground">{{ selectedElement()!.description }}</p>
</div>
}
</section>
<section class="theme-studio-card p-4">
<div class="flex items-center justify-between gap-3">
<p class="text-sm font-semibold text-foreground">Schema Hints</p>
<button
type="button"
(click)="addStarterAnimation()"
[disabled]="!draftIsValid()"
class="rounded-md border border-border bg-secondary px-3 py-1.5 text-xs font-medium text-foreground transition-colors hover:bg-secondary/80 disabled:cursor-not-allowed disabled:opacity-60"
>
Add fade animation
</button>
</div>
<div class="mt-4 grid gap-3 xl:grid-cols-2">
@for (field of visiblePropertyHints(); track field.key) {
<button
type="button"
(click)="applySuggestedProperty(field.key)"
[disabled]="!draftIsValid()"
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>
<p class="font-mono text-sm font-semibold text-foreground">{{ field.key }}</p>
<p class="mt-1 text-xs leading-5 text-muted-foreground">{{ field.description }}</p>
</div>
<span class="rounded-full bg-secondary px-2 py-0.5 text-[10px] font-medium text-muted-foreground">{{ field.type }}</span>
</div>
<div class="mt-3 inline-flex rounded-md bg-primary/10 px-2.5 py-1 font-mono text-[11px] text-primary">
{{ field.example }}
</div>
</button>
}
</div>
</section>
<section class="theme-studio-card p-4">
<p class="text-sm font-semibold text-foreground">Animation Keys</p>
@if (animationKeys().length > 0) {
<div class="mt-4 flex flex-wrap gap-2">
@for (animationKey of animationKeys(); track animationKey) {
<button
type="button"
(click)="jumpToAnimation(animationKey)"
class="rounded-md border border-border bg-secondary px-3 py-1.5 font-mono text-[11px] text-foreground transition-colors hover:bg-secondary/80"
>
{{ animationKey }}
</button>
}
</div>
} @else {
<div class="mt-4 rounded-lg border border-dashed border-border bg-secondary/10 px-4 py-5 text-sm text-muted-foreground">
No custom animation keys yet.
</div>
}
<div class="mt-4 rounded-lg border border-border/80 bg-secondary/20 p-4">
<div class="flex flex-wrap gap-2">
@for (field of THEME_ANIMATION_FIELDS; track field.key) {
<span class="rounded-md bg-secondary px-2.5 py-1 font-mono text-[11px] text-foreground/80">{{ field.key }}</span>
}
</div>
</div>
</section>
</div>
}
@if (activeWorkspace() === 'layout') {
<section class="theme-studio-card p-4">
<div class="flex flex-wrap items-start justify-between gap-4">
<p class="text-sm font-semibold text-foreground">Layout Grid</p>
<div class="flex flex-wrap gap-2">
@for (container of layoutContainers; track container.key) {
<button
type="button"
(click)="selectContainer(container.key)"
class="rounded-md border border-border bg-secondary px-3 py-1.5 text-xs font-medium text-foreground transition-colors hover:bg-secondary/80"
[class.bg-primary/10]="selectedContainer() === container.key"
[class.border-primary/40]="selectedContainer() === container.key"
>
{{ container.label }}
</button>
}
<button
type="button"
(click)="resetSelectedContainer()"
[disabled]="!draftIsValid()"
class="rounded-md border border-border bg-secondary px-3 py-1.5 text-xs font-medium text-foreground transition-colors hover:bg-secondary/80 disabled:cursor-not-allowed disabled:opacity-60"
>
Reset Container
</button>
</div>
</div>
<div class="mt-5">
<app-theme-grid-editor
[container]="selectedContainer() === 'appShell' ? layoutContainers[0] : layoutContainers[1]"
[items]="selectedContainerItems()"
[selectedKey]="selectedElementKey()"
[disabled]="!draftIsValid()"
(itemChanged)="handleGridChange($event)"
(itemSelected)="handleGridSelection($event)"
/>
</div>
@if (selectedElementGrid()) {
<div class="mt-4 grid gap-3 sm:grid-cols-4">
<div class="rounded-lg border border-border/80 bg-secondary/20 p-3">
<p class="text-[11px] font-semibold uppercase tracking-[0.18em] text-muted-foreground">x</p>
<p class="mt-2 font-mono text-lg font-semibold text-foreground">{{ selectedElementGrid()!.grid.x }}</p>
</div>
<div class="rounded-lg border border-border/80 bg-secondary/20 p-3">
<p class="text-[11px] font-semibold uppercase tracking-[0.18em] text-muted-foreground">y</p>
<p class="mt-2 font-mono text-lg font-semibold text-foreground">{{ selectedElementGrid()!.grid.y }}</p>
</div>
<div class="rounded-lg border border-border/80 bg-secondary/20 p-3">
<p class="text-[11px] font-semibold uppercase tracking-[0.18em] text-muted-foreground">w</p>
<p class="mt-2 font-mono text-lg font-semibold text-foreground">{{ selectedElementGrid()!.grid.w }}</p>
</div>
<div class="rounded-lg border border-border/80 bg-secondary/20 p-3">
<p class="text-[11px] font-semibold uppercase tracking-[0.18em] text-muted-foreground">h</p>
<p class="mt-2 font-mono text-lg font-semibold text-foreground">{{ selectedElementGrid()!.grid.h }}</p>
</div>
</div>
}
</section>
}
</main>
</div>
</div>