505 lines
23 KiB
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>
|