feat: Rename to Toju and add translation
Some checks failed
Deploy Web Apps / deploy (push) Successful in 5m52s
Build Android APK / build-android-apk (push) Failing after 23m15s
Queue Release Build / prepare (push) Successful in 1m42s
Queue Release Build / build-linux (push) Failing after 9m33s
Queue Release Build / build-windows (push) Successful in 26m5s
Queue Release Build / finalize (push) Has been skipped
Some checks failed
Deploy Web Apps / deploy (push) Successful in 5m52s
Build Android APK / build-android-apk (push) Failing after 23m15s
Queue Release Build / prepare (push) Successful in 1m42s
Queue Release Build / build-linux (push) Failing after 9m33s
Queue Release Build / build-windows (push) Successful in 26m5s
Queue Release Build / finalize (push) Has been skipped
This commit is contained in:
@@ -27,6 +27,7 @@ import {
|
||||
import { oneDark } from '@codemirror/theme-one-dark';
|
||||
import { EditorView, keymap } from '@codemirror/view';
|
||||
import { basicSetup } from 'codemirror';
|
||||
import { AppI18nService } from '../../../../../core/i18n';
|
||||
|
||||
import { formatPastedJsonText } from './theme-json-format.logic';
|
||||
|
||||
@@ -171,6 +172,7 @@ export class ThemeJsonCodeEditorComponent implements OnDestroy {
|
||||
readonly editorMinHeight = computed(() => (this.fullscreen() ? 'max(34rem, calc(100vh - 15rem))' : '28rem'));
|
||||
|
||||
private readonly zone = inject(NgZone);
|
||||
private readonly appI18n = inject(AppI18nService);
|
||||
|
||||
private editorView: EditorView | null = null;
|
||||
private isApplyingExternalValue = false;
|
||||
@@ -266,7 +268,9 @@ export class ThemeJsonCodeEditorComponent implements OnDestroy {
|
||||
spellcheck: 'false',
|
||||
autocapitalize: 'off',
|
||||
autocorrect: 'off',
|
||||
'aria-label': language === 'css' ? 'Theme CSS editor' : 'Theme JSON editor'
|
||||
'aria-label': language === 'css'
|
||||
? this.appI18n.instant('theme.editor.cssAria')
|
||||
: this.appI18n.instant('theme.editor.jsonAria')
|
||||
}),
|
||||
EditorView.domEventHandlers({
|
||||
paste: (event, view) => {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<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>
|
||||
<p class="text-xs font-semibold uppercase tracking-[0.18em] text-primary">{{ 'theme.studio.badge' | translate }}</p>
|
||||
<h4 class="mt-1 text-xl font-semibold text-foreground">{{ draftTheme().meta.name }}</h4>
|
||||
</div>
|
||||
|
||||
@@ -17,35 +17,35 @@
|
||||
(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
|
||||
{{ 'theme.studio.pickElement' | translate }}
|
||||
</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
|
||||
{{ 'theme.studio.formatJson' | translate }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
(click)="jumpToCss()"
|
||||
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"
|
||||
>
|
||||
Open CSS
|
||||
{{ 'theme.studio.openCss' | translate }}
|
||||
</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
|
||||
{{ 'theme.studio.copyLlmGuide' | translate }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
(click)="themeFileInput.click()"
|
||||
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"
|
||||
>
|
||||
Import File
|
||||
{{ 'theme.studio.importFile' | translate }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -53,7 +53,7 @@
|
||||
[disabled]="!draftIsValid()"
|
||||
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 disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
Export File
|
||||
{{ 'theme.studio.exportFile' | translate }}
|
||||
</button>
|
||||
<input
|
||||
#themeFileInput
|
||||
@@ -68,14 +68,14 @@
|
||||
[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"
|
||||
>
|
||||
{{ activeEditorTab() === 'cssOnly' ? 'Apply CSS Theme' : 'Apply Draft' }}
|
||||
{{ (activeEditorTab() === 'cssOnly' ? 'theme.studio.applyCssTheme' : 'theme.studio.applyDraft') | translate }}
|
||||
</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
|
||||
{{ 'theme.studio.restoreDefault' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -88,21 +88,21 @@
|
||||
|
||||
<div class="theme-settings__hero-grid mt-4">
|
||||
<div class="theme-settings__hero-stat">
|
||||
<span class="theme-settings__hero-label">Workspace</span>
|
||||
<span class="theme-settings__hero-label">{{ 'theme.studio.workspace' | translate }}</span>
|
||||
<strong class="theme-settings__hero-value">{{ activeWorkspaceInfo().label }}</strong>
|
||||
</div>
|
||||
<div class="theme-settings__hero-stat">
|
||||
<span class="theme-settings__hero-label">Regions</span>
|
||||
<span class="theme-settings__hero-label">{{ 'theme.studio.regions' | translate }}</span>
|
||||
<strong class="theme-settings__hero-value">{{ mountedEntryCount() }}</strong>
|
||||
</div>
|
||||
<div class="theme-settings__hero-stat">
|
||||
<span class="theme-settings__hero-label">Draft</span>
|
||||
<span class="theme-settings__hero-label">{{ 'theme.studio.draft' | translate }}</span>
|
||||
<strong
|
||||
class="theme-settings__hero-value"
|
||||
[class.text-amber-700]="isDraftDirty()"
|
||||
[class.text-emerald-700]="!isDraftDirty()"
|
||||
>
|
||||
{{ isDraftDirty() ? 'Unsaved changes' : 'In sync' }}
|
||||
{{ (isDraftDirty() ? 'theme.studio.unsavedChanges' : 'theme.studio.inSync') | translate }}
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
@@ -115,7 +115,7 @@
|
||||
|
||||
@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>
|
||||
<p class="text-sm font-semibold text-destructive">{{ 'theme.studio.invalidDraft' | translate }}</p>
|
||||
<ul class="mt-2 space-y-1 text-sm text-destructive/90">
|
||||
@for (error of draftErrors(); track error) {
|
||||
<li>{{ error }}</li>
|
||||
@@ -126,9 +126,9 @@
|
||||
|
||||
<nav
|
||||
class="theme-settings__workspace-tabs mt-4"
|
||||
aria-label="Theme Studio workspace"
|
||||
[attr.aria-label]="'theme.studio.workspaceAria' | translate"
|
||||
>
|
||||
@for (workspace of workspaceTabs; track workspace.key) {
|
||||
@for (workspace of workspaceTabs(); track workspace.key) {
|
||||
<button
|
||||
type="button"
|
||||
(click)="setWorkspace(workspace.key)"
|
||||
@@ -147,7 +147,7 @@
|
||||
for="theme-studio-workspace-select"
|
||||
class="theme-settings__workspace-selector-label"
|
||||
>
|
||||
Workspace
|
||||
{{ 'theme.studio.workspace' | translate }}
|
||||
</label>
|
||||
<select
|
||||
id="theme-studio-workspace-select"
|
||||
@@ -155,7 +155,7 @@
|
||||
[value]="activeWorkspace()"
|
||||
(change)="onWorkspaceSelect($event)"
|
||||
>
|
||||
@for (workspace of workspaceTabs; track workspace.key) {
|
||||
@for (workspace of workspaceTabs(); track workspace.key) {
|
||||
<option [value]="workspace.key">{{ workspace.label }}</option>
|
||||
}
|
||||
</select>
|
||||
@@ -166,9 +166,9 @@
|
||||
<aside class="theme-settings__sidebar">
|
||||
<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">Preset Themes</p>
|
||||
<p class="text-sm font-semibold text-foreground">{{ 'theme.studio.presetThemes' | translate }}</p>
|
||||
<span class="rounded-full bg-secondary px-2.5 py-1 text-[11px] font-medium text-muted-foreground">
|
||||
{{ builtInPresets.length }} built in
|
||||
{{ 'theme.studio.builtInCount' | translate: { count: builtInPresets.length } }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -182,17 +182,19 @@
|
||||
>
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<p class="text-sm font-semibold text-foreground">{{ preset.theme.meta.name }}</p>
|
||||
<p class="text-sm font-semibold text-foreground">{{ presetDisplayName(preset.key, preset.theme.meta.name) }}</p>
|
||||
<p class="mt-1 font-mono text-[11px] text-muted-foreground">{{ preset.key }}</p>
|
||||
</div>
|
||||
|
||||
@if (preset.theme.meta.name === 'Toju Default Dark 11') {
|
||||
<span class="rounded-full bg-primary/12 px-2 py-0.5 text-[10px] font-medium text-primary">Default</span>
|
||||
@if (isDefaultPresetName(preset.theme.meta.name)) {
|
||||
<span class="rounded-full bg-primary/12 px-2 py-0.5 text-[10px] font-medium text-primary">{{
|
||||
'theme.studio.defaultBadge' | translate
|
||||
}}</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (preset.theme.meta.description) {
|
||||
<p class="mt-2 text-xs leading-5 text-muted-foreground">{{ preset.theme.meta.description }}</p>
|
||||
<p class="mt-2 text-xs leading-5 text-muted-foreground">{{ presetDescription(preset.key, preset.theme.meta.description) }}</p>
|
||||
}
|
||||
</button>
|
||||
}
|
||||
@@ -202,9 +204,11 @@
|
||||
@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>
|
||||
<p class="text-sm font-semibold text-foreground">{{ 'theme.studio.savedThemes' | translate }}</p>
|
||||
<span class="rounded-full bg-secondary px-2.5 py-1 text-[11px] font-medium text-muted-foreground">
|
||||
{{ savedThemesBusy() ? 'Syncing' : savedThemes().length + ' saved' }}
|
||||
{{
|
||||
savedThemesBusy() ? ('theme.studio.syncing' | translate) : ('theme.studio.savedCount' | translate: { count: savedThemes().length })
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -215,7 +219,7 @@
|
||||
[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
|
||||
{{ 'theme.studio.saveNew' | translate }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -223,7 +227,7 @@
|
||||
[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
|
||||
{{ 'theme.studio.saveSelected' | translate }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -231,7 +235,7 @@
|
||||
[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
|
||||
{{ 'theme.studio.use' | translate }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -239,7 +243,7 @@
|
||||
[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
|
||||
{{ 'theme.studio.edit' | translate }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -247,7 +251,7 @@
|
||||
[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
|
||||
{{ 'theme.studio.remove' | translate }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -255,7 +259,7 @@
|
||||
[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
|
||||
{{ 'theme.studio.refresh' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -275,9 +279,13 @@
|
||||
</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>
|
||||
<span class="rounded-full bg-emerald-500/12 px-2 py-0.5 text-[10px] font-medium text-emerald-700">{{
|
||||
'theme.studio.ready' | translate
|
||||
}}</span>
|
||||
} @else {
|
||||
<span class="rounded-full bg-destructive/10 px-2 py-0.5 text-[10px] font-medium text-destructive">Invalid</span>
|
||||
<span class="rounded-full bg-destructive/10 px-2 py-0.5 text-[10px] font-medium text-destructive">{{
|
||||
'theme.studio.invalid' | translate
|
||||
}}</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -297,7 +305,7 @@
|
||||
</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.
|
||||
{{ 'theme.studio.emptySavedThemes' | translate }}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -309,9 +317,9 @@
|
||||
|
||||
<section class="theme-studio-card theme-settings__explorer-card p-3.5">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<p class="text-sm font-semibold text-foreground">Explorer</p>
|
||||
<p class="text-sm font-semibold text-foreground">{{ 'theme.studio.explorer' | translate }}</p>
|
||||
<span class="rounded-full bg-secondary px-2.5 py-1 text-[11px] font-medium text-muted-foreground">
|
||||
{{ filteredEntries().length }} shown
|
||||
{{ 'theme.studio.explorerShown' | translate: { count: filteredEntries().length } }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -320,13 +328,13 @@
|
||||
type="text"
|
||||
[value]="explorerQuery()"
|
||||
(input)="onExplorerQueryInput($event)"
|
||||
placeholder="Search theme keys"
|
||||
[placeholder]="'theme.studio.searchKeysPlaceholder' | translate"
|
||||
class="theme-settings__search-input mt-2 w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="theme-settings__entry-list theme-settings__explorer-list mt-4">
|
||||
@for (entry of filteredEntries(); track entry.key) {
|
||||
@for (entry of localizedFilteredEntries(); track entry.key) {
|
||||
<button
|
||||
type="button"
|
||||
(click)="selectThemeEntry(entry.key)"
|
||||
@@ -336,7 +344,9 @@
|
||||
<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>
|
||||
<span class="rounded-full bg-emerald-500/12 px-2 py-0.5 text-[10px] font-medium text-emerald-700">{{
|
||||
'theme.studio.mounted' | translate
|
||||
}}</span>
|
||||
}
|
||||
</div>
|
||||
<span class="mt-1 block font-mono text-[11px] text-muted-foreground">{{ entry.key }}</span>
|
||||
@@ -344,7 +354,7 @@
|
||||
</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.
|
||||
{{ 'theme.studio.noKeysMatch' | translate }}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -355,17 +365,17 @@
|
||||
<section class="theme-studio-card p-3.5">
|
||||
<div class="flex flex-wrap items-start justify-between gap-4">
|
||||
<div>
|
||||
@if (selectedElement()) {
|
||||
@if (localizedSelectedElement(); as element) {
|
||||
<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>
|
||||
<span class="text-base font-semibold text-foreground">{{ element.label }}</span>
|
||||
<span class="rounded-full bg-secondary px-2 py-0.5 font-mono text-[11px] text-muted-foreground">{{ element.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>
|
||||
<p class="mt-2 text-sm leading-6 text-muted-foreground">{{ element.description }}</p>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -376,7 +386,7 @@
|
||||
(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
|
||||
{{ 'theme.studio.openStylesJson' | translate }}
|
||||
</button>
|
||||
@if (selectedElement()!.layoutEditable) {
|
||||
<button
|
||||
@@ -384,7 +394,7 @@
|
||||
(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
|
||||
{{ 'theme.studio.openLayoutJson' | translate }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
@@ -405,24 +415,24 @@
|
||||
<div class="flex flex-wrap items-start justify-between gap-4 border-b border-border pb-3">
|
||||
<div class="min-w-0">
|
||||
<p class="text-sm font-semibold text-foreground">
|
||||
{{ activeEditorTab() === 'cssOnly' ? 'CSS-Only Theme' : 'Theme JSON' }}
|
||||
{{ (activeEditorTab() === 'cssOnly' ? 'theme.studio.cssOnlyTheme' : 'theme.studio.themeJson') | translate }}
|
||||
</p>
|
||||
@if (activeEditorTab() === 'cssOnly') {
|
||||
<p class="mt-1 text-xs leading-5 text-muted-foreground">CSS here is applied over the built-in default JSON theme.</p>
|
||||
<p class="mt-1 text-xs leading-5 text-muted-foreground">{{ 'theme.studio.cssOnlyHint' | translate }}</p>
|
||||
}
|
||||
</div>
|
||||
|
||||
<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"
|
||||
>{{ activeEditorTab() === 'cssOnly' ? cssOnlyText().split('\n').length : draftLineCount() }} lines</span
|
||||
>
|
||||
<span class="rounded-md bg-secondary px-2.5 py-1"
|
||||
>{{ activeEditorTab() === 'cssOnly' ? cssOnlyText().length : draftCharacterCount() }} chars</span
|
||||
>
|
||||
<span class="rounded-md bg-secondary px-2.5 py-1">{{
|
||||
'theme.studio.lines' | translate: { count: activeEditorTab() === 'cssOnly' ? cssOnlyText().split('\n').length : draftLineCount() }
|
||||
}}</span>
|
||||
<span class="rounded-md bg-secondary px-2.5 py-1">{{
|
||||
'theme.studio.chars' | translate: { count: activeEditorTab() === 'cssOnly' ? cssOnlyText().length : draftCharacterCount() }
|
||||
}}</span>
|
||||
@if (activeEditorTab() === 'json') {
|
||||
<span class="rounded-md bg-secondary px-2.5 py-1">{{ draftErrorCount() }} errors</span>
|
||||
<span class="rounded-md bg-secondary px-2.5 py-1">{{ 'theme.studio.errors' | translate: { count: draftErrorCount() } }}</span>
|
||||
}
|
||||
<span class="rounded-md 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">{{ 'theme.studio.ideEditor' | translate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -435,7 +445,7 @@
|
||||
[class.bg-primary/10]="activeEditorTab() === 'json'"
|
||||
[attr.aria-current]="activeEditorTab() === 'json' ? 'page' : null"
|
||||
>
|
||||
JSON Theme
|
||||
{{ 'theme.studio.jsonTheme' | translate }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -445,7 +455,7 @@
|
||||
[class.bg-primary/10]="activeEditorTab() === 'cssOnly'"
|
||||
[attr.aria-current]="activeEditorTab() === 'cssOnly' ? 'page' : null"
|
||||
>
|
||||
CSS Only
|
||||
{{ 'theme.studio.cssOnly' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -475,34 +485,36 @@
|
||||
<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>
|
||||
<p class="text-sm font-semibold text-foreground">{{ 'theme.studio.selection' | translate }}</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
|
||||
{{ 'theme.studio.pickLiveElement' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@if (selectedElement()) {
|
||||
@if (localizedSelectedElement(); as element) {
|
||||
<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>
|
||||
<p class="text-base font-semibold text-foreground">{{ element.label }}</p>
|
||||
<span class="rounded-full bg-secondary px-2 py-0.5 font-mono text-[11px] text-muted-foreground">{{ element.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>
|
||||
<span class="rounded-full bg-emerald-500/12 px-2 py-0.5 text-[11px] font-medium text-emerald-700">{{
|
||||
'theme.studio.mountedNow' | translate
|
||||
}}</span>
|
||||
}
|
||||
</div>
|
||||
<p class="mt-2 text-sm leading-6 text-muted-foreground">{{ selectedElement()!.description }}</p>
|
||||
<p class="mt-2 text-sm leading-6 text-muted-foreground">{{ element.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">Editable Attributes</p>
|
||||
<p class="text-sm font-semibold text-foreground">{{ 'theme.studio.editableAttributes' | translate }}</p>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
@@ -510,7 +522,7 @@
|
||||
[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
|
||||
{{ 'theme.studio.addFadeAnimation' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -538,7 +550,7 @@
|
||||
</section>
|
||||
|
||||
<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">{{ 'theme.studio.animationKeys' | translate }}</p>
|
||||
|
||||
@if (animationKeys().length > 0) {
|
||||
<div class="mt-4 flex flex-wrap gap-2">
|
||||
@@ -554,7 +566,7 @@
|
||||
</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.
|
||||
{{ 'theme.studio.noAnimationKeys' | translate }}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -572,7 +584,7 @@
|
||||
@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>
|
||||
<p class="text-sm font-semibold text-foreground">{{ 'theme.studio.layoutGrid' | translate }}</p>
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
@for (container of layoutContainers; track container.key) {
|
||||
@@ -583,7 +595,7 @@
|
||||
[class.bg-primary/10]="selectedContainer() === container.key"
|
||||
[class.border-primary/40]="selectedContainer() === container.key"
|
||||
>
|
||||
{{ container.label }}
|
||||
{{ layoutContainerLabel(container.key) }}
|
||||
</button>
|
||||
}
|
||||
|
||||
@@ -593,7 +605,7 @@
|
||||
[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
|
||||
{{ 'theme.studio.resetContainer' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { SettingsModalService } from '../../../../../core/services/settings-modal.service';
|
||||
import { AppI18nService, APP_TRANSLATE_IMPORTS } from '../../../../../core/i18n';
|
||||
import { getElectronApi } from '../../../../../core/platform/electron/get-electron-api';
|
||||
import {
|
||||
ThemeContainerKey,
|
||||
@@ -40,7 +41,8 @@ type ThemeStudioWorkspace = 'editor' | 'inspector' | 'layout';
|
||||
imports: [
|
||||
CommonModule,
|
||||
ThemeGridEditorComponent,
|
||||
ThemeJsonCodeEditorComponent
|
||||
ThemeJsonCodeEditorComponent,
|
||||
...APP_TRANSLATE_IMPORTS
|
||||
],
|
||||
templateUrl: './theme-settings.component.html',
|
||||
styleUrl: './theme-settings.component.scss'
|
||||
@@ -52,6 +54,7 @@ export class ThemeSettingsComponent {
|
||||
readonly registry = inject(ThemeRegistryService);
|
||||
readonly picker = inject(ElementPickerService);
|
||||
readonly layoutSync = inject(LayoutSyncService);
|
||||
private readonly appI18n = inject(AppI18nService);
|
||||
|
||||
readonly editorRef = viewChild<ThemeJsonCodeEditorComponent>('jsonEditorRef');
|
||||
|
||||
@@ -68,23 +71,23 @@ export class ThemeSettingsComponent {
|
||||
readonly animationKeys = this.theme.knownAnimationClasses;
|
||||
readonly layoutContainers = this.layoutSync.containers();
|
||||
readonly themeEntries = this.registry.entries();
|
||||
readonly workspaceTabs: readonly { key: ThemeStudioWorkspace; label: string; description: string }[] = [
|
||||
readonly workspaceTabs = computed(() => [
|
||||
{
|
||||
key: 'editor',
|
||||
label: 'JSON Editor',
|
||||
description: 'Edit the raw theme document in a fixed-contrast code view.'
|
||||
key: 'editor' as const,
|
||||
label: this.appI18n.instant('theme.studio.workspaces.editor.label'),
|
||||
description: this.appI18n.instant('theme.studio.workspaces.editor.description')
|
||||
},
|
||||
{
|
||||
key: 'inspector',
|
||||
label: 'Element Inspector',
|
||||
description: 'Browse themeable regions, supported overrides, and starter values.'
|
||||
key: 'inspector' as const,
|
||||
label: this.appI18n.instant('theme.studio.workspaces.inspector.label'),
|
||||
description: this.appI18n.instant('theme.studio.workspaces.inspector.description')
|
||||
},
|
||||
{
|
||||
key: 'layout',
|
||||
label: 'Layout Studio',
|
||||
description: 'Move shells around the grid without hunting through JSON.'
|
||||
key: 'layout' as const,
|
||||
label: this.appI18n.instant('theme.studio.workspaces.layout.label'),
|
||||
description: this.appI18n.instant('theme.studio.workspaces.layout.description')
|
||||
}
|
||||
];
|
||||
]);
|
||||
readonly mountedKeyCounts = computed(() => this.registry.mountedKeyCounts());
|
||||
readonly activeWorkspace = signal<ThemeStudioWorkspace>('editor');
|
||||
readonly activeEditorTab = signal<ThemeEditorTab>('json');
|
||||
@@ -102,10 +105,10 @@ export class ThemeSettingsComponent {
|
||||
}
|
||||
|
||||
return [
|
||||
selected.layoutEditable ? 'Layout editable' : null,
|
||||
selected.supportsTextOverride ? 'Text override' : null,
|
||||
selected.supportsLink ? 'Safe external link' : null,
|
||||
selected.supportsIcon ? 'Icon slot' : null
|
||||
selected.layoutEditable ? this.appI18n.instant('theme.studio.capabilities.layoutEditable') : null,
|
||||
selected.supportsTextOverride ? this.appI18n.instant('theme.studio.capabilities.textOverride') : null,
|
||||
selected.supportsLink ? this.appI18n.instant('theme.studio.capabilities.safeExternalLink') : null,
|
||||
selected.supportsIcon ? this.appI18n.instant('theme.studio.capabilities.iconSlot') : null
|
||||
].filter((value): value is string => value !== null);
|
||||
});
|
||||
readonly selectedContainerItems = computed(() => this.layoutSync.itemsForContainer(this.selectedContainer()));
|
||||
@@ -116,7 +119,15 @@ export class ThemeSettingsComponent {
|
||||
return this.selectedContainerItems().find((item) => item.key === this.selectedElementKey()) ?? null;
|
||||
});
|
||||
readonly activeWorkspaceInfo = computed(() => {
|
||||
return this.workspaceTabs.find((workspace) => workspace.key === this.activeWorkspace()) ?? this.workspaceTabs[0];
|
||||
return this.workspaceTabs().find((workspace) => workspace.key === this.activeWorkspace()) ?? this.workspaceTabs()[0];
|
||||
});
|
||||
readonly localizedFilteredEntries = computed(() =>
|
||||
this.filteredEntries().map((entry) => this.localizeRegistryEntry(entry))
|
||||
);
|
||||
readonly localizedSelectedElement = computed(() => {
|
||||
const selected = this.selectedElement();
|
||||
|
||||
return selected ? this.localizeRegistryEntry(selected) : null;
|
||||
});
|
||||
readonly visiblePropertyHints = computed(() => {
|
||||
const selected = this.selectedElement();
|
||||
@@ -260,6 +271,10 @@ export class ThemeSettingsComponent {
|
||||
this.setWorkspace(select.value as ThemeStudioWorkspace);
|
||||
}
|
||||
|
||||
layoutContainerLabel(key: string): string {
|
||||
return this.appI18n.instant(`theme.registry.${key}.label`);
|
||||
}
|
||||
|
||||
onExplorerQueryInput(event: Event): void {
|
||||
const input = event.target as HTMLInputElement;
|
||||
|
||||
@@ -281,7 +296,9 @@ export class ThemeSettingsComponent {
|
||||
const fileName = `${this.sanitizeThemeFileName(this.draftTheme().meta.name)}.json`;
|
||||
const saved = await this.saveTextAsFile(fileName, exportText);
|
||||
|
||||
this.theme.announceStatus(saved ? `${fileName} exported.` : 'Theme export cancelled.');
|
||||
this.theme.announceStatus(saved
|
||||
? this.appI18n.instant('theme.studio.exported', { fileName })
|
||||
: this.appI18n.instant('theme.studio.exportCancelled'));
|
||||
}
|
||||
|
||||
importThemeFile(event: Event): void {
|
||||
@@ -300,7 +317,9 @@ export class ThemeSettingsComponent {
|
||||
async copyLlmThemeGuide(): Promise<void> {
|
||||
const copied = await this.copyTextToClipboard(THEME_LLM_GUIDE);
|
||||
|
||||
this.setLlmGuideCopyMessage(copied ? 'LLM guide copied.' : 'Manual copy opened.');
|
||||
this.setLlmGuideCopyMessage(copied
|
||||
? this.appI18n.instant('theme.studio.llmGuideCopied')
|
||||
: this.appI18n.instant('theme.studio.llmGuideManualCopy'));
|
||||
}
|
||||
|
||||
startPicker(): void {
|
||||
@@ -501,7 +520,7 @@ export class ThemeSettingsComponent {
|
||||
}
|
||||
|
||||
if (!this.draftIsValid()) {
|
||||
this.theme.announceStatus('Fix JSON errors before exporting the theme.');
|
||||
this.theme.announceStatus(this.appI18n.instant('theme.studio.fixJsonBeforeExport'));
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -532,7 +551,7 @@ export class ThemeSettingsComponent {
|
||||
this.syncCssOnlyTextFromTheme();
|
||||
this.focusEditor();
|
||||
} catch {
|
||||
this.theme.announceStatus(`Unable to import ${file.name}.`);
|
||||
this.theme.announceStatus(this.appI18n.instant('theme.status.importFailed', { fileName: file.name }));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -773,13 +792,13 @@ export class ThemeSettingsComponent {
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
window.prompt('Copy this LLM theme guide', value);
|
||||
window.prompt(this.appI18n.instant('theme.studio.llmGuidePrompt'), value);
|
||||
return false;
|
||||
} finally {
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
|
||||
window.prompt('Copy this LLM theme guide', value);
|
||||
window.prompt(this.appI18n.instant('theme.studio.llmGuidePrompt'), value);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -796,6 +815,34 @@ export class ThemeSettingsComponent {
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
presetDisplayName(presetKey: string, fallbackName: string): string {
|
||||
const localized = this.appI18n.instant(`theme.presets.${presetKey}.name`);
|
||||
|
||||
return localized === `theme.presets.${presetKey}.name` ? fallbackName : localized;
|
||||
}
|
||||
|
||||
presetDescription(presetKey: string, fallbackDescription?: string): string | undefined {
|
||||
if (!fallbackDescription) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const localized = this.appI18n.instant(`theme.presets.${presetKey}.description`);
|
||||
|
||||
return localized === `theme.presets.${presetKey}.description` ? fallbackDescription : localized;
|
||||
}
|
||||
|
||||
isDefaultPresetName(name: string): boolean {
|
||||
return name === this.appI18n.instant('theme.presets.toju-default-dark-11.name');
|
||||
}
|
||||
|
||||
private localizeRegistryEntry(entry: ThemeRegistryEntry): ThemeRegistryEntry {
|
||||
return {
|
||||
...entry,
|
||||
label: this.appI18n.instant(`theme.registry.${entry.key}.label`),
|
||||
description: this.appI18n.instant(`theme.registry.${entry.key}.description`)
|
||||
};
|
||||
}
|
||||
|
||||
private findAnchorIndex(text: string, section: JumpSection, key: string): number {
|
||||
const sectionAnchor = `"${section}": {`;
|
||||
const sectionIndex = text.indexOf(sectionAnchor);
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
<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="text-xs font-semibold uppercase tracking-[0.18em] text-primary">{{ 'theme.pickerOverlay.activeBadge' | translate }}</p>
|
||||
<p class="mt-1 text-sm text-foreground">{{ 'theme.pickerOverlay.instruction' | translate }}</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>
|
||||
{{ 'theme.pickerOverlay.hovering' | translate }}
|
||||
<span class="font-medium text-foreground">{{ hoveredEntry()?.label || ('theme.pickerOverlay.hoverFallback' | translate) }}</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 }}
|
||||
@@ -21,7 +21,7 @@
|
||||
(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
|
||||
{{ 'common.cancel' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,12 +6,16 @@ import {
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { ElementPickerService } from '../../application/services/element-picker.service';
|
||||
import { APP_TRANSLATE_IMPORTS } from '../../../../core/i18n';
|
||||
import { ThemeRegistryService } from '../../application/services/theme-registry.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-theme-picker-overlay',
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
imports: [
|
||||
CommonModule,
|
||||
...APP_TRANSLATE_IMPORTS
|
||||
],
|
||||
templateUrl: './theme-picker-overlay.component.html'
|
||||
})
|
||||
export class ThemePickerOverlayComponent {
|
||||
|
||||
Reference in New Issue
Block a user