fontastic 1.0.1 → 1.1.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.
package/README.md CHANGED
@@ -1,18 +1,19 @@
1
1
  # Fontastic
2
2
 
3
- ![Maintained][maintained-badge]
4
- [![Make a pull request][prs-badge]][prs]
5
- [![License][license-badge]](LICENSE.md)
3
+ [![Angular](https://img.shields.io/badge/Angular-21-dd0031?style=plastic&logo=angular&logoColor=white)](https://angular.dev)
4
+ [![Electron](https://img.shields.io/badge/Electron-40-47848f?style=plastic&logo=electron&logoColor=white)](https://electronjs.org)
5
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.9-3178c6?style=plastic&logo=typescript&logoColor=white)](https://www.typescriptlang.org)
6
+ [![License](https://img.shields.io/badge/License-MIT-f59e0b?style=plastic)](LICENSE.md)
7
+ [![PRs Welcome](https://img.shields.io/badge/PRs-Welcome-22c55e?style=plastic)](http://makeapullrequest.com)
6
8
 
7
9
  [![Linux Build][linux-build-badge]][linux-build]
8
10
  [![MacOS Build][macos-build-badge]][macos-build]
9
11
  [![Windows Build][windows-build-badge]][windows-build]
10
12
 
11
- [![Watch on GitHub][github-watch-badge]][github-watch]
12
- [![Star on GitHub][github-star-badge]][github-star]
13
- [![Tweet][twitter-badge]][twitter]
13
+ [![GitHub Stars](https://img.shields.io/github/stars/tomshaw/fontastic?style=plastic&logo=github&label=Stars)](https://github.com/tomshaw/fontastic/stargazers)
14
+ [![GitHub Watchers](https://img.shields.io/github/watchers/tomshaw/fontastic?style=plastic&logo=github&label=Watchers)](https://github.com/tomshaw/fontastic/watchers)
14
15
 
15
- Fontastic is a cross-platform font management and cataloging application built with Angular and Electron.
16
+ Fontastic is an Electron-based font management and cataloging application built for organizing, browsing, and inspecting font libraries.
16
17
 
17
18
  ## Features
18
19
 
@@ -83,12 +84,6 @@ Fontastic is open-sourced software licensed under the [MIT license](https://open
83
84
 
84
85
  [repo]: https://github.com/tomshaw/fontastic
85
86
 
86
- [maintained-badge]: https://img.shields.io/badge/maintained-yes-brightgreen
87
- [license-badge]: https://img.shields.io/badge/license-MIT-blue.svg
88
- [license]: https://github.com/tomshaw/fontastic/blob/main/LICENSE.md
89
- [prs-badge]: https://img.shields.io/badge/PRs-welcome-red.svg
90
- [prs]: http://makeapullrequest.com
91
-
92
87
  [linux-build-badge]: https://github.com/tomshaw/fontastic/workflows/Linux%20Build/badge.svg
93
88
  [linux-build]: https://github.com/tomshaw/fontastic/actions?query=workflow%3A%22Linux+Build%22
94
89
  [macos-build-badge]: https://github.com/tomshaw/fontastic/workflows/MacOS%20Build/badge.svg
@@ -96,9 +91,3 @@ Fontastic is open-sourced software licensed under the [MIT license](https://open
96
91
  [windows-build-badge]: https://github.com/tomshaw/fontastic/workflows/Windows%20Build/badge.svg
97
92
  [windows-build]: https://github.com/tomshaw/fontastic/actions?query=workflow%3A%22Windows+Build%22
98
93
 
99
- [github-watch-badge]: https://img.shields.io/github/watchers/tomshaw/fontastic.svg?style=social
100
- [github-watch]: https://github.com/tomshaw/fontastic/watchers
101
- [github-star-badge]: https://img.shields.io/github/stars/tomshaw/fontastic.svg?style=social
102
- [github-star]: https://github.com/tomshaw/fontastic/stargazers
103
- [twitter]: https://twitter.com/intent/tweet?text=Check%20out%20fontastic!%20https://github.com/tomshaw/fontastic%20%F0%9F%91%8D
104
- [twitter-badge]: https://img.shields.io/twitter/url/https/github.com/tomshaw/fontastic.svg?style=social
@@ -122,7 +122,7 @@ class DarwinTemplate {
122
122
  { label: 'Toggle Navigation', panel: 'navigation', accelerator: 'Alt+Command+1' },
123
123
  { label: 'Toggle Aside', panel: 'aside', accelerator: 'Alt+Command+2' },
124
124
  { label: 'Toggle Preview', panel: 'preview', accelerator: 'Alt+Command+3' },
125
- { label: 'Toggle Inspect', panel: 'inspect', accelerator: 'Alt+Command+4' },
125
+ { label: 'Toggle Glyphs', panel: 'glyphs', accelerator: 'Alt+Command+4' },
126
126
  { label: 'Toggle Toolbar', panel: 'toolbar', accelerator: 'Alt+Command+5' },
127
127
  { label: 'Toggle Grid', panel: 'grid', accelerator: 'Alt+Command+6' },
128
128
  { label: 'Toggle Waterfall', panel: 'waterfall', accelerator: 'Alt+Command+7' },
@@ -142,7 +142,7 @@ export default class DarwinTemplate {
142
142
  { label: 'Toggle Navigation', panel: 'navigation', accelerator: 'Alt+Command+1' },
143
143
  { label: 'Toggle Aside', panel: 'aside', accelerator: 'Alt+Command+2' },
144
144
  { label: 'Toggle Preview', panel: 'preview', accelerator: 'Alt+Command+3' },
145
- { label: 'Toggle Inspect', panel: 'inspect', accelerator: 'Alt+Command+4' },
145
+ { label: 'Toggle Glyphs', panel: 'glyphs', accelerator: 'Alt+Command+4' },
146
146
  { label: 'Toggle Toolbar', panel: 'toolbar', accelerator: 'Alt+Command+5' },
147
147
  { label: 'Toggle Grid', panel: 'grid', accelerator: 'Alt+Command+6' },
148
148
  { label: 'Toggle Waterfall', panel: 'waterfall', accelerator: 'Alt+Command+7' },
@@ -119,7 +119,7 @@ class SystemTemplate {
119
119
  { label: 'Toggle Navigation', panel: 'navigation', accelerator: 'Alt+Ctrl+1' },
120
120
  { label: 'Toggle Aside', panel: 'aside', accelerator: 'Alt+Ctrl+2' },
121
121
  { label: 'Toggle Preview', panel: 'preview', accelerator: 'Alt+Ctrl+3' },
122
- { label: 'Toggle Inspect', panel: 'inspect', accelerator: 'Alt+Ctrl+4' },
122
+ { label: 'Toggle Glyphs', panel: 'glyphs', accelerator: 'Alt+Ctrl+4' },
123
123
  { label: 'Toggle Toolbar', panel: 'toolbar', accelerator: 'Alt+Ctrl+5' },
124
124
  { label: 'Toggle Grid', panel: 'grid', accelerator: 'Alt+Ctrl+6' },
125
125
  { label: 'Toggle Waterfall', panel: 'waterfall', accelerator: 'Alt+Ctrl+7' },
@@ -134,7 +134,7 @@ export default class SystemTemplate {
134
134
  { label: 'Toggle Navigation', panel: 'navigation', accelerator: 'Alt+Ctrl+1' },
135
135
  { label: 'Toggle Aside', panel: 'aside', accelerator: 'Alt+Ctrl+2' },
136
136
  { label: 'Toggle Preview', panel: 'preview', accelerator: 'Alt+Ctrl+3' },
137
- { label: 'Toggle Inspect', panel: 'inspect', accelerator: 'Alt+Ctrl+4' },
137
+ { label: 'Toggle Glyphs', panel: 'glyphs', accelerator: 'Alt+Ctrl+4' },
138
138
  { label: 'Toggle Toolbar', panel: 'toolbar', accelerator: 'Alt+Ctrl+5' },
139
139
  { label: 'Toggle Grid', panel: 'grid', accelerator: 'Alt+Ctrl+6' },
140
140
  { label: 'Toggle Waterfall', panel: 'waterfall', accelerator: 'Alt+Ctrl+7' },
@@ -17,5 +17,7 @@ var StorageType;
17
17
  StorageType["LayoutTheme"] = "layout.theme";
18
18
  StorageType["AiKeys"] = "ai.keys";
19
19
  StorageType["NavigationExpanded"] = "navigation.expanded";
20
+ StorageType["SortColumn"] = "datagrid.sort.column";
21
+ StorageType["SortDirection"] = "datagrid.sort.direction";
20
22
  })(StorageType || (exports.StorageType = StorageType = {}));
21
23
  //# sourceMappingURL=StorageType.js.map
@@ -13,4 +13,6 @@ export enum StorageType {
13
13
  LayoutTheme = 'layout.theme',
14
14
  AiKeys = 'ai.keys',
15
15
  NavigationExpanded = 'navigation.expanded',
16
+ SortColumn = 'datagrid.sort.column',
17
+ SortDirection = 'datagrid.sort.direction',
16
18
  }
package/app/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "name": "Tom Shaw",
6
6
  "email": ""
7
7
  },
8
- "version": "1.0.1",
8
+ "version": "1.1.0",
9
9
  "main": "main.js",
10
10
  "private": true,
11
11
  "dependencies": {
@@ -30,7 +30,7 @@ export interface LayoutPanelType {
30
30
  navigationEnabled: boolean;
31
31
  toolbarEnabled: boolean;
32
32
  previewEnabled: boolean;
33
- inspectEnabled: boolean;
33
+ glyphsEnabled: boolean;
34
34
  searchEnabled: boolean;
35
35
  waterfallEnabled: boolean;
36
36
  }
@@ -46,6 +46,7 @@ export interface LayoutPreviewType {
46
46
  displayText: string | null;
47
47
  wordSpacing: number;
48
48
  letterSpacing: number;
49
+ selectedGlyph: number | null;
49
50
  }
50
51
 
51
52
  export interface LayoutType {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "fontastic",
3
- "version": "1.0.1",
4
- "description": "A gorgeous multi-platform font management application.",
3
+ "version": "1.1.0",
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,
7
7
  "author": {
@@ -137,24 +137,26 @@ export class DatabaseService {
137
137
  private fetchCurrentPage(extraOptions: any = {}) {
138
138
  const skip = (this.currentPage() - 1) * this.pageSize();
139
139
  const take = this.pageSize();
140
+ const sortOrder = this.getSortOrder();
140
141
 
141
142
  const smartCollectionId = this.activeSmartCollectionId();
142
143
  if (smartCollectionId) {
143
- this.smartCollectionEvaluate(smartCollectionId, { skip, take });
144
+ this.smartCollectionEvaluate(smartCollectionId, { skip, take, ...(sortOrder ? { order: sortOrder } : {}) });
144
145
  return;
145
146
  }
146
147
 
147
148
  const searchWhere = this.activeSearchWhere();
148
149
  if (searchWhere) {
149
150
  const searchOrder = this.activeSearchOrder();
150
- this.storeSearch({ where: searchWhere, skip, take, ...(searchOrder ? { order: searchOrder } : {}) });
151
+ const order = sortOrder ?? searchOrder;
152
+ this.storeSearch({ where: searchWhere, skip, take, ...(order ? { order } : {}) });
151
153
  return;
152
154
  }
153
155
 
154
156
  const collectionId = this.collectionId();
155
157
  const filter = this.activeFilter();
156
158
 
157
- const options: any = { skip, take, ...extraOptions };
159
+ const options: any = { skip, take, ...extraOptions, ...(sortOrder ? { order: sortOrder } : {}) };
158
160
 
159
161
  if (collectionId) {
160
162
  options.collectionId = collectionId;
@@ -172,17 +174,24 @@ export class DatabaseService {
172
174
 
173
175
  constructor() {
174
176
  this.electron.ready.then(async () => {
175
- const [collections, smartCollections, savedCollectionId, savedStoreId] = await Promise.all([
177
+ const [collections, smartCollections, savedCollectionId, savedStoreId, savedSortColumn, savedSortDirection] = await Promise.all([
176
178
  this.message.collectionFetch({}),
177
179
  this.message.smartCollectionFind(),
178
180
  this.message.get(StorageType.CollectionId, null),
179
181
  this.message.get(StorageType.StoreId, null),
182
+ this.message.get(StorageType.SortColumn, null),
183
+ this.message.get(StorageType.SortDirection, null),
180
184
  ]);
181
185
 
182
186
  this.collections.set(collections);
183
187
  this.smartCollections.set(smartCollections);
184
188
  console.log('System Boot:', collections);
185
189
 
190
+ if (savedSortColumn) {
191
+ this.sortColumn.set(savedSortColumn);
192
+ this.sortDirection.set(savedSortDirection === 'DESC' ? 'DESC' : 'ASC');
193
+ }
194
+
186
195
  if (savedCollectionId) {
187
196
  this.collectionId.set(savedCollectionId);
188
197
  }
@@ -398,6 +407,44 @@ export class DatabaseService {
398
407
 
399
408
  readonly activeSearchOrder = signal<{ column: string; direction: string } | null>(null);
400
409
 
410
+ // Datagrid sort (persisted)
411
+ readonly sortColumn = signal<string | null>(null);
412
+ readonly sortDirection = signal<'ASC' | 'DESC'>('ASC');
413
+
414
+ toggleSort(column: string) {
415
+ const current = this.sortColumn();
416
+ if (current === column) {
417
+ if (this.sortDirection() === 'ASC') {
418
+ this.sortDirection.set('DESC');
419
+ } else {
420
+ // Clear sort
421
+ this.sortColumn.set(null);
422
+ this.sortDirection.set('ASC');
423
+ }
424
+ } else {
425
+ this.sortColumn.set(column);
426
+ this.sortDirection.set('ASC');
427
+ }
428
+
429
+ // Persist
430
+ const col = this.sortColumn();
431
+ if (col) {
432
+ this.message.set(StorageType.SortColumn, col);
433
+ this.message.set(StorageType.SortDirection, this.sortDirection());
434
+ } else {
435
+ this.message.set(StorageType.SortColumn, null);
436
+ this.message.set(StorageType.SortDirection, null);
437
+ }
438
+
439
+ this.currentPage.set(1);
440
+ this.fetchCurrentPage();
441
+ }
442
+
443
+ private getSortOrder(): { column: string; direction: string } | null {
444
+ const col = this.sortColumn();
445
+ return col ? { column: col, direction: this.sortDirection() } : null;
446
+ }
447
+
401
448
  selectSearch(where: { key: string; value: any }[], order?: { column: string; direction: string }) {
402
449
  this.parentId.set(null);
403
450
  this.collectionId.set(null);
@@ -61,7 +61,7 @@ export class PresentationService {
61
61
  navigationEnabled: this.navigationEnabled(),
62
62
  toolbarEnabled: this.toolbarEnabled(),
63
63
  previewEnabled: this.previewEnabled(),
64
- inspectEnabled: this.inspectEnabled(),
64
+ glyphsEnabled: this.glyphsEnabled(),
65
65
  searchEnabled: this.searchEnabled(),
66
66
  waterfallEnabled: this.waterfallEnabled(),
67
67
  };
@@ -89,6 +89,7 @@ export class PresentationService {
89
89
  displayText: this.customText(),
90
90
  wordSpacing: this.wordSpacing(),
91
91
  letterSpacing: this.letterSpacing(),
92
+ selectedGlyph: this.selectedGlyph(),
92
93
  };
93
94
  if (previewInitialized) {
94
95
  untracked(() => this.messageService.set(StorageType.LayoutPreview, settings));
@@ -130,12 +131,14 @@ export class PresentationService {
130
131
  readonly letterSpacing = signal(0);
131
132
  readonly wordSpacing = signal(0);
132
133
 
134
+ readonly selectedGlyph = signal<number | null>(null);
135
+
133
136
  readonly navigationExpandedIds = signal<number[]>([]);
134
137
 
135
138
  readonly gridEnabled = signal(true);
136
139
  readonly toolbarEnabled = signal(true);
137
140
  readonly previewEnabled = signal(true);
138
- readonly inspectEnabled = signal(false);
141
+ readonly glyphsEnabled = signal(false);
139
142
  readonly asideEnabled = signal(true);
140
143
  readonly navigationEnabled = signal(true);
141
144
  readonly searchEnabled = signal(false);
@@ -147,7 +150,7 @@ export class PresentationService {
147
150
  this.gridEnabled(),
148
151
  this.toolbarEnabled(),
149
152
  this.previewEnabled(),
150
- this.inspectEnabled(),
153
+ this.glyphsEnabled(),
151
154
  this.asideEnabled(),
152
155
  this.navigationEnabled(),
153
156
  ].filter(Boolean).length,
@@ -162,8 +165,8 @@ export class PresentationService {
162
165
  togglePreview() {
163
166
  this.previewEnabled.update((v) => !v);
164
167
  }
165
- toggleInspect() {
166
- this.inspectEnabled.update((v) => !v);
168
+ toggleGlyphs() {
169
+ this.glyphsEnabled.update((v) => !v);
167
170
  }
168
171
  toggleAside() {
169
172
  this.asideEnabled.update((v) => !v);
@@ -176,7 +179,7 @@ export class PresentationService {
176
179
  this.searchEnabled.set(enabling);
177
180
  if (enabling) {
178
181
  this.waterfallEnabled.set(false);
179
- this.inspectEnabled.set(false);
182
+ this.glyphsEnabled.set(false);
180
183
  this.previewEnabled.set(false);
181
184
  }
182
185
  }
@@ -188,7 +191,7 @@ export class PresentationService {
188
191
  navigation: () => this.toggleNavigation(),
189
192
  aside: () => this.toggleAside(),
190
193
  preview: () => this.togglePreview(),
191
- inspect: () => this.toggleInspect(),
194
+ glyphs: () => this.toggleGlyphs(),
192
195
  toolbar: () => this.toggleToolbar(),
193
196
  grid: () => this.toggleGrid(),
194
197
  waterfall: () => this.toggleWaterfall(),
@@ -253,7 +256,7 @@ export class PresentationService {
253
256
  this.navigationEnabled.set(settings.navigationEnabled);
254
257
  this.toolbarEnabled.set(settings.toolbarEnabled);
255
258
  this.previewEnabled.set(settings.previewEnabled);
256
- this.inspectEnabled.set(settings.inspectEnabled);
259
+ this.glyphsEnabled.set(settings.glyphsEnabled);
257
260
  if (settings.searchEnabled !== undefined) {
258
261
  this.searchEnabled.set(settings.searchEnabled);
259
262
  }
@@ -274,6 +277,9 @@ export class PresentationService {
274
277
  if (settings.displayText) {
275
278
  this.customText.set(settings.displayText);
276
279
  }
280
+ if (settings.selectedGlyph !== undefined) {
281
+ this.selectedGlyph.set(settings.selectedGlyph);
282
+ }
277
283
  }
278
284
  }
279
285
 
@@ -281,7 +287,7 @@ export class PresentationService {
281
287
  this.gridEnabled.set(true);
282
288
  this.toolbarEnabled.set(true);
283
289
  this.previewEnabled.set(true);
284
- this.inspectEnabled.set(false);
290
+ this.glyphsEnabled.set(false);
285
291
  this.asideEnabled.set(true);
286
292
  this.navigationEnabled.set(true);
287
293
  this.searchEnabled.set(false);
@@ -39,11 +39,11 @@
39
39
  >
40
40
  <a
41
41
  href="javascript:;"
42
- (click)="presentation.toggleInspect()"
42
+ (click)="presentation.toggleGlyphs()"
43
43
  class="cursor-pointer text-[10px] font-medium tracking-wide text-center no-underline py-1 px-2 inline-flex flex-col items-center outline-none whitespace-nowrap transition-colors"
44
- [style.color]="presentation.inspectEnabled() ? 'var(--text-primary)' : 'var(--text-muted)'"
45
- title="Toggle Inspect"
46
- ><i class="material-symbols-outlined block text-xl leading-none p-0 mb-0.5">info</i> Inspect</a
44
+ [style.color]="presentation.glyphsEnabled() ? 'var(--text-primary)' : 'var(--text-muted)'"
45
+ title="Toggle Glyphs"
46
+ ><i class="material-symbols-outlined block text-xl leading-none p-0 mb-0.5">info</i> Glyphs</a
47
47
  >
48
48
  <a
49
49
  href="javascript:;"
@@ -3,7 +3,7 @@
3
3
  [style.grid-template-rows]="
4
4
  (presentation.searchEnabled() ? '1fr ' : '') +
5
5
  (presentation.previewEnabled() ? '1fr ' : '') +
6
- (presentation.inspectEnabled() ? '1fr ' : '') +
6
+ (presentation.glyphsEnabled() ? '1fr ' : '') +
7
7
  (presentation.waterfallEnabled() ? '1fr ' : '') +
8
8
  (presentation.toolbarEnabled() ? '42px ' : '') +
9
9
  (presentation.gridEnabled() ? '1fr' : '')
@@ -19,9 +19,9 @@
19
19
  <app-preview />
20
20
  </app-panel>
21
21
  }
22
- @if (presentation.inspectEnabled()) {
22
+ @if (presentation.glyphsEnabled()) {
23
23
  <app-panel class="overflow-auto" [style.border-bottom]="'1px solid var(--border-subtle)'">
24
- <app-inspector />
24
+ <app-glyphs />
25
25
  </app-panel>
26
26
  }
27
27
  @if (presentation.waterfallEnabled()) {
@@ -4,7 +4,7 @@ import { PanelComponent } from '../../shared/components/panel/panel.component';
4
4
  import { PreviewComponent } from '../../shared/components/preview/preview.component';
5
5
  import { DatagridComponent } from '../../shared/components/datagrid/datagrid.component';
6
6
  import { ToolbarComponent } from '../../shared/components/toolbar/toolbar.component';
7
- import { InspectorComponent } from '../../shared/components/inspector/inspector.component';
7
+ import { GlyphsComponent } from '../../shared/components/glyphs/glyphs.component';
8
8
  import { SearchComponent } from '../../shared/components/search/search.component';
9
9
  import { WaterfallComponent } from '../../shared/components/waterfall/waterfall.component';
10
10
  import { PresentationService } from '../../core/services';
@@ -12,7 +12,7 @@ import { PresentationService } from '../../core/services';
12
12
  @Component({
13
13
  selector: 'app-main',
14
14
  standalone: true,
15
- imports: [PanelComponent, PreviewComponent, DatagridComponent, ToolbarComponent, InspectorComponent, SearchComponent, WaterfallComponent],
15
+ imports: [PanelComponent, PreviewComponent, DatagridComponent, ToolbarComponent, GlyphsComponent, SearchComponent, WaterfallComponent],
16
16
  templateUrl: './main.component.html',
17
17
  })
18
18
  export class MainComponent {
@@ -241,8 +241,8 @@ export class NavigationComponent implements OnInit, OnDestroy {
241
241
  this.contextMenuItems = [
242
242
  { label: 'Add Collection', action: 'add-collection', icon: 'create_new_folder' },
243
243
  { label: 'Add Fonts', action: 'add-fonts', icon: 'list_alt' },
244
- { label: 'Rename', action: 'rename' },
245
- { label: 'Delete', action: 'delete' },
244
+ { label: 'Rename', action: 'rename', icon: 'edit', separator: true },
245
+ { label: 'Delete', action: 'delete', icon: 'delete', separator: true },
246
246
  ];
247
247
 
248
248
  this.contextMenu = { x: event.clientX, y: event.clientY, collection };
@@ -413,7 +413,7 @@ export class NavigationComponent implements OnInit, OnDestroy {
413
413
 
414
414
  this.smartContextMenuItems = [
415
415
  { label: 'Edit Rules', action: 'edit-rules', icon: 'tune' },
416
- { label: 'Delete', action: 'delete' },
416
+ { label: 'Delete', action: 'delete', icon: 'delete', separator: true },
417
417
  ];
418
418
 
419
419
  this.smartContextMenu = { x: event.clientX, y: event.clientY, smartCollection: sc };
@@ -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>();
@@ -8,13 +8,31 @@
8
8
  >
9
9
  <tr class="text-left text-[11px] uppercase tracking-wide" [style.color]="'var(--text-muted)'">
10
10
  <th class="w-10 px-2 py-2 text-center font-medium"></th>
11
- <th class="px-4 py-2 font-medium">Name</th>
12
- <th class="px-4 py-2 font-medium">Family</th>
13
- <th class="px-4 py-2 font-medium">Style</th>
14
- <th class="px-4 py-2 font-medium">Type</th>
15
- <th class="px-4 py-2 font-medium">Size</th>
16
- <th class="px-4 py-2 font-medium">Version</th>
17
- <th class="px-4 py-2 font-medium">Designer</th>
11
+ @for (col of sortableColumns; track col.field) {
12
+ <th
13
+ class="px-4 py-2 font-medium cursor-pointer select-none transition-colors hover:brightness-125"
14
+ [class.max-w-48]="col.field === 'designer'"
15
+ (click)="db.toggleSort(col.field)"
16
+ >
17
+ <div class="flex items-center justify-between gap-1">
18
+ <span>{{ col.label }}</span>
19
+ @if (db.sortColumn() === col.field) {
20
+ <span
21
+ class="material-symbols-outlined"
22
+ style="
23
+ font-size: 14px;
24
+ font-variation-settings:
25
+ 'opsz' 20,
26
+ 'wght' 300;
27
+ "
28
+ [style.color]="'var(--accent)'"
29
+ >
30
+ {{ db.sortDirection() === 'ASC' ? 'arrow_upward' : 'arrow_downward' }}
31
+ </span>
32
+ }
33
+ </div>
34
+ </th>
35
+ }
18
36
  <th class="w-10 px-2 py-2 text-center font-medium"></th>
19
37
  <th class="w-10 px-2 py-2 text-center font-medium"></th>
20
38
  <th class="w-10 px-2 py-2 text-center font-medium"></th>
@@ -11,6 +11,16 @@ export class DatagridComponent {
11
11
  private messageService = inject(MessageService);
12
12
  private el = inject(ElementRef);
13
13
 
14
+ readonly sortableColumns = [
15
+ { field: 'full_name', label: 'Name' },
16
+ { field: 'font_family', label: 'Family' },
17
+ { field: 'font_subfamily', label: 'Style' },
18
+ { field: 'file_type', label: 'Type' },
19
+ { field: 'file_size', label: 'Size' },
20
+ { field: 'version', label: 'Version' },
21
+ { field: 'designer', label: 'Designer' },
22
+ ];
23
+
14
24
  constructor() {
15
25
  effect(() => {
16
26
  this.db.currentPage();
@@ -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
+ }
@@ -6,7 +6,7 @@ export * from './datagrid/datagrid.component';
6
6
  export * from './prompt-dialog/prompt-dialog.component';
7
7
  export * from './rule-builder/rule-builder.component';
8
8
  export * from './toolbar/toolbar.component';
9
- export * from './inspector/inspector.component';
9
+ export * from './glyphs/glyphs.component';
10
10
  export * from './search/search.component';
11
11
  export * from './spinner/spinner.component';
12
12
  export * from './waterfall/waterfall.component';
@@ -1,7 +1,8 @@
1
1
  .app-spinner {
2
2
  @apply relative block rounded-full w-[19px] h-[19px] m-0 p-0 opacity-0 overflow-hidden border-4 border-[#00a0be];
3
3
  border-left: 4px solid #fff;
4
- &.is-animating {
5
- @apply opacity-100 animate-spin;
6
- }
4
+ }
5
+
6
+ .app-spinner.is-animating {
7
+ @apply opacity-100 animate-spin;
7
8
  }
@@ -1,41 +0,0 @@
1
- import { Component, inject, signal, computed, effect } from '@angular/core';
2
- import { DatabaseService } from '../../../core/services';
3
-
4
- const PAGE_SIZE = 200;
5
-
6
- @Component({
7
- selector: 'app-inspector',
8
- standalone: true,
9
- templateUrl: './inspector.component.html',
10
- })
11
- export class InspectorComponent {
12
- readonly db = inject(DatabaseService);
13
-
14
- readonly currentPage = signal(1);
15
- readonly selectedGlyph = signal<number | null>(null);
16
-
17
- readonly totalPages = computed(() => Math.max(1, Math.ceil(this.db.glyphs().length / PAGE_SIZE)));
18
-
19
- readonly pageGlyphs = computed(() => {
20
- const glyphs = this.db.glyphs();
21
- const start = (this.currentPage() - 1) * PAGE_SIZE;
22
- return glyphs.slice(start, start + PAGE_SIZE);
23
- });
24
-
25
- readonly fontFamily = computed(() => {
26
- const store = this.db.store();
27
- return store ? store.full_name || store.font_family || '' : '';
28
- });
29
-
30
- constructor() {
31
- effect(() => {
32
- this.db.glyphs();
33
- this.currentPage.set(1);
34
- this.selectedGlyph.set(null);
35
- });
36
- }
37
-
38
- toChar(codePoint: number): string {
39
- return String.fromCodePoint(codePoint);
40
- }
41
- }