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
@@ -1,6 +1,7 @@
1
1
  import { Brackets, Equal } from 'typeorm';
2
2
  import { Store } from '../entity';
3
3
  import { searchDbColumns } from '../../config';
4
+ import type { SmartCollectionRule } from '../../types';
4
5
 
5
6
  export const StoreRepository = {
6
7
  async search(options: any) {
@@ -330,6 +331,111 @@ export const StoreRepository = {
330
331
  .catch((err: any) => console.log('insert-error', err));
331
332
  },
332
333
 
334
+ async evaluateSmartRules(rules: SmartCollectionRule[], matchType: string, options: any = {}) {
335
+ const db = this.createQueryBuilder();
336
+
337
+ const textFields = [
338
+ 'file_name',
339
+ 'file_path',
340
+ 'compatible_full_name',
341
+ 'copyright',
342
+ 'description',
343
+ 'designer',
344
+ 'designer_url',
345
+ 'font_family',
346
+ 'font_subfamily',
347
+ 'full_name',
348
+ 'license',
349
+ 'license_url',
350
+ 'manufacturer',
351
+ 'manufacturer_url',
352
+ 'post_script_name',
353
+ 'preferred_family',
354
+ 'preferred_sub_family',
355
+ 'sample_text',
356
+ 'trademark',
357
+ 'unique_id',
358
+ 'version',
359
+ ];
360
+ const booleanFields = ['favorite', 'system', 'installable', 'temporary'];
361
+ const numericFields = ['file_size'];
362
+ const dateFields = ['created'];
363
+
364
+ if (rules.length > 0) {
365
+ const conditions: ((qb: any) => void)[] = rules.map((rule, idx) => {
366
+ const field = `store.${rule.field}`;
367
+ const paramName = `p${idx}`;
368
+
369
+ return (qb: any) => {
370
+ if (textFields.includes(rule.field)) {
371
+ const val = String(rule.value).toLowerCase();
372
+ switch (rule.operator) {
373
+ case 'contains':
374
+ qb.where(`LOWER(${field}) LIKE :${paramName}`, { [paramName]: `%${val}%` });
375
+ break;
376
+ case 'equals':
377
+ qb.where(`LOWER(${field}) = :${paramName}`, { [paramName]: val });
378
+ break;
379
+ case 'starts_with':
380
+ qb.where(`LOWER(${field}) LIKE :${paramName}`, { [paramName]: `${val}%` });
381
+ break;
382
+ case 'ends_with':
383
+ qb.where(`LOWER(${field}) LIKE :${paramName}`, { [paramName]: `%${val}` });
384
+ break;
385
+ default:
386
+ qb.where(`LOWER(${field}) LIKE :${paramName}`, { [paramName]: `%${val}%` });
387
+ }
388
+ } else if (rule.field === 'file_type') {
389
+ qb.where(`${field} = :${paramName}`, { [paramName]: rule.value });
390
+ } else if (booleanFields.includes(rule.field)) {
391
+ const boolVal = rule.operator === 'is_not' ? 0 : 1;
392
+ qb.where(`${field} = :${paramName}`, { [paramName]: boolVal });
393
+ } else if (numericFields.includes(rule.field)) {
394
+ if (rule.operator === 'greater_than') {
395
+ qb.where(`${field} > :${paramName}`, { [paramName]: rule.value });
396
+ } else if (rule.operator === 'less_than') {
397
+ qb.where(`${field} < :${paramName}`, { [paramName]: rule.value });
398
+ } else {
399
+ qb.where(`${field} = :${paramName}`, { [paramName]: rule.value });
400
+ }
401
+ } else if (dateFields.includes(rule.field)) {
402
+ if (rule.operator === 'greater_than') {
403
+ qb.where(`${field} >= :${paramName}`, { [paramName]: rule.value });
404
+ } else if (rule.operator === 'less_than') {
405
+ qb.where(`${field} <= :${paramName}`, { [paramName]: rule.value });
406
+ } else {
407
+ qb.where(`${field} = :${paramName}`, { [paramName]: rule.value });
408
+ }
409
+ }
410
+ };
411
+ });
412
+
413
+ const joiner = matchType === 'OR' ? 'orWhere' : 'andWhere';
414
+ conditions.forEach((condition, i) => {
415
+ if (i === 0) {
416
+ db.where(new Brackets(condition));
417
+ } else {
418
+ db[joiner](new Brackets(condition));
419
+ }
420
+ });
421
+ }
422
+
423
+ if (options.take) {
424
+ db.limit(options.take);
425
+ }
426
+
427
+ if (options.skip) {
428
+ db.offset(options.skip);
429
+ }
430
+
431
+ if (options.order && options.order.column) {
432
+ const direction = options.order.direction === 'DESC' ? 'DESC' : 'ASC';
433
+ db.orderBy(`store.${options.order.column}`, direction);
434
+ }
435
+
436
+ return await db.getManyAndCount();
437
+ },
438
+
333
439
  async fetchSystemStats() {
334
440
  const rowCount = await this.createQueryBuilder().select('COUNT(*)', 'total').getRawOne();
335
441
 
@@ -15,6 +15,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./Collection.repository"), exports);
18
+ __exportStar(require("./SmartCollection.repository"), exports);
18
19
  __exportStar(require("./Store.repository"), exports);
19
20
  __exportStar(require("./Logger.repository"), exports);
20
21
  //# sourceMappingURL=index.js.map
@@ -1,3 +1,4 @@
1
1
  export * from './Collection.repository';
2
+ export * from './SmartCollection.repository';
2
3
  export * from './Store.repository';
3
- export * from './Logger.repository';
4
+ export * from './Logger.repository';
@@ -58,6 +58,11 @@ var ChannelType;
58
58
  ChannelType["IPC_LOGGER_DELETE"] = "IPC_LOGGER_DELETE";
59
59
  ChannelType["IPC_LOGGER_QUERY"] = "IPC_LOGGER_QUERY";
60
60
  ChannelType["IPC_LOGGER_TRUNCATE"] = "IPC_LOGGER_TRUNCATE";
61
+ ChannelType["IPC_SMART_COLLECTION_FIND"] = "IPC_SMART_COLLECTION_FIND";
62
+ ChannelType["IPC_SMART_COLLECTION_CREATE"] = "IPC_SMART_COLLECTION_CREATE";
63
+ ChannelType["IPC_SMART_COLLECTION_UPDATE"] = "IPC_SMART_COLLECTION_UPDATE";
64
+ ChannelType["IPC_SMART_COLLECTION_DELETE"] = "IPC_SMART_COLLECTION_DELETE";
65
+ ChannelType["IPC_SMART_COLLECTION_EVALUATE"] = "IPC_SMART_COLLECTION_EVALUATE";
61
66
  ChannelType["IPC_TOGGLE_PANEL"] = "IPC_TOGGLE_PANEL";
62
67
  })(ChannelType || (exports.ChannelType = ChannelType = {}));
63
68
  //# sourceMappingURL=ChannelType.js.map
@@ -63,5 +63,11 @@ export enum ChannelType {
63
63
  IPC_LOGGER_QUERY = 'IPC_LOGGER_QUERY',
64
64
  IPC_LOGGER_TRUNCATE = 'IPC_LOGGER_TRUNCATE',
65
65
 
66
+ IPC_SMART_COLLECTION_FIND = 'IPC_SMART_COLLECTION_FIND',
67
+ IPC_SMART_COLLECTION_CREATE = 'IPC_SMART_COLLECTION_CREATE',
68
+ IPC_SMART_COLLECTION_UPDATE = 'IPC_SMART_COLLECTION_UPDATE',
69
+ IPC_SMART_COLLECTION_DELETE = 'IPC_SMART_COLLECTION_DELETE',
70
+ IPC_SMART_COLLECTION_EVALUATE = 'IPC_SMART_COLLECTION_EVALUATE',
71
+
66
72
  IPC_TOGGLE_PANEL = 'IPC_TOGGLE_PANEL',
67
73
  }
@@ -16,5 +16,6 @@ var StorageType;
16
16
  StorageType["LayoutPreview"] = "layout.preview";
17
17
  StorageType["LayoutTheme"] = "layout.theme";
18
18
  StorageType["AiKeys"] = "ai.keys";
19
+ StorageType["NavigationExpanded"] = "navigation.expanded";
19
20
  })(StorageType || (exports.StorageType = StorageType = {}));
20
21
  //# sourceMappingURL=StorageType.js.map
@@ -12,4 +12,5 @@ export enum StorageType {
12
12
  LayoutPreview = 'layout.preview',
13
13
  LayoutTheme = 'layout.theme',
14
14
  AiKeys = 'ai.keys',
15
+ NavigationExpanded = 'navigation.expanded',
15
16
  }
package/app/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "name": "Tom Shaw",
6
6
  "email": ""
7
7
  },
8
- "version": "1.0.0",
8
+ "version": "1.0.2",
9
9
  "main": "main.js",
10
10
  "private": true,
11
11
  "dependencies": {
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=FontMetrics.js.map
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=SmartCollection.js.map
@@ -0,0 +1,5 @@
1
+ export interface SmartCollectionRule {
2
+ field: string;
3
+ operator: string;
4
+ value: string | number;
5
+ }
@@ -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 {
@@ -15,6 +15,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./AppAlert"), exports);
18
+ __exportStar(require("./FontMetrics"), exports);
18
19
  __exportStar(require("./AuthUser"), exports);
19
20
  __exportStar(require("./Breadcrumb"), exports);
20
21
  __exportStar(require("./QueryOptions"), exports);
@@ -22,4 +23,5 @@ __exportStar(require("./SystemTheme"), exports);
22
23
  __exportStar(require("./SystemStats"), exports);
23
24
  __exportStar(require("./SystemConfig"), exports);
24
25
  __exportStar(require("./ImportOptions"), exports);
26
+ __exportStar(require("./SmartCollection"), exports);
25
27
  //# sourceMappingURL=index.js.map
@@ -7,3 +7,4 @@ export * from './SystemTheme';
7
7
  export * from './SystemStats';
8
8
  export * from './SystemConfig';
9
9
  export * from './ImportOptions';
10
+ export * from './SmartCollection';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fontastic",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "A gorgeous multi-platform font management application.",
5
5
  "homepage": "https://github.com/tomshaw/fontastic",
6
6
  "private": false,
@@ -7,6 +7,7 @@ import type { Collection } from '@main/database/entity/Collection.schema';
7
7
  import type { Logger } from '@main/database/entity/Logger.schema';
8
8
  import type { Store, StoreManyAndCountType } from '@main/database/entity/Store.schema';
9
9
  import type { FontMetrics, SystemStats } from '@main/types';
10
+ import type { SmartCollection } from '@main/database/entity/SmartCollection.schema';
10
11
 
11
12
  @Injectable({
12
13
  providedIn: 'root',
@@ -18,6 +19,8 @@ export class DatabaseService {
18
19
 
19
20
  // Reactive state
20
21
  readonly collections = signal<Collection[]>([]);
22
+ readonly smartCollections = signal<SmartCollection[]>([]);
23
+ readonly activeSmartCollectionId = signal<number | null>(null);
21
24
  readonly parentId = signal<number | null>(null);
22
25
  readonly collectionId = signal<number | null>(null);
23
26
  readonly stores = signal<Store[]>([]);
@@ -56,6 +59,7 @@ export class DatabaseService {
56
59
  this.collectionId.set(parentId);
57
60
  this.activeFilter.set(null);
58
61
  this.activeSearchWhere.set(null);
62
+ this.activeSmartCollectionId.set(null);
59
63
  this.currentPage.set(1);
60
64
  }
61
65
 
@@ -64,6 +68,7 @@ export class DatabaseService {
64
68
  this.collectionId.set(child.id);
65
69
  this.activeFilter.set(null);
66
70
  this.activeSearchWhere.set(null);
71
+ this.activeSmartCollectionId.set(null);
67
72
  this.currentPage.set(1);
68
73
  }
69
74
 
@@ -78,11 +83,22 @@ export class DatabaseService {
78
83
  return current.id;
79
84
  }
80
85
 
86
+ selectSmartCollection(id: number) {
87
+ this.parentId.set(null);
88
+ this.collectionId.set(null);
89
+ this.activeFilter.set(null);
90
+ this.activeSearchWhere.set(null);
91
+ this.activeSmartCollectionId.set(id);
92
+ this.currentPage.set(1);
93
+ this.fetchCurrentPage();
94
+ }
95
+
81
96
  selectFilter(filter: string) {
82
97
  this.parentId.set(null);
83
98
  this.collectionId.set(null);
84
99
  this.activeFilter.set(filter);
85
100
  this.activeSearchWhere.set(null);
101
+ this.activeSmartCollectionId.set(null);
86
102
  this.currentPage.set(1);
87
103
 
88
104
  const whereMap: Record<string, { key: string; value: number }[]> = {
@@ -122,6 +138,12 @@ export class DatabaseService {
122
138
  const skip = (this.currentPage() - 1) * this.pageSize();
123
139
  const take = this.pageSize();
124
140
 
141
+ const smartCollectionId = this.activeSmartCollectionId();
142
+ if (smartCollectionId) {
143
+ this.smartCollectionEvaluate(smartCollectionId, { skip, take });
144
+ return;
145
+ }
146
+
125
147
  const searchWhere = this.activeSearchWhere();
126
148
  if (searchWhere) {
127
149
  const searchOrder = this.activeSearchOrder();
@@ -150,13 +172,15 @@ export class DatabaseService {
150
172
 
151
173
  constructor() {
152
174
  this.electron.ready.then(async () => {
153
- const [collections, savedCollectionId, savedStoreId] = await Promise.all([
175
+ const [collections, smartCollections, savedCollectionId, savedStoreId] = await Promise.all([
154
176
  this.message.collectionFetch({}),
177
+ this.message.smartCollectionFind(),
155
178
  this.message.get(StorageType.CollectionId, null),
156
179
  this.message.get(StorageType.StoreId, null),
157
180
  ]);
158
181
 
159
182
  this.collections.set(collections);
183
+ this.smartCollections.set(smartCollections);
160
184
  console.log('System Boot:', collections);
161
185
 
162
186
  if (savedCollectionId) {
@@ -294,6 +318,50 @@ export class DatabaseService {
294
318
  );
295
319
  }
296
320
 
321
+ // Smart Collection
322
+
323
+ smartCollectionCreate(args: any): Promise<SmartCollection[]> {
324
+ return this.track(
325
+ this.message.smartCollectionCreate(args).then((result) => {
326
+ this.smartCollections.set(result);
327
+ return result;
328
+ }),
329
+ );
330
+ }
331
+
332
+ smartCollectionUpdate(id: number, data: any): Promise<SmartCollection[]> {
333
+ return this.track(
334
+ this.message.smartCollectionUpdate(id, data).then((result) => {
335
+ this.smartCollections.set(result);
336
+ return result;
337
+ }),
338
+ );
339
+ }
340
+
341
+ smartCollectionDelete(id: number): Promise<SmartCollection[]> {
342
+ return this.track(
343
+ this.message.smartCollectionDelete(id).then((result) => {
344
+ this.smartCollections.set(result);
345
+ if (this.activeSmartCollectionId() === id) {
346
+ this.activeSmartCollectionId.set(null);
347
+ this.stores.set([]);
348
+ this.storeCount.set(0);
349
+ }
350
+ return result;
351
+ }),
352
+ );
353
+ }
354
+
355
+ smartCollectionEvaluate(id: number, options: any): Promise<StoreManyAndCountType> {
356
+ return this.track(
357
+ this.message.smartCollectionEvaluate(id, options).then((result) => {
358
+ this.stores.set(result[0] as Store[]);
359
+ this.storeCount.set(result[1] as number);
360
+ return result;
361
+ }),
362
+ );
363
+ }
364
+
297
365
  // Store
298
366
 
299
367
  storeFind(args: any): Promise<Store[]> {
@@ -336,6 +404,7 @@ export class DatabaseService {
336
404
  this.activeFilter.set(null);
337
405
  this.activeSearchWhere.set(where);
338
406
  this.activeSearchOrder.set(order ?? null);
407
+ this.activeSmartCollectionId.set(null);
339
408
  this.currentPage.set(1);
340
409
  this.fetchCurrentPage();
341
410
  }
@@ -5,6 +5,7 @@ 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
7
  import type { FontMetrics, SystemConfig, SystemStats } from '@main/types';
8
+ import type { SmartCollection } from '@main/database/entity/SmartCollection.schema';
8
9
 
9
10
  @Injectable({
10
11
  providedIn: 'root',
@@ -213,6 +214,28 @@ export class MessageService {
213
214
  return this.invoke<Collection[]>(ChannelType.IPC_COLLECTION_MOVE, { collectionId, newParentId, newIndex });
214
215
  }
215
216
 
217
+ // Smart Collection
218
+
219
+ smartCollectionFind(): Promise<SmartCollection[]> {
220
+ return this.invoke<SmartCollection[]>(ChannelType.IPC_SMART_COLLECTION_FIND);
221
+ }
222
+
223
+ smartCollectionCreate(args: any): Promise<SmartCollection[]> {
224
+ return this.invoke<SmartCollection[]>(ChannelType.IPC_SMART_COLLECTION_CREATE, args);
225
+ }
226
+
227
+ smartCollectionUpdate(id: number, data: any): Promise<SmartCollection[]> {
228
+ return this.invoke<SmartCollection[]>(ChannelType.IPC_SMART_COLLECTION_UPDATE, { id, data });
229
+ }
230
+
231
+ smartCollectionDelete(id: number): Promise<SmartCollection[]> {
232
+ return this.invoke<SmartCollection[]>(ChannelType.IPC_SMART_COLLECTION_DELETE, { id });
233
+ }
234
+
235
+ smartCollectionEvaluate(id: number, options: any): Promise<StoreManyAndCountType> {
236
+ return this.invoke<StoreManyAndCountType>(ChannelType.IPC_SMART_COLLECTION_EVALUATE, { id, ...options });
237
+ }
238
+
216
239
  // Store
217
240
 
218
241
  storeFind(args: any): Promise<Store[]> {
@@ -41,6 +41,7 @@ export class PresentationService {
41
41
  this.loadThemeSettings();
42
42
  this.loadLayoutSettings();
43
43
  this.loadPreviewSettings();
44
+ this.loadNavigationExpandedSettings();
44
45
  this.listenForMenuToggle();
45
46
 
46
47
  let themeInitialized = false;
@@ -60,7 +61,7 @@ export class PresentationService {
60
61
  navigationEnabled: this.navigationEnabled(),
61
62
  toolbarEnabled: this.toolbarEnabled(),
62
63
  previewEnabled: this.previewEnabled(),
63
- inspectEnabled: this.inspectEnabled(),
64
+ glyphsEnabled: this.glyphsEnabled(),
64
65
  searchEnabled: this.searchEnabled(),
65
66
  waterfallEnabled: this.waterfallEnabled(),
66
67
  };
@@ -70,6 +71,15 @@ export class PresentationService {
70
71
  panelInitialized = true;
71
72
  });
72
73
 
74
+ let navExpandedInitialized = false;
75
+ effect(() => {
76
+ const ids = this.navigationExpandedIds();
77
+ if (navExpandedInitialized) {
78
+ untracked(() => this.messageService.set(StorageType.NavigationExpanded, ids));
79
+ }
80
+ navExpandedInitialized = true;
81
+ });
82
+
73
83
  let previewInitialized = false;
74
84
  effect(() => {
75
85
  const settings: LayoutPreviewType = {
@@ -79,6 +89,7 @@ export class PresentationService {
79
89
  displayText: this.customText(),
80
90
  wordSpacing: this.wordSpacing(),
81
91
  letterSpacing: this.letterSpacing(),
92
+ selectedGlyph: this.selectedGlyph(),
82
93
  };
83
94
  if (previewInitialized) {
84
95
  untracked(() => this.messageService.set(StorageType.LayoutPreview, settings));
@@ -120,10 +131,14 @@ export class PresentationService {
120
131
  readonly letterSpacing = signal(0);
121
132
  readonly wordSpacing = signal(0);
122
133
 
134
+ readonly selectedGlyph = signal<number | null>(null);
135
+
136
+ readonly navigationExpandedIds = signal<number[]>([]);
137
+
123
138
  readonly gridEnabled = signal(true);
124
139
  readonly toolbarEnabled = signal(true);
125
140
  readonly previewEnabled = signal(true);
126
- readonly inspectEnabled = signal(false);
141
+ readonly glyphsEnabled = signal(false);
127
142
  readonly asideEnabled = signal(true);
128
143
  readonly navigationEnabled = signal(true);
129
144
  readonly searchEnabled = signal(false);
@@ -135,7 +150,7 @@ export class PresentationService {
135
150
  this.gridEnabled(),
136
151
  this.toolbarEnabled(),
137
152
  this.previewEnabled(),
138
- this.inspectEnabled(),
153
+ this.glyphsEnabled(),
139
154
  this.asideEnabled(),
140
155
  this.navigationEnabled(),
141
156
  ].filter(Boolean).length,
@@ -150,8 +165,8 @@ export class PresentationService {
150
165
  togglePreview() {
151
166
  this.previewEnabled.update((v) => !v);
152
167
  }
153
- toggleInspect() {
154
- this.inspectEnabled.update((v) => !v);
168
+ toggleGlyphs() {
169
+ this.glyphsEnabled.update((v) => !v);
155
170
  }
156
171
  toggleAside() {
157
172
  this.asideEnabled.update((v) => !v);
@@ -164,7 +179,7 @@ export class PresentationService {
164
179
  this.searchEnabled.set(enabling);
165
180
  if (enabling) {
166
181
  this.waterfallEnabled.set(false);
167
- this.inspectEnabled.set(false);
182
+ this.glyphsEnabled.set(false);
168
183
  this.previewEnabled.set(false);
169
184
  }
170
185
  }
@@ -176,7 +191,7 @@ export class PresentationService {
176
191
  navigation: () => this.toggleNavigation(),
177
192
  aside: () => this.toggleAside(),
178
193
  preview: () => this.togglePreview(),
179
- inspect: () => this.toggleInspect(),
194
+ glyphs: () => this.toggleGlyphs(),
180
195
  toolbar: () => this.toggleToolbar(),
181
196
  grid: () => this.toggleGrid(),
182
197
  waterfall: () => this.toggleWaterfall(),
@@ -191,6 +206,41 @@ export class PresentationService {
191
206
  });
192
207
  }
193
208
 
209
+ isNavigationExpanded(id: number): boolean {
210
+ return this.navigationExpandedIds().includes(id);
211
+ }
212
+
213
+ toggleNavigationExpanded(id: number) {
214
+ const ids = this.navigationExpandedIds();
215
+ if (ids.includes(id)) {
216
+ this.navigationExpandedIds.set(ids.filter((i) => i !== id));
217
+ } else {
218
+ this.navigationExpandedIds.set([...ids, id]);
219
+ }
220
+ }
221
+
222
+ expandNavigationId(id: number) {
223
+ const ids = this.navigationExpandedIds();
224
+ if (!ids.includes(id)) {
225
+ this.navigationExpandedIds.set([...ids, id]);
226
+ }
227
+ }
228
+
229
+ setAllNavigationExpanded(allIds: number[]) {
230
+ this.navigationExpandedIds.set(allIds);
231
+ }
232
+
233
+ clearAllNavigationExpanded() {
234
+ this.navigationExpandedIds.set([]);
235
+ }
236
+
237
+ private async loadNavigationExpandedSettings() {
238
+ const ids = (await this.messageService.get(StorageType.NavigationExpanded, null)) as number[] | null;
239
+ if (Array.isArray(ids)) {
240
+ this.navigationExpandedIds.set(ids);
241
+ }
242
+ }
243
+
194
244
  private async loadThemeSettings() {
195
245
  const settings = (await this.messageService.get(StorageType.LayoutTheme, null)) as { theme: string } | null;
196
246
  if (settings?.theme && PresentationService.themes.includes(settings.theme as (typeof PresentationService.themes)[number])) {
@@ -206,7 +256,7 @@ export class PresentationService {
206
256
  this.navigationEnabled.set(settings.navigationEnabled);
207
257
  this.toolbarEnabled.set(settings.toolbarEnabled);
208
258
  this.previewEnabled.set(settings.previewEnabled);
209
- this.inspectEnabled.set(settings.inspectEnabled);
259
+ this.glyphsEnabled.set(settings.glyphsEnabled);
210
260
  if (settings.searchEnabled !== undefined) {
211
261
  this.searchEnabled.set(settings.searchEnabled);
212
262
  }
@@ -227,6 +277,9 @@ export class PresentationService {
227
277
  if (settings.displayText) {
228
278
  this.customText.set(settings.displayText);
229
279
  }
280
+ if (settings.selectedGlyph !== undefined) {
281
+ this.selectedGlyph.set(settings.selectedGlyph);
282
+ }
230
283
  }
231
284
  }
232
285
 
@@ -234,7 +287,7 @@ export class PresentationService {
234
287
  this.gridEnabled.set(true);
235
288
  this.toolbarEnabled.set(true);
236
289
  this.previewEnabled.set(true);
237
- this.inspectEnabled.set(false);
290
+ this.glyphsEnabled.set(false);
238
291
  this.asideEnabled.set(true);
239
292
  this.navigationEnabled.set(true);
240
293
  this.searchEnabled.set(false);
@@ -1,4 +1,4 @@
1
- <header class="grid grid-cols-[220px_1fr_220px] min-[1440px]:grid-cols-[280px_1fr_280px] w-full h-full">
1
+ <header class="grid w-full h-full" [style.grid-template-columns]="'var(--panel-width) 1fr var(--panel-width)'">
2
2
  <div class="relative">
3
3
  <div class="p-0 h-full flex justify-items-start items-center">
4
4
  <a
@@ -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:;"
@@ -7,7 +7,7 @@
7
7
  <div
8
8
  class="grid overflow-hidden"
9
9
  [style.grid-template-columns]="
10
- (presentation.navigationEnabled() ? '280px ' : '') + '1fr' + (presentation.asideEnabled() ? ' 280px' : '')
10
+ (presentation.navigationEnabled() ? 'var(--panel-width) ' : '') + '1fr' + (presentation.asideEnabled() ? ' var(--panel-width)' : '')
11
11
  "
12
12
  >
13
13
  @if (presentation.navigationEnabled()) {
@@ -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 {