pxt-core 13.2.0 → 13.2.1

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 (53) hide show
  1. package/built/cli.js +20 -10
  2. package/built/pxt.js +74 -11
  3. package/built/pxtlib.d.ts +19 -0
  4. package/built/pxtlib.js +54 -1
  5. package/built/pxtsim.d.ts +5 -1
  6. package/built/react-common/components/controls/Card.d.ts +1 -0
  7. package/built/react-common/components/controls/Modal.d.ts +1 -0
  8. package/built/react-common/components/share/Share.d.ts +2 -1
  9. package/built/react-common/components/share/ShareInfo.d.ts +2 -1
  10. package/built/react-common/components/theming/SimulatorThemePickerModal.d.ts +15 -0
  11. package/built/react-common/components/theming/ThemeCard.d.ts +2 -1
  12. package/built/react-common/components/theming/ThemePickerModal.d.ts +10 -1
  13. package/built/react-common/components/theming/simulatorThemeDefaults.d.ts +5 -0
  14. package/built/target.js +1 -1
  15. package/built/targetlight.js +1 -1
  16. package/built/web/main.js +30 -30
  17. package/built/web/multiplayer/js/{main.db72096c.js → main.d98f7e21.js} +2 -2
  18. package/built/web/pxtapp.js +1 -1
  19. package/built/web/pxtasseteditor.js +1 -1
  20. package/built/web/pxtembed.js +2 -2
  21. package/built/web/pxtlib.js +1 -1
  22. package/built/web/pxtworker.js +1 -1
  23. package/built/web/react-common-authcode.css +1 -1
  24. package/built/web/react-common-multiplayer.css +1 -1
  25. package/built/web/react-common-skillmap.css +1 -1
  26. package/built/web/rtlreact-common-authcode.css +1 -1
  27. package/built/web/rtlreact-common-multiplayer.css +1 -1
  28. package/built/web/rtlreact-common-skillmap.css +1 -1
  29. package/built/web/rtlsemantic.css +1 -1
  30. package/built/web/runnerembed.js +1 -1
  31. package/built/web/semantic.css +1 -1
  32. package/built/web/skillmap/js/main.6352b119.js +2 -0
  33. package/built/web/teachertool/js/{main.168add6b.js → main.138b6a8e.js} +2 -2
  34. package/built/web/tutorialtool/js/{main.16de6eb5.js → main.9b70fa5a.js} +2 -2
  35. package/localtypings/pxtarget.d.ts +25 -0
  36. package/localtypings/pxteditor.d.ts +11 -2
  37. package/package.json +1 -1
  38. package/react-common/components/controls/Card.tsx +3 -0
  39. package/react-common/components/controls/Modal.tsx +4 -2
  40. package/react-common/components/share/Share.tsx +4 -1
  41. package/react-common/components/share/ShareInfo.tsx +21 -4
  42. package/react-common/components/theming/SimulatorThemePickerModal.tsx +207 -0
  43. package/react-common/components/theming/ThemeCard.tsx +6 -4
  44. package/react-common/components/theming/ThemePickerModal.tsx +65 -4
  45. package/react-common/components/theming/simulatorThemeDefaults.ts +77 -0
  46. package/react-common/styles/controls/Modal.less +6 -6
  47. package/react-common/styles/theming/ThemePickerModal.less +193 -4
  48. package/theme/pxtjson.less +12 -0
  49. package/webapp/public/multiplayer.html +1 -1
  50. package/webapp/public/skillmap.html +1 -1
  51. package/webapp/public/teachertool.html +1 -1
  52. package/webapp/public/tutorialtool.html +1 -1
  53. package/built/web/skillmap/js/main.fb626575.js +0 -2
@@ -262,6 +262,28 @@ declare namespace pxt {
262
262
  order?: number; // Sort order
263
263
  }
264
264
 
265
+ interface SimulatorTheme extends Map<string> {
266
+ layout: string;
267
+ }
268
+
269
+ interface SimulatorThemePreset {
270
+ id: string;
271
+ name: string;
272
+ theme: SimulatorTheme;
273
+ }
274
+
275
+ interface SimulatorThemeColorField {
276
+ property: string;
277
+ label: string;
278
+ defaultValue: string;
279
+ }
280
+
281
+ interface SimulatorThemeLayout {
282
+ id: string;
283
+ name: string;
284
+ colorFields?: SimulatorThemeColorField[]; // replaces the standard color fields for this layout
285
+ }
286
+
265
287
  interface AppSimulator {
266
288
  autoRun?: boolean; // enable autoRun in regular mode, not light mode
267
289
  autoRunLight?: boolean; // force autorun in light mode
@@ -286,6 +308,8 @@ declare namespace pxt {
286
308
  invalidatedClass?: string; // CSS class to be applied to the sim iFrame when it needs to be updated (defaults to sepia filter)
287
309
  stoppedClass?: string; // CSS class to be applied to the sim iFrame when it isn't running (defaults to grayscale filter)
288
310
  keymap?: boolean; // when non-empty and autoRun is disabled, this code is run upon simulator first start
311
+ themePresets?: SimulatorThemePreset[]; // customizable simulator themes offered by the editor
312
+ themeLayouts?: SimulatorThemeLayout[]; // additional simulator layouts offered by the theme editor; Default is always included
289
313
 
290
314
  // a map of allowed simulator channel to URL to handle specific control messages
291
315
  // DEPRECATED. Use `simx` in targetconfig.json approvedRepoLib instead.
@@ -641,6 +665,7 @@ declare namespace pxt {
641
665
  id: string; // Unique identifier
642
666
  name: string; // Human-readable name
643
667
  weight?: number; // Lower weights appear first in theme list, no value = go to end
668
+ defaultSimulatorTheme?: string | Partial<SimulatorTheme>; // Simulator preset id or partial inline theme used by Default
644
669
  overrideCss?: string; // Special css to apply for the theme
645
670
  monacoBaseTheme?: string; // Theme for monaco editor, see https://code.visualstudio.com/docs/getstarted/themes
646
671
  colors: { [key: string]: string }; // Values for theme variables
@@ -83,6 +83,7 @@ declare namespace pxt.editor {
83
83
  | "showthemepicker"
84
84
  | "togglehighcontrast"
85
85
  | "sethighcontrast" // EditorMessageSetHighContrastRequest
86
+ | "setsimulatortheme" // EditorMessageSetSimulatorThemeRequest
86
87
  | "togglegreenscreen"
87
88
  | "togglekeyboardcontrols"
88
89
  | "togglescreenreadermode"
@@ -406,6 +407,12 @@ declare namespace pxt.editor {
406
407
  on: boolean;
407
408
  }
408
409
 
410
+ export interface EditorMessageSetSimulatorThemeRequest extends EditorMessageRequest {
411
+ action: "setsimulatortheme";
412
+ preference: pxt.auth.SimulatorThemePreference;
413
+ savePreference?: boolean;
414
+ }
415
+
409
416
  export interface EditorMessageStartActivity extends EditorMessageRequest {
410
417
  action: "startactivity";
411
418
  activityType: "tutorial" | "example" | "recipe";
@@ -830,6 +837,7 @@ declare namespace pxt.editor {
830
837
  areaMenuOpen?: boolean;
831
838
  feedback?: FeedbackState;
832
839
  themePickerOpen?: boolean;
840
+ simulatorThemePickerOpen?: boolean;
833
841
  errorListNote?: string;
834
842
  timeMachine?: boolean;
835
843
  }
@@ -992,8 +1000,8 @@ declare namespace pxt.editor {
992
1000
  setEditorOffset(): void;
993
1001
 
994
1002
  anonymousPublishHeaderByIdAsync(headerId: string, projectName?: string): Promise<ShareData>;
995
- publishCurrentHeaderAsync(persistent: boolean, screenshotUri?: string): Promise<string>;
996
- publishAsync (name: string, description?: string,screenshotUri?: string, forceAnonymous?: boolean): Promise<ShareData>;
1003
+ publishCurrentHeaderAsync(persistent: boolean, screenshotUri?: string, simulatorTheme?: pxt.SimulatorTheme): Promise<string>;
1004
+ publishAsync (name: string, description?: string,screenshotUri?: string, forceAnonymous?: boolean, simulatorTheme?: pxt.SimulatorTheme): Promise<ShareData>;
997
1005
 
998
1006
  startStopSimulator(opts?: SimulatorStartOptions): void;
999
1007
  stopSimulator(unload?: boolean, opts?: SimulatorStartOptions): void;
@@ -1123,6 +1131,7 @@ declare namespace pxt.editor {
1123
1131
  getSharePreferenceForHeader(): boolean;
1124
1132
  saveSharePreferenceForHeaderAsync(anonymousByDefault: boolean): Promise<void>;
1125
1133
  setColorThemeById(colorThemeId: string, savePreference: boolean): void;
1134
+ setSimulatorThemePreference(preference: pxt.auth.SimulatorThemePreference, savePreference?: boolean): Promise<void>;
1126
1135
  }
1127
1136
 
1128
1137
  export interface IHexFileImporter {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pxt-core",
3
- "version": "13.2.0",
3
+ "version": "13.2.1",
4
4
  "description": "Microsoft MakeCode provides Blocks / JavaScript / Python tools and editors",
5
5
  "keywords": [
6
6
  "TypeScript",
@@ -5,6 +5,7 @@ export interface CardProps extends ContainerProps {
5
5
  onClick?: () => void;
6
6
  tabIndex?: number;
7
7
  ariaLabelledBy?: string;
8
+ ariaPressed?: boolean;
8
9
  label?: React.ReactNode;
9
10
  labelClass?: string;
10
11
  }
@@ -17,6 +18,7 @@ export const Card = (props: CardProps) => {
17
18
  children,
18
19
  ariaDescribedBy,
19
20
  ariaLabelledBy,
21
+ ariaPressed,
20
22
  ariaHidden,
21
23
  ariaLabel,
22
24
  onClick,
@@ -38,6 +40,7 @@ export const Card = (props: CardProps) => {
38
40
  tabIndex={tabIndex}
39
41
  aria-describedby={ariaDescribedBy}
40
42
  aria-labelledby={ariaLabelledBy}
43
+ aria-pressed={ariaPressed}
41
44
  onClick={onClick}
42
45
  />
43
46
  }
@@ -22,6 +22,7 @@ export interface ModalAction {
22
22
 
23
23
  export interface ModalProps extends ContainerProps {
24
24
  title: string;
25
+ hideTitle?: boolean;
25
26
  leftIcon?: string;
26
27
  ariaDescribedBy?: string;
27
28
  actions?: ModalAction[];
@@ -42,6 +43,7 @@ export const Modal = (props: ModalProps) => {
42
43
  ariaDescribedBy,
43
44
  role,
44
45
  title,
46
+ hideTitle,
45
47
  leftIcon,
46
48
  actions,
47
49
  onClose,
@@ -106,11 +108,11 @@ export const Modal = (props: ModalProps) => {
106
108
  />
107
109
  </div>
108
110
  }
109
- <div id="modal-title" className="common-modal-title">
111
+ <div id="modal-title" className={classList("common-modal-title", hideTitle && "sr-only")}>
110
112
  {leftIcon && <i className={leftIcon} aria-hidden={true}/>}
111
113
  {title}
112
114
  </div>
113
- {fullscreen && rightHeader &&
115
+ {rightHeader &&
114
116
  <div className="common-modal-right-menu">
115
117
  {rightHeader}
116
118
  </div>
@@ -24,10 +24,11 @@ export interface ShareProps {
24
24
  hasProjectBeenPersistentShared?: boolean;
25
25
  anonymousShareByDefault?: boolean;
26
26
  isMultiplayerGame?: boolean; // Arcade: Does the game being shared have multiplayer enabled?
27
+ simulatorTheme?: pxt.SimulatorTheme;
27
28
  kind?: "multiplayer" | "vscode" | "share"; // Arcade: Was the share dialog opened specifically for hosting a multiplayer game?
28
29
  setAnonymousSharePreference?: (anonymousByDefault: boolean) => void;
29
30
  simRecorder: SimRecorder;
30
- publishAsync: (name: string, description?: string, screenshotUri?: string, forceAnonymous?: boolean) => Promise<ShareData>;
31
+ publishAsync: (name: string, description?: string, screenshotUri?: string, forceAnonymous?: boolean, simulatorTheme?: pxt.SimulatorTheme) => Promise<ShareData>;
31
32
  onClose: () => void;
32
33
  }
33
34
 
@@ -43,6 +44,7 @@ export const Share = (props: ShareProps) => {
43
44
  anonymousShareByDefault,
44
45
  setAnonymousSharePreference,
45
46
  isMultiplayerGame,
47
+ simulatorTheme,
46
48
  kind,
47
49
  onClose
48
50
  } = props;
@@ -55,6 +57,7 @@ export const Share = (props: ShareProps) => {
55
57
  simRecorder={simRecorder}
56
58
  publishAsync={publishAsync}
57
59
  isMultiplayerGame={isMultiplayerGame}
60
+ simulatorTheme={simulatorTheme}
58
61
  kind={kind}
59
62
  hasProjectBeenPersistentShared={hasProjectBeenPersistentShared}
60
63
  anonymousShareByDefault={anonymousShareByDefault}
@@ -25,8 +25,9 @@ export interface ShareInfoProps {
25
25
  isLoggedIn?: boolean;
26
26
  hasProjectBeenPersistentShared?: boolean;
27
27
  simRecorder: SimRecorder;
28
- publishAsync: (name: string, description?: string, screenshotUri?: string, forceAnonymous?: boolean) => Promise<ShareData>;
28
+ publishAsync: (name: string, description?: string, screenshotUri?: string, forceAnonymous?: boolean, simulatorTheme?: pxt.SimulatorTheme) => Promise<ShareData>;
29
29
  isMultiplayerGame?: boolean; // Arcade: Does the game being shared have multiplayer enabled?
30
+ simulatorTheme?: pxt.SimulatorTheme;
30
31
  kind?: "multiplayer" | "vscode" | "share"; // Arcade: Was the share dialog opened specifically for hosting a multiplayer game?
31
32
  anonymousShareByDefault?: boolean;
32
33
  setAnonymousSharePreference?: (anonymousByDefault: boolean) => void;
@@ -46,6 +47,7 @@ export const ShareInfo = (props: ShareInfoProps) => {
46
47
  anonymousShareByDefault,
47
48
  setAnonymousSharePreference,
48
49
  isMultiplayerGame,
50
+ simulatorTheme,
49
51
  kind = "share",
50
52
  onClose,
51
53
  } = props;
@@ -62,6 +64,7 @@ export const ShareInfo = (props: ShareInfoProps) => {
62
64
  const [ kioskState, setKioskState ] = React.useState(false);
63
65
  const [ isAnonymous, setIsAnonymous ] = React.useState(!isLoggedIn || anonymousShareByDefault);
64
66
  const [ isShowingMultiConfirmation, setIsShowingMultiConfirmation ] = React.useState(false);
67
+ const [ shareWithSimulatorTheme, setShareWithSimulatorTheme ] = React.useState(false);
65
68
 
66
69
  const { simScreenshot, simGif } = pxt.appTarget.appTheme;
67
70
  const showSimulator = (simScreenshot || simGif) && !!simRecorder;
@@ -96,7 +99,8 @@ export const ShareInfo = (props: ShareInfoProps) => {
96
99
  const handlePublishClick = async () => {
97
100
  setShareState("publishing");
98
101
  setLastShareWasAnonymous(isAnonymous);
99
- let publishedShareData = await publishAsync(name, description, thumbnailUri, isAnonymous);
102
+ let publishedShareData = await publishAsync(name, description, thumbnailUri, isAnonymous,
103
+ shareWithSimulatorTheme ? simulatorTheme : undefined);
100
104
  setShareData(publishedShareData);
101
105
  if (!publishedShareData?.error) setShareState("publish");
102
106
  else setShareState("share")
@@ -105,7 +109,8 @@ export const ShareInfo = (props: ShareInfoProps) => {
105
109
  const handlePublishInVscodeClick = async () => {
106
110
  setShareState("publishing");
107
111
  setLastShareWasAnonymous(isAnonymous);
108
- let publishedShareData = await publishAsync(name, description, thumbnailUri, isAnonymous);
112
+ let publishedShareData = await publishAsync(name, description, thumbnailUri, isAnonymous,
113
+ shareWithSimulatorTheme ? simulatorTheme : undefined);
109
114
  setShareData(publishedShareData);
110
115
  if (!publishedShareData?.error) {
111
116
  setShareState("publish-vscode");
@@ -241,7 +246,8 @@ export const ShareInfo = (props: ShareInfoProps) => {
241
246
  setIsShowingMultiConfirmation(false);
242
247
  setLastShareWasAnonymous(isAnonymous);
243
248
 
244
- const publishedShareData = await publishAsync(name, description, thumbnailUri, isAnonymous);
249
+ const publishedShareData = await publishAsync(name, description, thumbnailUri, isAnonymous,
250
+ shareWithSimulatorTheme ? simulatorTheme : undefined);
245
251
 
246
252
  // TODO multiplayer: This won't work on staging (parseScriptId domains check doesn't include staging urls)
247
253
  // but those wouldn't load anyways (as staging multiplayer is currently fetching games from prod links)
@@ -323,6 +329,11 @@ export const ShareInfo = (props: ShareInfoProps) => {
323
329
  if (setAnonymousSharePreference) setAnonymousSharePreference(!newValue);
324
330
  }
325
331
 
332
+ const handleShareWithSimulatorThemeClick = (newValue: boolean) => {
333
+ pxt.tickEvent("share.simulatorTheme", { checked: newValue.toString() });
334
+ setShareWithSimulatorTheme(newValue);
335
+ }
336
+
326
337
  const inputTitle = prePublish ? lf("Project Name") :
327
338
  (shareState === "publish-vscode" ? lf("Share Successful") : lf("Project Link"));
328
339
 
@@ -400,6 +411,12 @@ export const ShareInfo = (props: ShareInfoProps) => {
400
411
  isChecked={!isAnonymous}
401
412
  onChange={handleAnonymousShareClick}
402
413
  />}
414
+ {simulatorTheme && <Checkbox
415
+ id="share-simulator-theme-checkbox"
416
+ label={lf("Share with your simulator theme")}
417
+ isChecked={shareWithSimulatorTheme}
418
+ onChange={handleShareWithSimulatorThemeClick}
419
+ />}
403
420
  </>
404
421
  }
405
422
  {prePublish && <>
@@ -0,0 +1,207 @@
1
+ import * as React from "react";
2
+
3
+ import { Input } from "../controls/Input";
4
+ import { Modal } from "../controls/Modal";
5
+ import { copySimulatorTheme, getSimulatorThemeForLayout } from "./simulatorThemeDefaults";
6
+ import { ThemePickerToggle } from "./ThemePickerModal";
7
+
8
+ export interface SimulatorThemePickerModalProps {
9
+ presets: pxt.SimulatorThemePreset[];
10
+ layouts?: pxt.SimulatorThemeLayout[];
11
+ initialPreference?: pxt.auth.SimulatorThemePreference;
12
+ defaultTheme?: pxt.SimulatorTheme;
13
+ accountTheme?: pxt.SimulatorTheme;
14
+ renderPreview: (theme: pxt.SimulatorTheme) => React.ReactNode;
15
+ onEditorThemeClicked?: () => void;
16
+ onUseAccountTheme?: () => void | Promise<void>;
17
+ onThemeChanged?: (preference: pxt.auth.SimulatorThemePreference) => void;
18
+ onSave: (preference: pxt.auth.SimulatorThemePreference) => void | Promise<void>;
19
+ onClose: () => void;
20
+ }
21
+
22
+ const CUSTOM_PRESET_ID = "custom";
23
+ const ACCOUNT_PRESET_ID = "account";
24
+
25
+ function getDefaultColorFields(theme: pxt.SimulatorTheme): pxt.SimulatorThemeColorField[] {
26
+ const labels: pxt.Map<string> = {
27
+ "background-color": lf("Console"),
28
+ "button-stroke": lf("Button outline"),
29
+ "text-color": lf("Button labels"),
30
+ "button-fill": lf("Buttons"),
31
+ "dpad-fill": lf("D-pad"),
32
+ "joystick-handle-stroke": lf("Joystick handle outline"),
33
+ };
34
+ return pxt.auth.DEFAULT_SIMULATOR_THEME_COLOR_PROPERTIES.map(property => ({
35
+ property,
36
+ label: labels[property],
37
+ defaultValue: theme[property],
38
+ }));
39
+ }
40
+
41
+ function getPresetId(theme: pxt.SimulatorTheme, presets: pxt.SimulatorThemePreset[]): string | undefined {
42
+ return presets.find(preset => pxt.auth.simulatorThemeColorsEqual(preset.theme, theme))?.id;
43
+ }
44
+
45
+ function normalizeColor(value: string): string | undefined {
46
+ const normalized = value.startsWith("#") ? value : `#${value}`;
47
+ return /^#[0-9a-f]{6}$/i.test(normalized) ? normalized.toUpperCase() : undefined;
48
+ }
49
+
50
+ export const SimulatorThemePickerModal = (props: SimulatorThemePickerModalProps) => {
51
+ const {
52
+ presets,
53
+ layouts,
54
+ initialPreference,
55
+ defaultTheme,
56
+ accountTheme,
57
+ renderPreview,
58
+ onEditorThemeClicked,
59
+ onUseAccountTheme,
60
+ onThemeChanged,
61
+ onSave,
62
+ onClose,
63
+ } = props;
64
+ const defaultColorFields = getDefaultColorFields(presets[0].theme);
65
+ const getColorFields = (layoutId: string) => {
66
+ const fields = layouts?.find(layout => layout.id === layoutId)?.colorFields;
67
+ return fields?.map(field => ({
68
+ ...field,
69
+ label: pxt.Util.rlf(`{id:simulator-theme-field}${field.label}`),
70
+ })) || defaultColorFields;
71
+ };
72
+ const savedPreset = presets.find(preset => preset.id === initialPreference?.presetId);
73
+ const initialPresetId = savedPreset
74
+ ? savedPreset.id
75
+ : initialPreference
76
+ ? getPresetId(initialPreference.theme, presets) || CUSTOM_PRESET_ID
77
+ : onUseAccountTheme
78
+ ? ACCOUNT_PRESET_ID
79
+ : presets[0].id;
80
+ const initialThemeSource = initialPresetId === "default"
81
+ ? defaultTheme || initialPreference?.theme || savedPreset?.theme || presets[0].theme
82
+ : initialPreference?.theme || savedPreset?.theme || accountTheme || presets[0].theme;
83
+ const initialLayoutId = initialPreference?.theme.layout
84
+ || initialThemeSource.layout
85
+ || pxt.auth.DEFAULT_SIMULATOR_LAYOUT;
86
+ const initialTheme = getSimulatorThemeForLayout(
87
+ initialThemeSource,
88
+ initialLayoutId,
89
+ getColorFields(initialLayoutId)
90
+ );
91
+ const [presetId, setPresetId] = React.useState(initialPresetId);
92
+ const [theme, setTheme] = React.useState<pxt.SimulatorTheme>(initialTheme);
93
+ const layoutId = theme.layout;
94
+ const layoutIsKnown = layoutId === pxt.auth.DEFAULT_SIMULATOR_LAYOUT || layouts?.some(layout => layout.id === layoutId);
95
+ const colorFields = getColorFields(layoutId);
96
+
97
+ const selectPreset = (id: string) => {
98
+ if (id === ACCOUNT_PRESET_ID && onUseAccountTheme) {
99
+ setPresetId(id);
100
+ const nextTheme = copySimulatorTheme(accountTheme || presets[0].theme);
101
+ setTheme(getSimulatorThemeForLayout(nextTheme, nextTheme.layout, getColorFields(nextTheme.layout)));
102
+ return;
103
+ }
104
+ const preset = presets.find(candidate => candidate.id === id);
105
+ if (!preset) return;
106
+ const presetTheme = copySimulatorTheme(id === "default" ? defaultTheme || preset.theme : preset.theme);
107
+ const nextTheme = getSimulatorThemeForLayout(
108
+ presetTheme,
109
+ presetTheme.layout,
110
+ getColorFields(presetTheme.layout)
111
+ );
112
+ setPresetId(id);
113
+ setTheme(nextTheme);
114
+ onThemeChanged?.({ presetId: id, theme: nextTheme });
115
+ };
116
+
117
+ const updateColor = (property: string, color: string) => {
118
+ const normalized = normalizeColor(color);
119
+ if (!normalized) return;
120
+ const nextTheme = {
121
+ ...theme,
122
+ [property]: normalized,
123
+ };
124
+ setPresetId(CUSTOM_PRESET_ID);
125
+ setTheme(nextTheme);
126
+ onThemeChanged?.({ presetId: CUSTOM_PRESET_ID, theme: nextTheme });
127
+ };
128
+
129
+ const selectLayout = (id: string) => {
130
+ const nextTheme = getSimulatorThemeForLayout(theme, id, getColorFields(id));
131
+ setPresetId(CUSTOM_PRESET_ID);
132
+ setTheme(nextTheme);
133
+ onThemeChanged?.({ presetId: CUSTOM_PRESET_ID, theme: nextTheme });
134
+ };
135
+
136
+ return <Modal
137
+ id="simulator-theme-picker-modal"
138
+ title={lf("Simulator Theme")}
139
+ hideTitle={!!onEditorThemeClicked}
140
+ className={`simulator-theme-picker-modal${onEditorThemeClicked ? "" : " standalone"}`}
141
+ onClose={onClose}
142
+ rightHeader={onEditorThemeClicked && <ThemePickerToggle
143
+ selected="simulator"
144
+ onModeChanged={onEditorThemeClicked} />}
145
+ actions={[
146
+ {
147
+ label: lf("Apply"),
148
+ className: "primary",
149
+ onClick: () => presetId === ACCOUNT_PRESET_ID
150
+ ? onUseAccountTheme?.()
151
+ : onSave({ presetId, theme }),
152
+ },
153
+ ]}>
154
+ <div className="simulator-theme-picker">
155
+ {renderPreview(theme)}
156
+ <div className="simulator-theme-controls">
157
+ <div className="simulator-theme-select-field">
158
+ <label htmlFor="simulator-theme-preset">
159
+ {onUseAccountTheme ? lf("Theme") : lf("Built-in theme")}
160
+ </label>
161
+ <select
162
+ id="simulator-theme-preset"
163
+ aria-label={onUseAccountTheme ? lf("Project simulator theme") : lf("Built-in simulator theme")}
164
+ className="simulator-theme-select"
165
+ value={presetId}
166
+ onChange={event => selectPreset(event.target.value)}>
167
+ {onUseAccountTheme && <option value={ACCOUNT_PRESET_ID}>{lf("Use account theme")}</option>}
168
+ {presetId === CUSTOM_PRESET_ID && <option value={CUSTOM_PRESET_ID} disabled>{lf("Custom")}</option>}
169
+ {presets.map(preset => <option key={preset.id} value={preset.id}>
170
+ {pxt.Util.rlf(`{id:simulator-theme-name}${preset.name}`)}
171
+ </option>)}
172
+ </select>
173
+ </div>
174
+ {!!layouts?.length && <div className="simulator-theme-select-field">
175
+ <label htmlFor="simulator-theme-layout">{lf("Layout")}</label>
176
+ <select
177
+ id="simulator-theme-layout"
178
+ aria-label={lf("Simulator layout")}
179
+ className="simulator-theme-select"
180
+ value={layoutId}
181
+ onChange={event => selectLayout(event.target.value)}>
182
+ <option value={pxt.auth.DEFAULT_SIMULATOR_LAYOUT}>{lf("Default")}</option>
183
+ {!layoutIsKnown && <option value={layoutId} disabled>{lf("Custom ({0})", layoutId)}</option>}
184
+ {layouts.map(layout => <option key={layout.id} value={layout.id}>
185
+ {pxt.Util.rlf(`{id:simulator-layout-name}${layout.name}`)}
186
+ </option>)}
187
+ </select>
188
+ </div>}
189
+ <div className="simulator-theme-color-list">
190
+ {colorFields.map(field => <div className="simulator-theme-color" key={field.property}>
191
+ <span>{field.label}</span>
192
+ <input
193
+ type="color"
194
+ aria-label={lf("{0} color", field.label)}
195
+ value={theme[field.property]}
196
+ onChange={event => updateColor(field.property, event.target.value)} />
197
+ <Input
198
+ ariaLabel={lf("{0} hex color", field.label)}
199
+ initialValue={theme[field.property]}
200
+ onChange={value => updateColor(field.property, value)}
201
+ validator={(value, previousValue) => normalizeColor(value) || previousValue} />
202
+ </div>)}
203
+ </div>
204
+ </div>
205
+ </div>
206
+ </Modal>;
207
+ };
@@ -3,22 +3,24 @@ import { Card } from "../controls/Card";
3
3
 
4
4
  interface ThemeCardProps {
5
5
  theme: pxt.ColorThemeInfo;
6
- onClick?: (theme: pxt.ColorThemeInfo) => void;
6
+ selected: boolean;
7
+ onClick: (theme: pxt.ColorThemeInfo) => void;
7
8
  }
8
9
 
9
10
  export const ThemeCard = (props: ThemeCardProps) => {
10
- const { onClick, theme } = props;
11
+ const { onClick, selected, theme } = props;
11
12
 
12
13
  const themeName = pxt.Util.rlf(`{id:color-theme-name}${theme.name}`);
13
14
 
14
15
  return (
15
16
  <Card
16
- className="theme-card"
17
+ className={`theme-card${selected ? " selected" : ""}`}
17
18
  role="listitem"
18
19
  ariaLabelledBy={theme.id + "-title"}
20
+ ariaPressed={selected}
19
21
  key={theme.id}
20
22
  onClick={() => onClick(theme)}
21
- tabIndex={onClick && 0}
23
+ tabIndex={0}
22
24
  >
23
25
  <div className="theme-info-box">
24
26
  <ThemePreview theme={theme} />
@@ -1,29 +1,90 @@
1
+ import * as React from "react";
1
2
  import { Modal } from "../controls/Modal";
3
+ import { EditorToggle } from "../controls/EditorToggle";
2
4
  import { ThemeCard } from "./ThemeCard";
3
5
 
6
+ export type ThemePickerMode = "editor" | "simulator";
7
+
8
+ export interface ThemePickerToggleProps {
9
+ selected: ThemePickerMode;
10
+ onModeChanged: (mode: ThemePickerMode) => void;
11
+ }
12
+
13
+ export const ThemePickerToggle = (props: ThemePickerToggleProps) => {
14
+ const { selected, onModeChanged } = props;
15
+ const selectMode = (mode: ThemePickerMode) => {
16
+ if (mode !== selected) onModeChanged(mode);
17
+ };
18
+
19
+ return <div className="theme-picker-toggle">
20
+ <EditorToggle
21
+ id="theme-picker-toggle"
22
+ ariaLabel={lf("Theme type")}
23
+ selected={selected === "editor" ? 0 : 1}
24
+ items={[
25
+ {
26
+ label: lf("Editor"),
27
+ title: lf("Editor theme"),
28
+ icon: "fas fa-paint-brush",
29
+ focusable: true,
30
+ onClick: () => selectMode("editor"),
31
+ },
32
+ {
33
+ label: lf("Simulator"),
34
+ title: lf("Simulator theme"),
35
+ icon: "fas fa-gamepad",
36
+ focusable: true,
37
+ onClick: () => selectMode("simulator"),
38
+ },
39
+ ]} />
40
+ </div>;
41
+ };
42
+
4
43
  export interface ThemePickerModalProps {
5
44
  themes: pxt.ColorThemeInfo[];
6
- onThemeClicked(them: pxt.ColorThemeInfo): void;
45
+ selectedThemeId?: string;
46
+ onThemeChanged: (theme: pxt.ColorThemeInfo) => void;
47
+ onSave: (theme: pxt.ColorThemeInfo) => void;
48
+ onSimulatorThemeClicked?: () => void;
7
49
  onClose(): void;
8
50
  }
9
51
  export const ThemePickerModal = (props: ThemePickerModalProps) => {
52
+ const [selectedThemeId, setSelectedThemeId] = React.useState(
53
+ props.selectedThemeId || props.themes[0]?.id
54
+ );
55
+ const selectedTheme = props.themes.find(theme => theme.id === selectedThemeId);
56
+
10
57
  return (
11
58
  <Modal
12
- id="theme-picker-modal"
59
+ id="theme-picker-modal"
13
60
  title={lf("Choose a Theme")}
61
+ hideTitle={!!props.onSimulatorThemeClicked}
14
62
  onClose={props.onClose}
15
63
  className="theme-picker-modal"
64
+ rightHeader={props.onSimulatorThemeClicked && <ThemePickerToggle
65
+ selected="editor"
66
+ onModeChanged={props.onSimulatorThemeClicked} />}
67
+ actions={[{
68
+ label: lf("Apply"),
69
+ className: "primary",
70
+ disabled: !selectedTheme,
71
+ onClick: () => selectedTheme && props.onSave(selectedTheme),
72
+ }]}
16
73
  >
17
74
  <div
18
75
  className="theme-picker"
19
76
  role="list"
20
77
  aria-label={lf("List of available themes")}
21
78
  >
22
- {props.themes && props.themes.map(theme =>
79
+ {props.themes.map(theme =>
23
80
  <ThemeCard
24
81
  key={theme.id}
25
82
  theme={theme}
26
- onClick={props.onThemeClicked}
83
+ selected={theme.id === selectedThemeId}
84
+ onClick={selected => {
85
+ setSelectedThemeId(selected.id);
86
+ props.onThemeChanged(selected);
87
+ }}
27
88
  />
28
89
  )}
29
90
  </div>