@focus8/settings-registry 0.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/src/index.ts ADDED
@@ -0,0 +1,715 @@
1
+ /**
2
+ * Settings Registry — single source of truth for ALL FocusPlanner app settings.
3
+ *
4
+ * This shared package defines every setting key, its category, data type,
5
+ * default value, and whether it should be synced to the server.
6
+ *
7
+ * Used by:
8
+ * - The mobile app (local SQLite settings table, React Query hooks)
9
+ * - The API (validate & serialize settings profiles)
10
+ * - The web portal (render settings viewer/editor)
11
+ *
12
+ * App-specific defaults (theme, locale, enrolled mode) are parameterized
13
+ * via `createRegistry(config)`.
14
+ */
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Setting types shared across mobile, API, and web
18
+ // ---------------------------------------------------------------------------
19
+
20
+ export type ThemeSetting = 'light' | 'dark' | 'system';
21
+ export type AlarmSound =
22
+ | 'none'
23
+ | 'alarm1'
24
+ | 'alarm2'
25
+ | 'alarm3'
26
+ | 'alarm4'
27
+ | 'alarm5'
28
+ | 'alarm6'
29
+ | 'alarm7'
30
+ | 'alarm8'
31
+ | 'alarm9';
32
+ export type LocaleCode = 'en' | 'nb';
33
+
34
+ export type CalendarType = 'chronological' | 'time-based';
35
+ export type ClockType = 'digital' | 'analog';
36
+ export type TimePickerMode = 'dials' | 'keypad';
37
+ export type AlarmTimeout = 1 | 2 | 3 | 5 | 10;
38
+ export const ALARM_TIMEOUTS: AlarmTimeout[] = [1, 2, 3, 5, 10];
39
+ export type CalendarViewMode =
40
+ | 'day'
41
+ | '3-days'
42
+ | '5-days'
43
+ | '7-days'
44
+ | 'week'
45
+ | 'month'
46
+ | 'overview';
47
+ export type CalendarDayViewCellZoom = 15 | 30 | 60;
48
+ export type InactivityTimeoutMinutes = 1 | 5 | 10 | 15 | 30 | 45;
49
+ export type LockScreenClockDisplay = 'none' | 'digital' | 'analog';
50
+ export type LockScreenImageMode = 'none' | 'background' | 'photoFrame';
51
+ export type PhotoFrameIntervalSeconds = 30 | 60 | 120 | 300;
52
+ export type EventVisibility = 'Public' | 'Private' | 'Custom';
53
+ export type SuggestEndTimeUnit = 'minutes' | 'hours';
54
+
55
+ export type WeatherLocation = {
56
+ address: string;
57
+ name?: string;
58
+ latitude: number;
59
+ longitude: number;
60
+ placeId?: string;
61
+ };
62
+
63
+ export type LockScreenImage = {
64
+ uri: string;
65
+ filePath: string;
66
+ fileName?: string;
67
+ fileSize?: number;
68
+ width?: number;
69
+ height?: number;
70
+ order: number;
71
+ };
72
+
73
+ export const LOCK_SCREEN_MAX_IMAGES = 10;
74
+
75
+ export type AllDayPreset = {
76
+ daysBefore: number;
77
+ time: string;
78
+ };
79
+
80
+ // ---------------------------------------------------------------------------
81
+ // Setting categories — used for UI grouping and profile organization
82
+ // ---------------------------------------------------------------------------
83
+
84
+ export const SETTINGS_CATEGORIES = [
85
+ 'appearance',
86
+ 'calendarView',
87
+ 'calendars',
88
+ 'sound',
89
+ 'timer',
90
+ 'media',
91
+ 'lockScreen',
92
+ 'touch',
93
+ 'device',
94
+ 'language',
95
+ 'notification',
96
+ 'chronological',
97
+ 'eventForm',
98
+ 'chronologicalEventForm',
99
+ ] as const;
100
+
101
+ export type SettingsCategory = (typeof SETTINGS_CATEGORIES)[number];
102
+
103
+ // ---------------------------------------------------------------------------
104
+ // Data type discriminator — stored alongside each setting in the DB
105
+ // ---------------------------------------------------------------------------
106
+
107
+ export type SettingsDataType = 'string' | 'number' | 'boolean' | 'json';
108
+
109
+ // ---------------------------------------------------------------------------
110
+ // Single registry entry definition
111
+ // ---------------------------------------------------------------------------
112
+
113
+ export type SettingDef<T = unknown> = {
114
+ category: SettingsCategory;
115
+ type: SettingsDataType;
116
+ default: T;
117
+ /** Whether this setting should be synced to the server. Default true. */
118
+ sync: boolean;
119
+ };
120
+
121
+ // ---------------------------------------------------------------------------
122
+ // Configuration for app-specific defaults
123
+ // ---------------------------------------------------------------------------
124
+
125
+ export type RegistryConfig = {
126
+ /** Whether the device is in enrolled/kiosk mode */
127
+ isEnrolled: boolean;
128
+ /** Default theme code */
129
+ defaultTheme: ThemeSetting;
130
+ /** Default locale code */
131
+ defaultLocale: LocaleCode;
132
+ };
133
+
134
+ /** Default config for non-enrolled mode */
135
+ export const DEFAULT_REGISTRY_CONFIG: RegistryConfig = {
136
+ isEnrolled: false,
137
+ defaultTheme: 'system',
138
+ defaultLocale: 'nb',
139
+ };
140
+
141
+ // ---------------------------------------------------------------------------
142
+ // Helper
143
+ // ---------------------------------------------------------------------------
144
+
145
+ function def<T>(
146
+ category: SettingsCategory,
147
+ type: SettingsDataType,
148
+ defaultValue: T,
149
+ sync = true,
150
+ ): SettingDef<T> {
151
+ return { category, type, default: defaultValue, sync };
152
+ }
153
+
154
+ // ---------------------------------------------------------------------------
155
+ // Entry builder (internal — used by SettingsRegistry class and factory)
156
+ // ---------------------------------------------------------------------------
157
+
158
+ function buildEntries(config: RegistryConfig) {
159
+ return {
160
+ // ═══════════════════════════════════════════════════════════════════════
161
+ // Appearance
162
+ // ═══════════════════════════════════════════════════════════════════════
163
+ 'appearance.theme': def<ThemeSetting>(
164
+ 'appearance',
165
+ 'string',
166
+ config.defaultTheme,
167
+ ),
168
+ 'appearance.clockType': def<ClockType>('appearance', 'string', 'digital'),
169
+ 'appearance.enableDayColors': def<boolean>('appearance', 'boolean', false),
170
+
171
+ // ═══════════════════════════════════════════════════════════════════════
172
+ // Calendar view
173
+ // ═══════════════════════════════════════════════════════════════════════
174
+ 'calendarView.type': def<CalendarType>(
175
+ 'calendarView',
176
+ 'string',
177
+ 'time-based',
178
+ ),
179
+ 'calendarView.view': def<CalendarViewMode>('calendarView', 'string', 'day'),
180
+ 'calendarView.dayViewZoom': def<CalendarDayViewCellZoom>(
181
+ 'calendarView',
182
+ 'number',
183
+ 60,
184
+ ),
185
+ 'calendarView.weekViewZoom': def<CalendarDayViewCellZoom>(
186
+ 'calendarView',
187
+ 'number',
188
+ 60,
189
+ ),
190
+ 'calendarView.splitView': def<boolean>('calendarView', 'boolean', false),
191
+ 'calendarView.showCalendarNames': def<boolean>(
192
+ 'calendarView',
193
+ 'boolean',
194
+ true,
195
+ ),
196
+ 'calendarView.calendarColumns': def<unknown[]>('calendarView', 'json', []),
197
+ 'calendarView.autoReturnToTodayEnabled': def<boolean>(
198
+ 'calendarView',
199
+ 'boolean',
200
+ config.isEnrolled,
201
+ ),
202
+ 'calendarView.autoReturnToTodayTimeoutSeconds': def<number>(
203
+ 'calendarView',
204
+ 'number',
205
+ 300,
206
+ ),
207
+ 'calendarView.showWeatherOnEvents': def<boolean>(
208
+ 'calendarView',
209
+ 'boolean',
210
+ false,
211
+ ),
212
+ 'calendarView.showWeatherOnTimeline': def<boolean>(
213
+ 'calendarView',
214
+ 'boolean',
215
+ false,
216
+ ),
217
+ 'calendarView.weatherLocation': def<WeatherLocation | null>(
218
+ 'calendarView',
219
+ 'json',
220
+ null,
221
+ ),
222
+
223
+ // ═══════════════════════════════════════════════════════════════════════
224
+ // Event form field visibility (time-based)
225
+ // ═══════════════════════════════════════════════════════════════════════
226
+ 'eventForm.recurrence': def<boolean>('eventForm', 'boolean', true),
227
+ 'eventForm.reminders': def<boolean>('eventForm', 'boolean', true),
228
+ 'eventForm.emailReminders': def<boolean>('eventForm', 'boolean', false),
229
+ 'eventForm.location': def<boolean>('eventForm', 'boolean', true),
230
+ 'eventForm.travelTime': def<boolean>('eventForm', 'boolean', false),
231
+ 'eventForm.description': def<boolean>('eventForm', 'boolean', true),
232
+ 'eventForm.checklist': def<boolean>('eventForm', 'boolean', true),
233
+ 'eventForm.images': def<boolean>('eventForm', 'boolean', false),
234
+ 'eventForm.audioClips': def<boolean>('eventForm', 'boolean', false),
235
+ 'eventForm.notificationReceivers': def<boolean>(
236
+ 'eventForm',
237
+ 'boolean',
238
+ true,
239
+ ),
240
+ 'eventForm.visibility': def<boolean>('eventForm', 'boolean', false),
241
+
242
+ // ═══════════════════════════════════════════════════════════════════════
243
+ // Sound & alerts
244
+ // ═══════════════════════════════════════════════════════════════════════
245
+ 'sound.timerVolume': def<number>('sound', 'number', 0.5),
246
+ 'sound.reminderVolume': def<number>('sound', 'number', 0.5),
247
+ 'sound.mediaVolume': def<number>('sound', 'number', 0.5),
248
+ 'sound.alarmSound': def<AlarmSound>('sound', 'string', 'alarm1'),
249
+ 'sound.reminderAlarmSound': def<AlarmSound>('sound', 'string', 'alarm1'),
250
+ 'sound.timerAlarmSound': def<AlarmSound>('sound', 'string', 'alarm1'),
251
+ 'sound.timerAlarmTimeout': def<AlarmTimeout>('sound', 'number', 3),
252
+ 'sound.reminderAlarmTimeout': def<AlarmTimeout>('sound', 'number', 3),
253
+ 'sound.allowCustomReminderSounds': def<boolean>('sound', 'boolean', false),
254
+ 'sound.ttsEnabled': def<boolean>('sound', 'boolean', config.isEnrolled),
255
+ 'sound.ttsRate': def<number>('sound', 'number', 1.0),
256
+
257
+ // ═══════════════════════════════════════════════════════════════════════
258
+ // Timer
259
+ // ═══════════════════════════════════════════════════════════════════════
260
+ 'timer.showTimeRemaining': def<boolean>('timer', 'boolean', true),
261
+ 'timer.showEndTime': def<boolean>('timer', 'boolean', true),
262
+ 'timer.showRestartButton': def<boolean>('timer', 'boolean', true),
263
+ 'timer.showPauseButton': def<boolean>('timer', 'boolean', true),
264
+
265
+ // ═══════════════════════════════════════════════════════════════════════
266
+ // Lock screen
267
+ // ═══════════════════════════════════════════════════════════════════════
268
+ 'lockScreen.pin': def<string>('lockScreen', 'string', ''),
269
+ 'lockScreen.inactivityLockEnabled': def<boolean>(
270
+ 'lockScreen',
271
+ 'boolean',
272
+ false,
273
+ ),
274
+ 'lockScreen.inactivityTimeoutMinutes': def<InactivityTimeoutMinutes>(
275
+ 'lockScreen',
276
+ 'number',
277
+ 5,
278
+ ),
279
+ 'lockScreen.clockDisplay': def<LockScreenClockDisplay>(
280
+ 'lockScreen',
281
+ 'string',
282
+ 'digital',
283
+ ),
284
+ 'lockScreen.showDate': def<boolean>('lockScreen', 'boolean', true),
285
+ 'lockScreen.showHourNumbers': def<boolean>('lockScreen', 'boolean', true),
286
+ 'lockScreen.imageMode': def<LockScreenImageMode>(
287
+ 'lockScreen',
288
+ 'string',
289
+ 'none',
290
+ ),
291
+ 'lockScreen.backgroundImage': def<string | null>(
292
+ 'lockScreen',
293
+ 'json',
294
+ null,
295
+ ),
296
+ 'lockScreen.photoFrameImages': def<LockScreenImage[]>(
297
+ 'lockScreen',
298
+ 'json',
299
+ [],
300
+ ),
301
+ 'lockScreen.photoFrameIntervalSeconds': def<PhotoFrameIntervalSeconds>(
302
+ 'lockScreen',
303
+ 'number',
304
+ 60,
305
+ ),
306
+
307
+ // ═══════════════════════════════════════════════════════════════════════
308
+ // Touch / gestures
309
+ // ═══════════════════════════════════════════════════════════════════════
310
+ 'touch.enableTapToCreate': def<boolean>('touch', 'boolean', false),
311
+ 'touch.enableDragDrop': def<boolean>('touch', 'boolean', false),
312
+
313
+ // ═══════════════════════════════════════════════════════════════════════
314
+ // Device (not synced unless noted)
315
+ // ═══════════════════════════════════════════════════════════════════════
316
+ 'device.id': def<string>('device', 'string', '', false),
317
+ 'device.timePickerMode': def<TimePickerMode>('device', 'string', 'dials'),
318
+ 'device.devMenuEnabled': def<boolean>('device', 'boolean', false, false),
319
+ 'device.authWarningDismissTtlDays': def<number>(
320
+ 'device',
321
+ 'number',
322
+ 3,
323
+ false,
324
+ ),
325
+
326
+ // ═══════════════════════════════════════════════════════════════════════
327
+ // Language
328
+ // ═══════════════════════════════════════════════════════════════════════
329
+ 'language.locale': def<LocaleCode>(
330
+ 'language',
331
+ 'string',
332
+ config.defaultLocale,
333
+ ),
334
+
335
+ // ═══════════════════════════════════════════════════════════════════════
336
+ // Notifications
337
+ // ═══════════════════════════════════════════════════════════════════════
338
+ 'notification.enabled': def<boolean>('notification', 'boolean', false),
339
+ 'notification.notifyAllCalendars': def<boolean>(
340
+ 'notification',
341
+ 'boolean',
342
+ true,
343
+ ),
344
+ 'notification.enabledCalendarIds': def<string[]>(
345
+ 'notification',
346
+ 'json',
347
+ [],
348
+ ),
349
+ 'notification.hasBeenPrompted': def<boolean>(
350
+ 'notification',
351
+ 'boolean',
352
+ false,
353
+ false,
354
+ ),
355
+
356
+ // ═══════════════════════════════════════════════════════════════════════
357
+ // Chronological features (header, footer, menu, quick settings, timer)
358
+ // ═══════════════════════════════════════════════════════════════════════
359
+ // Header
360
+ 'chronological.header.showNavigationArrows': def<boolean>(
361
+ 'chronological',
362
+ 'boolean',
363
+ true,
364
+ ),
365
+ 'chronological.header.showClock': def<boolean>(
366
+ 'chronological',
367
+ 'boolean',
368
+ true,
369
+ ),
370
+ 'chronological.header.showCurrentYearInDate': def<boolean>(
371
+ 'chronological',
372
+ 'boolean',
373
+ false,
374
+ ),
375
+ 'chronological.header.showTimeOfDay': def<boolean>(
376
+ 'chronological',
377
+ 'boolean',
378
+ false,
379
+ ),
380
+ // Footer
381
+ 'chronological.footer.showMenuButton': def<boolean>(
382
+ 'chronological',
383
+ 'boolean',
384
+ true,
385
+ ),
386
+ 'chronological.footer.showViewSwitcherDay': def<boolean>(
387
+ 'chronological',
388
+ 'boolean',
389
+ true,
390
+ ),
391
+ 'chronological.footer.showViewSwitcherWeek': def<boolean>(
392
+ 'chronological',
393
+ 'boolean',
394
+ true,
395
+ ),
396
+ 'chronological.footer.showViewSwitcherMonth': def<boolean>(
397
+ 'chronological',
398
+ 'boolean',
399
+ true,
400
+ ),
401
+ 'chronological.footer.showTimerButton': def<boolean>(
402
+ 'chronological',
403
+ 'boolean',
404
+ true,
405
+ ),
406
+ 'chronological.footer.showNewEventButton': def<boolean>(
407
+ 'chronological',
408
+ 'boolean',
409
+ true,
410
+ ),
411
+ 'chronological.footer.showSettingsButton': def<boolean>(
412
+ 'chronological',
413
+ 'boolean',
414
+ true,
415
+ ),
416
+ // Timer features
417
+ 'chronological.timer.showNewCountdown': def<boolean>(
418
+ 'chronological',
419
+ 'boolean',
420
+ true,
421
+ ),
422
+ 'chronological.timer.showFromTemplate': def<boolean>(
423
+ 'chronological',
424
+ 'boolean',
425
+ true,
426
+ ),
427
+ 'chronological.timer.showEditTemplate': def<boolean>(
428
+ 'chronological',
429
+ 'boolean',
430
+ true,
431
+ ),
432
+ 'chronological.timer.showDeleteTemplate': def<boolean>(
433
+ 'chronological',
434
+ 'boolean',
435
+ true,
436
+ ),
437
+ 'chronological.timer.showAddTemplate': def<boolean>(
438
+ 'chronological',
439
+ 'boolean',
440
+ true,
441
+ ),
442
+ // Menu
443
+ 'chronological.menu.showSettingsButton': def<boolean>(
444
+ 'chronological',
445
+ 'boolean',
446
+ true,
447
+ ),
448
+ // Quick settings
449
+ 'chronological.quickSettings.showTimerVolume': def<boolean>(
450
+ 'chronological',
451
+ 'boolean',
452
+ true,
453
+ ),
454
+ 'chronological.quickSettings.showReminderVolume': def<boolean>(
455
+ 'chronological',
456
+ 'boolean',
457
+ true,
458
+ ),
459
+ 'chronological.quickSettings.showMediaVolume': def<boolean>(
460
+ 'chronological',
461
+ 'boolean',
462
+ true,
463
+ ),
464
+ 'chronological.quickSettings.showBrightness': def<boolean>(
465
+ 'chronological',
466
+ 'boolean',
467
+ true,
468
+ ),
469
+ 'chronological.quickSettings.showLockScreen': def<boolean>(
470
+ 'chronological',
471
+ 'boolean',
472
+ true,
473
+ ),
474
+ // Time-of-day periods
475
+ 'chronological.timeOfDay.morningStart': def<number>(
476
+ 'chronological',
477
+ 'number',
478
+ 6,
479
+ ),
480
+ 'chronological.timeOfDay.forenoonStart': def<number>(
481
+ 'chronological',
482
+ 'number',
483
+ 9,
484
+ ),
485
+ 'chronological.timeOfDay.afternoonStart': def<number>(
486
+ 'chronological',
487
+ 'number',
488
+ 12,
489
+ ),
490
+ 'chronological.timeOfDay.eveningStart': def<number>(
491
+ 'chronological',
492
+ 'number',
493
+ 18,
494
+ ),
495
+ 'chronological.timeOfDay.nightStart': def<number>(
496
+ 'chronological',
497
+ 'number',
498
+ 0,
499
+ ),
500
+
501
+ // ═══════════════════════════════════════════════════════════════════════
502
+ // Chronological event form
503
+ // ═══════════════════════════════════════════════════════════════════════
504
+ // Toggleable fields
505
+ 'chronologicalEventForm.field.description': def<boolean>(
506
+ 'chronologicalEventForm',
507
+ 'boolean',
508
+ true,
509
+ ),
510
+ 'chronologicalEventForm.field.recurrence': def<boolean>(
511
+ 'chronologicalEventForm',
512
+ 'boolean',
513
+ true,
514
+ ),
515
+ 'chronologicalEventForm.field.acknowledge': def<boolean>(
516
+ 'chronologicalEventForm',
517
+ 'boolean',
518
+ true,
519
+ ),
520
+ 'chronologicalEventForm.field.checklist': def<boolean>(
521
+ 'chronologicalEventForm',
522
+ 'boolean',
523
+ true,
524
+ ),
525
+ 'chronologicalEventForm.field.extraImages': def<boolean>(
526
+ 'chronologicalEventForm',
527
+ 'boolean',
528
+ true,
529
+ ),
530
+ 'chronologicalEventForm.field.reminders': def<boolean>(
531
+ 'chronologicalEventForm',
532
+ 'boolean',
533
+ true,
534
+ ),
535
+ 'chronologicalEventForm.field.audioClips': def<boolean>(
536
+ 'chronologicalEventForm',
537
+ 'boolean',
538
+ true,
539
+ ),
540
+ // Fixed fields
541
+ 'chronologicalEventForm.fixedField.allDay': def<boolean>(
542
+ 'chronologicalEventForm',
543
+ 'boolean',
544
+ true,
545
+ ),
546
+ 'chronologicalEventForm.fixedField.endTime': def<boolean>(
547
+ 'chronologicalEventForm',
548
+ 'boolean',
549
+ true,
550
+ ),
551
+ 'chronologicalEventForm.fixedField.visibility': def<boolean>(
552
+ 'chronologicalEventForm',
553
+ 'boolean',
554
+ true,
555
+ ),
556
+ // Suggest end time
557
+ 'chronologicalEventForm.suggestEndTime.enabled': def<boolean>(
558
+ 'chronologicalEventForm',
559
+ 'boolean',
560
+ false,
561
+ ),
562
+ 'chronologicalEventForm.suggestEndTime.value': def<number>(
563
+ 'chronologicalEventForm',
564
+ 'number',
565
+ 30,
566
+ ),
567
+ 'chronologicalEventForm.suggestEndTime.unit': def<SuggestEndTimeUnit>(
568
+ 'chronologicalEventForm',
569
+ 'string',
570
+ 'minutes',
571
+ ),
572
+ // Default visibility
573
+ 'chronologicalEventForm.defaultVisibility': def<EventVisibility>(
574
+ 'chronologicalEventForm',
575
+ 'string',
576
+ 'Private',
577
+ ),
578
+ // Reminder presets
579
+ 'chronologicalEventForm.reminderPresets.timed': def<number[]>(
580
+ 'chronologicalEventForm',
581
+ 'json',
582
+ [5, 15, 30, 60, 120, 1440],
583
+ ),
584
+ 'chronologicalEventForm.reminderPresets.allDay': def<AllDayPreset[]>(
585
+ 'chronologicalEventForm',
586
+ 'json',
587
+ [
588
+ { daysBefore: 0, time: '09:00' },
589
+ { daysBefore: 1, time: '18:00' },
590
+ { daysBefore: 2, time: '09:00' },
591
+ ],
592
+ ),
593
+ } as const satisfies Record<string, SettingDef>;
594
+ }
595
+
596
+ // ---------------------------------------------------------------------------
597
+ // Derived types (based on entry shape — all configs produce the same keys)
598
+ // ---------------------------------------------------------------------------
599
+
600
+ type SettingsEntries = ReturnType<typeof buildEntries>;
601
+
602
+ /** Union type of every valid setting key */
603
+ export type SettingKey = keyof SettingsEntries;
604
+
605
+ /** Type-safe value type for a given setting key */
606
+ export type SettingValue<K extends SettingKey> = SettingsEntries[K]['default'];
607
+
608
+ /** Complete settings map — all keys resolved to their value types */
609
+ export type SettingsMap = {
610
+ [K in SettingKey]: SettingValue<K>;
611
+ };
612
+
613
+ // ---------------------------------------------------------------------------
614
+ // Registry class
615
+ // ---------------------------------------------------------------------------
616
+
617
+ export class SettingsRegistry {
618
+ /** The raw setting definitions (category, type, default, sync) */
619
+ readonly entries: SettingsEntries;
620
+
621
+ /** All setting keys as an array */
622
+ readonly keys: SettingKey[];
623
+
624
+ constructor(config: Partial<RegistryConfig> = {}) {
625
+ const full: RegistryConfig = { ...DEFAULT_REGISTRY_CONFIG, ...config };
626
+ this.entries = buildEntries(full);
627
+ this.keys = Object.keys(this.entries) as SettingKey[];
628
+ }
629
+
630
+ /** Get the default value for a setting */
631
+ getDefault<K extends SettingKey>(key: K): SettingValue<K> {
632
+ return this.entries[key].default as SettingValue<K>;
633
+ }
634
+
635
+ /** Get a complete map of all default values */
636
+ getAllDefaults(): SettingsMap {
637
+ const defaults = {} as Record<string, unknown>;
638
+ for (const key of this.keys) {
639
+ defaults[key] = this.entries[key].default;
640
+ }
641
+ return defaults as SettingsMap;
642
+ }
643
+
644
+ /** Get the data type for a setting key */
645
+ getDataType(key: SettingKey): SettingsDataType {
646
+ return this.entries[key].type;
647
+ }
648
+
649
+ /** Check whether a setting should be synced */
650
+ isSynced(key: SettingKey): boolean {
651
+ return this.entries[key].sync;
652
+ }
653
+
654
+ /** Get all setting keys for a category */
655
+ getByCategory(category: SettingsCategory): SettingKey[] {
656
+ return this.keys.filter((key) => this.entries[key].category === category);
657
+ }
658
+
659
+ /** Serialize a setting value to a string for DB storage */
660
+ serialize(key: SettingKey, value: unknown): string {
661
+ const dataType = this.getDataType(key);
662
+ switch (dataType) {
663
+ case 'string':
664
+ return String(value ?? '');
665
+ case 'number':
666
+ return String(value ?? 0);
667
+ case 'boolean':
668
+ return value ? 'true' : 'false';
669
+ case 'json':
670
+ return JSON.stringify(value);
671
+ }
672
+ }
673
+
674
+ /** Deserialize a DB string back to a typed setting value */
675
+ deserialize<K extends SettingKey>(
676
+ key: K,
677
+ raw: string | null,
678
+ ): SettingValue<K> {
679
+ if (raw === null || raw === undefined) {
680
+ return this.getDefault(key);
681
+ }
682
+
683
+ const dataType = this.getDataType(key);
684
+ switch (dataType) {
685
+ case 'string':
686
+ return raw as SettingValue<K>;
687
+ case 'number':
688
+ return Number(raw) as SettingValue<K>;
689
+ case 'boolean':
690
+ return (raw === 'true') as SettingValue<K>;
691
+ case 'json':
692
+ try {
693
+ return JSON.parse(raw) as SettingValue<K>;
694
+ } catch {
695
+ return this.getDefault(key);
696
+ }
697
+ }
698
+ }
699
+ }
700
+
701
+ // ---------------------------------------------------------------------------
702
+ // Default registry instance (non-enrolled, system theme, nb locale)
703
+ // ---------------------------------------------------------------------------
704
+
705
+ export const defaultRegistry = new SettingsRegistry();
706
+
707
+ // ---------------------------------------------------------------------------
708
+ // Factory function — returns raw entry map for local type derivation.
709
+ // Consumers that need precise generic inference (e.g. useQuery<K>) can use
710
+ // this to anchor types on a local const variable.
711
+ // ---------------------------------------------------------------------------
712
+
713
+ export function createSettingsRegistry(config: Partial<RegistryConfig> = {}) {
714
+ return buildEntries({ ...DEFAULT_REGISTRY_CONFIG, ...config });
715
+ }