fontastic 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/.github/workflows/claude-code-review.yml +0 -6
  2. package/.github/workflows/release-please.yml +25 -0
  3. package/.github/workflows/release.yml +5 -109
  4. package/.release-please-manifest.json +3 -0
  5. package/CHANGELOG.md +39 -0
  6. package/README.md +6 -0
  7. package/app/Application.js +4 -4
  8. package/app/Application.ts +13 -13
  9. package/app/config/database.js +1 -1
  10. package/app/config/database.ts +1 -1
  11. package/app/config/mimes.js +23 -23
  12. package/app/config/mimes.ts +35 -29
  13. package/app/core/ConfigManager.js +27 -0
  14. package/app/core/ConfigManager.ts +28 -0
  15. package/app/core/FontFinder.js +66 -15
  16. package/app/core/FontFinder.ts +81 -22
  17. package/app/core/FontManager.js +20 -18
  18. package/app/core/FontManager.ts +21 -19
  19. package/app/core/FontObject.js +44 -24
  20. package/app/core/FontObject.ts +47 -27
  21. package/app/core/MessageHandler.js +70 -19
  22. package/app/core/MessageHandler.ts +82 -21
  23. package/app/core/SystemManager.js +5 -1
  24. package/app/core/SystemManager.ts +5 -1
  25. package/app/database/entity/Collection.schema.js +20 -18
  26. package/app/database/entity/Collection.schema.ts +22 -21
  27. package/app/database/repository/Collection.repository.js +17 -18
  28. package/app/database/repository/Collection.repository.ts +27 -18
  29. package/app/database/repository/Store.repository.js +13 -18
  30. package/app/database/repository/Store.repository.ts +13 -21
  31. package/app/enums/ChannelType.js +18 -0
  32. package/app/enums/ChannelType.ts +24 -0
  33. package/app/main.js +98 -10
  34. package/app/main.ts +126 -19
  35. package/app/package.json +1 -1
  36. package/app/types/NativeThemeState.js +3 -0
  37. package/app/types/NativeThemeState.ts +4 -0
  38. package/app/types/ScanProgress.js +3 -0
  39. package/app/types/ScanProgress.ts +6 -0
  40. package/app/types/SystemPreferencesState.js +3 -0
  41. package/app/types/SystemPreferencesState.ts +4 -0
  42. package/app/types/index.js +3 -0
  43. package/app/types/index.ts +3 -0
  44. package/package.json +2 -2
  45. package/release-please-config.json +20 -0
  46. package/scripts/patch-electron-plist.js +41 -0
  47. package/src/app/core/services/database/database.service.ts +6 -0
  48. package/src/app/core/services/message/message.service.ts +33 -1
  49. package/src/app/core/services/presentation/presentation.service.ts +100 -1
  50. package/src/app/layout/footer/footer.component.html +13 -2
  51. package/src/app/layout/footer/footer.component.ts +18 -2
  52. package/src/app/layout/header/header.component.html +0 -10
  53. package/src/app/layout/header/header.component.ts +4 -23
  54. package/src/app/layout/navigation/navigation.component.html +66 -16
  55. package/src/app/layout/navigation/navigation.component.ts +65 -12
  56. package/src/app/settings/ai-keys/ai-keys.component.ts +13 -18
  57. package/src/app/settings/danger-zone/danger-zone.component.html +8 -0
  58. package/src/app/settings/danger-zone/danger-zone.component.ts +12 -0
  59. package/src/app/settings/news-api/news-api.component.ts +6 -8
  60. package/src/app/settings/theme/theme.component.html +15 -2
  61. package/src/app/settings/theme/theme.component.ts +4 -0
  62. package/src/app/shared/components/datagrid/datagrid.component.html +8 -17
  63. package/src/app/shared/components/datagrid/datagrid.component.ts +6 -10
  64. package/src/app/shared/components/glyphs/glyphs.component.html +5 -15
  65. package/src/app/shared/components/glyphs/glyphs.component.ts +3 -0
  66. package/src/app/shared/components/preview/preview.component.html +1 -1
  67. package/src/app/shared/components/preview/preview.component.ts +3 -8
  68. package/src/app/shared/components/prompt-dialog/prompt-dialog.component.html +2 -2
  69. package/src/app/shared/components/prompt-dialog/prompt-dialog.component.ts +2 -1
  70. package/src/app/shared/components/rule-builder/rule-builder.component.html +18 -6
  71. package/src/app/shared/components/rule-builder/rule-builder.component.ts +34 -2
  72. package/src/app/shared/components/search/search.component.html +9 -36
  73. package/src/app/shared/components/search/search.component.ts +2 -1
  74. package/src/app/shared/components/waterfall/waterfall.component.html +1 -3
  75. package/src/app/shared/components/waterfall/waterfall.component.ts +2 -1
  76. package/src/app/shared/directives/disabled-opacity/disabled-opacity.directive.ts +18 -0
  77. package/src/app/shared/directives/hover-highlight/hover-highlight.directive.ts +38 -0
  78. package/src/app/shared/directives/index.ts +5 -0
  79. package/src/app/shared/directives/modal-backdrop/modal-backdrop.directive.ts +18 -0
  80. package/src/app/shared/directives/scroll-reset/scroll-reset.directive.ts +15 -0
  81. package/src/app/shared/directives/stop-propagation/stop-propagation.directive.ts +12 -0
  82. package/src/assets/icons/favicon.256x256.png +0 -0
  83. package/src/assets/icons/favicon.512x512.png +0 -0
  84. package/src/assets/icons/favicon.icns +0 -0
  85. package/src/assets/icons/favicon.ico +0 -0
  86. package/src/assets/icons/favicon.png +0 -0
  87. package/src/favicon.ico +0 -0
@@ -8,3 +8,6 @@ export * from './SystemStats';
8
8
  export * from './SystemConfig';
9
9
  export * from './ImportOptions';
10
10
  export * from './SmartCollection';
11
+ export * from './NativeThemeState';
12
+ export * from './SystemPreferencesState';
13
+ export * from './ScanProgress';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fontastic",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "Fontastic is an Electron-based font management and cataloging application built for organizing, browsing, and inspecting font libraries.",
5
5
  "homepage": "https://github.com/tomshaw/fontastic",
6
6
  "private": false,
@@ -24,7 +24,7 @@
24
24
  ],
25
25
  "main": "app/main.js",
26
26
  "scripts": {
27
- "postinstall": "electron-builder install-app-deps",
27
+ "postinstall": "electron-builder install-app-deps && node scripts/patch-electron-plist.js",
28
28
  "ng": "ng",
29
29
  "start": "npm-run-all -p electron:serve ng:serve",
30
30
  "ng:serve": "ng serve -c dev -o",
@@ -0,0 +1,20 @@
1
+ {
2
+ "last-release-sha": "c7f3f9afba466a150f90e374f8a401daf1a56d11",
3
+ "packages": {
4
+ ".": {
5
+ "release-type": "node",
6
+ "extra-files": ["app/package.json"],
7
+ "changelog-sections": [
8
+ { "type": "feat", "section": "🎉 Features", "hidden": false },
9
+ { "type": "fix", "section": "🛠️ Fixes", "hidden": false },
10
+ { "type": "docs", "section": "📄 Documentation", "hidden": false },
11
+ { "type": "perf", "section": "⚡ Performance", "hidden": false },
12
+ { "type": "refactor", "section": "🏗️ Refactor", "hidden": false },
13
+ { "type": "chore", "section": "♻️ Chores", "hidden": true },
14
+ { "type": "test", "section": "♻️ Chores", "hidden": true },
15
+ { "type": "build", "section": "⚙️ Automation", "hidden": true },
16
+ { "type": "ci", "section": "⚙️ Automation", "hidden": true }
17
+ ]
18
+ }
19
+ }
20
+ }
@@ -0,0 +1,41 @@
1
+ const { execSync } = require('child_process');
2
+ const path = require('path');
3
+ const fs = require('fs');
4
+
5
+ if (process.platform !== 'darwin') {
6
+ process.exit(0);
7
+ }
8
+
9
+ const electronAppDir = path.join(
10
+ __dirname,
11
+ '..',
12
+ 'node_modules',
13
+ 'electron',
14
+ 'dist',
15
+ 'Electron.app'
16
+ );
17
+
18
+ const plist = path.join(electronAppDir, 'Contents', 'Info.plist');
19
+
20
+ if (!fs.existsSync(plist)) {
21
+ process.exit(0);
22
+ }
23
+
24
+ const pkg = require(path.join(__dirname, '..', 'package.json'));
25
+ const appName = 'Fontastic';
26
+ const appVersion = pkg.version;
27
+
28
+ execSync(`/usr/libexec/PlistBuddy -c "Set :CFBundleName ${appName}" "${plist}"`);
29
+ execSync(`/usr/libexec/PlistBuddy -c "Set :CFBundleDisplayName ${appName}" "${plist}"`);
30
+ execSync(`/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString ${appVersion}" "${plist}"`);
31
+
32
+ // Copy app icon into the Electron.app bundle
33
+ const srcIcon = path.join(__dirname, '..', 'src', 'assets', 'icons', 'favicon.icns');
34
+ const destIcon = path.join(electronAppDir, 'Contents', 'Resources', 'electron.icns');
35
+
36
+ if (fs.existsSync(srcIcon)) {
37
+ fs.copyFileSync(srcIcon, destIcon);
38
+ console.log('Copied app icon into Electron.app bundle');
39
+ }
40
+
41
+ console.log(`Patched Electron.app plist: name="${appName}", version="${appVersion}"`);
@@ -371,6 +371,12 @@ export class DatabaseService {
371
371
  );
372
372
  }
373
373
 
374
+ smartCollectionPreview(rules: any[], matchType: string): Promise<number> {
375
+ return this.message.smartCollectionPreview(rules, matchType).then((result) => {
376
+ return result[1] as number;
377
+ });
378
+ }
379
+
374
380
  // Store
375
381
 
376
382
  storeFind(args: any): Promise<Store[]> {
@@ -4,7 +4,7 @@ import type { Collection } from '@main/database/entity/Collection.schema';
4
4
  import type { Logger } from '@main/database/entity/Logger.schema';
5
5
  import type { Store, StoreManyAndCountType } from '@main/database/entity/Store.schema';
6
6
  import { ChannelType } from '@main/enums';
7
- import type { FontMetrics, SystemConfig, SystemStats } from '@main/types';
7
+ import type { FontMetrics, NativeThemeState, SystemConfig, SystemPreferencesState, SystemStats } from '@main/types';
8
8
  import type { SmartCollection } from '@main/database/entity/SmartCollection.schema';
9
9
 
10
10
  @Injectable({
@@ -236,6 +236,10 @@ export class MessageService {
236
236
  return this.invoke<StoreManyAndCountType>(ChannelType.IPC_SMART_COLLECTION_EVALUATE, { id, ...options });
237
237
  }
238
238
 
239
+ smartCollectionPreview(rules: any[], matchType: string): Promise<StoreManyAndCountType> {
240
+ return this.invoke<StoreManyAndCountType>(ChannelType.IPC_SMART_COLLECTION_PREVIEW, { rules, match_type: matchType });
241
+ }
242
+
239
243
  // Store
240
244
 
241
245
  storeFind(args: any): Promise<Store[]> {
@@ -311,4 +315,32 @@ export class MessageService {
311
315
  loggerTruncate(): Promise<Logger[]> {
312
316
  return this.invoke<Logger[]>(ChannelType.IPC_LOGGER_TRUNCATE);
313
317
  }
318
+
319
+ // Native Theme
320
+
321
+ getNativeTheme(): Promise<NativeThemeState> {
322
+ return this.invoke<NativeThemeState>(ChannelType.IPC_GET_NATIVE_THEME);
323
+ }
324
+
325
+ // Safe Storage
326
+
327
+ safeStore(key: string, value: string): Promise<void> {
328
+ return this.invoke(ChannelType.IPC_SAFE_STORE, { key, value });
329
+ }
330
+
331
+ safeRetrieve(key: string): Promise<string | null> {
332
+ return this.invoke<string | null>(ChannelType.IPC_SAFE_RETRIEVE, key);
333
+ }
334
+
335
+ // Session
336
+
337
+ clearCache(): Promise<void> {
338
+ return this.invoke(ChannelType.IPC_CLEAR_CACHE);
339
+ }
340
+
341
+ // System Preferences
342
+
343
+ getSystemPreferences(): Promise<SystemPreferencesState> {
344
+ return this.invoke<SystemPreferencesState>(ChannelType.IPC_GET_SYSTEM_PREFERENCES);
345
+ }
314
346
  }
@@ -2,7 +2,7 @@ import { Injectable, inject, signal, computed, effect, untracked } from '@angula
2
2
  import { ElectronService } from '../electron/electron.service';
3
3
  import { MessageService } from '../message/message.service';
4
4
  import { ChannelType, StorageType } from '@main/enums';
5
- import type { LayoutPanelType, LayoutPreviewType } from '@main/types';
5
+ import type { LayoutPanelType, LayoutPreviewType, NativeThemeState, ScanProgress } from '@main/types';
6
6
 
7
7
  @Injectable({ providedIn: 'root' })
8
8
  export class PresentationService {
@@ -21,6 +21,7 @@ export class PresentationService {
21
21
  this.loadingCount--;
22
22
  if (this.loadingCount === 0) {
23
23
  this.loading.set(false);
24
+ this.scanProgress.set(null);
24
25
  }
25
26
  }
26
27
 
@@ -28,6 +29,20 @@ export class PresentationService {
28
29
 
29
30
  readonly theme = signal<string>('light');
30
31
 
32
+ // Native Theme: auto-sync with OS dark mode
33
+ readonly autoTheme = signal(false);
34
+ readonly nativeThemeState = signal<NativeThemeState | null>(null);
35
+
36
+ // System Preferences
37
+ readonly systemAccentColor = signal('');
38
+ readonly reduceMotion = signal(false);
39
+
40
+ // Power Monitor
41
+ readonly suspended = signal(false);
42
+
43
+ // Scan Progress (MessageChannelMain)
44
+ readonly scanProgress = signal<ScanProgress | null>(null);
45
+
31
46
  constructor() {
32
47
  effect(() => {
33
48
  const current = this.theme();
@@ -37,12 +52,21 @@ export class PresentationService {
37
52
  }
38
53
  });
39
54
 
55
+ // Apply reduce-motion class based on OS preference
56
+ effect(() => {
57
+ document.documentElement.classList.toggle('reduce-motion', this.reduceMotion());
58
+ });
59
+
40
60
  if (this.electronService.isElectron) {
41
61
  this.loadThemeSettings();
42
62
  this.loadLayoutSettings();
43
63
  this.loadPreviewSettings();
44
64
  this.loadNavigationExpandedSettings();
45
65
  this.listenForMenuToggle();
66
+ this.initNativeTheme();
67
+ this.initSystemPreferences();
68
+ this.initPowerMonitor();
69
+ this.initScanProgress();
46
70
 
47
71
  let themeInitialized = false;
48
72
  effect(() => {
@@ -133,6 +157,13 @@ export class PresentationService {
133
157
 
134
158
  readonly selectedGlyph = signal<number | null>(null);
135
159
 
160
+ readonly createRootCollectionRequest = signal(0);
161
+
162
+ requestCreateRootCollection() {
163
+ this.navigationEnabled.set(true);
164
+ this.createRootCollectionRequest.update((v) => v + 1);
165
+ }
166
+
136
167
  readonly navigationExpandedIds = signal<number[]>([]);
137
168
 
138
169
  readonly gridEnabled = signal(true);
@@ -234,6 +265,74 @@ export class PresentationService {
234
265
  this.navigationExpandedIds.set([]);
235
266
  }
236
267
 
268
+ // --- Native Theme ---
269
+
270
+ private async initNativeTheme() {
271
+ const state = await this.messageService.getNativeTheme();
272
+ this.nativeThemeState.set(state);
273
+ this.applyAutoTheme(state);
274
+
275
+ this.messageService.on(ChannelType.IPC_NATIVE_THEME_CHANGED, (_event: any, state: NativeThemeState) => {
276
+ this.nativeThemeState.set(state);
277
+ this.applyAutoTheme(state);
278
+ });
279
+ }
280
+
281
+ private applyAutoTheme(state: NativeThemeState) {
282
+ if (this.autoTheme()) {
283
+ this.theme.set(state.shouldUseDarkColors ? 'midnight' : 'light');
284
+ }
285
+ }
286
+
287
+ setAutoTheme(enabled: boolean) {
288
+ this.autoTheme.set(enabled);
289
+ if (enabled) {
290
+ const state = this.nativeThemeState();
291
+ if (state) {
292
+ this.theme.set(state.shouldUseDarkColors ? 'midnight' : 'light');
293
+ }
294
+ }
295
+ }
296
+
297
+ // --- System Preferences ---
298
+
299
+ private async initSystemPreferences() {
300
+ const prefs = await this.messageService.getSystemPreferences();
301
+ this.systemAccentColor.set(prefs.accentColor);
302
+ this.reduceMotion.set(prefs.reduceMotion);
303
+ }
304
+
305
+ // --- Power Monitor ---
306
+
307
+ private initPowerMonitor() {
308
+ this.messageService.on(ChannelType.IPC_POWER_SUSPEND, () => {
309
+ this.suspended.set(true);
310
+ });
311
+
312
+ this.messageService.on(ChannelType.IPC_POWER_RESUME, () => {
313
+ this.suspended.set(false);
314
+ });
315
+ }
316
+
317
+ // --- Scan Progress (MessageChannelMain) ---
318
+
319
+ private initScanProgress() {
320
+ this.electronService.ipcRenderer.on(ChannelType.IPC_SCAN_PROGRESS_PORT, (event: any) => {
321
+ const port = event.ports[0];
322
+ if (!port) return;
323
+
324
+ port.onmessage = (msgEvent: MessageEvent<ScanProgress>) => {
325
+ this.scanProgress.set(msgEvent.data);
326
+ };
327
+
328
+ port.onclose = () => {
329
+ this.scanProgress.set(null);
330
+ };
331
+
332
+ port.start();
333
+ });
334
+ }
335
+
237
336
  private async loadNavigationExpandedSettings() {
238
337
  const ids = (await this.messageService.get(StorageType.NavigationExpanded, null)) as number[] | null;
239
338
  if (Array.isArray(ids)) {
@@ -1,5 +1,16 @@
1
- <div class="panel">
2
- <div class="flex items-center px-2">
1
+ <div class="panel h-full w-full flex items-center justify-between px-2">
2
+ <div class="flex items-center gap-2">
3
3
  <app-spinner [animating]="presentation.loading()" />
4
+ @if (presentation.scanProgress(); as progress) {
5
+ <span class="text-[10px] truncate" [style.color]="'var(--text-muted)'">
6
+ {{ progress.currentFile }} ({{ progress.processed }}/{{ progress.total }})
7
+ </span>
8
+ }
9
+ </div>
10
+ <div class="flex items-center gap-3">
11
+ <span class="text-[10px]" [style.color]="'var(--text-muted)'">Fontastic v{{ appVersion() }}</span>
12
+ <span class="text-[10px]" [style.color]="'var(--text-muted)'">Electron {{ electronVersion() }}</span>
13
+ <span class="text-[10px]" [style.color]="'var(--text-muted)'">Chrome {{ chromeVersion() }}</span>
14
+ <span class="text-[10px]" [style.color]="'var(--text-muted)'">Node {{ nodeVersion() }}</span>
4
15
  </div>
5
16
  </div>
@@ -1,6 +1,6 @@
1
- import { Component, inject } from '@angular/core';
1
+ import { Component, inject, signal } from '@angular/core';
2
2
  import { SpinnerComponent } from '../../shared/components';
3
- import { PresentationService } from '../../core/services';
3
+ import { ElectronService, PresentationService } from '../../core/services';
4
4
 
5
5
  @Component({
6
6
  selector: 'app-footer',
@@ -10,4 +10,20 @@ import { PresentationService } from '../../core/services';
10
10
  })
11
11
  export class FooterComponent {
12
12
  readonly presentation = inject(PresentationService);
13
+ private readonly electron = inject(ElectronService);
14
+
15
+ readonly appVersion = signal('');
16
+ readonly electronVersion = signal('');
17
+ readonly chromeVersion = signal('');
18
+ readonly nodeVersion = signal('');
19
+
20
+ constructor() {
21
+ if (this.electron.isElectron) {
22
+ const versions = (window as any).process.versions;
23
+ this.electronVersion.set(versions.electron ?? '');
24
+ this.chromeVersion.set(versions.chrome ?? '');
25
+ this.nodeVersion.set(versions.node ?? '');
26
+ this.electron.ipcRenderer.invoke('app:get-version').then((v: string) => this.appVersion.set(v));
27
+ }
28
+ }
13
29
  }
@@ -106,13 +106,3 @@
106
106
  </div>
107
107
  </div>
108
108
  </header>
109
-
110
- @if (showCollectionDialog) {
111
- <app-prompt-dialog
112
- title="New Collection"
113
- placeholder="Collection name"
114
- confirmText="Create"
115
- (confirmed)="onCollectionConfirmed($event)"
116
- (cancelled)="onCollectionCancelled()"
117
- />
118
- }
@@ -1,42 +1,23 @@
1
- import { Component, inject, effect } from '@angular/core';
1
+ import { Component, inject } from '@angular/core';
2
2
  import { Router } from '@angular/router';
3
- import { PromptDialogComponent } from '../../shared/components';
4
- import { DatabaseService, PresentationService } from '../../core/services';
3
+ import { PresentationService } from '../../core/services';
5
4
 
6
5
  @Component({
7
6
  selector: 'app-header',
8
7
  standalone: true,
9
- imports: [PromptDialogComponent],
8
+ imports: [],
10
9
  templateUrl: './header.component.html',
11
10
  })
12
11
  export class HeaderComponent {
13
- readonly db = inject(DatabaseService);
14
12
  private router = inject(Router);
15
13
  readonly presentation = inject(PresentationService);
16
14
 
17
- constructor() {
18
- effect(() => {
19
- console.log('Selected collection ID:', this.db.collectionId());
20
- });
21
- }
22
-
23
15
  currentUser: { name: string } | null = null;
24
16
  gravatarUrl = '';
25
17
 
26
- showCollectionDialog = false;
27
-
28
18
  handleCreateCollection(event: Event) {
29
19
  event.stopPropagation();
30
- this.showCollectionDialog = true;
31
- }
32
-
33
- onCollectionConfirmed(name: string) {
34
- this.db.collectionCreate({ title: name });
35
- this.showCollectionDialog = false;
36
- }
37
-
38
- onCollectionCancelled() {
39
- this.showCollectionDialog = false;
20
+ this.presentation.requestCreateRootCollection();
40
21
  }
41
22
 
42
23
  handleToggleSearch(_event: Event) {
@@ -16,6 +16,7 @@
16
16
  'wght' 300;
17
17
  "
18
18
  title="New Smart Collection"
19
+ appStopPropagation
19
20
  (click)="openCreateSmartCollection()"
20
21
  >add</span
21
22
  >
@@ -24,13 +25,12 @@
24
25
  <li>
25
26
  <a
26
27
  class="flex items-center px-3 py-1 text-xs font-normal cursor-pointer transition-colors"
27
- [style.background-color]="isSmartSelected(sc) ? 'var(--selected-bg)' : 'transparent'"
28
- [style.color]="isSmartSelected(sc) ? 'var(--text-primary)' : 'inherit'"
28
+ [appHoverHighlight]="isSmartSelected(sc)"
29
+ selectedColor="var(--text-primary)"
30
+ normalColor="inherit"
29
31
  (click)="selectSmartCollection(sc)"
30
32
  (dblclick)="editSmartCollection(sc)"
31
33
  (contextmenu)="onSmartContextMenu($event, sc)"
32
- (mouseenter)="$any($event.target).style.backgroundColor = isSmartSelected(sc) ? 'var(--selected-bg)' : 'var(--hover-bg)'"
33
- (mouseleave)="$any($event.target).style.backgroundColor = isSmartSelected(sc) ? 'var(--selected-bg)' : 'transparent'"
34
34
  >
35
35
  <span
36
36
  class="material-symbols-outlined icon-sm mr-1"
@@ -64,10 +64,40 @@
64
64
  'wght' 300;
65
65
  "
66
66
  title="New Collection"
67
+ appStopPropagation
67
68
  (click)="openCreateRootCollection()"
68
69
  >add</span
69
70
  >
70
71
  <ul class="flex-1 overflow-auto flex flex-col py-0.5">
72
+ @if (isCreating && pendingParentId === 0) {
73
+ <li>
74
+ <a class="flex items-center pr-3 py-1 text-xs font-normal" style="padding-left: 14px">
75
+ <span
76
+ class="material-symbols-outlined icon-sm mr-1"
77
+ [style.color]="'var(--text-muted)'"
78
+ style="
79
+ font-variation-settings:
80
+ 'opsz' 20,
81
+ 'wght' 300;
82
+ "
83
+ >chevron_right</span
84
+ >
85
+ <input
86
+ class="border rounded px-1 w-full outline-none text-xs"
87
+ [style.background-color]="'var(--input-bg)'"
88
+ [style.border-color]="'var(--input-border)'"
89
+ [style.color]="'var(--text-primary)'"
90
+ [(ngModel)]="creatingTitle"
91
+ (blur)="saveCreating()"
92
+ (keydown.enter)="saveCreating()"
93
+ (keydown.escape)="cancelCreating()"
94
+ appStopPropagation
95
+ placeholder="Collection name"
96
+ appAutofocus
97
+ />
98
+ </a>
99
+ </li>
100
+ }
71
101
  <ng-container *ngTemplateOutlet="subtree; context: { $implicit: tree(), level: 0 }" />
72
102
  <li
73
103
  class="flex-1 min-h-6"
@@ -135,15 +165,44 @@
135
165
  (blur)="saveEditing(node.collection.id)"
136
166
  (keydown.enter)="saveEditing(node.collection.id)"
137
167
  (keydown.escape)="cancelEditing()"
138
- (click)="$event.stopPropagation()"
168
+ appStopPropagation
139
169
  appAutofocus
140
170
  />
141
171
  } @else {
142
172
  {{ node.collection.title }}
143
173
  }
144
174
  </a>
145
- @if (node.children.length && isExpanded(node.collection.id)) {
175
+ @if ((node.children.length && isExpanded(node.collection.id)) || (isCreating && pendingParentId === node.collection.id)) {
146
176
  <ul>
177
+ @if (isCreating && pendingParentId === node.collection.id) {
178
+ <li>
179
+ <a class="flex items-center pr-3 py-1 text-xs font-normal" [style.padding-left.px]="14 + (level + 1) * 14">
180
+ <span
181
+ class="material-symbols-outlined icon-sm mr-1"
182
+ [style.color]="'var(--text-muted)'"
183
+ style="
184
+ font-variation-settings:
185
+ 'opsz' 20,
186
+ 'wght' 300;
187
+ "
188
+ >chevron_right</span
189
+ >
190
+ <input
191
+ class="border rounded px-1 w-full outline-none text-xs"
192
+ [style.background-color]="'var(--input-bg)'"
193
+ [style.border-color]="'var(--input-border)'"
194
+ [style.color]="'var(--text-primary)'"
195
+ [(ngModel)]="creatingTitle"
196
+ (blur)="saveCreating()"
197
+ (keydown.enter)="saveCreating()"
198
+ (keydown.escape)="cancelCreating()"
199
+ appStopPropagation
200
+ placeholder="Collection name"
201
+ appAutofocus
202
+ />
203
+ </a>
204
+ </li>
205
+ }
147
206
  <ng-container *ngTemplateOutlet="subtree; context: { $implicit: node.children, level: level + 1 }" />
148
207
  </ul>
149
208
  }
@@ -161,16 +220,6 @@
161
220
  />
162
221
  }
163
222
 
164
- @if (showCollectionDialog) {
165
- <app-prompt-dialog
166
- title="New Collection"
167
- placeholder="Collection name"
168
- confirmText="Create"
169
- (confirmed)="onCollectionConfirmed($event)"
170
- (cancelled)="onCollectionCancelled()"
171
- />
172
- }
173
-
174
223
  @if (smartContextMenu) {
175
224
  <app-context-menu
176
225
  [x]="smartContextMenu.x"
@@ -185,6 +234,7 @@
185
234
  <app-rule-builder
186
235
  [smartCollection]="editingSmartCollection"
187
236
  (saved)="onRuleBuilderSaved($event)"
237
+ (savedAndSync)="onRuleBuilderSavedAndSync($event)"
188
238
  (cancelled)="onRuleBuilderCancelled()"
189
239
  />
190
240
  }