Skip to content

Commit

Permalink
Categorized colors and previews
Browse files Browse the repository at this point in the history
  • Loading branch information
thsparks committed Feb 28, 2025
1 parent ae28c3d commit 07714ac
Show file tree
Hide file tree
Showing 16 changed files with 616 additions and 53 deletions.
6 changes: 3 additions & 3 deletions themebuilder/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { setTargetConfig } from "./state/actions";
import { EditorPanel } from "./components/EditorPanel";
import { authCheckAsync } from "./services/authClient";
import { ThemeManager } from "react-common/components/theming/themeManager";
import { setCurrentFrameTheme } from "./transforms/setCurrentFrameTheme";
import { setCurrentEditingTheme } from "./transforms/setCurrentEditingTheme";

export const App = () => {
const { state, dispatch } = useContext(AppStateContext);
Expand Down Expand Up @@ -47,7 +47,7 @@ export const App = () => {
if (ready && inited) {
// This is here just in case this finishes after initialization is complete.
// The useEffect that normally sets this will have already run and won't have had the theme.
setCurrentFrameTheme(themeManager.getCurrentColorTheme());
setCurrentEditingTheme(themeManager.getCurrentColorTheme());
}
}
}, []);
Expand All @@ -58,7 +58,7 @@ export const App = () => {
const themeManager = ThemeManager.getInstance(document);
const currentTheme = themeManager.getCurrentColorTheme();
if (currentTheme) {
setCurrentFrameTheme(currentTheme);
setCurrentEditingTheme(currentTheme);
}
}
}, [ready, inited])
Expand Down
39 changes: 39 additions & 0 deletions themebuilder/src/components/ThemeColorFamily.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import * as React from "react";
import css from "./styling/ThemeEditor.module.scss";
import { ThemeColorSetter } from "./ThemeColorSetter";
import { Button } from "react-common/components/controls/Button";

export interface ThemeColorFamilyProps {
key: string;
baseColorId: string;
derivedColorIds: string[];
}
export const ThemeColorFamily = (props: ThemeColorFamilyProps) => {
const { key, baseColorId, derivedColorIds } = props;
const [expanded, setExpanded] = React.useState<boolean>(false);

function toggleExpanded() {
setExpanded(!expanded);
}

return (
<div className={css["theme-color-family-root"]} key={key}>
<div className={css["theme-color-family-base-row"]}>
<Button
onClick={toggleExpanded}
className={css["expand-collapse-button"]}
title={expanded ? lf("Collapse Derived Colors") : lf("Expand Derived Colors")}
leftIcon={expanded ? "fas fa-caret-down" : "fas fa-caret-right"}
/>
<ThemeColorSetter colorId={baseColorId} />
</div>
{expanded && (
<div className={css["derived-color-set"]}>
{derivedColorIds.map(colorId => (
<ThemeColorSetter key={`derived-color-setter-${colorId}`} colorId={colorId} />
))}
</div>
)}
</div>
);
};
50 changes: 50 additions & 0 deletions themebuilder/src/components/ThemeColorSetter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import * as React from "react";
import css from "./styling/ThemeEditor.module.scss";
import { Input } from "react-common/components/controls/Input";
import { AppStateContext } from "../state/appStateContext";
import { Button } from "react-common/components/controls/Button";
import { toggleColorHighlight } from "../transforms/toggleColorHighlight";
import { classList } from "react-common/components/util";
import { setColorValue } from "../transforms/setColorValue";

export interface ThemeColorSetterProps {
key?: string;
colorId: string;
}
export const ThemeColorSetter = (props: ThemeColorSetterProps) => {
const { state } = React.useContext(AppStateContext);
const { editingTheme } = state;
const { key, colorId } = props;
const [isHighlighted, setIsHighlighted] = React.useState<boolean>(false);

React.useEffect(() => {
setIsHighlighted(!!state.colorsToHighlight?.includes(colorId));
}, [state.colorsToHighlight]);

const color = editingTheme?.colors[colorId];
if (!color) return null;
return (
<div key={key} className={css["theme-color-setter"]}>
<Button
className={classList(css["highlight-color-button"], isHighlighted ? css["highlighted"] : undefined)}
style={isHighlighted ? { borderColor: state.highlightColor, color: state.highlightColor } : undefined}
leftIcon="fas fa-search"
title={lf("Highlight color")}
onClick={() => toggleColorHighlight(colorId)}
/>
<Input
className={css["theme-color-input"]}
label={colorId}
initialValue={color}
onBlur={value => setColorValue(colorId, value)}
onEnterKey={value => setColorValue(colorId, value)}
/>
<input
type="color"
className={css["theme-color-button"]}
value={color}
onChange={e => setColorValue(colorId, e.target.value)}
/>
</div>
);
};
54 changes: 26 additions & 28 deletions themebuilder/src/components/ThemeEditorPane.tsx
Original file line number Diff line number Diff line change
@@ -1,40 +1,38 @@
import * as React from "react";
import { Input } from "react-common/components/controls/Input";
import { setCurrentFrameTheme } from "../transforms/setCurrentFrameTheme";
import css from "./styling/ThemeEditor.module.scss";
import { Input } from "react-common/components/controls/Input";
import { AppStateContext } from "../state/appStateContext";

import { setThemeName } from "../transforms/setThemeName";
import { getColorHeirarchy } from "../utils/colorUtils";
import { ThemeColorFamily } from "./ThemeColorFamily";

export const ThemeEditorPane = () => {
const { state } = React.useContext(AppStateContext);
const { theme } = state;

function setThemeName(name: string) {
if (theme) {
const id = name.toLocaleLowerCase().replace(" ", "-").replace(/[^a-z0-9-]/g, "");
setCurrentFrameTheme ({...theme, id, name});
}
}
const { editingTheme } = state;

function setColorValue(colorId: string, value: string) {
if (theme) {
setCurrentFrameTheme({ ...theme, colors: { ...theme.colors, [colorId]: value } });
}
}
const colorHeirarchy: { [baseColorId: string]: string[] } = React.useMemo(() => {
return state.editingTheme?.colors ? getColorHeirarchy(Object.keys(state.editingTheme.colors)) : {};
}, [state.editingTheme?.colors]);
const baseColorIds = Object.keys(colorHeirarchy);

return !theme ? null : (
return (
<div className={css["theme-editor-container"]}>
<Input className={css["theme-name-input"]} label={lf("Theme Name")} onBlur={setThemeName} onEnterKey={setThemeName} initialValue={theme.name} />
<div className={css["theme-colors-list"]} >
{Object.keys(theme.colors).map((colorId) => {
const color = theme.colors[colorId];
return <div key={`theme-color-wrapper-${colorId}`} className={css["theme-color-wrapper"]}>
<Input className={css["theme-color-input"]} label={colorId} initialValue={color} onBlur={value => setColorValue(colorId, value)} onEnterKey={value => setColorValue(colorId, value)} />
{/* <Button className={css["color-preview"]} style={{ backgroundColor: color }} onClick={() => {}} title={lf("Choose color: {0}", colorId)} /> */}
<input type="color" className={css["theme-color-button"]} value={color} onChange={e => setColorValue(colorId, e.target.value)} />
</div>
})}
<Input
className={css["theme-name-input"]}
label={lf("Theme Name")}
onBlur={setThemeName}
onEnterKey={setThemeName}
initialValue={editingTheme?.name}
/>
<div className={css["theme-colors-list"]}>
{baseColorIds.map(baseColorId => (
<ThemeColorFamily
key={`base-color-${baseColorId}`}
baseColorId={baseColorId}
derivedColorIds={colorHeirarchy[baseColorId]}
/>
))}
</div>
</div>
);
}
};
89 changes: 70 additions & 19 deletions themebuilder/src/components/styling/ThemeEditor.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -17,38 +17,89 @@
flex-direction: column;
gap: 1rem;

.theme-color-wrapper {
width: 100%;
.theme-color-family-root {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
flex-direction: column;
gap: 0.5rem;
align-items: flex-start;
justify-content: space-between;
padding: 0 1rem;
width: 100%;

&:nth-child(odd) {
background-color: var(--pxt-neutral-alpha10);
background-color: var(--pxt-neutral-stencil1);
}

.expand-collapse-button {
background: none;
margin: 0;
padding: 0.2rem 0.5rem;
width: 2rem;

i {
margin: 0;
padding: 0 0.8rem 0 0;
}

&:hover {
background-color: var(--pxt-neutral-alpha20);
}
}


.theme-color-family-base-row {
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}

.theme-color-input {
flex-grow: 1;
.derived-color-set {
width: 100%;
margin-left: 2rem;
}

.theme-color-setter {
width: 100%;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
gap: 0.5rem;
padding: 0 1rem;

.highlight-color-button {
padding: 0.3rem 0.2rem;
border: 1px solid var(--pxt-neutral-foreground1);

label[class*="common-input-label"] {
width: 33%;
}
i {
margin: 0;
padding: 0;
}

div[class*="common-input-group"] {
margin: 1rem;
.highlighted {
border-width: 2px;
// Color set programmatically
}
}

.theme-color-input {
flex-grow: 1;
display: flex;
flex-direction: row;
align-items: center;

label[class*="common-input-label"] {
width: 33%;
}

div[class*="common-input-group"] {
margin: 1rem;
flex-grow: 1;
}
}
}

// .theme-color-button {
// margin-left: 1rem;
// }
};
};
}
}
}
24 changes: 24 additions & 0 deletions themebuilder/src/state/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@ type SetFrameTheme = ActionBase & {
theme: pxt.ColorThemeInfo;
};

type SetEditingTheme = ActionBase & {
type: "SET_EDITING_THEME";
theme: pxt.ColorThemeInfo;
}

type SetColorsToHighlight = ActionBase & {
type: "SET_COLORS_TO_HIGHLIGHT";
colors: string[];
};

type SetUserProfile = ActionBase & {
type: "SET_USER_PROFILE";
profile: pxt.auth.UserProfile;
Expand All @@ -43,6 +53,8 @@ type ShowModal = ActionBase & {
export type Action =
| SetTargetConfig
| SetFrameTheme
| SetEditingTheme
| SetColorsToHighlight
| SetUserProfile
| ClearUserProfile
| HideModal
Expand All @@ -63,6 +75,16 @@ const setFrameTheme = (theme: pxt.ColorThemeInfo): SetFrameTheme => ({
theme,
})

const setEditingTheme = (theme: pxt.ColorThemeInfo): SetEditingTheme => ({
type: "SET_EDITING_THEME",
theme,
});

const setColorsToHighlight = (colors: string[]): SetColorsToHighlight => ({
type: "SET_COLORS_TO_HIGHLIGHT",
colors,
});

const setUserProfile = (profile: pxt.auth.UserProfile): SetUserProfile => ({
type: "SET_USER_PROFILE",
profile
Expand All @@ -84,6 +106,8 @@ const showModal = (modalOptions: ModalOptions): ShowModal => ({
export {
setTargetConfig,
setFrameTheme,
setEditingTheme,
setColorsToHighlight,
setUserProfile,
clearUserProfile,
hideModal,
Expand Down
14 changes: 13 additions & 1 deletion themebuilder/src/state/reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,19 @@ export default function reducer(state: AppState, action: Action): AppState {
case "SET_FRAME_THEME": {
return {
...state,
theme: action.theme,
frameTheme: action.theme,
};
}
case "SET_EDITING_THEME": {
return {
...state,
editingTheme: action.theme,
};
}
case "SET_COLORS_TO_HIGHLIGHT": {
return {
...state,
colorsToHighlight: action.colors
};
}
case "SET_USER_PROFILE": {
Expand Down
7 changes: 5 additions & 2 deletions themebuilder/src/state/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ import { ModalOptions } from "../types/modalOptions";

export type AppState = {
targetConfig?: pxt.TargetConfig;
theme?: pxt.ColorThemeInfo;
frameTheme?: pxt.ColorThemeInfo; // Theme actually being sent to the iframe (may contain temporary highlights, etc...)
editingTheme?: pxt.ColorThemeInfo; // Theme being edited
userProfile?: pxt.auth.UserProfile;
modal?: ModalOptions;
colorsToHighlight?: string[];
highlightColor: string;
};

export const initialAppState: AppState = {

highlightColor: "hotpink"
};
15 changes: 15 additions & 0 deletions themebuilder/src/transforms/clearHighlight.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { stateAndDispatch } from "../state";
import { setColorsToHighlight } from "../state/actions";
import { setCurrentFrameTheme } from "./setCurrentFrameTheme";

export function clearHighlight() {
const { state, dispatch } = stateAndDispatch();
const { editingTheme } = state;

// Go back to the editing theme, clearing any discrepancies between it and the iframe theme.
if (editingTheme) {
setCurrentFrameTheme(editingTheme);
}

dispatch(setColorsToHighlight([]));
}
Loading

0 comments on commit 07714ac

Please sign in to comment.