@aglyn/shared-ui-theme 1.0.0-beta.246 → 1.0.0-beta.249

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.
@@ -424,8 +424,12 @@ export const FONT_FAMILY_FIELD = {
424
424
  step: 1
425
425
  }
426
426
  };
427
- /** The raw-JSON component overrides, limited to the sanitizer's whitelist. */ export const COMPONENT_OVERRIDES_FIELD = {
428
- label: 'Component overrides',
427
+ /**
428
+ * The site-wide look of components: the click-only fields in
429
+ * {@link THEME_COMPONENT_LOOK_FIELDS}, with the full override set behind
430
+ * them limited to the sanitizer's whitelist.
431
+ */ export const COMPONENT_OVERRIDES_FIELD = {
432
+ label: 'Style components',
429
433
  components: HOST_THEME_COMPONENT_WHITELIST
430
434
  };
431
435
  /**
@@ -506,7 +510,7 @@ export const FONT_FAMILY_FIELD = {
506
510
  {
507
511
  id: 'components',
508
512
  label: COMPONENT_OVERRIDES_FIELD.label,
509
- kind: 'json',
513
+ kind: 'components',
510
514
  perScheme: false,
511
515
  options: COMPONENT_OVERRIDES_FIELD.components
512
516
  }
@@ -541,8 +545,12 @@ export const FONT_FAMILY_FIELD = {
541
545
  }
542
546
  /**
543
547
  * Sets one color for one scheme, or clears it with `undefined` so the slot
544
- * inherits again. A palette color writes `main` and keeps whatever else its
545
- * record carries, and clearing it drops the record; a group member that
548
+ * inherits again. A palette color writes a record of `main` alone: its
549
+ * `light`, `dark` and `contrastText` were derived from (or authored for) the
550
+ * old `main`, and the editor offers no control for them, so a kept shade
551
+ * would go on painting the old color (an outlined button reads `dark`) with
552
+ * no way for the owner to see or change it. Dropped, the theme derives them
553
+ * from the new `main`. Clearing it drops the record; a group member that
546
554
  * leaves its group empty drops the group.
547
555
  */ export function writeThemeColor(theme, scheme, token, hex) {
548
556
  return setSchemeValue(theme, scheme, (colors)=>{
@@ -555,10 +563,19 @@ export const FONT_FAMILY_FIELD = {
555
563
  const [group, key] = token.split('.');
556
564
  if (!key) {
557
565
  const paletteKey = group;
558
- if (hex) next[paletteKey] = _extends({}, next[paletteKey], {
566
+ if (hex) next[paletteKey] = {
559
567
  main: hex
560
- });
568
+ };
561
569
  else delete next[paletteKey];
570
+ // An accent's tint is a wash of its old `main` for the same reason, so
571
+ // it goes too and the theme washes one from the new color.
572
+ const tint = next.tint;
573
+ if (tint && paletteKey in tint) {
574
+ const rest = _extends({}, tint);
575
+ delete rest[paletteKey];
576
+ if (Object.keys(rest).length) next.tint = rest;
577
+ else delete next.tint;
578
+ }
562
579
  return next;
563
580
  }
564
581
  const groupKey = group;
@@ -865,5 +882,520 @@ function fontEntry(choice) {
865
882
  components
866
883
  });
867
884
  }
885
+ /**
886
+ * Removes one leaf from the site's component overrides, and every slot,
887
+ * branch and component it leaves empty, so a field set back to "Theme
888
+ * default" stores nothing and the site inherits that property again.
889
+ */ export function clearComponentOverride(theme, target) {
890
+ var _theme_components;
891
+ const existing = (_theme_components = theme.components) == null ? void 0 : _theme_components[target.component];
892
+ if (!existing) return theme;
893
+ const entry = _extends({}, existing);
894
+ if (target.target === 'defaultProps') {
895
+ const props = _extends({}, entry.defaultProps);
896
+ delete props[target.property];
897
+ if (Object.keys(props).length) entry.defaultProps = props;
898
+ else delete entry.defaultProps;
899
+ } else {
900
+ var _target_slot;
901
+ const branch = target.target === 'sx' ? 'sx' : 'styleOverrides';
902
+ const styles = _extends({}, entry[branch]);
903
+ const slot = (_target_slot = target.slot) != null ? _target_slot : 'root';
904
+ const slotStyles = _extends({}, styles[slot]);
905
+ if (target.media) {
906
+ const query = THEME_EDITOR_MEDIA_QUERIES[target.media];
907
+ const scoped = _extends({}, slotStyles[query]);
908
+ delete scoped[target.property];
909
+ if (Object.keys(scoped).length) slotStyles[query] = scoped;
910
+ else delete slotStyles[query];
911
+ } else {
912
+ delete slotStyles[target.property];
913
+ }
914
+ if (Object.keys(slotStyles).length) styles[slot] = slotStyles;
915
+ else delete styles[slot];
916
+ if (Object.keys(styles).length) {
917
+ if (branch === 'sx') entry.sx = styles;
918
+ else entry.styleOverrides = styles;
919
+ } else {
920
+ delete entry[branch];
921
+ }
922
+ }
923
+ const components = _extends({}, theme.components);
924
+ if (Object.keys(entry).length) components[target.component] = entry;
925
+ else delete components[target.component];
926
+ const next = _extends({}, theme);
927
+ if (Object.keys(components).length) next.components = components;
928
+ else delete next.components;
929
+ return next;
930
+ }
931
+ /** The value one override leaf holds; `undefined` when the site does not set it. */ export function readComponentOverride(theme, target) {
932
+ var _target_slot;
933
+ var _theme_components, _entry_defaultProps, _slotStyles_query;
934
+ const entry = theme == null ? void 0 : (_theme_components = theme.components) == null ? void 0 : _theme_components[target.component];
935
+ if (!entry) return undefined;
936
+ if (target.target === 'defaultProps') return (_entry_defaultProps = entry.defaultProps) == null ? void 0 : _entry_defaultProps[target.property];
937
+ const styles = target.target === 'sx' ? entry.sx : entry.styleOverrides;
938
+ const slotStyles = styles == null ? void 0 : styles[(_target_slot = target.slot) != null ? _target_slot : 'root'];
939
+ if (!target.media) return slotStyles == null ? void 0 : slotStyles[target.property];
940
+ const query = THEME_EDITOR_MEDIA_QUERIES[target.media];
941
+ return slotStyles == null ? void 0 : (_slotStyles_query = slotStyles[query]) == null ? void 0 : _slotStyles_query[target.property];
942
+ }
943
+ export const THEME_COMPONENT_LOOK_GROUPS = [
944
+ {
945
+ group: 'button',
946
+ label: 'Buttons'
947
+ },
948
+ {
949
+ group: 'input',
950
+ label: 'Form inputs'
951
+ },
952
+ {
953
+ group: 'card',
954
+ label: 'Cards'
955
+ }
956
+ ];
957
+ /** The select's value for a field the site does not set: the theme's own look. */ export const THEME_LOOK_DEFAULT = '';
958
+ /** The select's value for a leaf set to something no choice names, by hand or by AI. */ export const THEME_LOOK_CUSTOM = '__custom__';
959
+ function lookTarget(component, target, property, slot = target === 'defaultProps' ? null : 'root') {
960
+ return {
961
+ component,
962
+ target,
963
+ slot,
964
+ property,
965
+ media: null
966
+ };
967
+ }
968
+ /**
969
+ * Corner choices, as multiples of the site's Border radius (an `sx`
970
+ * `borderRadius` number is one), so a component follows the Shape & spacing
971
+ * slider instead of freezing a pixel value.
972
+ */ const CORNER_OPTIONS = [
973
+ {
974
+ value: 'square',
975
+ label: 'Square',
976
+ stored: 0
977
+ },
978
+ {
979
+ value: 'subtle',
980
+ label: 'Half the site radius',
981
+ stored: 0.5
982
+ },
983
+ {
984
+ value: 'site',
985
+ label: 'The site radius',
986
+ stored: 1
987
+ },
988
+ {
989
+ value: 'rounded',
990
+ label: 'Twice the site radius',
991
+ stored: 2
992
+ },
993
+ {
994
+ value: 'extra',
995
+ label: 'Three times the site radius',
996
+ stored: 3
997
+ }
998
+ ];
999
+ const PILL_OPTION = {
1000
+ value: 'pill',
1001
+ label: 'Pill',
1002
+ stored: '999px'
1003
+ };
1004
+ /**
1005
+ * Every component look field, in the order the card shows them.
1006
+ *
1007
+ * Inputs style both of MUI's boxes: `MuiOutlinedInput` and `MuiFilledInput`,
1008
+ * because the Input style choice picks between them and the other choices
1009
+ * have to hold whichever is in use. A border color sets the outlined box's
1010
+ * own border, which the Besigner's Input element draws, and the notched
1011
+ * outline a stock text field draws.
1012
+ */ export const THEME_COMPONENT_LOOK_FIELDS = [
1013
+ {
1014
+ id: 'button.variant',
1015
+ group: 'button',
1016
+ label: 'Button style',
1017
+ kind: 'select',
1018
+ targets: [
1019
+ lookTarget('MuiButton', 'defaultProps', 'variant')
1020
+ ],
1021
+ options: [
1022
+ {
1023
+ value: 'contained',
1024
+ label: 'Filled',
1025
+ stored: 'contained'
1026
+ },
1027
+ {
1028
+ value: 'outlined',
1029
+ label: 'Outlined',
1030
+ stored: 'outlined'
1031
+ },
1032
+ {
1033
+ value: 'text',
1034
+ label: 'Text only',
1035
+ stored: 'text'
1036
+ }
1037
+ ]
1038
+ },
1039
+ {
1040
+ id: 'button.corners',
1041
+ group: 'button',
1042
+ label: 'Button corners',
1043
+ kind: 'select',
1044
+ targets: [
1045
+ lookTarget('MuiButton', 'sx', 'borderRadius')
1046
+ ],
1047
+ options: [
1048
+ ...CORNER_OPTIONS,
1049
+ PILL_OPTION
1050
+ ]
1051
+ },
1052
+ {
1053
+ id: 'button.textCase',
1054
+ group: 'button',
1055
+ label: 'Button text case',
1056
+ kind: 'select',
1057
+ targets: [
1058
+ lookTarget('MuiButton', 'styleOverrides', 'textTransform')
1059
+ ],
1060
+ options: [
1061
+ {
1062
+ value: 'none',
1063
+ label: 'As typed',
1064
+ stored: 'none'
1065
+ },
1066
+ {
1067
+ value: 'uppercase',
1068
+ label: 'UPPERCASE',
1069
+ stored: 'uppercase'
1070
+ },
1071
+ {
1072
+ value: 'capitalize',
1073
+ label: 'Capitalize Each Word',
1074
+ stored: 'capitalize'
1075
+ }
1076
+ ]
1077
+ },
1078
+ {
1079
+ id: 'button.weight',
1080
+ group: 'button',
1081
+ label: 'Button text weight',
1082
+ kind: 'select',
1083
+ targets: [
1084
+ lookTarget('MuiButton', 'styleOverrides', 'fontWeight')
1085
+ ],
1086
+ options: [
1087
+ {
1088
+ value: '400',
1089
+ label: 'Regular',
1090
+ stored: 400
1091
+ },
1092
+ {
1093
+ value: '500',
1094
+ label: 'Medium',
1095
+ stored: 500
1096
+ },
1097
+ {
1098
+ value: '600',
1099
+ label: 'Semibold',
1100
+ stored: 600
1101
+ },
1102
+ {
1103
+ value: '700',
1104
+ label: 'Bold',
1105
+ stored: 700
1106
+ }
1107
+ ]
1108
+ },
1109
+ {
1110
+ id: 'button.padding',
1111
+ group: 'button',
1112
+ label: 'Button padding',
1113
+ kind: 'select',
1114
+ // Spacing units, so padding follows the site's Spacing unit.
1115
+ targets: [
1116
+ lookTarget('MuiButton', 'sx', 'px'),
1117
+ lookTarget('MuiButton', 'sx', 'py')
1118
+ ],
1119
+ options: [
1120
+ {
1121
+ value: 'compact',
1122
+ label: 'Compact',
1123
+ stored: [
1124
+ 1.5,
1125
+ 0.5
1126
+ ]
1127
+ },
1128
+ {
1129
+ value: 'comfortable',
1130
+ label: 'Comfortable',
1131
+ stored: [
1132
+ 2,
1133
+ 0.75
1134
+ ]
1135
+ },
1136
+ {
1137
+ value: 'roomy',
1138
+ label: 'Roomy',
1139
+ stored: [
1140
+ 3,
1141
+ 1
1142
+ ]
1143
+ },
1144
+ {
1145
+ value: 'wide',
1146
+ label: 'Wide',
1147
+ stored: [
1148
+ 4,
1149
+ 1.25
1150
+ ]
1151
+ }
1152
+ ]
1153
+ },
1154
+ {
1155
+ id: 'input.variant',
1156
+ group: 'input',
1157
+ label: 'Input style',
1158
+ kind: 'select',
1159
+ targets: [
1160
+ lookTarget('MuiTextField', 'defaultProps', 'variant')
1161
+ ],
1162
+ options: [
1163
+ {
1164
+ value: 'outlined',
1165
+ label: 'Outlined',
1166
+ stored: 'outlined'
1167
+ },
1168
+ {
1169
+ value: 'filled',
1170
+ label: 'Filled',
1171
+ stored: 'filled'
1172
+ }
1173
+ ]
1174
+ },
1175
+ {
1176
+ id: 'input.corners',
1177
+ group: 'input',
1178
+ label: 'Input corners',
1179
+ kind: 'select',
1180
+ targets: [
1181
+ lookTarget('MuiOutlinedInput', 'sx', 'borderRadius'),
1182
+ lookTarget('MuiFilledInput', 'sx', 'borderRadius')
1183
+ ],
1184
+ // A filled box's underline runs edge to edge; clipping it to the corners
1185
+ // keeps it inside a rounded box.
1186
+ companions: [
1187
+ _extends({}, lookTarget('MuiFilledInput', 'styleOverrides', 'overflow'), {
1188
+ value: 'hidden'
1189
+ })
1190
+ ],
1191
+ options: [
1192
+ ...CORNER_OPTIONS,
1193
+ PILL_OPTION
1194
+ ]
1195
+ },
1196
+ {
1197
+ id: 'input.background',
1198
+ group: 'input',
1199
+ label: 'Input background',
1200
+ kind: 'color',
1201
+ targets: [
1202
+ lookTarget('MuiOutlinedInput', 'sx', 'bgcolor'),
1203
+ lookTarget('MuiFilledInput', 'sx', 'bgcolor')
1204
+ ],
1205
+ options: [
1206
+ {
1207
+ value: 'transparent',
1208
+ label: 'None',
1209
+ stored: 'transparent'
1210
+ },
1211
+ {
1212
+ value: 'paper',
1213
+ label: 'Paper',
1214
+ stored: 'background.paper',
1215
+ token: 'background.paper'
1216
+ },
1217
+ {
1218
+ value: 'background',
1219
+ label: 'Background',
1220
+ stored: 'background.default',
1221
+ token: 'background.default'
1222
+ },
1223
+ {
1224
+ value: 'tintPrimary',
1225
+ label: 'Primary tint',
1226
+ stored: 'tint.primary',
1227
+ token: 'tint.primary'
1228
+ },
1229
+ {
1230
+ value: 'tintSecondary',
1231
+ label: 'Secondary tint',
1232
+ stored: 'tint.secondary',
1233
+ token: 'tint.secondary'
1234
+ },
1235
+ {
1236
+ value: 'tintTertiary',
1237
+ label: 'Tertiary tint',
1238
+ stored: 'tint.tertiary',
1239
+ token: 'tint.tertiary'
1240
+ }
1241
+ ]
1242
+ },
1243
+ {
1244
+ id: 'input.border',
1245
+ group: 'input',
1246
+ label: 'Input border color',
1247
+ kind: 'color',
1248
+ targets: [
1249
+ lookTarget('MuiOutlinedInput', 'sx', 'borderColor'),
1250
+ lookTarget('MuiOutlinedInput', 'sx', 'borderColor', 'notchedOutline')
1251
+ ],
1252
+ options: [
1253
+ {
1254
+ value: 'divider',
1255
+ label: 'Divider',
1256
+ stored: 'divider',
1257
+ token: 'divider'
1258
+ },
1259
+ {
1260
+ value: 'textSecondary',
1261
+ label: 'Secondary text',
1262
+ stored: 'text.secondary',
1263
+ token: 'text.secondary'
1264
+ },
1265
+ {
1266
+ value: 'text',
1267
+ label: 'Text',
1268
+ stored: 'text.primary',
1269
+ token: 'text.primary'
1270
+ },
1271
+ {
1272
+ value: 'primary',
1273
+ label: 'Primary',
1274
+ stored: 'primary.main',
1275
+ token: 'primary'
1276
+ },
1277
+ {
1278
+ value: 'secondary',
1279
+ label: 'Secondary',
1280
+ stored: 'secondary.main',
1281
+ token: 'secondary'
1282
+ },
1283
+ {
1284
+ value: 'tertiary',
1285
+ label: 'Tertiary',
1286
+ stored: 'tertiary.main',
1287
+ token: 'tertiary'
1288
+ }
1289
+ ]
1290
+ },
1291
+ {
1292
+ id: 'card.variant',
1293
+ group: 'card',
1294
+ label: 'Card style',
1295
+ kind: 'select',
1296
+ targets: [
1297
+ lookTarget('MuiCard', 'defaultProps', 'variant')
1298
+ ],
1299
+ options: [
1300
+ {
1301
+ value: 'elevation',
1302
+ label: 'Shadow',
1303
+ stored: 'elevation'
1304
+ },
1305
+ {
1306
+ value: 'outlined',
1307
+ label: 'Outline',
1308
+ stored: 'outlined'
1309
+ }
1310
+ ]
1311
+ },
1312
+ {
1313
+ id: 'card.elevation',
1314
+ group: 'card',
1315
+ label: 'Card shadow depth',
1316
+ kind: 'select',
1317
+ targets: [
1318
+ lookTarget('MuiCard', 'defaultProps', 'elevation')
1319
+ ],
1320
+ options: [
1321
+ {
1322
+ value: '0',
1323
+ label: 'Flat',
1324
+ stored: 0
1325
+ },
1326
+ {
1327
+ value: '1',
1328
+ label: 'Soft',
1329
+ stored: 1
1330
+ },
1331
+ {
1332
+ value: '3',
1333
+ label: 'Medium',
1334
+ stored: 3
1335
+ },
1336
+ {
1337
+ value: '8',
1338
+ label: 'Strong',
1339
+ stored: 8
1340
+ }
1341
+ ]
1342
+ },
1343
+ {
1344
+ id: 'card.corners',
1345
+ group: 'card',
1346
+ label: 'Card corners',
1347
+ kind: 'select',
1348
+ targets: [
1349
+ lookTarget('MuiCard', 'sx', 'borderRadius')
1350
+ ],
1351
+ options: CORNER_OPTIONS
1352
+ }
1353
+ ];
1354
+ /** The catalog entry for a look field id. */ export function themeComponentLookField(id) {
1355
+ const field = THEME_COMPONENT_LOOK_FIELDS.find((entry)=>entry.id === id);
1356
+ if (!field) throw new Error(`unknown component look field ${id}`);
1357
+ return field;
1358
+ }
1359
+ function storedFor(option, index) {
1360
+ return Array.isArray(option.stored) ? option.stored[index] : option.stored;
1361
+ }
1362
+ /**
1363
+ * A look field as its select shows it: the choice the site's overrides hold,
1364
+ * {@link THEME_LOOK_DEFAULT} when they set nothing, or
1365
+ * {@link THEME_LOOK_CUSTOM} when they set a value no choice names.
1366
+ */ export function readComponentLook(theme, field) {
1367
+ const values = field.targets.map((target)=>readComponentOverride(theme, target));
1368
+ if (values.every((value)=>value === undefined)) return THEME_LOOK_DEFAULT;
1369
+ const match = field.options.find((option)=>values.every((value, index)=>value === storedFor(option, index)));
1370
+ return match ? match.value : THEME_LOOK_CUSTOM;
1371
+ }
1372
+ /**
1373
+ * Sets a look field to one of its choices, or back to the theme's own look
1374
+ * with {@link THEME_LOOK_DEFAULT}, which clears every leaf it sets. Any other
1375
+ * value changes nothing.
1376
+ */ export function writeComponentLook(theme, field, value) {
1377
+ var _field_companions;
1378
+ if (value === THEME_LOOK_DEFAULT) {
1379
+ var _field_companions1;
1380
+ let next = theme;
1381
+ for (const target of [
1382
+ ...field.targets,
1383
+ ...(_field_companions1 = field.companions) != null ? _field_companions1 : []
1384
+ ]){
1385
+ next = clearComponentOverride(next, target);
1386
+ }
1387
+ return next;
1388
+ }
1389
+ const option = field.options.find((entry)=>entry.value === value);
1390
+ if (!option) return theme;
1391
+ let next = theme;
1392
+ field.targets.forEach((target, index)=>{
1393
+ next = writeComponentOverride(next, _extends({}, target, {
1394
+ value: storedFor(option, index)
1395
+ }));
1396
+ });
1397
+ for (const companion of (_field_companions = field.companions) != null ? _field_companions : [])next = writeComponentOverride(next, companion);
1398
+ return next;
1399
+ }
868
1400
 
869
1401
  //# sourceMappingURL=theme-editor-fields.js.map