fontastic 1.0.0 → 1.0.2

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 (55) hide show
  1. package/.github/workflows/macos.yml +0 -7
  2. package/.github/workflows/ubuntu.yml +0 -7
  3. package/.github/workflows/windows.yml +0 -7
  4. package/README.md +1 -0
  5. package/app/core/ConnectionManager.js +7 -1
  6. package/app/core/ConnectionManager.ts +11 -3
  7. package/app/core/MessageHandler.js +24 -0
  8. package/app/core/MessageHandler.ts +29 -0
  9. package/app/core/menu/templates/DarwinTemplate.js +16 -1
  10. package/app/core/menu/templates/DarwinTemplate.ts +16 -1
  11. package/app/core/menu/templates/SystemTemplate.js +16 -1
  12. package/app/core/menu/templates/SystemTemplate.ts +16 -1
  13. package/app/database/entity/SmartCollection.schema.js +66 -0
  14. package/app/database/entity/SmartCollection.schema.ts +39 -0
  15. package/app/database/entity/index.js +1 -0
  16. package/app/database/entity/index.ts +2 -1
  17. package/app/database/repository/SmartCollection.repository.js +47 -0
  18. package/app/database/repository/SmartCollection.repository.ts +30 -0
  19. package/app/database/repository/Store.repository.js +107 -0
  20. package/app/database/repository/Store.repository.ts +106 -0
  21. package/app/database/repository/index.js +1 -0
  22. package/app/database/repository/index.ts +2 -1
  23. package/app/enums/ChannelType.js +5 -0
  24. package/app/enums/ChannelType.ts +6 -0
  25. package/app/enums/StorageType.js +1 -0
  26. package/app/enums/StorageType.ts +1 -0
  27. package/app/package.json +1 -1
  28. package/app/types/FontMetrics.js +3 -0
  29. package/app/types/SmartCollection.js +3 -0
  30. package/app/types/SmartCollection.ts +5 -0
  31. package/app/types/SystemConfig.ts +2 -1
  32. package/app/types/index.js +2 -0
  33. package/app/types/index.ts +1 -0
  34. package/package.json +1 -1
  35. package/src/app/core/services/database/database.service.ts +70 -1
  36. package/src/app/core/services/message/message.service.ts +23 -0
  37. package/src/app/core/services/presentation/presentation.service.ts +62 -9
  38. package/src/app/layout/header/header.component.html +5 -5
  39. package/src/app/layout/layout.component.html +1 -1
  40. package/src/app/layout/main/main.component.html +3 -3
  41. package/src/app/layout/main/main.component.ts +2 -2
  42. package/src/app/layout/navigation/navigation.component.html +70 -4
  43. package/src/app/layout/navigation/navigation.component.ts +131 -27
  44. package/src/app/shared/components/context-menu/context-menu.component.ts +70 -21
  45. package/src/app/shared/components/datagrid/datagrid.component.html +82 -2
  46. package/src/app/shared/components/{inspector/inspector.component.html → glyphs/glyphs.component.html} +1 -1
  47. package/src/app/shared/components/glyphs/glyphs.component.ts +60 -0
  48. package/src/app/shared/components/index.ts +2 -1
  49. package/src/app/shared/components/rule-builder/rule-builder.component.html +94 -0
  50. package/src/app/shared/components/rule-builder/rule-builder.component.ts +136 -0
  51. package/src/app/shared/components/toolbar/toolbar.component.html +0 -81
  52. package/src/app/shared/components/toolbar/toolbar.component.ts +1 -2
  53. package/src/styles/base/variables.css +16 -0
  54. package/src/styles/components/spinner.css +4 -3
  55. package/src/app/shared/components/inspector/inspector.component.ts +0 -41
@@ -4,10 +4,58 @@
4
4
  <app-library />
5
5
  </app-collapsible-panel>
6
6
 
7
+ <app-collapsible-panel title="Smart Collections">
8
+ <span
9
+ panelActions
10
+ class="material-symbols-outlined cursor-pointer"
11
+ [style.color]="'var(--text-muted)'"
12
+ style="
13
+ font-size: 18px;
14
+ font-variation-settings:
15
+ 'opsz' 20,
16
+ 'wght' 300;
17
+ "
18
+ title="New Smart Collection"
19
+ (click)="openCreateSmartCollection()"
20
+ >add</span
21
+ >
22
+ <ul class="flex flex-col py-0.5">
23
+ @for (sc of db.smartCollections(); track sc.id) {
24
+ <li>
25
+ <a
26
+ 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'"
29
+ (click)="selectSmartCollection(sc)"
30
+ (dblclick)="editSmartCollection(sc)"
31
+ (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
+ >
35
+ <span
36
+ class="material-symbols-outlined icon-sm mr-1"
37
+ [style.color]="'var(--text-muted)'"
38
+ style="
39
+ font-size: 16px;
40
+ font-variation-settings:
41
+ 'opsz' 20,
42
+ 'wght' 300;
43
+ "
44
+ >auto_awesome</span
45
+ >
46
+ {{ sc.title }}
47
+ </a>
48
+ </li>
49
+ } @empty {
50
+ <li class="px-3 py-1 text-xs" [style.color]="'var(--text-muted)'">No smart collections</li>
51
+ }
52
+ </ul>
53
+ </app-collapsible-panel>
54
+
7
55
  <app-collapsible-panel title="Explorer" class="flex-1 min-h-0">
8
56
  <span
9
57
  panelActions
10
- class="material-symbols-outlined cursor-pointer transition-transform duration-200"
58
+ class="material-symbols-outlined cursor-pointer"
11
59
  [style.color]="'var(--text-muted)'"
12
60
  style="
13
61
  font-size: 18px;
@@ -15,9 +63,9 @@
15
63
  'opsz' 20,
16
64
  'wght' 300;
17
65
  "
18
- [title]="allExpanded ? 'Collapse All' : 'Expand All'"
19
- (click)="toggleExpandAll($event)"
20
- >{{ allExpanded ? 'unfold_less' : 'unfold_more' }}</span
66
+ title="New Collection"
67
+ (click)="openCreateRootCollection()"
68
+ >add</span
21
69
  >
22
70
  <ul class="flex-1 overflow-auto flex flex-col py-0.5">
23
71
  <ng-container *ngTemplateOutlet="subtree; context: { $implicit: tree(), level: 0 }" />
@@ -122,5 +170,23 @@
122
170
  (cancelled)="onCollectionCancelled()"
123
171
  />
124
172
  }
173
+
174
+ @if (smartContextMenu) {
175
+ <app-context-menu
176
+ [x]="smartContextMenu.x"
177
+ [y]="smartContextMenu.y"
178
+ [items]="smartContextMenuItems"
179
+ (menuSelect)="onSmartContextMenuSelect($event)"
180
+ (menuClose)="closeSmartContextMenu()"
181
+ />
182
+ }
183
+
184
+ @if (showRuleBuilder) {
185
+ <app-rule-builder
186
+ [smartCollection]="editingSmartCollection"
187
+ (saved)="onRuleBuilderSaved($event)"
188
+ (cancelled)="onRuleBuilderCancelled()"
189
+ />
190
+ }
125
191
  </div>
126
192
  </nav>
@@ -1,14 +1,17 @@
1
- import { Component, inject, computed } from '@angular/core';
1
+ import { Component, inject, computed, OnInit, OnDestroy } from '@angular/core';
2
2
  import { CommonModule } from '@angular/common';
3
3
  import { FormsModule } from '@angular/forms';
4
4
  import { DatabaseService, MessageService, PresentationService } from '../../core/services';
5
5
  import { CollapsiblePanelComponent } from '../../shared/components/collapsible-panel/collapsible-panel.component';
6
6
  import { ContextMenuComponent, ContextMenuItem } from '../../shared/components/context-menu/context-menu.component';
7
- import { PromptDialogComponent } from '../../shared/components';
7
+ import { PromptDialogComponent, RuleBuilderComponent } from '../../shared/components';
8
8
  import { AutofocusDirective } from '../../shared/directives/autofocus/autofocus.directive';
9
9
  import { LibraryComponent } from './library/library.component';
10
10
  import { NewsStatsComponent } from './stats/stats.component';
11
11
  import type { Collection } from '@main/database/entity/Collection.schema';
12
+ import type { SmartCollection } from '@main/database/entity/SmartCollection.schema';
13
+ import type { SmartCollectionRule } from '@main/types';
14
+ import { ChannelType } from '@main/enums';
12
15
 
13
16
  export interface TreeNode {
14
17
  collection: Collection;
@@ -24,17 +27,34 @@ export interface TreeNode {
24
27
  CollapsiblePanelComponent,
25
28
  ContextMenuComponent,
26
29
  PromptDialogComponent,
30
+ RuleBuilderComponent,
27
31
  AutofocusDirective,
28
32
  LibraryComponent,
29
33
  NewsStatsComponent,
30
34
  ],
31
35
  templateUrl: './navigation.component.html',
32
36
  })
33
- export class NavigationComponent {
37
+ export class NavigationComponent implements OnInit, OnDestroy {
34
38
  readonly db = inject(DatabaseService);
35
39
  private message = inject(MessageService);
36
40
  private presentation = inject(PresentationService);
37
41
 
42
+ private menuToggleListener = (_event: any, panel: string) => {
43
+ if (panel === 'expand-collections') {
44
+ this.expandAll();
45
+ } else if (panel === 'collapse-collections') {
46
+ this.collapseAll();
47
+ }
48
+ };
49
+
50
+ ngOnInit() {
51
+ this.message.on(ChannelType.IPC_TOGGLE_PANEL, this.menuToggleListener);
52
+ }
53
+
54
+ ngOnDestroy() {
55
+ this.message.removeListener(ChannelType.IPC_TOGGLE_PANEL, this.menuToggleListener);
56
+ }
57
+
38
58
  readonly tree = computed<TreeNode[]>(() => {
39
59
  const all = this.db.collections();
40
60
  const buildChildren = (parentId: number): TreeNode[] =>
@@ -50,12 +70,17 @@ export class NavigationComponent {
50
70
  contextMenu: { x: number; y: number; collection: Collection } | null = null;
51
71
  editingId: number | null = null;
52
72
  editingTitle = '';
53
- expandedIds = new Set<number>();
54
73
  allExpanded = false;
55
74
 
56
75
  showCollectionDialog = false;
57
76
  pendingParentId: number | null = null;
58
77
 
78
+ // Smart Collection state
79
+ showRuleBuilder = false;
80
+ editingSmartCollection: SmartCollection | null = null;
81
+ smartContextMenu: { x: number; y: number; smartCollection: SmartCollection } | null = null;
82
+ smartContextMenuItems: ContextMenuItem[] = [];
83
+
59
84
  // Drag state
60
85
  draggedNode: TreeNode | null = null;
61
86
  dropTargetId: number | null = null;
@@ -187,7 +212,7 @@ export class NavigationComponent {
187
212
  this.db.collections.set(updated);
188
213
 
189
214
  if (newParentId !== 0) {
190
- this.expandedIds.add(newParentId);
215
+ this.presentation.expandNavigationId(newParentId);
191
216
  }
192
217
 
193
218
  try {
@@ -216,8 +241,8 @@ export class NavigationComponent {
216
241
  this.contextMenuItems = [
217
242
  { label: 'Add Collection', action: 'add-collection', icon: 'create_new_folder' },
218
243
  { label: 'Add Fonts', action: 'add-fonts', icon: 'list_alt' },
219
- { label: 'Rename', action: 'rename' },
220
- { label: 'Delete', action: 'delete' },
244
+ { label: 'Rename', action: 'rename', icon: 'edit', separator: true },
245
+ { label: 'Delete', action: 'delete', icon: 'delete', separator: true },
221
246
  ];
222
247
 
223
248
  this.contextMenu = { x: event.clientX, y: event.clientY, collection };
@@ -246,6 +271,11 @@ export class NavigationComponent {
246
271
  }
247
272
  }
248
273
 
274
+ openCreateRootCollection() {
275
+ this.pendingParentId = 0;
276
+ this.showCollectionDialog = true;
277
+ }
278
+
249
279
  onCollectionConfirmed(name: string) {
250
280
  this.db.collectionCreate({ title: name, parentId: this.pendingParentId });
251
281
  this.showCollectionDialog = false;
@@ -302,32 +332,30 @@ export class NavigationComponent {
302
332
  }
303
333
 
304
334
  isExpanded(id: number): boolean {
305
- return this.expandedIds.has(id);
335
+ return this.presentation.isNavigationExpanded(id);
306
336
  }
307
337
 
308
338
  toggleExpand(event: Event, id: number) {
309
339
  event.stopPropagation();
310
- if (this.expandedIds.has(id)) {
311
- this.expandedIds.delete(id);
312
- } else {
313
- this.expandedIds.add(id);
314
- }
340
+ this.presentation.toggleNavigationExpanded(id);
315
341
  }
316
342
 
317
- toggleExpandAll(event: Event) {
318
- event.stopPropagation();
319
- if (this.allExpanded) {
320
- this.expandedIds.clear();
321
- } else {
322
- const collectIds = (nodes: TreeNode[]) => {
323
- for (const node of nodes) {
324
- this.expandedIds.add(node.collection.id);
325
- collectIds(node.children);
326
- }
327
- };
328
- collectIds(this.tree());
329
- }
330
- this.allExpanded = !this.allExpanded;
343
+ expandAll() {
344
+ const allIds: number[] = [];
345
+ const collectIds = (nodes: TreeNode[]) => {
346
+ for (const node of nodes) {
347
+ allIds.push(node.collection.id);
348
+ collectIds(node.children);
349
+ }
350
+ };
351
+ collectIds(this.tree());
352
+ this.presentation.setAllNavigationExpanded(allIds);
353
+ this.allExpanded = true;
354
+ }
355
+
356
+ collapseAll() {
357
+ this.presentation.clearAllNavigationExpanded();
358
+ this.allExpanded = false;
331
359
  }
332
360
 
333
361
  isSelected(collection: Collection): boolean {
@@ -359,6 +387,82 @@ export class NavigationComponent {
359
387
  this.editingTitle = '';
360
388
  }
361
389
 
390
+ // Smart Collection methods
391
+
392
+ selectSmartCollection(sc: SmartCollection) {
393
+ this.db.selectSmartCollection(sc.id);
394
+ }
395
+
396
+ isSmartSelected(sc: SmartCollection): boolean {
397
+ return this.db.activeSmartCollectionId() === sc.id;
398
+ }
399
+
400
+ openCreateSmartCollection() {
401
+ this.editingSmartCollection = null;
402
+ this.showRuleBuilder = true;
403
+ }
404
+
405
+ editSmartCollection(sc: SmartCollection) {
406
+ this.editingSmartCollection = sc;
407
+ this.showRuleBuilder = true;
408
+ }
409
+
410
+ onSmartContextMenu(event: MouseEvent, sc: SmartCollection) {
411
+ event.preventDefault();
412
+ event.stopPropagation();
413
+
414
+ this.smartContextMenuItems = [
415
+ { label: 'Edit Rules', action: 'edit-rules', icon: 'tune' },
416
+ { label: 'Delete', action: 'delete', icon: 'delete', separator: true },
417
+ ];
418
+
419
+ this.smartContextMenu = { x: event.clientX, y: event.clientY, smartCollection: sc };
420
+ }
421
+
422
+ onSmartContextMenuSelect(action: string) {
423
+ if (!this.smartContextMenu) return;
424
+ const sc = this.smartContextMenu.smartCollection;
425
+ this.smartContextMenu = null;
426
+
427
+ switch (action) {
428
+ case 'edit-rules':
429
+ this.editingSmartCollection = sc;
430
+ this.showRuleBuilder = true;
431
+ break;
432
+ case 'delete':
433
+ this.db.smartCollectionDelete(sc.id);
434
+ break;
435
+ }
436
+ }
437
+
438
+ closeSmartContextMenu() {
439
+ this.smartContextMenu = null;
440
+ }
441
+
442
+ onRuleBuilderSaved(data: { title: string; rules: SmartCollectionRule[]; match_type: string }) {
443
+ const rulesJson = JSON.stringify(data.rules);
444
+ if (this.editingSmartCollection) {
445
+ this.db.smartCollectionUpdate(this.editingSmartCollection.id, {
446
+ title: data.title,
447
+ rules: rulesJson,
448
+ match_type: data.match_type,
449
+ });
450
+ } else {
451
+ this.db.smartCollectionCreate({
452
+ title: data.title,
453
+ rules: rulesJson,
454
+ match_type: data.match_type,
455
+ });
456
+ }
457
+ this.showRuleBuilder = false;
458
+ this.editingSmartCollection = null;
459
+ }
460
+
461
+ onRuleBuilderCancelled() {
462
+ this.showRuleBuilder = false;
463
+ this.editingSmartCollection = null;
464
+ }
465
+
362
466
  findRootParentId(collection: Collection): number {
363
467
  const all = this.db.collections();
364
468
  let current = collection;
@@ -4,38 +4,87 @@ export interface ContextMenuItem {
4
4
  label: string;
5
5
  action: string;
6
6
  icon?: string;
7
+ separator?: boolean;
7
8
  }
8
9
 
9
10
  @Component({
10
11
  selector: 'app-context-menu',
11
12
  standalone: true,
12
13
  template: `
13
- <div
14
- class="fixed z-50 min-w-[160px] rounded-lg py-1"
15
- [style.left.px]="x()"
16
- [style.top.px]="y()"
17
- [style.background-color]="'var(--context-bg)'"
18
- [style.border]="'1px solid var(--context-border)'"
19
- [style.box-shadow]="'var(--context-shadow)'"
20
- [style.color]="'var(--text-primary)'"
21
- >
14
+ <div class="context-menu fixed z-50" [style.left.px]="x()" [style.top.px]="y()">
22
15
  @for (item of items(); track item.action) {
23
- <button
24
- class="w-full text-left px-4 py-1.5 text-xs cursor-pointer transition-colors"
25
- [style.color]="'var(--text-secondary)'"
26
- (mouseenter)="
27
- $any($event.target).style.backgroundColor = 'var(--hover-bg)'; $any($event.target).style.color = 'var(--text-primary)'
28
- "
29
- (mouseleave)="
30
- $any($event.target).style.backgroundColor = 'transparent'; $any($event.target).style.color = 'var(--text-secondary)'
31
- "
32
- (click)="menuSelect.emit(item.action)"
33
- >
34
- {{ item.label }}
16
+ @if (item.separator) {
17
+ <div class="context-menu-separator"></div>
18
+ }
19
+ <button class="context-menu-item" (click)="menuSelect.emit(item.action)">
20
+ <span class="context-menu-icon">
21
+ @if (item.icon) {
22
+ <span class="material-symbols-outlined" style="font-size: 16px; font-variation-settings: 'opsz' 20, 'wght' 300;">{{
23
+ item.icon
24
+ }}</span>
25
+ }
26
+ </span>
27
+ <span class="context-menu-label">{{ item.label }}</span>
35
28
  </button>
36
29
  }
37
30
  </div>
38
31
  `,
32
+ styles: `
33
+ .context-menu {
34
+ min-width: 180px;
35
+ padding: 4px 0;
36
+ border-radius: 6px;
37
+ background-color: var(--context-bg);
38
+ border: 1px solid var(--context-border);
39
+ box-shadow: var(--context-shadow);
40
+ color: var(--text-primary);
41
+ }
42
+
43
+ .context-menu-item {
44
+ display: flex;
45
+ align-items: center;
46
+ width: 100%;
47
+ padding: 4px 12px 4px 4px;
48
+ margin: 0;
49
+ border: none;
50
+ background: transparent;
51
+ color: var(--text-secondary);
52
+ font-size: 12px;
53
+ line-height: 1.4;
54
+ cursor: pointer;
55
+ border-radius: 0;
56
+ text-align: left;
57
+ gap: 0;
58
+ }
59
+
60
+ .context-menu-item:hover {
61
+ background-color: var(--hover-bg);
62
+ color: var(--text-primary);
63
+ }
64
+
65
+ .context-menu-icon {
66
+ display: flex;
67
+ align-items: center;
68
+ justify-content: center;
69
+ width: 28px;
70
+ flex-shrink: 0;
71
+ color: var(--text-muted);
72
+ }
73
+
74
+ .context-menu-item:hover .context-menu-icon {
75
+ color: var(--text-primary);
76
+ }
77
+
78
+ .context-menu-label {
79
+ flex: 1;
80
+ }
81
+
82
+ .context-menu-separator {
83
+ height: 1px;
84
+ margin: 4px 0;
85
+ background-color: var(--context-border);
86
+ }
87
+ `,
39
88
  })
40
89
  export class ContextMenuComponent {
41
90
  readonly x = input.required<number>();
@@ -1,5 +1,5 @@
1
- <div class="panel">
2
- <div class="h-full scrollbox-y">
1
+ <div class="panel grid" [style.grid-template-rows]="db.totalPages() > 1 ? '1fr 32px' : '1fr'">
2
+ <div class="overflow-hidden scrollbox-y">
3
3
  <table class="w-full border-separate border-spacing-0">
4
4
  <thead
5
5
  class="sticky top-0 z-10"
@@ -93,4 +93,84 @@
93
93
  </tbody>
94
94
  </table>
95
95
  </div>
96
+ @if (db.totalPages() > 1) {
97
+ <div
98
+ class="flex items-center gap-2 px-3"
99
+ [style.background-color]="'var(--surface-2)'"
100
+ [style.border-top]="'1px solid var(--border-subtle)'"
101
+ >
102
+ <div class="flex items-center gap-0.5 text-[11px]">
103
+ <button
104
+ class="px-1 py-0.5 rounded cursor-pointer disabled:cursor-default transition-colors"
105
+ [style.color]="'var(--text-secondary)'"
106
+ [disabled]="db.currentPage() === 1"
107
+ [class.opacity-30]="db.currentPage() === 1"
108
+ (click)="db.firstPage()"
109
+ >
110
+ <span
111
+ class="material-symbols-outlined text-sm"
112
+ style="
113
+ font-variation-settings:
114
+ 'opsz' 20,
115
+ 'wght' 300;
116
+ "
117
+ >first_page</span
118
+ >
119
+ </button>
120
+ <button
121
+ class="px-1 py-0.5 rounded cursor-pointer disabled:cursor-default transition-colors"
122
+ [style.color]="'var(--text-secondary)'"
123
+ [disabled]="db.currentPage() === 1"
124
+ [class.opacity-30]="db.currentPage() === 1"
125
+ (click)="db.prevPage()"
126
+ >
127
+ <span
128
+ class="material-symbols-outlined text-sm"
129
+ style="
130
+ font-variation-settings:
131
+ 'opsz' 20,
132
+ 'wght' 300;
133
+ "
134
+ >chevron_left</span
135
+ >
136
+ </button>
137
+ <span class="px-1.5 select-none" [style.color]="'var(--text-muted)'">{{ db.currentPage() }} / {{ db.totalPages() }}</span>
138
+ <button
139
+ class="px-1 py-0.5 rounded cursor-pointer disabled:cursor-default transition-colors"
140
+ [style.color]="'var(--text-secondary)'"
141
+ [disabled]="db.currentPage() === db.totalPages()"
142
+ [class.opacity-30]="db.currentPage() === db.totalPages()"
143
+ (click)="db.nextPage()"
144
+ >
145
+ <span
146
+ class="material-symbols-outlined text-sm"
147
+ style="
148
+ font-variation-settings:
149
+ 'opsz' 20,
150
+ 'wght' 300;
151
+ "
152
+ >chevron_right</span
153
+ >
154
+ </button>
155
+ <button
156
+ class="px-1 py-0.5 rounded cursor-pointer disabled:cursor-default transition-colors"
157
+ [style.color]="'var(--text-secondary)'"
158
+ [disabled]="db.currentPage() === db.totalPages()"
159
+ [class.opacity-30]="db.currentPage() === db.totalPages()"
160
+ (click)="db.lastPage()"
161
+ >
162
+ <span
163
+ class="material-symbols-outlined text-sm"
164
+ style="
165
+ font-variation-settings:
166
+ 'opsz' 20,
167
+ 'wght' 300;
168
+ "
169
+ >last_page</span
170
+ >
171
+ </button>
172
+ </div>
173
+ <span class="text-[11px]" [style.color]="'var(--text-muted)'"> {{ db.storeCount() }} fonts </span>
174
+ </div>
175
+ }
96
176
  </div>
@@ -133,7 +133,7 @@
133
133
  }
134
134
  </div>
135
135
  } @else {
136
- <p class="text-center py-8 text-xs" [style.color]="'var(--text-muted)'">Select a font to inspect glyphs</p>
136
+ <p class="text-center py-8 text-xs" [style.color]="'var(--text-muted)'">Select a font to view glyphs</p>
137
137
  }
138
138
  </div>
139
139
  </div>
@@ -0,0 +1,60 @@
1
+ import { Component, inject, signal, computed, effect } from '@angular/core';
2
+ import { DatabaseService, PresentationService } from '../../../core/services';
3
+
4
+ const PAGE_SIZE = 200;
5
+
6
+ @Component({
7
+ selector: 'app-glyphs',
8
+ standalone: true,
9
+ templateUrl: './glyphs.component.html',
10
+ })
11
+ export class GlyphsComponent {
12
+ readonly db = inject(DatabaseService);
13
+ private readonly presentation = inject(PresentationService);
14
+
15
+ readonly currentPage = signal(1);
16
+ readonly selectedGlyph = this.presentation.selectedGlyph;
17
+
18
+ readonly totalPages = computed(() => Math.max(1, Math.ceil(this.db.glyphs().length / PAGE_SIZE)));
19
+
20
+ readonly pageGlyphs = computed(() => {
21
+ const glyphs = this.db.glyphs();
22
+ const start = (this.currentPage() - 1) * PAGE_SIZE;
23
+ return glyphs.slice(start, start + PAGE_SIZE);
24
+ });
25
+
26
+ readonly fontFamily = computed(() => {
27
+ const store = this.db.store();
28
+ return store ? store.full_name || store.font_family || '' : '';
29
+ });
30
+
31
+ constructor() {
32
+ // Reset selection when user switches to a different font.
33
+ // Skip the null → savedId transition on startup (restore, not a switch).
34
+ let lastStoreId: number | null = null;
35
+ effect(() => {
36
+ const storeId = this.db.storeId();
37
+ if (lastStoreId !== null && storeId !== lastStoreId) {
38
+ this.currentPage.set(1);
39
+ this.presentation.selectedGlyph.set(null);
40
+ }
41
+ lastStoreId = storeId;
42
+ });
43
+
44
+ // Navigate to the correct page for the restored/selected glyph.
45
+ effect(() => {
46
+ const glyphs = this.db.glyphs();
47
+ const selected = this.presentation.selectedGlyph();
48
+ if (selected !== null && glyphs.length) {
49
+ const index = glyphs.indexOf(selected);
50
+ if (index >= 0) {
51
+ this.currentPage.set(Math.floor(index / PAGE_SIZE) + 1);
52
+ }
53
+ }
54
+ });
55
+ }
56
+
57
+ toChar(codePoint: number): string {
58
+ return String.fromCodePoint(codePoint);
59
+ }
60
+ }
@@ -4,8 +4,9 @@ export * from './page-not-found/page-not-found.component';
4
4
  export * from './panel/panel.component';
5
5
  export * from './datagrid/datagrid.component';
6
6
  export * from './prompt-dialog/prompt-dialog.component';
7
+ export * from './rule-builder/rule-builder.component';
7
8
  export * from './toolbar/toolbar.component';
8
- export * from './inspector/inspector.component';
9
+ export * from './glyphs/glyphs.component';
9
10
  export * from './search/search.component';
10
11
  export * from './spinner/spinner.component';
11
12
  export * from './waterfall/waterfall.component';