added controls

This commit is contained in:
Aminejvm 2021-03-24 18:55:12 +01:00
parent fc685affdd
commit c2f92ffd8a
5 changed files with 449 additions and 95 deletions

View File

@ -1,9 +1,14 @@
import * as React from "react";
import * as SVG from "~/common/svg";
import * as Constants from "~/common/constants";
import { P } from "~/components/system/components/Typography";
import { Slider } from "~/components/system/components/Slider";
import { css } from "@emotion/react";
const CONTROLS_STYLES_WRAPPER = (dark) => (theme) => css`
import Select from "./Select";
const CONTROLS_STYLES_WRAPPER = (theme) => css`
width: fit-content;
display: flex;
margin: 0 auto;
@ -11,15 +16,16 @@ const CONTROLS_STYLES_WRAPPER = (dark) => (theme) => css`
margin-left: 8px;
}
path {
stroke: ${dark ? theme.system.white : theme.system.black};
stroke: ${theme.fontPreviewDarkMode ? theme.system.white : theme.system.black};
}
`;
const CONTROLS_DARKMODE_WRAPPER = (dark) => (theme) => css`
display: inline-block;
const CONTROLS_DARKMODE_WRAPPER = (theme) => css`
display: flex;
border-radius: 4px;
border: 1px solid ${dark ? theme.system.textGrayDark : theme.system.gray20};
border: 1px solid ${theme.fontPreviewDarkMode ? theme.system.textGrayDark : theme.system.gray20};
button {
display: block;
box-sizing: border-box;
cursor: pointer;
padding: 8px 12px;
@ -27,60 +33,57 @@ const CONTROLS_DARKMODE_WRAPPER = (dark) => (theme) => css`
background: none;
border: none;
}
button:focus {
outline: none;
}
svg {
display: block;
}
.lightmode_btn {
path {
stroke: ${dark ? theme.system.gray50 : theme.system.black};
stroke: ${theme.fontPreviewDarkMode ? theme.system.gray50 : theme.system.black};
}
background-color: ${!dark ? theme.system.gray20 : "none"};
background-color: ${!theme.fontPreviewDarkMode ? theme.system.gray20 : "none"};
}
.darkmode_btn {
path {
stroke: ${dark ? theme.system.white : theme.system.textGray};
stroke: ${theme.fontPreviewDarkMode ? theme.system.white : theme.system.textGray};
}
background-color: ${dark ? theme.system.gray80 : "none"};
background-color: ${theme.fontPreviewDarkMode ? theme.system.gray80 : "none"};
}
`;
const CONTROLS_SETTINGS_BUTTON = (dark, isActive) => (theme) => css`
const CONTROLS_SETTINGS_BUTTON = (isActive) => (theme) => css`
padding: 8px 12px;
margin: 0;
border-radius: 4px;
background: none;
border: 1px solid ${dark ? theme.system.textGrayDark : theme.system.gray20};
border: 1px solid ${theme.fontPreviewDarkMode ? theme.system.textGrayDark : theme.system.gray20};
cursor: pointer;
${isActive &&
css`
background-color: ${dark ? theme.system.gray80 : theme.system.gray20};
background-color: ${theme.fontPreviewDarkMode ? theme.system.gray80 : theme.system.gray20};
`};
path {
${isActive
? css`
stroke: ${dark ? theme.system.white : theme.system.black};
stroke: ${theme.fontPreviewDarkMode ? theme.system.white : theme.system.black};
`
: css`
stroke: ${dark ? theme.system.gray50 : theme.system.textGray};
stroke: ${theme.fontPreviewDarkMode ? theme.system.gray50 : theme.system.textGray};
`}
}
`;
export default function Controls({
onDarkMode,
onLightMode,
onToggleSettings,
isDarkMode,
isSettingsVisible,
}) {
export const FixedControls = ({ onDarkMode, onLightMode, onToggleSettings, isSettingsVisible }) => {
return (
<div css={CONTROLS_STYLES_WRAPPER(isDarkMode)}>
<div css={CONTROLS_STYLES_WRAPPER}>
<div>
<button
css={CONTROLS_SETTINGS_BUTTON(isDarkMode, isSettingsVisible)}
onClick={onToggleSettings}
>
<button css={CONTROLS_SETTINGS_BUTTON(isSettingsVisible)} onClick={onToggleSettings}>
<SVG.Sliders />
</button>
</div>
<div css={CONTROLS_DARKMODE_WRAPPER(isDarkMode)}>
<div css={CONTROLS_DARKMODE_WRAPPER}>
<button onClick={onLightMode} className="lightmode_btn">
<SVG.Sun />
</button>
@ -90,4 +93,235 @@ export default function Controls({
</div>
</div>
);
}
};
const STYLES_CONTROLLER_WRAPPER = (theme) =>
css`
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
grid-column-gap: 48px;
grid-row-gap: 32px;
background-color: ${theme.fontPreviewDarkMode ? "#212124" : "#fbfbfb"};
width: 100%;
padding: 25px 32px 32px;
flex-wrap: wrap;
`;
const STYLES_CONTENT_SELECT = (theme) => css`
width: 100%;
border: 1px solid ${theme.fontPreviewDarkMode ? Constants.system.gray80 : Constants.system.gray20};
padding: 8px 12px;
border-radius: 4px;
`;
export const Controls = ({
settings,
updateFontSize,
updateLineHeight,
updateTracking,
updateColumn,
updateTextAlign,
updateVerticalAlign,
}) => {
const VerticalOptions = [
{ value: "top", name: "Top" },
{ value: "center", name: "Center" },
{ value: "bottom", name: "Bottom" },
];
return (
<div css={STYLES_CONTROLLER_WRAPPER}>
<div>
<p css={STYLES_LABEL}>Content</p>
<Select
inputStyle={STYLES_CONTENT_SELECT}
options={[{ value: "custom", name: "Custom" }]}
value={"Custom"}
/>
</div>
<AlignmentControl
options={VerticalOptions}
vAlign={settings.valign}
textAlign={settings.textAlign}
onChange={(e) => updateVerticalAlign(e.target.value)}
updateTextAlign={updateTextAlign}
/>
<Controller
selectSuffix="px"
label="Size"
min={12}
max={72}
step={2}
options={[
{ value: 12, name: "12px" },
{ value: 14, name: "14px" },
]}
value={settings.fontSize}
onChange={(e) => {
updateFontSize(e.target.value);
}}
/>
<Controller
label="Line Height"
selectSuffix="%"
min={40}
max={400}
step={20}
options={[
{ value: 100, name: "100%" },
{ value: 200, name: "200%" },
{ value: 300, name: "300%" },
{ value: 400, name: "400%" },
]}
value={settings.lineHeight}
onChange={(e) => updateLineHeight(e.target.value)}
/>
<Controller
label="Tracking"
selectSuffix="em"
min={-1}
max={1.5}
step={0.05}
options={[
{ value: -1, name: "-1em" },
{ value: 0, name: "0em" },
{ value: 1, name: "1em" },
{ value: 2, name: "2em" },
]}
value={settings.tracking}
onChange={(e) => updateTracking(e.target.value)}
/>
<Controller
label="Column"
min={1}
max={6}
step={1}
options={[
{ value: 1, name: "1" },
{ value: 2, name: "2" },
{ value: 3, name: "3" },
{ value: 4, name: "4" },
]}
value={settings.column}
onChange={(e) => updateColumn(e.target.value)}
/>
</div>
);
};
const STYLES_ALIGNEMENT_BUTTON = (theme) => css`
display: flex;
border-radius: 4px;
border: 1px solid ${theme.fontPreviewDarkMode ? theme.system.textGrayDark : theme.system.gray20};
margin-left: 16px;
button {
display: block;
box-sizing: border-box;
cursor: pointer;
padding: 8px 12px;
margin: 0;
border: none;
}
button:focus {
outline: none;
}
svg {
display: block;
}
/* .left_btn {
path {
stroke: ${dark ? theme.system.gray50 : theme.system.black};
}
background-color: ${dark ? theme.system.gray20 : "none"};
}
.center_btn {
path {
stroke: ${dark ? theme.system.white : theme.system.textGray};
}
background-color: ${dark ? theme.system.gray80 : "none"};
}
.right_btn {
path {
stroke: ${dark ? theme.system.white : theme.system.textGray};
}
background-color: ${dark ? theme.system.gray80 : "none"};
} */
`;
const getIconColor = (isActive, theme) => {
const darkMode = isActive ? theme.system.white : theme.system.textGray;
const lightMode = isActive ? theme.system.gray80 : theme.system.textGray;
return theme.fontPreviewDarkMode ? darkMode : lightMode;
};
const getBackgroundColor = (isActive, theme) => {
if (!isActive) return "transparent";
return theme.fontPreviewDarkMode ? theme.system.gray80 : theme.system.gray20;
};
const STYLES_ALIGN_BUTTON = (isActive) => (theme) => css`
background-color: ${getBackgroundColor(isActive, theme)};
path {
stroke: ${getIconColor(isActive, theme)};
}
`;
const AlignmentControl = ({ vAlign, textAlign, options, onChange, updateTextAlign }) => {
return (
<div>
<p css={STYLES_LABEL}>Alignment</p>
<div css={css({ display: "flex", alignItems: "center" })}>
<Select options={options} value={vAlign} onChange={onChange} />
<div css={STYLES_ALIGNEMENT_BUTTON}>
<button
onClick={() => updateTextAlign("left")}
css={STYLES_ALIGN_BUTTON(textAlign === "left")}
>
<SVG.AlignLeft height={16} width={16} />
</button>
<button
onClick={() => updateTextAlign("center")}
css={STYLES_ALIGN_BUTTON(textAlign === "center")}
>
<SVG.AlignCenter height={16} width={16} />
</button>
<button
onClick={() => updateTextAlign("right")}
css={STYLES_ALIGN_BUTTON(textAlign === "right")}
>
<SVG.AlignRight height={16} width={16} />
</button>
</div>
</div>
</div>
);
};
const STYLES_LABEL = (theme) => css`
font-size: 0.875rem;
color: ${theme.fontPreviewDarkMode ? theme.system.gray70 : theme.system.textGrayLight};
margin-bottom: 4px;
`;
const Controller = ({ value, options, onChange, selectSuffix = "", label, min, max, step }) => {
return (
<div>
<P css={STYLES_LABEL}>{label}</P>
<div css={css({ display: "flex", alignItems: "center" })}>
<Select
options={options}
value={value}
onChange={onChange}
placeholderSuffix={selectSuffix}
/>
<div style={{ width: "100%" }}>
<Slider
min={min}
max={max}
step={step}
value={value}
onChange={onChange}
containerStyle={{ height: "auto", marginLeft: "18px" }}
/>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,64 @@
import * as React from "react";
import * as SVG from "~/common/svg";
import { css } from "@emotion/react";
import { P } from "~/components/system/components/Typography";
export default function Select({
options = [],
value,
placeholderSuffix = "",
inputStyle,
...props
}) {
return (
<div
css={css`
position: relative;
`}
>
<div
css={[
(theme) => css`
display: flex;
padding: 4px 0px;
color: ${theme.fontPreviewDarkMode ? theme.system.white : theme.system.textGrayDark};
align-items: center;
& > * + * {
margin-left: 8px;
}
`,
inputStyle,
]}
>
<P
dark
css={css`
font-size: 14px;
`}
>
{value}
{placeholderSuffix}
</P>
<SVG.ChevronDown height="16px" display="block" />
</div>
<select
css={css`
opacity: 0;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
`}
value={value}
name="fontSize"
{...props}
>
{options.map((item) => (
<option value={item.value}>{item.name}</option>
))}
</select>
</div>
);
}

View File

@ -1,4 +1,5 @@
import * as React from "react";
import * as Events from "~/common/custom-events";
export const useFont = ({ url, name }, deps) => {
const [loading, setLoading] = React.useState(false);
@ -14,11 +15,14 @@ export const useFont = ({ url, name }, deps) => {
const customFonts = new FontFace(name, `url(${url})`);
customFonts.load().then((loadedFont) => {
document.fonts.add(loadedFont);
prevName.current = name;
setLoading(false);
window.$SLATES_LOADED_FONTS.push(name);
});
}, deps);
return { isFontLoading: loading, fontName: alreadyLoaded || !loading ? name : prevName.current };
return { isFontLoading: loading, fontName: alreadyLoaded ? name : prevName.current };
};
const initialState = {
@ -30,15 +34,21 @@ const initialState = {
textAlign: "left",
fontSize: 24,
column: 1,
lineHeight: 1,
lineHeight: 100,
tracking: 0,
leading: 0,
},
},
view: "paragraph",
};
const reducer = (state, action) => {
const updateSettingsField = (field, newValue) => ({
...state,
context: {
...state.context,
settings: { ...state.context.settings, [field]: newValue },
},
});
switch (action.type) {
case "SET_DARK_MODE":
return { ...state, context: { ...state.context, darkmode: true } };
@ -46,6 +56,18 @@ const reducer = (state, action) => {
return { ...state, context: { ...state.context, darkmode: false } };
case "TOGGLE_SETTINGS_VISIBILITY":
return { ...state, context: { ...state.context, showSettings: !state.context.showSettings } };
case "UPDATE_FONT_SIZE":
return updateSettingsField("fontSize", action.value);
case "UPDATE_LINE_HEIGHT":
return updateSettingsField("lineHeight", action.value);
case "UPDATE_TRACKING":
return updateSettingsField("tracking", action.value);
case "UPDATE_COLUMN":
return updateSettingsField("column", action.value);
case "UPDATE_TEXT_ALIGN":
return updateSettingsField("textAlign", action.value);
case "UPDATE_VERTICAL_ALIGN":
return updateSettingsField("valign", action.value);
default:
return state;
}
@ -53,13 +75,21 @@ const reducer = (state, action) => {
export const useFontControls = () => {
const [current, send] = React.useReducer(reducer, initialState);
const handlers = React.useMemo(
() => ({
setDarkMode: () => send({ type: "SET_DARK_MODE" }),
setLightMode: () => send({ type: "SET_LIGHT_MODE" }),
toggleSettings: () => send({ type: "TOGGLE_SETTINGS_VISIBILITY" }),
}),
[]
);
const handleDarkMode = (state) =>
Events.dispatchCustomEvent({
name: "set-slate-theme",
detail: { fontPreviewDarkMode: state },
});
const handlers = React.useMemo(() => ({
setDarkMode: () => handleDarkMode(true),
setLightMode: () => handleDarkMode(false),
toggleSettings: () => send({ type: "TOGGLE_SETTINGS_VISIBILITY" }),
updateFontSize: (v) => send({ type: "UPDATE_FONT_SIZE", value: v }),
updateLineHeight: (v) => send({ type: "UPDATE_LINE_HEIGHT", value: v }),
updateTracking: (v) => send({ type: "UPDATE_TRACKING", value: v }),
updateColumn: (v) => send({ type: "UPDATE_COLUMN", value: v }),
updateTextAlign: (v) => send({ type: "UPDATE_TEXT_ALIGN", value: v }),
updateVerticalAlign: (v) => send({ type: "UPDATE_VERTICAL_ALIGN", value: v }),
}));
return [current, handlers];
};

View File

@ -1,9 +1,10 @@
import * as React from "react";
import * as SVG from "~/common/svg";
import { css } from "@emotion/react";
import Controls from "./Controls";
import { useFont, useFontControls } from "./hooks";
import { FixedControls, Controls } from "./Controls";
const Glyphs = ({ dark }) => (
<div>
@ -14,88 +15,114 @@ const Glyphs = ({ dark }) => (
</div>
);
const GET_STYLES_CONTAINER = (dark) => (theme) => css`
const GET_STYLES_CONTAINER = (theme) => css`
position: relative;
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
background-color: ${dark ? theme.system.pitchBlack : theme.system.white};
padding: 14px 32px 28px;
background-color: ${theme.fontPreviewDarkMode ? theme.system.pitchBlack : theme.system.white};
padding-top: 14px;
`;
export default function FontFrame({ cid, url, ...props }) {
const { isFontLoading, fontName } = useFont({ url, name: cid }, [cid]);
const [currentState, { setLightMode, setDarkMode, toggleSettings }] = useFontControls();
const [
currentState,
{
setLightMode,
setDarkMode,
toggleSettings,
updateFontSize,
updateLineHeight,
updateTracking,
updateColumn,
updateTextAlign,
updateVerticalAlign,
},
] = useFontControls();
return (
<div
css={GET_STYLES_CONTAINER(currentState.context.darkmode)}
style={{ fontFamily: fontName }}
{...props}
>
<Controls
<div css={GET_STYLES_CONTAINER} style={{ fontFamily: fontName }} {...props}>
<FixedControls
onDarkMode={setDarkMode}
onLightMode={setLightMode}
onToggleSettings={toggleSettings}
isDarkMode={currentState.context.darkmode}
isSettingsVisible={currentState.context.showSettings}
/>
{isFontLoading && (
<div
css={css({
display: "flex",
justifyContent: "center",
alignItems: "center",
position: "absolute",
width: "100%",
height: "100%",
backgroundColor: "rgba(0,0,0,0.4)",
})}
>
<p>loading...</p>
</div>
)}
<Sentence
content="The sun is gone but I have a light The day is done but I'm having fun I think I'm dumb or maybe I'm just happy I think I'm just happy I think I'm just happy I'm not like them but I can pretend The sun is gone but I have a light The day is done but I'm having fun I think I'm dumb or maybe I'm just happy"
valign={currentState.context.settings.valign}
textAlign={currentState.context.settings.textAlign}
fontSize={currentState.context.settings.fontSize}
lineHeight={currentState.context.settings.lineHeight}
letterSpacing={currentState.context.settings.tracking}
<div style={{ position: "relative", flexGrow: 1 }}>
{isFontLoading && (
<div
css={css({
display: "flex",
justifyContent: "center",
alignItems: "center",
position: "absolute",
width: "100%",
height: "100%",
backgroundColor: "rgba(0,0,0,0.4)",
})}
>
<p>loading...</p>
</div>
)}
<Sentence
content="The sun is gone but I have a light The day is done but I'm having fun I think I'm dumb or maybe I'm just happy I think I'm just happy I think I'm just happy I'm not like them but I can pretend The sun is gone but I have a light The day is done but I'm having fun I think I'm dumb or maybe I'm just happy"
valign={currentState.context.settings.valign}
textAlign={currentState.context.settings.textAlign}
fontSize={currentState.context.settings.fontSize}
lineHeight={currentState.context.settings.lineHeight}
tracking={currentState.context.settings.tracking}
/>
</div>
<Controls
dark={currentState.context.darkmode}
settings={currentState.context.settings}
updateFontSize={updateFontSize}
updateLineHeight={updateLineHeight}
updateTracking={updateTracking}
updateColumn={updateColumn}
updateTextAlign={updateTextAlign}
updateVerticalAlign={updateVerticalAlign}
/>
</div>
);
}
const Sentence = ({ content, valign, textAlign, fontSize, lineHeight, tracking, dark }) => {
const STYLES_SENTENCE = (theme) => css`
display: flex;
height: 100%;
flex-direction: column;
width: 100%;
margin-top: 12px;
overflow-y: scroll;
color: ${theme.fontPreviewDarkMode ? theme.system.white : theme.system.pitchBlack};
padding: 0px 32px 28px;
&:focus {
outline: none;
}
`;
const Sentence = ({ content, valign, textAlign, fontSize, lineHeight, tracking }) => {
const [value, setValue] = React.useState(`<p>${content}</p>`);
const mapAlignToFlex = { center: "center", top: "flex-start", bottom: "flex-end" };
return (
<div
contentEditable="true"
suppressContentEditableWarning={true}
dangerouslySetInnerHTML={{ __html: value }}
onInput={(e) => setValue(e.target.innerText)}
css={(theme) =>
css({
display: "flex",
flexGrow: 1,
flexDirection: "column",
width: "100%",
marginTop: "12px",
overflowY: "scroll",
color: dark ? theme.system.white : theme.system.pitchBlack,
justifyContent: valign,
textAlign,
fontSize,
lineHeight,
letterSpacing: tracking,
})
}
style={{
fontSize: `${fontSize}px`,
lineHeight: `${lineHeight}%`,
letterSpacing: `${tracking}em`,
justifyContent: mapAlignToFlex[valign],
textAlign,
}}
css={STYLES_SENTENCE}
onKeyDown={(e) => {
e.stopPropagation();
}}
></div>
>
{content}
</div>
);
};

View File

@ -17,9 +17,8 @@ const STYLES_CONTAINER = css`
const STYLES_BAR_CONTAINER = css`
height: 24px;
width: calc(100% - 12px);
width: calc(100%);
position: absolute;
left: 12px;
`;
const STYLES_SLIDER_BAR = css`
@ -63,7 +62,7 @@ const STYLES_SLIDER_HANDLE = css`
background-color: ${Constants.system.brand};
cursor: pointer;
position: relative;
bottom: 16px;
bottom: 18px;
right: 12px;
:hover {