@focus8/settings-registry 0.6.0 → 0.7.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 CHANGED
@@ -77,6 +77,47 @@ export type AllDayPreset = {
77
77
  time: string;
78
78
  };
79
79
 
80
+ // ---------------------------------------------------------------------------
81
+ // UI type discriminator — determines which component renders each setting
82
+ // ---------------------------------------------------------------------------
83
+
84
+ export type SettingUiType =
85
+ | 'TOGGLE'
86
+ | 'SELECT'
87
+ | 'VOLUME_SLIDER'
88
+ | 'SLIDER'
89
+ | 'NUMBER_INPUT'
90
+ | 'TEXT_INPUT'
91
+ | 'PIN_INPUT'
92
+ | 'CUSTOM_THEME_PICKER'
93
+ | 'CUSTOM_LANGUAGE_PICKER'
94
+ | 'CUSTOM_CALENDAR_TYPE'
95
+ | 'CUSTOM_CLOCK_TYPE'
96
+ | 'CUSTOM_WEATHER_LOCATION'
97
+ | 'CUSTOM_IMAGE'
98
+ | 'CUSTOM_IMAGE_ARRAY'
99
+ | 'CUSTOM_CALENDAR_IDS'
100
+ | 'CUSTOM_REMINDER_PRESETS'
101
+ | 'CUSTOM_EVENT_FORM'
102
+ | 'CUSTOM_CHRONOLOGICAL_EVENT_FORM'
103
+ | 'CUSTOM_DISPLAY_DENSITY'
104
+ | 'CUSTOM_BRIGHTNESS'
105
+ | 'CUSTOM_SPLIT_VIEW_CONFIG'
106
+ | 'HIDDEN';
107
+
108
+ export type SettingOption<T = unknown> = {
109
+ /** The option value */
110
+ value: T;
111
+ /** i18n message key for the option label */
112
+ labelKey: string;
113
+ };
114
+
115
+ export type SliderConfig = {
116
+ min: number;
117
+ max: number;
118
+ step: number;
119
+ };
120
+
80
121
  // ---------------------------------------------------------------------------
81
122
  // Setting categories — used for UI grouping and profile organization
82
123
  // ---------------------------------------------------------------------------
@@ -219,6 +260,8 @@ export type SettingsGroupDef = {
219
260
  keys: string[];
220
261
  /** Only show for this calendar type (undefined = always) */
221
262
  calendarType?: CalendarType;
263
+ /** Only show for this app mode (undefined = always) */
264
+ appMode?: 'ENROLLED';
222
265
  };
223
266
 
224
267
  /**
@@ -298,12 +341,14 @@ export const SETTINGS_GROUPS: readonly SettingsGroupDef[] = [
298
341
  'sound.timerAlarmTimeout',
299
342
  'timer.',
300
343
  ],
344
+ appMode: 'ENROLLED',
301
345
  },
302
346
  {
303
347
  id: 'lockScreen',
304
348
  labelKey: 'Settings.LockScreen',
305
349
  icon: 'Lock',
306
350
  keys: ['lockScreen.'],
351
+ appMode: 'ENROLLED',
307
352
  },
308
353
  {
309
354
  id: 'touch',
@@ -613,10 +658,11 @@ function matchesGroup(key: string, group: SettingsGroupDef): boolean {
613
658
  export function groupSettingsForDevice(
614
659
  allSettings: ParsedSettingEntry[],
615
660
  calendarType: CalendarType,
661
+ appMode?: 'ENROLLED',
616
662
  ): { id: SettingsGroupId; labelKey: string; icon: string; settings: ParsedSettingEntry[] }[] {
617
- // Filter excluded
663
+ // Filter excluded and hidden settings
618
664
  const settings = allSettings.filter(
619
- (s) => !EXCLUDED_DEVICE_SETTINGS.has(s.key),
665
+ (s) => !EXCLUDED_DEVICE_SETTINGS.has(s.key) && s.uiType !== 'HIDDEN',
620
666
  );
621
667
 
622
668
  // Calendar type filtering for event form keys
@@ -645,6 +691,11 @@ export function groupSettingsForDevice(
645
691
  continue;
646
692
  }
647
693
 
694
+ // Skip groups not relevant for current app mode
695
+ if (group.appMode && group.appMode !== appMode) {
696
+ continue;
697
+ }
698
+
648
699
  const matched = filteredSettings.filter(
649
700
  (s) => !claimed.has(s.key) && matchesGroup(s.key, group),
650
701
  );
@@ -683,6 +734,12 @@ export type SettingDef<T = unknown> = {
683
734
  default: T;
684
735
  /** Whether this setting should be synced to the server. Default true. */
685
736
  sync: boolean;
737
+ /** UI component type for rendering this setting */
738
+ uiType: SettingUiType;
739
+ /** Available options for SELECT-type settings */
740
+ options?: readonly SettingOption<T>[];
741
+ /** Slider configuration for SLIDER-type settings */
742
+ sliderConfig?: SliderConfig;
686
743
  };
687
744
 
688
745
  // ---------------------------------------------------------------------------
@@ -705,6 +762,39 @@ export const DEFAULT_REGISTRY_CONFIG: RegistryConfig = {
705
762
  defaultLocale: 'nb',
706
763
  };
707
764
 
765
+ // ---------------------------------------------------------------------------
766
+ // Shared option arrays (reused across multiple settings)
767
+ // ---------------------------------------------------------------------------
768
+
769
+ const ALARM_SOUND_OPTIONS: readonly SettingOption<AlarmSound>[] = [
770
+ { value: 'none', labelKey: 'Settings.Option.AlarmSound.None' },
771
+ { value: 'alarm1', labelKey: 'Settings.Option.AlarmSound.Alarm1' },
772
+ { value: 'alarm2', labelKey: 'Settings.Option.AlarmSound.Alarm2' },
773
+ { value: 'alarm3', labelKey: 'Settings.Option.AlarmSound.Alarm3' },
774
+ { value: 'alarm4', labelKey: 'Settings.Option.AlarmSound.Alarm4' },
775
+ { value: 'alarm5', labelKey: 'Settings.Option.AlarmSound.Alarm5' },
776
+ { value: 'alarm6', labelKey: 'Settings.Option.AlarmSound.Alarm6' },
777
+ { value: 'alarm7', labelKey: 'Settings.Option.AlarmSound.Alarm7' },
778
+ { value: 'alarm8', labelKey: 'Settings.Option.AlarmSound.Alarm8' },
779
+ { value: 'alarm9', labelKey: 'Settings.Option.AlarmSound.Alarm9' },
780
+ ];
781
+
782
+ const ALARM_TIMEOUT_OPTIONS: readonly SettingOption<AlarmTimeout>[] = [
783
+ { value: 1, labelKey: 'Settings.Option.AlarmTimeout.1min' },
784
+ { value: 2, labelKey: 'Settings.Option.AlarmTimeout.2min' },
785
+ { value: 3, labelKey: 'Settings.Option.AlarmTimeout.3min' },
786
+ { value: 5, labelKey: 'Settings.Option.AlarmTimeout.5min' },
787
+ { value: 10, labelKey: 'Settings.Option.AlarmTimeout.10min' },
788
+ ];
789
+
790
+ const DAY_VIEW_ZOOM_OPTIONS: readonly SettingOption<CalendarDayViewCellZoom>[] = [
791
+ { value: 15, labelKey: 'Settings.Option.DayViewZoom.15min' },
792
+ { value: 30, labelKey: 'Settings.Option.DayViewZoom.30min' },
793
+ { value: 60, labelKey: 'Settings.Option.DayViewZoom.60min' },
794
+ ];
795
+
796
+ const TIME_OF_DAY_SLIDER: SliderConfig = { min: 0, max: 23, step: 1 };
797
+
708
798
  // ---------------------------------------------------------------------------
709
799
  // Helper
710
800
  // ---------------------------------------------------------------------------
@@ -713,9 +803,27 @@ function def<T>(
713
803
  category: SettingsCategory,
714
804
  type: SettingsDataType,
715
805
  defaultValue: T,
716
- sync = true,
806
+ uiType: SettingUiType,
807
+ extra?: {
808
+ sync?: boolean;
809
+ options?: readonly SettingOption<T>[];
810
+ sliderConfig?: SliderConfig;
811
+ },
717
812
  ): SettingDef<T> {
718
- return { category, type, default: defaultValue, sync };
813
+ const result: SettingDef<T> = {
814
+ category,
815
+ type,
816
+ default: defaultValue,
817
+ sync: extra?.sync ?? true,
818
+ uiType,
819
+ };
820
+ if (extra?.options) {
821
+ result.options = extra.options;
822
+ }
823
+ if (extra?.sliderConfig) {
824
+ result.sliderConfig = extra.sliderConfig;
825
+ }
826
+ return result;
719
827
  }
720
828
 
721
829
  // ---------------------------------------------------------------------------
@@ -731,9 +839,33 @@ function buildEntries(config: RegistryConfig) {
731
839
  'appearance',
732
840
  'string',
733
841
  config.defaultTheme,
842
+ 'CUSTOM_THEME_PICKER',
843
+ {
844
+ options: [
845
+ { value: 'light', labelKey: 'Settings.Option.Theme.Light' },
846
+ { value: 'dark', labelKey: 'Settings.Option.Theme.Dark' },
847
+ { value: 'system', labelKey: 'Settings.Option.Theme.System' },
848
+ ],
849
+ },
850
+ ),
851
+ 'appearance.clockType': def<ClockType>(
852
+ 'appearance',
853
+ 'string',
854
+ 'digital',
855
+ 'CUSTOM_CLOCK_TYPE',
856
+ {
857
+ options: [
858
+ { value: 'digital', labelKey: 'Settings.Option.ClockType.Digital' },
859
+ { value: 'analog', labelKey: 'Settings.Option.ClockType.Analog' },
860
+ ],
861
+ },
862
+ ),
863
+ 'appearance.enableDayColors': def<boolean>(
864
+ 'appearance',
865
+ 'boolean',
866
+ false,
867
+ 'TOGGLE',
734
868
  ),
735
- 'appearance.clockType': def<ClockType>('appearance', 'string', 'digital'),
736
- 'appearance.enableDayColors': def<boolean>('appearance', 'boolean', false),
737
869
 
738
870
  // ═══════════════════════════════════════════════════════════════════════
739
871
  // Calendar view
@@ -742,152 +874,346 @@ function buildEntries(config: RegistryConfig) {
742
874
  'calendarView',
743
875
  'string',
744
876
  'time-based',
877
+ 'CUSTOM_CALENDAR_TYPE',
878
+ {
879
+ options: [
880
+ {
881
+ value: 'chronological',
882
+ labelKey: 'Settings.Option.CalendarType.Chronological',
883
+ },
884
+ {
885
+ value: 'time-based',
886
+ labelKey: 'Settings.Option.CalendarType.TimeBased',
887
+ },
888
+ ],
889
+ },
890
+ ),
891
+ 'calendarView.view': def<CalendarViewMode>(
892
+ 'calendarView',
893
+ 'string',
894
+ 'day',
895
+ 'SELECT',
896
+ {
897
+ options: [
898
+ { value: 'day', labelKey: 'Settings.Option.CalendarView.Day' },
899
+ {
900
+ value: '3-days',
901
+ labelKey: 'Settings.Option.CalendarView.3Days',
902
+ },
903
+ {
904
+ value: '5-days',
905
+ labelKey: 'Settings.Option.CalendarView.5Days',
906
+ },
907
+ {
908
+ value: '7-days',
909
+ labelKey: 'Settings.Option.CalendarView.7Days',
910
+ },
911
+ { value: 'week', labelKey: 'Settings.Option.CalendarView.Week' },
912
+ {
913
+ value: 'month',
914
+ labelKey: 'Settings.Option.CalendarView.Month',
915
+ },
916
+ {
917
+ value: 'overview',
918
+ labelKey: 'Settings.Option.CalendarView.Overview',
919
+ },
920
+ ],
921
+ },
745
922
  ),
746
- 'calendarView.view': def<CalendarViewMode>('calendarView', 'string', 'day'),
747
923
  'calendarView.dayViewZoom': def<CalendarDayViewCellZoom>(
748
924
  'calendarView',
749
925
  'number',
750
926
  60,
927
+ 'SELECT',
928
+ { options: DAY_VIEW_ZOOM_OPTIONS },
751
929
  ),
752
930
  'calendarView.weekViewZoom': def<CalendarDayViewCellZoom>(
753
931
  'calendarView',
754
932
  'number',
755
933
  60,
934
+ 'SELECT',
935
+ { options: DAY_VIEW_ZOOM_OPTIONS },
936
+ ),
937
+ 'calendarView.splitView': def<boolean>(
938
+ 'calendarView',
939
+ 'boolean',
940
+ false,
941
+ 'TOGGLE',
756
942
  ),
757
- 'calendarView.splitView': def<boolean>('calendarView', 'boolean', false),
758
943
  'calendarView.showCalendarNames': def<boolean>(
759
944
  'calendarView',
760
945
  'boolean',
761
946
  true,
947
+ 'TOGGLE',
948
+ ),
949
+ 'calendarView.calendarColumns': def<unknown[]>(
950
+ 'calendarView',
951
+ 'json',
952
+ [],
953
+ 'CUSTOM_SPLIT_VIEW_CONFIG',
762
954
  ),
763
- 'calendarView.calendarColumns': def<unknown[]>('calendarView', 'json', []),
764
955
  'calendarView.autoReturnToTodayEnabled': def<boolean>(
765
956
  'calendarView',
766
957
  'boolean',
767
958
  config.isEnrolled,
959
+ 'TOGGLE',
768
960
  ),
769
961
  'calendarView.autoReturnToTodayTimeoutSeconds': def<number>(
770
962
  'calendarView',
771
963
  'number',
772
964
  300,
965
+ 'SLIDER',
966
+ { sliderConfig: { min: 30, max: 600, step: 30 } },
773
967
  ),
774
968
  'calendarView.showWeatherOnEvents': def<boolean>(
775
969
  'calendarView',
776
970
  'boolean',
777
971
  false,
972
+ 'TOGGLE',
778
973
  ),
779
974
  'calendarView.showWeatherOnTimeline': def<boolean>(
780
975
  'calendarView',
781
976
  'boolean',
782
977
  false,
978
+ 'TOGGLE',
783
979
  ),
784
980
  'calendarView.weatherLocation': def<WeatherLocation | null>(
785
981
  'calendarView',
786
982
  'json',
787
983
  null,
984
+ 'CUSTOM_WEATHER_LOCATION',
788
985
  ),
789
986
 
790
987
  // ═══════════════════════════════════════════════════════════════════════
791
988
  // Event form field visibility (time-based)
792
989
  // ═══════════════════════════════════════════════════════════════════════
793
- 'eventForm.recurrence': def<boolean>('eventForm', 'boolean', true),
794
- 'eventForm.reminders': def<boolean>('eventForm', 'boolean', true),
795
- 'eventForm.emailReminders': def<boolean>('eventForm', 'boolean', false),
796
- 'eventForm.location': def<boolean>('eventForm', 'boolean', true),
797
- 'eventForm.travelTime': def<boolean>('eventForm', 'boolean', false),
798
- 'eventForm.description': def<boolean>('eventForm', 'boolean', true),
799
- 'eventForm.checklist': def<boolean>('eventForm', 'boolean', true),
800
- 'eventForm.images': def<boolean>('eventForm', 'boolean', false),
801
- 'eventForm.audioClips': def<boolean>('eventForm', 'boolean', false),
990
+ 'eventForm.recurrence': def<boolean>('eventForm', 'boolean', true, 'TOGGLE'),
991
+ 'eventForm.reminders': def<boolean>('eventForm', 'boolean', true, 'TOGGLE'),
992
+ 'eventForm.emailReminders': def<boolean>('eventForm', 'boolean', false, 'TOGGLE'),
993
+ 'eventForm.location': def<boolean>('eventForm', 'boolean', true, 'TOGGLE'),
994
+ 'eventForm.travelTime': def<boolean>('eventForm', 'boolean', false, 'TOGGLE'),
995
+ 'eventForm.description': def<boolean>('eventForm', 'boolean', true, 'TOGGLE'),
996
+ 'eventForm.checklist': def<boolean>('eventForm', 'boolean', true, 'TOGGLE'),
997
+ 'eventForm.images': def<boolean>('eventForm', 'boolean', false, 'TOGGLE'),
998
+ 'eventForm.audioClips': def<boolean>('eventForm', 'boolean', false, 'TOGGLE'),
802
999
  'eventForm.notificationReceivers': def<boolean>(
803
1000
  'eventForm',
804
1001
  'boolean',
805
1002
  true,
1003
+ 'TOGGLE',
806
1004
  ),
807
- 'eventForm.visibility': def<boolean>('eventForm', 'boolean', false),
1005
+ 'eventForm.visibility': def<boolean>('eventForm', 'boolean', false, 'TOGGLE'),
808
1006
 
809
1007
  // ═══════════════════════════════════════════════════════════════════════
810
1008
  // Sound & alerts
811
1009
  // ═══════════════════════════════════════════════════════════════════════
812
- 'sound.timerVolume': def<number>('sound', 'number', 0.5),
813
- 'sound.reminderVolume': def<number>('sound', 'number', 0.5),
814
- 'sound.mediaVolume': def<number>('sound', 'number', 0.5),
815
- 'sound.alarmSound': def<AlarmSound>('sound', 'string', 'alarm1'),
816
- 'sound.reminderAlarmSound': def<AlarmSound>('sound', 'string', 'alarm1'),
817
- 'sound.timerAlarmSound': def<AlarmSound>('sound', 'string', 'alarm1'),
818
- 'sound.timerAlarmTimeout': def<AlarmTimeout>('sound', 'number', 3),
819
- 'sound.reminderAlarmTimeout': def<AlarmTimeout>('sound', 'number', 3),
820
- 'sound.allowCustomReminderSounds': def<boolean>('sound', 'boolean', false),
821
- 'sound.ttsEnabled': def<boolean>('sound', 'boolean', config.isEnrolled),
822
- 'sound.ttsRate': def<number>('sound', 'number', 1.0),
1010
+ 'sound.timerVolume': def<number>('sound', 'number', 0.5, 'VOLUME_SLIDER'),
1011
+ 'sound.reminderVolume': def<number>('sound', 'number', 0.5, 'VOLUME_SLIDER'),
1012
+ 'sound.mediaVolume': def<number>('sound', 'number', 0.5, 'VOLUME_SLIDER'),
1013
+ 'sound.alarmSound': def<AlarmSound>(
1014
+ 'sound',
1015
+ 'string',
1016
+ 'alarm1',
1017
+ 'SELECT',
1018
+ { options: ALARM_SOUND_OPTIONS },
1019
+ ),
1020
+ 'sound.reminderAlarmSound': def<AlarmSound>(
1021
+ 'sound',
1022
+ 'string',
1023
+ 'alarm1',
1024
+ 'SELECT',
1025
+ { options: ALARM_SOUND_OPTIONS },
1026
+ ),
1027
+ 'sound.timerAlarmSound': def<AlarmSound>(
1028
+ 'sound',
1029
+ 'string',
1030
+ 'alarm1',
1031
+ 'SELECT',
1032
+ { options: ALARM_SOUND_OPTIONS },
1033
+ ),
1034
+ 'sound.timerAlarmTimeout': def<AlarmTimeout>(
1035
+ 'sound',
1036
+ 'number',
1037
+ 3,
1038
+ 'SELECT',
1039
+ { options: ALARM_TIMEOUT_OPTIONS },
1040
+ ),
1041
+ 'sound.reminderAlarmTimeout': def<AlarmTimeout>(
1042
+ 'sound',
1043
+ 'number',
1044
+ 3,
1045
+ 'SELECT',
1046
+ { options: ALARM_TIMEOUT_OPTIONS },
1047
+ ),
1048
+ 'sound.allowCustomReminderSounds': def<boolean>(
1049
+ 'sound',
1050
+ 'boolean',
1051
+ false,
1052
+ 'TOGGLE',
1053
+ ),
1054
+ 'sound.ttsEnabled': def<boolean>(
1055
+ 'sound',
1056
+ 'boolean',
1057
+ config.isEnrolled,
1058
+ 'TOGGLE',
1059
+ ),
1060
+ 'sound.ttsRate': def<number>(
1061
+ 'sound',
1062
+ 'number',
1063
+ 1.0,
1064
+ 'SLIDER',
1065
+ { sliderConfig: { min: 0.5, max: 2, step: 0.1 } },
1066
+ ),
823
1067
 
824
1068
  // ═══════════════════════════════════════════════════════════════════════
825
1069
  // Timer
826
1070
  // ═══════════════════════════════════════════════════════════════════════
827
- 'timer.showTimeRemaining': def<boolean>('timer', 'boolean', true),
828
- 'timer.showEndTime': def<boolean>('timer', 'boolean', true),
829
- 'timer.showRestartButton': def<boolean>('timer', 'boolean', true),
830
- 'timer.showPauseButton': def<boolean>('timer', 'boolean', true),
1071
+ 'timer.showTimeRemaining': def<boolean>('timer', 'boolean', true, 'TOGGLE'),
1072
+ 'timer.showEndTime': def<boolean>('timer', 'boolean', true, 'TOGGLE'),
1073
+ 'timer.showRestartButton': def<boolean>('timer', 'boolean', true, 'TOGGLE'),
1074
+ 'timer.showPauseButton': def<boolean>('timer', 'boolean', true, 'TOGGLE'),
831
1075
 
832
1076
  // ═══════════════════════════════════════════════════════════════════════
833
1077
  // Lock screen
834
1078
  // ═══════════════════════════════════════════════════════════════════════
835
- 'lockScreen.pin': def<string>('lockScreen', 'string', ''),
1079
+ 'lockScreen.pin': def<string>('lockScreen', 'string', '', 'PIN_INPUT'),
836
1080
  'lockScreen.inactivityLockEnabled': def<boolean>(
837
1081
  'lockScreen',
838
1082
  'boolean',
839
1083
  false,
1084
+ 'TOGGLE',
840
1085
  ),
841
1086
  'lockScreen.inactivityTimeoutMinutes': def<InactivityTimeoutMinutes>(
842
1087
  'lockScreen',
843
1088
  'number',
844
1089
  5,
1090
+ 'SELECT',
1091
+ {
1092
+ options: [
1093
+ { value: 1, labelKey: 'Settings.Option.InactivityTimeout.1min' },
1094
+ { value: 5, labelKey: 'Settings.Option.InactivityTimeout.5min' },
1095
+ { value: 10, labelKey: 'Settings.Option.InactivityTimeout.10min' },
1096
+ { value: 15, labelKey: 'Settings.Option.InactivityTimeout.15min' },
1097
+ { value: 30, labelKey: 'Settings.Option.InactivityTimeout.30min' },
1098
+ { value: 45, labelKey: 'Settings.Option.InactivityTimeout.45min' },
1099
+ ],
1100
+ },
845
1101
  ),
846
1102
  'lockScreen.clockDisplay': def<LockScreenClockDisplay>(
847
1103
  'lockScreen',
848
1104
  'string',
849
1105
  'digital',
1106
+ 'SELECT',
1107
+ {
1108
+ options: [
1109
+ { value: 'none', labelKey: 'Settings.Option.ClockDisplay.None' },
1110
+ {
1111
+ value: 'digital',
1112
+ labelKey: 'Settings.Option.ClockDisplay.Digital',
1113
+ },
1114
+ {
1115
+ value: 'analog',
1116
+ labelKey: 'Settings.Option.ClockDisplay.Analog',
1117
+ },
1118
+ ],
1119
+ },
1120
+ ),
1121
+ 'lockScreen.showDate': def<boolean>(
1122
+ 'lockScreen',
1123
+ 'boolean',
1124
+ true,
1125
+ 'TOGGLE',
1126
+ ),
1127
+ 'lockScreen.showHourNumbers': def<boolean>(
1128
+ 'lockScreen',
1129
+ 'boolean',
1130
+ true,
1131
+ 'TOGGLE',
850
1132
  ),
851
- 'lockScreen.showDate': def<boolean>('lockScreen', 'boolean', true),
852
- 'lockScreen.showHourNumbers': def<boolean>('lockScreen', 'boolean', true),
853
1133
  'lockScreen.imageMode': def<LockScreenImageMode>(
854
1134
  'lockScreen',
855
1135
  'string',
856
1136
  'none',
1137
+ 'SELECT',
1138
+ {
1139
+ options: [
1140
+ { value: 'none', labelKey: 'Settings.Option.ImageMode.None' },
1141
+ {
1142
+ value: 'background',
1143
+ labelKey: 'Settings.Option.ImageMode.Background',
1144
+ },
1145
+ {
1146
+ value: 'photoFrame',
1147
+ labelKey: 'Settings.Option.ImageMode.PhotoFrame',
1148
+ },
1149
+ ],
1150
+ },
857
1151
  ),
858
1152
  'lockScreen.backgroundImage': def<string | null>(
859
1153
  'lockScreen',
860
1154
  'json',
861
1155
  null,
1156
+ 'CUSTOM_IMAGE',
862
1157
  ),
863
1158
  'lockScreen.photoFrameImages': def<LockScreenImage[]>(
864
1159
  'lockScreen',
865
1160
  'json',
866
1161
  [],
1162
+ 'CUSTOM_IMAGE_ARRAY',
867
1163
  ),
868
1164
  'lockScreen.photoFrameIntervalSeconds': def<PhotoFrameIntervalSeconds>(
869
1165
  'lockScreen',
870
1166
  'number',
871
1167
  60,
1168
+ 'SELECT',
1169
+ {
1170
+ options: [
1171
+ { value: 30, labelKey: 'Settings.Option.PhotoFrameInterval.30sec' },
1172
+ { value: 60, labelKey: 'Settings.Option.PhotoFrameInterval.1min' },
1173
+ { value: 120, labelKey: 'Settings.Option.PhotoFrameInterval.2min' },
1174
+ { value: 300, labelKey: 'Settings.Option.PhotoFrameInterval.5min' },
1175
+ ],
1176
+ },
872
1177
  ),
873
1178
 
874
1179
  // ═══════════════════════════════════════════════════════════════════════
875
1180
  // Touch / gestures
876
1181
  // ═══════════════════════════════════════════════════════════════════════
877
- 'touch.enableTapToCreate': def<boolean>('touch', 'boolean', false),
878
- 'touch.enableDragDrop': def<boolean>('touch', 'boolean', false),
1182
+ 'touch.enableTapToCreate': def<boolean>('touch', 'boolean', false, 'TOGGLE'),
1183
+ 'touch.enableDragDrop': def<boolean>('touch', 'boolean', false, 'TOGGLE'),
879
1184
 
880
1185
  // ═══════════════════════════════════════════════════════════════════════
881
1186
  // Device (not synced unless noted)
882
1187
  // ═══════════════════════════════════════════════════════════════════════
883
- 'device.id': def<string>('device', 'string', '', false),
884
- 'device.timePickerMode': def<TimePickerMode>('device', 'string', 'dials'),
885
- 'device.devMenuEnabled': def<boolean>('device', 'boolean', false, false),
1188
+ 'device.id': def<string>('device', 'string', '', 'HIDDEN', { sync: false }),
1189
+ 'device.timePickerMode': def<TimePickerMode>(
1190
+ 'device',
1191
+ 'string',
1192
+ 'dials',
1193
+ 'SELECT',
1194
+ {
1195
+ options: [
1196
+ { value: 'dials', labelKey: 'Settings.Option.TimePickerMode.Dials' },
1197
+ {
1198
+ value: 'keypad',
1199
+ labelKey: 'Settings.Option.TimePickerMode.Keypad',
1200
+ },
1201
+ ],
1202
+ },
1203
+ ),
1204
+ 'device.devMenuEnabled': def<boolean>(
1205
+ 'device',
1206
+ 'boolean',
1207
+ false,
1208
+ 'HIDDEN',
1209
+ { sync: false },
1210
+ ),
886
1211
  'device.authWarningDismissTtlDays': def<number>(
887
1212
  'device',
888
1213
  'number',
889
1214
  3,
890
- false,
1215
+ 'HIDDEN',
1216
+ { sync: false },
891
1217
  ),
892
1218
 
893
1219
  // ═══════════════════════════════════════════════════════════════════════
@@ -897,27 +1223,42 @@ function buildEntries(config: RegistryConfig) {
897
1223
  'language',
898
1224
  'string',
899
1225
  config.defaultLocale,
1226
+ 'CUSTOM_LANGUAGE_PICKER',
1227
+ {
1228
+ options: [
1229
+ { value: 'nb', labelKey: 'Settings.Option.Language.Norwegian' },
1230
+ { value: 'en', labelKey: 'Settings.Option.Language.English' },
1231
+ ],
1232
+ },
900
1233
  ),
901
1234
 
902
1235
  // ═══════════════════════════════════════════════════════════════════════
903
1236
  // Notifications
904
1237
  // ═══════════════════════════════════════════════════════════════════════
905
- 'notification.enabled': def<boolean>('notification', 'boolean', false),
1238
+ 'notification.enabled': def<boolean>(
1239
+ 'notification',
1240
+ 'boolean',
1241
+ false,
1242
+ 'TOGGLE',
1243
+ ),
906
1244
  'notification.notifyAllCalendars': def<boolean>(
907
1245
  'notification',
908
1246
  'boolean',
909
1247
  true,
1248
+ 'TOGGLE',
910
1249
  ),
911
1250
  'notification.enabledCalendarIds': def<string[]>(
912
1251
  'notification',
913
1252
  'json',
914
1253
  [],
1254
+ 'CUSTOM_CALENDAR_IDS',
915
1255
  ),
916
1256
  'notification.hasBeenPrompted': def<boolean>(
917
1257
  'notification',
918
1258
  'boolean',
919
1259
  false,
920
- false,
1260
+ 'HIDDEN',
1261
+ { sync: false },
921
1262
  ),
922
1263
 
923
1264
  // ═══════════════════════════════════════════════════════════════════════
@@ -928,141 +1269,173 @@ function buildEntries(config: RegistryConfig) {
928
1269
  'chronological',
929
1270
  'boolean',
930
1271
  true,
1272
+ 'TOGGLE',
931
1273
  ),
932
1274
  'chronological.header.showClock': def<boolean>(
933
1275
  'chronological',
934
1276
  'boolean',
935
1277
  true,
1278
+ 'TOGGLE',
936
1279
  ),
937
1280
  'chronological.header.showCurrentYearInDate': def<boolean>(
938
1281
  'chronological',
939
1282
  'boolean',
940
1283
  false,
1284
+ 'TOGGLE',
941
1285
  ),
942
1286
  'chronological.header.showTimeOfDay': def<boolean>(
943
1287
  'chronological',
944
1288
  'boolean',
945
1289
  false,
1290
+ 'TOGGLE',
946
1291
  ),
947
1292
  // Footer
948
1293
  'chronological.footer.showMenuButton': def<boolean>(
949
1294
  'chronological',
950
1295
  'boolean',
951
1296
  true,
1297
+ 'TOGGLE',
952
1298
  ),
953
1299
  'chronological.footer.showViewSwitcherDay': def<boolean>(
954
1300
  'chronological',
955
1301
  'boolean',
956
1302
  true,
1303
+ 'TOGGLE',
957
1304
  ),
958
1305
  'chronological.footer.showViewSwitcherWeek': def<boolean>(
959
1306
  'chronological',
960
1307
  'boolean',
961
1308
  true,
1309
+ 'TOGGLE',
962
1310
  ),
963
1311
  'chronological.footer.showViewSwitcherMonth': def<boolean>(
964
1312
  'chronological',
965
1313
  'boolean',
966
1314
  true,
1315
+ 'TOGGLE',
967
1316
  ),
968
1317
  'chronological.footer.showTimerButton': def<boolean>(
969
1318
  'chronological',
970
1319
  'boolean',
971
1320
  true,
1321
+ 'TOGGLE',
972
1322
  ),
973
1323
  'chronological.footer.showNewEventButton': def<boolean>(
974
1324
  'chronological',
975
1325
  'boolean',
976
1326
  true,
1327
+ 'TOGGLE',
977
1328
  ),
978
1329
  'chronological.footer.showSettingsButton': def<boolean>(
979
1330
  'chronological',
980
1331
  'boolean',
981
1332
  true,
1333
+ 'TOGGLE',
982
1334
  ),
983
1335
  // Timer features
984
1336
  'chronological.timer.showNewCountdown': def<boolean>(
985
1337
  'chronological',
986
1338
  'boolean',
987
1339
  true,
1340
+ 'TOGGLE',
988
1341
  ),
989
1342
  'chronological.timer.showFromTemplate': def<boolean>(
990
1343
  'chronological',
991
1344
  'boolean',
992
1345
  true,
1346
+ 'TOGGLE',
993
1347
  ),
994
1348
  'chronological.timer.showEditTemplate': def<boolean>(
995
1349
  'chronological',
996
1350
  'boolean',
997
1351
  true,
1352
+ 'TOGGLE',
998
1353
  ),
999
1354
  'chronological.timer.showDeleteTemplate': def<boolean>(
1000
1355
  'chronological',
1001
1356
  'boolean',
1002
1357
  true,
1358
+ 'TOGGLE',
1003
1359
  ),
1004
1360
  'chronological.timer.showAddTemplate': def<boolean>(
1005
1361
  'chronological',
1006
1362
  'boolean',
1007
1363
  true,
1364
+ 'TOGGLE',
1008
1365
  ),
1009
1366
  // Menu
1010
1367
  'chronological.menu.showSettingsButton': def<boolean>(
1011
1368
  'chronological',
1012
1369
  'boolean',
1013
1370
  true,
1371
+ 'TOGGLE',
1014
1372
  ),
1015
1373
  // Quick settings
1016
1374
  'chronological.quickSettings.showTimerVolume': def<boolean>(
1017
1375
  'chronological',
1018
1376
  'boolean',
1019
1377
  true,
1378
+ 'TOGGLE',
1020
1379
  ),
1021
1380
  'chronological.quickSettings.showReminderVolume': def<boolean>(
1022
1381
  'chronological',
1023
1382
  'boolean',
1024
1383
  true,
1384
+ 'TOGGLE',
1025
1385
  ),
1026
1386
  'chronological.quickSettings.showMediaVolume': def<boolean>(
1027
1387
  'chronological',
1028
1388
  'boolean',
1029
1389
  true,
1390
+ 'TOGGLE',
1030
1391
  ),
1031
1392
  'chronological.quickSettings.showBrightness': def<boolean>(
1032
1393
  'chronological',
1033
1394
  'boolean',
1034
1395
  true,
1396
+ 'TOGGLE',
1035
1397
  ),
1036
1398
  'chronological.quickSettings.showLockScreen': def<boolean>(
1037
1399
  'chronological',
1038
1400
  'boolean',
1039
1401
  true,
1402
+ 'TOGGLE',
1040
1403
  ),
1041
1404
  // Time-of-day periods
1042
1405
  'chronological.timeOfDay.morningStart': def<number>(
1043
1406
  'chronological',
1044
1407
  'number',
1045
1408
  6,
1409
+ 'SLIDER',
1410
+ { sliderConfig: TIME_OF_DAY_SLIDER },
1046
1411
  ),
1047
1412
  'chronological.timeOfDay.forenoonStart': def<number>(
1048
1413
  'chronological',
1049
1414
  'number',
1050
1415
  9,
1416
+ 'SLIDER',
1417
+ { sliderConfig: TIME_OF_DAY_SLIDER },
1051
1418
  ),
1052
1419
  'chronological.timeOfDay.afternoonStart': def<number>(
1053
1420
  'chronological',
1054
1421
  'number',
1055
1422
  12,
1423
+ 'SLIDER',
1424
+ { sliderConfig: TIME_OF_DAY_SLIDER },
1056
1425
  ),
1057
1426
  'chronological.timeOfDay.eveningStart': def<number>(
1058
1427
  'chronological',
1059
1428
  'number',
1060
1429
  18,
1430
+ 'SLIDER',
1431
+ { sliderConfig: TIME_OF_DAY_SLIDER },
1061
1432
  ),
1062
1433
  'chronological.timeOfDay.nightStart': def<number>(
1063
1434
  'chronological',
1064
1435
  'number',
1065
1436
  0,
1437
+ 'SLIDER',
1438
+ { sliderConfig: TIME_OF_DAY_SLIDER },
1066
1439
  ),
1067
1440
 
1068
1441
  // ═══════════════════════════════════════════════════════════════════════
@@ -1073,80 +1446,115 @@ function buildEntries(config: RegistryConfig) {
1073
1446
  'chronologicalEventForm',
1074
1447
  'boolean',
1075
1448
  true,
1449
+ 'TOGGLE',
1076
1450
  ),
1077
1451
  'chronologicalEventForm.field.recurrence': def<boolean>(
1078
1452
  'chronologicalEventForm',
1079
1453
  'boolean',
1080
1454
  true,
1455
+ 'TOGGLE',
1081
1456
  ),
1082
1457
  'chronologicalEventForm.field.acknowledge': def<boolean>(
1083
1458
  'chronologicalEventForm',
1084
1459
  'boolean',
1085
1460
  true,
1461
+ 'TOGGLE',
1086
1462
  ),
1087
1463
  'chronologicalEventForm.field.checklist': def<boolean>(
1088
1464
  'chronologicalEventForm',
1089
1465
  'boolean',
1090
1466
  true,
1467
+ 'TOGGLE',
1091
1468
  ),
1092
1469
  'chronologicalEventForm.field.extraImages': def<boolean>(
1093
1470
  'chronologicalEventForm',
1094
1471
  'boolean',
1095
1472
  true,
1473
+ 'TOGGLE',
1096
1474
  ),
1097
1475
  'chronologicalEventForm.field.reminders': def<boolean>(
1098
1476
  'chronologicalEventForm',
1099
1477
  'boolean',
1100
1478
  true,
1479
+ 'TOGGLE',
1101
1480
  ),
1102
1481
  'chronologicalEventForm.field.audioClips': def<boolean>(
1103
1482
  'chronologicalEventForm',
1104
1483
  'boolean',
1105
1484
  true,
1485
+ 'TOGGLE',
1106
1486
  ),
1107
1487
  // Fixed fields
1108
1488
  'chronologicalEventForm.fixedField.allDay': def<boolean>(
1109
1489
  'chronologicalEventForm',
1110
1490
  'boolean',
1111
1491
  true,
1492
+ 'TOGGLE',
1112
1493
  ),
1113
1494
  'chronologicalEventForm.fixedField.endTime': def<boolean>(
1114
1495
  'chronologicalEventForm',
1115
1496
  'boolean',
1116
1497
  true,
1498
+ 'TOGGLE',
1117
1499
  ),
1118
1500
  'chronologicalEventForm.fixedField.visibility': def<boolean>(
1119
1501
  'chronologicalEventForm',
1120
1502
  'boolean',
1121
1503
  true,
1504
+ 'TOGGLE',
1122
1505
  ),
1123
1506
  // Suggest end time
1124
1507
  'chronologicalEventForm.suggestEndTime.enabled': def<boolean>(
1125
1508
  'chronologicalEventForm',
1126
1509
  'boolean',
1127
1510
  false,
1511
+ 'TOGGLE',
1128
1512
  ),
1129
1513
  'chronologicalEventForm.suggestEndTime.value': def<number>(
1130
1514
  'chronologicalEventForm',
1131
1515
  'number',
1132
1516
  30,
1517
+ 'SLIDER',
1518
+ { sliderConfig: { min: 5, max: 480, step: 5 } },
1133
1519
  ),
1134
1520
  'chronologicalEventForm.suggestEndTime.unit': def<SuggestEndTimeUnit>(
1135
1521
  'chronologicalEventForm',
1136
1522
  'string',
1137
1523
  'minutes',
1524
+ 'SELECT',
1525
+ {
1526
+ options: [
1527
+ {
1528
+ value: 'minutes',
1529
+ labelKey: 'Settings.Option.SuggestEndTimeUnit.Minutes',
1530
+ },
1531
+ {
1532
+ value: 'hours',
1533
+ labelKey: 'Settings.Option.SuggestEndTimeUnit.Hours',
1534
+ },
1535
+ ],
1536
+ },
1138
1537
  ),
1139
1538
  // Default visibility
1140
1539
  'chronologicalEventForm.defaultVisibility': def<EventVisibility>(
1141
1540
  'chronologicalEventForm',
1142
1541
  'string',
1143
1542
  'Private',
1543
+ 'SELECT',
1544
+ {
1545
+ options: [
1546
+ { value: 'Public', labelKey: 'Settings.Option.Visibility.Public' },
1547
+ { value: 'Private', labelKey: 'Settings.Option.Visibility.Private' },
1548
+ { value: 'Custom', labelKey: 'Settings.Option.Visibility.Custom' },
1549
+ ],
1550
+ },
1144
1551
  ),
1145
1552
  // Reminder presets
1146
1553
  'chronologicalEventForm.reminderPresets.timed': def<number[]>(
1147
1554
  'chronologicalEventForm',
1148
1555
  'json',
1149
1556
  [5, 15, 30, 60, 120, 1440],
1557
+ 'CUSTOM_REMINDER_PRESETS',
1150
1558
  ),
1151
1559
  'chronologicalEventForm.reminderPresets.allDay': def<AllDayPreset[]>(
1152
1560
  'chronologicalEventForm',
@@ -1156,6 +1564,7 @@ function buildEntries(config: RegistryConfig) {
1156
1564
  { daysBefore: 1, time: '18:00' },
1157
1565
  { daysBefore: 2, time: '09:00' },
1158
1566
  ],
1567
+ 'CUSTOM_REMINDER_PRESETS',
1159
1568
  ),
1160
1569
  } as const satisfies Record<string, SettingDef>;
1161
1570
  }
@@ -1316,6 +1725,12 @@ export type ParsedSettingEntry = {
1316
1725
  descriptionKey?: string;
1317
1726
  /** The setting value */
1318
1727
  value: unknown;
1728
+ /** UI component type for rendering this setting */
1729
+ uiType: SettingUiType;
1730
+ /** Available options for SELECT-type settings */
1731
+ options?: readonly SettingOption[];
1732
+ /** Slider configuration for SLIDER-type settings */
1733
+ sliderConfig?: SliderConfig;
1319
1734
  };
1320
1735
 
1321
1736
  export type ParsedSettingsGroup = {
@@ -1374,6 +1789,9 @@ export function parseSettingsSnapshot(
1374
1789
  }
1375
1790
 
1376
1791
  const labelDef = SETTINGS_LABELS[key];
1792
+ const entryDef = key in registry.entries
1793
+ ? registry.entries[key as SettingKey]
1794
+ : undefined;
1377
1795
  groups.get(category)!.push({
1378
1796
  key,
1379
1797
  name,
@@ -1381,6 +1799,9 @@ export function parseSettingsSnapshot(
1381
1799
  labelKey: labelDef?.labelKey,
1382
1800
  descriptionKey: labelDef?.descriptionKey,
1383
1801
  value,
1802
+ uiType: entryDef?.uiType ?? 'TEXT_INPUT',
1803
+ ...(entryDef?.options && { options: entryDef.options as readonly SettingOption[] }),
1804
+ ...(entryDef?.sliderConfig && { sliderConfig: entryDef.sliderConfig }),
1384
1805
  });
1385
1806
  }
1386
1807