refactor: stricter domain: theme
This commit is contained in:
91
toju-app/src/app/domains/theme/README.md
Normal file
91
toju-app/src/app/domains/theme/README.md
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
# Theme Domain
|
||||||
|
|
||||||
|
Manages the theming engine: CSS variable injection, layout grid, theme library persistence, element picker for live editing, and schema validation. Drives the theme settings UI and the `ThemeNodeDirective` used across the app.
|
||||||
|
|
||||||
|
## Module map
|
||||||
|
|
||||||
|
```
|
||||||
|
theme/
|
||||||
|
├── application/
|
||||||
|
│ └── services/
|
||||||
|
│ ├── element-picker.service.ts DOM element picker for live theme editing
|
||||||
|
│ ├── layout-sync.service.ts Syncs grid layout state between editor and theme document
|
||||||
|
│ ├── theme-library.service.ts Saved-theme CRUD via Electron file system
|
||||||
|
│ ├── theme-registry.service.ts Wraps static registry/container lookups as signals
|
||||||
|
│ └── theme.service.ts Core orchestrator: CSS injection, draft/active, validation
|
||||||
|
│
|
||||||
|
├── domain/
|
||||||
|
│ ├── constants/
|
||||||
|
│ │ └── theme-llm-guide.constants.ts LLM prompt context for AI-assisted theme editing
|
||||||
|
│ ├── logic/
|
||||||
|
│ │ ├── theme-defaults.logic.ts Default theme document, JSON template, legacy detection
|
||||||
|
│ │ ├── theme-registry.logic.ts Static registry of themeable elements and layout containers
|
||||||
|
│ │ ├── theme-schema.logic.ts Schema field definitions, animation starters, suggested defaults
|
||||||
|
│ │ └── theme-validation.logic.ts Theme document validation against registry + schema
|
||||||
|
│ └── models/
|
||||||
|
│ └── theme.model.ts All domain types (ThemeDocument, ThemeElementStyles, etc.)
|
||||||
|
│
|
||||||
|
├── infrastructure/
|
||||||
|
│ ├── services/
|
||||||
|
│ │ └── theme-library-storage.service.ts Electron bridge for saving/loading theme files
|
||||||
|
│ └── util/
|
||||||
|
│ └── theme-storage.util.ts localStorage read/write for active + draft theme text
|
||||||
|
│
|
||||||
|
├── feature/
|
||||||
|
│ ├── settings/
|
||||||
|
│ │ ├── theme-grid-editor.component.* Visual grid layout editor
|
||||||
|
│ │ ├── theme-json-code-editor.component.* CodeMirror JSON editor
|
||||||
|
│ │ └── theme-settings.component.* Main theme settings page
|
||||||
|
│ ├── theme-node.directive.ts Applies theme styles + picker interaction to host elements
|
||||||
|
│ └── theme-picker-overlay.component.* Floating overlay showing picked element info
|
||||||
|
│
|
||||||
|
└── index.ts Barrel exports
|
||||||
|
```
|
||||||
|
|
||||||
|
## Layer composition
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
ThemeService[ThemeService]
|
||||||
|
Registry[ThemeRegistryService]
|
||||||
|
Library[ThemeLibraryService]
|
||||||
|
Layout[LayoutSyncService]
|
||||||
|
Picker[ElementPickerService]
|
||||||
|
LibStorage[ThemeLibraryStorageService]
|
||||||
|
Storage[theme-storage.util]
|
||||||
|
Defaults[theme-defaults.logic]
|
||||||
|
Validation[theme-validation.logic]
|
||||||
|
Schema[theme-schema.logic]
|
||||||
|
RegistryLogic[theme-registry.logic]
|
||||||
|
Models[theme.model]
|
||||||
|
|
||||||
|
ThemeService --> Defaults
|
||||||
|
ThemeService --> Schema
|
||||||
|
ThemeService --> RegistryLogic
|
||||||
|
ThemeService --> Validation
|
||||||
|
ThemeService --> Storage
|
||||||
|
Library --> LibStorage
|
||||||
|
Library --> ThemeService
|
||||||
|
Layout --> ThemeService
|
||||||
|
Layout --> Registry
|
||||||
|
Layout --> Defaults
|
||||||
|
Registry --> RegistryLogic
|
||||||
|
Picker --> Registry
|
||||||
|
Validation --> Defaults
|
||||||
|
Validation --> RegistryLogic
|
||||||
|
Defaults --> RegistryLogic
|
||||||
|
Defaults --> Models
|
||||||
|
|
||||||
|
click ThemeService "application/services/theme.service.ts" "Core orchestrator" _blank
|
||||||
|
click Registry "application/services/theme-registry.service.ts" "Registry wrapper" _blank
|
||||||
|
click Library "application/services/theme-library.service.ts" "Theme library CRUD" _blank
|
||||||
|
click Layout "application/services/layout-sync.service.ts" "Grid layout sync" _blank
|
||||||
|
click Picker "application/services/element-picker.service.ts" "Element picker" _blank
|
||||||
|
click LibStorage "infrastructure/services/theme-library-storage.service.ts" "Electron file I/O" _blank
|
||||||
|
click Storage "infrastructure/util/theme-storage.util.ts" "localStorage" _blank
|
||||||
|
click Defaults "domain/logic/theme-defaults.logic.ts" "Default theme" _blank
|
||||||
|
click Validation "domain/logic/theme-validation.logic.ts" "Validation" _blank
|
||||||
|
click Schema "domain/logic/theme-schema.logic.ts" "Schema fields" _blank
|
||||||
|
click RegistryLogic "domain/logic/theme-registry.logic.ts" "Static registry" _blank
|
||||||
|
click Models "domain/models/theme.model.ts" "Domain types" _blank
|
||||||
|
```
|
||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
signal
|
signal
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
|
|
||||||
import { SettingsModalService, type SettingsPage } from '../../../core/services/settings-modal.service';
|
import { SettingsModalService, type SettingsPage } from '../../../../core/services/settings-modal.service';
|
||||||
import { ThemeRegistryService } from './theme-registry.service';
|
import { ThemeRegistryService } from './theme-registry.service';
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
@@ -8,8 +8,8 @@ import {
|
|||||||
ThemeContainerKey,
|
ThemeContainerKey,
|
||||||
ThemeGridEditorItem,
|
ThemeGridEditorItem,
|
||||||
ThemeGridRect
|
ThemeGridRect
|
||||||
} from '../domain/theme.models';
|
} from '../../domain/models/theme.model';
|
||||||
import { createDefaultThemeDocument } from '../domain/theme.defaults';
|
import { createDefaultThemeDocument } from '../../domain/logic/theme-defaults.logic';
|
||||||
import { ThemeRegistryService } from './theme-registry.service';
|
import { ThemeRegistryService } from './theme-registry.service';
|
||||||
import { ThemeService } from './theme.service';
|
import { ThemeService } from './theme.service';
|
||||||
|
|
||||||
@@ -4,8 +4,8 @@ import {
|
|||||||
inject,
|
inject,
|
||||||
signal
|
signal
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import type { SavedThemeSummary } from '../domain/theme.models';
|
import type { SavedThemeSummary } from '../../domain/models/theme.model';
|
||||||
import { ThemeLibraryStorageService } from '../infrastructure/theme-library.storage';
|
import { ThemeLibraryStorageService } from '../../infrastructure/services/theme-library-storage.service';
|
||||||
import { ThemeService } from './theme.service';
|
import { ThemeService } from './theme.service';
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
import { Injectable, signal } from '@angular/core';
|
import { Injectable, signal } from '@angular/core';
|
||||||
|
|
||||||
import { ThemeLayoutContainerDefinition, ThemeRegistryEntry } from '../domain/theme.models';
|
import { ThemeLayoutContainerDefinition, ThemeRegistryEntry } from '../../domain/models/theme.model';
|
||||||
import {
|
import {
|
||||||
THEME_LAYOUT_CONTAINERS,
|
THEME_LAYOUT_CONTAINERS,
|
||||||
THEME_REGISTRY,
|
THEME_REGISTRY,
|
||||||
findThemeLayoutContainer,
|
findThemeLayoutContainer,
|
||||||
findThemeRegistryEntry
|
findThemeRegistryEntry
|
||||||
} from '../domain/theme.registry';
|
} from '../../domain/logic/theme-registry.logic';
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class ThemeRegistryService {
|
export class ThemeRegistryService {
|
||||||
@@ -13,20 +13,20 @@ import {
|
|||||||
ThemeDocument,
|
ThemeDocument,
|
||||||
ThemeElementStyleProperty,
|
ThemeElementStyleProperty,
|
||||||
ThemeElementStyles
|
ThemeElementStyles
|
||||||
} from '../domain/theme.models';
|
} from '../../domain/models/theme.model';
|
||||||
import {
|
import {
|
||||||
DEFAULT_THEME_JSON,
|
DEFAULT_THEME_JSON,
|
||||||
createDefaultThemeDocument,
|
createDefaultThemeDocument,
|
||||||
isLegacyDefaultThemeDocument
|
isLegacyDefaultThemeDocument
|
||||||
} from '../domain/theme.defaults';
|
} from '../../domain/logic/theme-defaults.logic';
|
||||||
import { createAnimationStarterDefinition } from '../domain/theme.schema';
|
import { createAnimationStarterDefinition } from '../../domain/logic/theme-schema.logic';
|
||||||
import { findThemeLayoutContainer } from '../domain/theme.registry';
|
import { findThemeLayoutContainer } from '../../domain/logic/theme-registry.logic';
|
||||||
import { validateThemeDocument } from '../domain/theme.validation';
|
import { validateThemeDocument } from '../../domain/logic/theme-validation.logic';
|
||||||
import {
|
import {
|
||||||
loadThemeStorageSnapshot,
|
loadThemeStorageSnapshot,
|
||||||
saveActiveThemeText,
|
saveActiveThemeText,
|
||||||
saveDraftThemeText
|
saveDraftThemeText
|
||||||
} from '../infrastructure/theme.storage';
|
} from '../../infrastructure/util/theme-storage.util';
|
||||||
|
|
||||||
function toKebabCase(value: string): string {
|
function toKebabCase(value: string): string {
|
||||||
return value
|
return value
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import { DEFAULT_THEME_DOCUMENT } from './theme.defaults';
|
import { DEFAULT_THEME_DOCUMENT } from '../logic/theme-defaults.logic';
|
||||||
import { THEME_LAYOUT_CONTAINERS, THEME_REGISTRY } from './theme.registry';
|
import { THEME_LAYOUT_CONTAINERS, THEME_REGISTRY } from '../logic/theme-registry.logic';
|
||||||
import {
|
import {
|
||||||
THEME_ANIMATION_FIELDS,
|
THEME_ANIMATION_FIELDS,
|
||||||
THEME_ELEMENT_STYLE_FIELDS,
|
THEME_ELEMENT_STYLE_FIELDS,
|
||||||
createAnimationStarterDefinition
|
createAnimationStarterDefinition
|
||||||
} from './theme.schema';
|
} from '../logic/theme-schema.logic';
|
||||||
|
|
||||||
function formatExample(example: string | number): string {
|
function formatExample(example: string | number): string {
|
||||||
return typeof example === 'number'
|
return typeof example === 'number'
|
||||||
@@ -2,12 +2,12 @@ import {
|
|||||||
ThemeDocument,
|
ThemeDocument,
|
||||||
ThemeElementStyles,
|
ThemeElementStyles,
|
||||||
ThemeLayoutEntry
|
ThemeLayoutEntry
|
||||||
} from './theme.models';
|
} from '../models/theme.model';
|
||||||
import {
|
import {
|
||||||
THEME_LAYOUT_CONTAINERS,
|
THEME_LAYOUT_CONTAINERS,
|
||||||
THEME_REGISTRY,
|
THEME_REGISTRY,
|
||||||
getLayoutEditableThemeKeys
|
getLayoutEditableThemeKeys
|
||||||
} from './theme.registry';
|
} from './theme-registry.logic';
|
||||||
|
|
||||||
const APP_ROOT_BASE_GRADIENT =
|
const APP_ROOT_BASE_GRADIENT =
|
||||||
'radial-gradient(circle at top, '
|
'radial-gradient(circle at top, '
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ThemeLayoutContainerDefinition, ThemeRegistryEntry } from './theme.models';
|
import { ThemeLayoutContainerDefinition, ThemeRegistryEntry } from '../models/theme.model';
|
||||||
|
|
||||||
export const THEME_LAYOUT_CONTAINERS: readonly ThemeLayoutContainerDefinition[] = [
|
export const THEME_LAYOUT_CONTAINERS: readonly ThemeLayoutContainerDefinition[] = [
|
||||||
{
|
{
|
||||||
@@ -2,7 +2,7 @@ import {
|
|||||||
ThemeDocument,
|
ThemeDocument,
|
||||||
ThemeElementStyleProperty,
|
ThemeElementStyleProperty,
|
||||||
ThemeSchemaField
|
ThemeSchemaField
|
||||||
} from './theme.models';
|
} from '../models/theme.model';
|
||||||
|
|
||||||
const RADIAL_GRADIENT_EXAMPLE =
|
const RADIAL_GRADIENT_EXAMPLE =
|
||||||
'radial-gradient(circle at top, rgba(255,255,255,0.12), '
|
'radial-gradient(circle at top, rgba(255,255,255,0.12), '
|
||||||
@@ -4,13 +4,13 @@ import {
|
|||||||
ThemeElementStyleProperty,
|
ThemeElementStyleProperty,
|
||||||
ThemeElementStyles,
|
ThemeElementStyles,
|
||||||
ThemeValidationResult
|
ThemeValidationResult
|
||||||
} from './theme.models';
|
} from '../models/theme.model';
|
||||||
import { createDefaultThemeDocument } from './theme.defaults';
|
import { createDefaultThemeDocument } from './theme-defaults.logic';
|
||||||
import {
|
import {
|
||||||
THEME_LAYOUT_CONTAINERS,
|
THEME_LAYOUT_CONTAINERS,
|
||||||
THEME_REGISTRY,
|
THEME_REGISTRY,
|
||||||
getLayoutEditableThemeKeys
|
getLayoutEditableThemeKeys
|
||||||
} from './theme.registry';
|
} from './theme-registry.logic';
|
||||||
|
|
||||||
const TOP_LEVEL_KEYS = [
|
const TOP_LEVEL_KEYS = [
|
||||||
'meta',
|
'meta',
|
||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
ThemeGridEditorItem,
|
ThemeGridEditorItem,
|
||||||
ThemeGridRect,
|
ThemeGridRect,
|
||||||
ThemeLayoutContainerDefinition
|
ThemeLayoutContainerDefinition
|
||||||
} from '../../domain/theme.models';
|
} from '../../domain/models/theme.model';
|
||||||
|
|
||||||
type DragMode = 'move' | 'resize';
|
type DragMode = 'move' | 'resize';
|
||||||
|
|
||||||
|
|||||||
@@ -13,19 +13,19 @@ import {
|
|||||||
ThemeContainerKey,
|
ThemeContainerKey,
|
||||||
ThemeElementStyleProperty,
|
ThemeElementStyleProperty,
|
||||||
ThemeRegistryEntry
|
ThemeRegistryEntry
|
||||||
} from '../../domain/theme.models';
|
} from '../../domain/models/theme.model';
|
||||||
import {
|
import {
|
||||||
THEME_ANIMATION_FIELDS as THEME_ANIMATION_FIELD_HINTS,
|
THEME_ANIMATION_FIELDS as THEME_ANIMATION_FIELD_HINTS,
|
||||||
THEME_ELEMENT_STYLE_FIELDS,
|
THEME_ELEMENT_STYLE_FIELDS,
|
||||||
createAnimationStarterDefinition,
|
createAnimationStarterDefinition,
|
||||||
getSuggestedFieldDefault
|
getSuggestedFieldDefault
|
||||||
} from '../../domain/theme.schema';
|
} from '../../domain/logic/theme-schema.logic';
|
||||||
import { ElementPickerService } from '../../application/element-picker.service';
|
import { ElementPickerService } from '../../application/services/element-picker.service';
|
||||||
import { LayoutSyncService } from '../../application/layout-sync.service';
|
import { LayoutSyncService } from '../../application/services/layout-sync.service';
|
||||||
import { ThemeLibraryService } from '../../application/theme-library.service';
|
import { ThemeLibraryService } from '../../application/services/theme-library.service';
|
||||||
import { ThemeRegistryService } from '../../application/theme-registry.service';
|
import { ThemeRegistryService } from '../../application/services/theme-registry.service';
|
||||||
import { ThemeService } from '../../application/theme.service';
|
import { ThemeService } from '../../application/services/theme.service';
|
||||||
import { THEME_LLM_GUIDE } from '../../domain/theme-llm-guide';
|
import { THEME_LLM_GUIDE } from '../../domain/constants/theme-llm-guide.constants';
|
||||||
import { ThemeGridEditorComponent } from './theme-grid-editor.component';
|
import { ThemeGridEditorComponent } from './theme-grid-editor.component';
|
||||||
import { ThemeJsonCodeEditorComponent } from './theme-json-code-editor.component';
|
import { ThemeJsonCodeEditorComponent } from './theme-json-code-editor.component';
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ import {
|
|||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
|
|
||||||
import { ExternalLinkService } from '../../../core/platform';
|
import { ExternalLinkService } from '../../../core/platform';
|
||||||
import { ElementPickerService } from '../application/element-picker.service';
|
import { ElementPickerService } from '../application/services/element-picker.service';
|
||||||
import { ThemeRegistryService } from '../application/theme-registry.service';
|
import { ThemeRegistryService } from '../application/services/theme-registry.service';
|
||||||
import { ThemeService } from '../application/theme.service';
|
import { ThemeService } from '../application/services/theme.service';
|
||||||
|
|
||||||
function looksLikeImageReference(value: string): boolean {
|
function looksLikeImageReference(value: string): boolean {
|
||||||
return value.startsWith('url(')
|
return value.startsWith('url(')
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import {
|
|||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
|
|
||||||
import { ElementPickerService } from '../application/element-picker.service';
|
import { ElementPickerService } from '../application/services/element-picker.service';
|
||||||
import { ThemeRegistryService } from '../application/theme-registry.service';
|
import { ThemeRegistryService } from '../application/services/theme-registry.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-theme-picker-overlay',
|
selector: 'app-theme-picker-overlay',
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
export * from './application/theme.service';
|
export * from './application/services/theme.service';
|
||||||
export * from './application/theme-library.service';
|
export * from './application/services/theme-library.service';
|
||||||
export * from './application/theme-registry.service';
|
export * from './application/services/theme-registry.service';
|
||||||
export * from './application/element-picker.service';
|
export * from './application/services/element-picker.service';
|
||||||
export * from './application/layout-sync.service';
|
export * from './application/services/layout-sync.service';
|
||||||
export * from './domain/theme.models';
|
export * from './domain/models/theme.model';
|
||||||
export * from './domain/theme.defaults';
|
export * from './domain/logic/theme-defaults.logic';
|
||||||
export * from './domain/theme.registry';
|
export * from './domain/logic/theme-registry.logic';
|
||||||
export * from './domain/theme.schema';
|
export * from './domain/logic/theme-schema.logic';
|
||||||
export * from './domain/theme.validation';
|
export * from './domain/logic/theme-validation.logic';
|
||||||
|
|
||||||
export { ThemeNodeDirective } from './feature/theme-node.directive';
|
export { ThemeNodeDirective } from './feature/theme-node.directive';
|
||||||
export { ThemePickerOverlayComponent } from './feature/theme-picker-overlay.component';
|
export { ThemePickerOverlayComponent } from './feature/theme-picker-overlay.component';
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Injectable, inject } from '@angular/core';
|
import { Injectable, inject } from '@angular/core';
|
||||||
import { ElectronBridgeService } from '../../../core/platform/electron/electron-bridge.service';
|
import { ElectronBridgeService } from '../../../../core/platform/electron/electron-bridge.service';
|
||||||
import type { SavedThemeFileDescriptor } from '../../../core/platform/electron/electron-api.models';
|
import type { SavedThemeFileDescriptor } from '../../../../core/platform/electron/electron-api.models';
|
||||||
import type { SavedThemeSummary } from '../domain/theme.models';
|
import type { SavedThemeSummary } from '../../domain/models/theme.model';
|
||||||
import { validateThemeDocument } from '../domain/theme.validation';
|
import { validateThemeDocument } from '../../domain/logic/theme-validation.logic';
|
||||||
|
|
||||||
const THEME_LIBRARY_REQUEST_TIMEOUT_MS = 4000;
|
const THEME_LIBRARY_REQUEST_TIMEOUT_MS = 4000;
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { STORAGE_KEY_THEME_ACTIVE, STORAGE_KEY_THEME_DRAFT } from '../../../core/constants';
|
import { STORAGE_KEY_THEME_ACTIVE, STORAGE_KEY_THEME_DRAFT } from '../../../../core/constants';
|
||||||
|
|
||||||
export interface ThemeStorageSnapshot {
|
export interface ThemeStorageSnapshot {
|
||||||
activeText: string | null;
|
activeText: string | null;
|
||||||
Reference in New Issue
Block a user