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.
- package/built/cli.js +20 -10
- package/built/pxt.js +74 -11
- package/built/pxtlib.d.ts +19 -0
- package/built/pxtlib.js +54 -1
- package/built/pxtsim.d.ts +5 -1
- package/built/react-common/components/controls/Card.d.ts +1 -0
- package/built/react-common/components/controls/Modal.d.ts +1 -0
- package/built/react-common/components/share/Share.d.ts +2 -1
- package/built/react-common/components/share/ShareInfo.d.ts +2 -1
- package/built/react-common/components/theming/SimulatorThemePickerModal.d.ts +15 -0
- package/built/react-common/components/theming/ThemeCard.d.ts +2 -1
- package/built/react-common/components/theming/ThemePickerModal.d.ts +10 -1
- package/built/react-common/components/theming/simulatorThemeDefaults.d.ts +5 -0
- package/built/target.js +1 -1
- package/built/targetlight.js +1 -1
- package/built/web/main.js +30 -30
- package/built/web/multiplayer/js/{main.db72096c.js → main.d98f7e21.js} +2 -2
- package/built/web/pxtapp.js +1 -1
- package/built/web/pxtasseteditor.js +1 -1
- package/built/web/pxtembed.js +2 -2
- package/built/web/pxtlib.js +1 -1
- package/built/web/pxtworker.js +1 -1
- package/built/web/react-common-authcode.css +1 -1
- package/built/web/react-common-multiplayer.css +1 -1
- package/built/web/react-common-skillmap.css +1 -1
- package/built/web/rtlreact-common-authcode.css +1 -1
- package/built/web/rtlreact-common-multiplayer.css +1 -1
- package/built/web/rtlreact-common-skillmap.css +1 -1
- package/built/web/rtlsemantic.css +1 -1
- package/built/web/runnerembed.js +1 -1
- package/built/web/semantic.css +1 -1
- package/built/web/skillmap/js/main.6352b119.js +2 -0
- package/built/web/teachertool/js/{main.168add6b.js → main.138b6a8e.js} +2 -2
- package/built/web/tutorialtool/js/{main.16de6eb5.js → main.9b70fa5a.js} +2 -2
- package/localtypings/pxtarget.d.ts +25 -0
- package/localtypings/pxteditor.d.ts +11 -2
- package/package.json +1 -1
- package/react-common/components/controls/Card.tsx +3 -0
- package/react-common/components/controls/Modal.tsx +4 -2
- package/react-common/components/share/Share.tsx +4 -1
- package/react-common/components/share/ShareInfo.tsx +21 -4
- package/react-common/components/theming/SimulatorThemePickerModal.tsx +207 -0
- package/react-common/components/theming/ThemeCard.tsx +6 -4
- package/react-common/components/theming/ThemePickerModal.tsx +65 -4
- package/react-common/components/theming/simulatorThemeDefaults.ts +77 -0
- package/react-common/styles/controls/Modal.less +6 -6
- package/react-common/styles/theming/ThemePickerModal.less +193 -4
- package/theme/pxtjson.less +12 -0
- package/webapp/public/multiplayer.html +1 -1
- package/webapp/public/skillmap.html +1 -1
- package/webapp/public/teachertool.html +1 -1
- package/webapp/public/tutorialtool.html +1 -1
- 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
|
@@ -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
|
-
{
|
|
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
|
-
|
|
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=
|
|
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={
|
|
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
|
-
|
|
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
|
|
79
|
+
{props.themes.map(theme =>
|
|
23
80
|
<ThemeCard
|
|
24
81
|
key={theme.id}
|
|
25
82
|
theme={theme}
|
|
26
|
-
|
|
83
|
+
selected={theme.id === selectedThemeId}
|
|
84
|
+
onClick={selected => {
|
|
85
|
+
setSelectedThemeId(selected.id);
|
|
86
|
+
props.onThemeChanged(selected);
|
|
87
|
+
}}
|
|
27
88
|
/>
|
|
28
89
|
)}
|
|
29
90
|
</div>
|