Merge pull request #777 from quantified-uncertainty/pause-autoplay

Manual play mode
This commit is contained in:
Ozzie Gooen 2022-07-05 19:52:29 -04:00 committed by GitHub
commit f2ccf574ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 641 additions and 352 deletions

View File

@ -8,7 +8,8 @@
"@hookform/resolvers": "^2.9.3",
"@quri/squiggle-lang": "^0.2.8",
"@react-hook/size": "^2.1.2",
"clsx": "^1.2.0",
"clsx": "^1.1.1",
"framer-motion": "^6.4.1",
"lodash": "^4.17.21",
"react": "^18.1.0",
"react-ace": "^10.1.0",

View File

@ -1,5 +1,5 @@
import _ from "lodash";
import React, { FC, useMemo } from "react";
import React, { FC, useMemo, useRef } from "react";
import AceEditor from "react-ace";
import "ace-builds/src-noconflict/mode-golang";
@ -8,6 +8,7 @@ import "ace-builds/src-noconflict/theme-github";
interface CodeEditorProps {
value: string;
onChange: (value: string) => void;
onSubmit?: () => void;
oneLine?: boolean;
width?: number;
height: number;
@ -17,6 +18,7 @@ interface CodeEditorProps {
export const CodeEditor: FC<CodeEditorProps> = ({
value,
onChange,
onSubmit,
oneLine = false,
showGutter = false,
height,
@ -24,6 +26,10 @@ export const CodeEditor: FC<CodeEditorProps> = ({
const lineCount = value.split("\n").length;
const id = useMemo(() => _.uniqueId(), []);
// this is necessary because AceEditor binds commands on mount, see https://github.com/securingsincity/react-ace/issues/684
const onSubmitRef = useRef<typeof onSubmit | null>(null);
onSubmitRef.current = onSubmit;
return (
<AceEditor
value={value}
@ -46,6 +52,13 @@ export const CodeEditor: FC<CodeEditorProps> = ({
enableBasicAutocompletion: false,
enableLiveAutocompletion: false,
}}
commands={[
{
name: "submit",
bindKey: { mac: "Cmd-Enter", win: "Ctrl-Enter" },
exec: () => onSubmitRef.current?.(),
},
]}
/>
);
};

View File

@ -48,57 +48,59 @@ export interface SquiggleChartProps {
const defaultOnChange = () => {};
export const SquiggleChart: React.FC<SquiggleChartProps> = ({
code = "",
environment,
onChange = defaultOnChange, // defaultOnChange must be constant, don't move its definition here
height = 200,
bindings = defaultBindings,
jsImports = defaultImports,
showSummary = false,
width,
showTypes = false,
showControls = false,
logX = false,
expY = false,
diagramStart = 0,
diagramStop = 10,
diagramCount = 100,
}) => {
const result = useSquiggle({
code,
bindings,
export const SquiggleChart: React.FC<SquiggleChartProps> = React.memo(
({
code = "",
environment,
jsImports,
onChange,
});
onChange = defaultOnChange, // defaultOnChange must be constant, don't move its definition here
height = 200,
bindings = defaultBindings,
jsImports = defaultImports,
showSummary = false,
width,
showTypes = false,
showControls = false,
logX = false,
expY = false,
diagramStart = 0,
diagramStop = 10,
diagramCount = 100,
}) => {
const result = useSquiggle({
code,
bindings,
environment,
jsImports,
onChange,
});
if (result.tag !== "Ok") {
return <SquiggleErrorAlert error={result.value} />;
if (result.tag !== "Ok") {
return <SquiggleErrorAlert error={result.value} />;
}
let distributionPlotSettings = {
showControls,
showSummary,
logX,
expY,
};
let chartSettings = {
start: diagramStart,
stop: diagramStop,
count: diagramCount,
};
return (
<SquiggleItem
expression={result.value}
width={width}
height={height}
distributionPlotSettings={distributionPlotSettings}
showTypes={showTypes}
chartSettings={chartSettings}
environment={environment ?? defaultEnvironment}
/>
);
}
let distributionPlotSettings = {
showControls,
showSummary,
logX,
expY,
};
let chartSettings = {
start: diagramStart,
stop: diagramStop,
count: diagramCount,
};
return (
<SquiggleItem
expression={result.value}
width={width}
height={height}
distributionPlotSettings={distributionPlotSettings}
showTypes={showTypes}
chartSettings={chartSettings}
environment={environment ?? defaultEnvironment}
/>
);
};
);

View File

@ -1,15 +1,18 @@
import React, { FC, Fragment, useState, useEffect } from "react";
import React, { FC, useState, useEffect, useMemo } from "react";
import { Path, useForm, UseFormRegister, useWatch } from "react-hook-form";
import * as yup from "yup";
import { useMaybeControlledValue } from "../lib/hooks";
import { yupResolver } from "@hookform/resolvers/yup";
import { Tab } from "@headlessui/react";
import {
ChartSquareBarIcon,
CheckCircleIcon,
CodeIcon,
CogIcon,
CurrencyDollarIcon,
EyeIcon,
PauseIcon,
PlayIcon,
RefreshIcon,
} from "@heroicons/react/solid";
import clsx from "clsx";
@ -20,6 +23,9 @@ import { CodeEditor } from "./CodeEditor";
import { JsonEditor } from "./JsonEditor";
import { ErrorAlert, SuccessAlert } from "./Alert";
import { SquiggleContainer } from "./SquiggleContainer";
import { Toggle } from "./ui/Toggle";
import { Checkbox } from "./ui/Checkbox";
import { StyledTab } from "./ui/StyledTab";
interface PlaygroundProps {
/** The initial squiggle string to put in the playground */
@ -44,104 +50,45 @@ interface PlaygroundProps {
showEditor?: boolean;
}
const schema = yup
.object()
.shape({
sampleCount: yup
.number()
.required()
.positive()
.integer()
.default(1000)
.min(10)
.max(1000000),
xyPointLength: yup
.number()
.required()
.positive()
.integer()
.default(1000)
.min(10)
.max(10000),
chartHeight: yup.number().required().positive().integer().default(350),
leftSizePercent: yup
.number()
.required()
.positive()
.integer()
.min(10)
.max(100)
.default(50),
showTypes: yup.boolean(),
showControls: yup.boolean(),
showSummary: yup.boolean(),
showEditor: yup.boolean(),
logX: yup.boolean(),
expY: yup.boolean(),
showSettingsPage: yup.boolean().default(false),
diagramStart: yup
.number()
.required()
.positive()
.integer()
.default(0)
.min(0),
diagramStop: yup
.number()
.required()
.positive()
.integer()
.default(10)
.min(0),
diagramCount: yup
.number()
.required()
.positive()
.integer()
.default(20)
.min(2),
})
.required();
const schema = yup.object({}).shape({
sampleCount: yup
.number()
.required()
.positive()
.integer()
.default(1000)
.min(10)
.max(1000000),
xyPointLength: yup
.number()
.required()
.positive()
.integer()
.default(1000)
.min(10)
.max(10000),
chartHeight: yup.number().required().positive().integer().default(350),
leftSizePercent: yup
.number()
.required()
.positive()
.integer()
.min(10)
.max(100)
.default(50),
showTypes: yup.boolean().required(),
showControls: yup.boolean().required(),
showSummary: yup.boolean().required(),
showEditor: yup.boolean().required(),
logX: yup.boolean().required(),
expY: yup.boolean().required(),
showSettingsPage: yup.boolean().default(false),
diagramStart: yup.number().required().positive().integer().default(0).min(0),
diagramStop: yup.number().required().positive().integer().default(10).min(0),
diagramCount: yup.number().required().positive().integer().default(20).min(2),
});
type StyledTabProps = {
name: string;
icon: (props: React.ComponentProps<"svg">) => JSX.Element;
};
const StyledTab: React.FC<StyledTabProps> = ({ name, icon: Icon }) => {
return (
<Tab key={name} as={Fragment}>
{({ selected }) => (
<button className="group flex rounded-md focus:outline-none focus-visible:ring-offset-gray-100">
<span
className={clsx(
"p-1 pl-2.5 pr-3.5 rounded-md flex items-center text-sm font-medium",
selected && "bg-white shadow-sm ring-1 ring-black ring-opacity-5"
)}
>
<Icon
className={clsx(
"-ml-0.5 mr-2 h-4 w-4",
selected
? "text-slate-500"
: "text-gray-400 group-hover:text-gray-900"
)}
/>
<span
className={clsx(
selected
? "text-gray-900"
: "text-gray-600 group-hover:text-gray-900"
)}
>
{name}
</span>
</span>
</button>
)}
</Tab>
);
};
type FormFields = yup.InferType<typeof schema>;
const HeadedSection: FC<{ title: string; children: React.ReactNode }> = ({
title,
@ -182,27 +129,253 @@ function InputItem<T>({
);
}
function Checkbox<T>({
name,
label,
const SamplingSettings: React.FC<{ register: UseFormRegister<FormFields> }> = ({
register,
}: {
name: Path<T>;
label: string;
register: UseFormRegister<T>;
}) {
return (
<label className="flex items-center">
<input
type="checkbox"
{...register(name)}
className="form-checkbox focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded"
}) => (
<div className="space-y-6 p-3 max-w-xl">
<div>
<InputItem
name="sampleCount"
type="number"
label="Sample Count"
register={register}
/>
{/* Clicking on the div makes the checkbox lose focus while mouse button is pressed, leading to annoying blinking; I couldn't figure out how to fix this. */}
<div className="ml-3 text-sm font-medium text-gray-700">{label}</div>
</label>
<div className="mt-2">
<Text>
How many samples to use for Monte Carlo simulations. This can
occasionally be overridden by specific Squiggle programs.
</Text>
</div>
</div>
<div>
<InputItem
name="xyPointLength"
type="number"
register={register}
label="Coordinate Count (For PointSet Shapes)"
/>
<div className="mt-2">
<Text>
When distributions are converted into PointSet shapes, we need to know
how many coordinates to use.
</Text>
</div>
</div>
</div>
);
const ViewSettings: React.FC<{ register: UseFormRegister<FormFields> }> = ({
register,
}) => (
<div className="space-y-6 p-3 divide-y divide-gray-200 max-w-xl">
<HeadedSection title="General Display Settings">
<div className="space-y-4">
<Checkbox
name="showEditor"
register={register}
label="Show code editor on left"
/>
<InputItem
name="chartHeight"
type="number"
register={register}
label="Chart Height (in pixels)"
/>
<Checkbox
name="showTypes"
register={register}
label="Show information about displayed types"
/>
</div>
</HeadedSection>
<div className="pt-8">
<HeadedSection title="Distribution Display Settings">
<div className="space-y-2">
<Checkbox
register={register}
name="logX"
label="Show x scale logarithmically"
/>
<Checkbox
register={register}
name="expY"
label="Show y scale exponentially"
/>
<Checkbox
register={register}
name="showControls"
label="Show toggles to adjust scale of x and y axes"
/>
<Checkbox
register={register}
name="showSummary"
label="Show summary statistics"
/>
</div>
</HeadedSection>
</div>
<div className="pt-8">
<HeadedSection title="Function Display Settings">
<div className="space-y-6">
<Text>
When displaying functions of single variables that return numbers or
distributions, we need to use defaults for the x-axis. We need to
select a minimum and maximum value of x to sample, and a number n of
the number of points to sample.
</Text>
<div className="space-y-4">
<InputItem
type="number"
name="diagramStart"
register={register}
label="Min X Value"
/>
<InputItem
type="number"
name="diagramStop"
register={register}
label="Max X Value"
/>
<InputItem
type="number"
name="diagramCount"
register={register}
label="Points between X min and X max to sample"
/>
</div>
</div>
</HeadedSection>
</div>
</div>
);
const InputVariablesSettings: React.FC<{
initialImports: any; // TODO - any json type
setImports: (imports: any) => void;
}> = ({ initialImports, setImports }) => {
const [importString, setImportString] = useState(() =>
JSON.stringify(initialImports)
);
}
const [importsAreValid, setImportsAreValid] = useState(true);
const onChange = (value: string) => {
setImportString(value);
let imports = {} as any;
try {
imports = JSON.parse(value);
setImportsAreValid(true);
} catch (e) {
setImportsAreValid(false);
}
setImports(imports);
};
return (
<div className="p-3 max-w-3xl">
<HeadedSection title="Import Variables from JSON">
<div className="space-y-6">
<Text>
You can import variables from JSON into your Squiggle code.
Variables are accessed with dollar signs. For example, "timeNow"
would be accessed as "$timeNow".
</Text>
<div className="border border-slate-200 mt-6 mb-2">
<JsonEditor
value={importString}
onChange={onChange}
oneLine={false}
showGutter={true}
height={150}
/>
</div>
<div className="p-1 pt-2">
{importsAreValid ? (
<SuccessAlert heading="Valid JSON" />
) : (
<ErrorAlert heading="Invalid JSON">
You must use valid JSON in this editor.
</ErrorAlert>
)}
</div>
</div>
</HeadedSection>
</div>
);
};
const RunControls: React.FC<{
autorunMode: boolean;
isRunning: boolean;
isStale: boolean;
onAutorunModeChange: (value: boolean) => void;
run: () => void;
}> = ({ autorunMode, isRunning, isStale, onAutorunModeChange, run }) => {
const CurrentPlayIcon = isRunning ? RefreshIcon : PlayIcon;
return (
<div className="flex space-x-1 items-center">
{autorunMode ? null : (
<button onClick={run}>
<CurrentPlayIcon
className={clsx(
"w-8 h-8",
isRunning && "animate-spin",
isStale ? "text-indigo-500" : "text-gray-400"
)}
/>
</button>
)}
<Toggle
texts={["Autorun", "Paused"]}
icons={[CheckCircleIcon, PauseIcon]}
status={autorunMode}
onChange={onAutorunModeChange}
/>
</div>
);
};
const useRunnerState = (code: string) => {
const [autorunMode, setAutorunMode] = useState(true);
const [renderedCode, setRenderedCode] = useState(code); // used in manual run mode only
const [isRunning, setIsRunning] = useState(false); // used in manual run mode only
// This part is tricky and fragile; we need to re-render first to make sure that the icon is spinning,
// and only then evaluate the squiggle code (which freezes the UI).
// Also note that `useEffect` execution order matters here.
// Hopefully it'll all go away after we make squiggle code evaluation async.
useEffect(() => {
if (renderedCode === code && isRunning) {
// It's not possible to put this after `setRenderedCode(code)` below because React would apply
// `setIsRunning` and `setRenderedCode` together and spinning icon will disappear immediately.
setIsRunning(false);
}
}, [renderedCode, code, isRunning]);
useEffect(() => {
if (!autorunMode && isRunning) {
setRenderedCode(code); // TODO - force run even if code hasn't changed
}
}, [autorunMode, code, isRunning]);
const run = () => {
// The rest will be handled by useEffects above, but we need to update the spinner first.
setIsRunning(true);
};
return {
run,
renderedCode: autorunMode ? code : renderedCode,
isRunning,
autorunMode,
setAutorunMode: (newValue: boolean) => {
if (!newValue) setRenderedCode(code);
setAutorunMode(newValue);
},
};
};
export const SquigglePlayground: FC<PlaygroundProps> = ({
defaultCode = "",
@ -222,9 +395,9 @@ export const SquigglePlayground: FC<PlaygroundProps> = ({
defaultValue: defaultCode,
onChange: onCodeChange,
});
const [importString, setImportString] = useState("{}");
const [imports, setImports] = useState({});
const [importsAreValid, setImportsAreValid] = useState(true);
const { register, control } = useForm({
resolver: yupResolver(schema),
defaultValues: {
@ -252,173 +425,20 @@ export const SquigglePlayground: FC<PlaygroundProps> = ({
onSettingsChange?.(vars);
}, [vars, onSettingsChange]);
const env: environment = {
sampleCount: Number(vars.sampleCount),
xyPointLength: Number(vars.xyPointLength),
};
const getChangeJson = (r: string) => {
setImportString(r);
try {
setImports(JSON.parse(r));
setImportsAreValid(true);
} catch (e) {
setImportsAreValid(false);
}
};
const samplingSettings = (
<div className="space-y-6 p-3 max-w-xl">
<div>
<InputItem
name="sampleCount"
type="number"
label="Sample Count"
register={register}
/>
<div className="mt-2">
<Text>
How many samples to use for Monte Carlo simulations. This can
occasionally be overridden by specific Squiggle programs.
</Text>
</div>
</div>
<div>
<InputItem
name="xyPointLength"
type="number"
register={register}
label="Coordinate Count (For PointSet Shapes)"
/>
<div className="mt-2">
<Text>
When distributions are converted into PointSet shapes, we need to
know how many coordinates to use.
</Text>
</div>
</div>
</div>
const env: environment = useMemo(
() => ({
sampleCount: Number(vars.sampleCount),
xyPointLength: Number(vars.xyPointLength),
}),
[vars.sampleCount, vars.xyPointLength]
);
const viewSettings = (
<div className="space-y-6 p-3 divide-y divide-gray-200 max-w-xl">
<HeadedSection title="General Display Settings">
<div className="space-y-4">
<Checkbox
name="showEditor"
register={register}
label="Show code editor on left"
/>
<InputItem
name="chartHeight"
type="number"
register={register}
label="Chart Height (in pixels)"
/>
<Checkbox
name="showTypes"
register={register}
label="Show information about displayed types"
/>
</div>
</HeadedSection>
<div className="pt-8">
<HeadedSection title="Distribution Display Settings">
<div className="space-y-2">
<Checkbox
register={register}
name="logX"
label="Show x scale logarithmically"
/>
<Checkbox
register={register}
name="expY"
label="Show y scale exponentially"
/>
<Checkbox
register={register}
name="showControls"
label="Show toggles to adjust scale of x and y axes"
/>
<Checkbox
register={register}
name="showSummary"
label="Show summary statistics"
/>
</div>
</HeadedSection>
</div>
<div className="pt-8">
<HeadedSection title="Function Display Settings">
<div className="space-y-6">
<Text>
When displaying functions of single variables that return numbers
or distributions, we need to use defaults for the x-axis. We need
to select a minimum and maximum value of x to sample, and a number
n of the number of points to sample.
</Text>
<div className="space-y-4">
<InputItem
type="number"
name="diagramStart"
register={register}
label="Min X Value"
/>
<InputItem
type="number"
name="diagramStop"
register={register}
label="Max X Value"
/>
<InputItem
type="number"
name="diagramCount"
register={register}
label="Points between X min and X max to sample"
/>
</div>
</div>
</HeadedSection>
</div>
</div>
);
const inputVariableSettings = (
<div className="p-3 max-w-3xl">
<HeadedSection title="Import Variables from JSON">
<div className="space-y-6">
<Text>
You can import variables from JSON into your Squiggle code.
Variables are accessed with dollar signs. For example, "timeNow"
would be accessed as "$timeNow".
</Text>
<div className="border border-slate-200 mt-6 mb-2">
<JsonEditor
value={importString}
onChange={getChangeJson}
oneLine={false}
showGutter={true}
height={150}
/>
</div>
<div className="p-1 pt-2">
{importsAreValid ? (
<SuccessAlert heading="Valid JSON" />
) : (
<ErrorAlert heading="Invalid JSON">
You must use valid JSON in this editor.
</ErrorAlert>
)}
</div>
</div>
</HeadedSection>
</div>
);
const { run, autorunMode, setAutorunMode, isRunning, renderedCode } =
useRunnerState(code);
const squiggleChart = (
<SquiggleChart
code={code}
code={renderedCode}
environment={env}
diagramStart={Number(vars.diagramStart)}
diagramStop={Number(vars.diagramStop)}
@ -437,8 +457,9 @@ export const SquigglePlayground: FC<PlaygroundProps> = ({
const firstTab = vars.showEditor ? (
<div className="border border-slate-200">
<CodeEditor
value={code ?? ""}
value={code}
onChange={setCode}
onSubmit={run}
oneLine={false}
showGutter={true}
height={height - 1}
@ -449,12 +470,21 @@ export const SquigglePlayground: FC<PlaygroundProps> = ({
);
const tabs = (
<Tab.Panels>
<Tab.Panel>{firstTab}</Tab.Panel>
<Tab.Panel>{samplingSettings}</Tab.Panel>
<Tab.Panel>{viewSettings}</Tab.Panel>
<Tab.Panel>{inputVariableSettings}</Tab.Panel>
</Tab.Panels>
<StyledTab.Panels>
<StyledTab.Panel>{firstTab}</StyledTab.Panel>
<StyledTab.Panel>
<SamplingSettings register={register} />
</StyledTab.Panel>
<StyledTab.Panel>
<ViewSettings register={register} />
</StyledTab.Panel>
<StyledTab.Panel>
<InputVariablesSettings
initialImports={imports}
setImports={setImports}
/>
</StyledTab.Panel>
</StyledTab.Panels>
);
const withEditor = (
@ -468,20 +498,29 @@ export const SquigglePlayground: FC<PlaygroundProps> = ({
return (
<SquiggleContainer>
<Tab.Group>
<StyledTab.Group>
<div className="pb-4">
<Tab.List className="flex w-fit p-0.5 mt-2 rounded-md bg-slate-100 hover:bg-slate-200">
<StyledTab
name={vars.showEditor ? "Code" : "Display"}
icon={vars.showEditor ? CodeIcon : EyeIcon}
<div className="flex justify-between items-center mt-2">
<StyledTab.List>
<StyledTab
name={vars.showEditor ? "Code" : "Display"}
icon={vars.showEditor ? CodeIcon : EyeIcon}
/>
<StyledTab name="Sampling Settings" icon={CogIcon} />
<StyledTab name="View Settings" icon={ChartSquareBarIcon} />
<StyledTab name="Input Variables" icon={CurrencyDollarIcon} />
</StyledTab.List>
<RunControls
autorunMode={autorunMode}
isStale={renderedCode !== code}
run={run}
isRunning={isRunning}
onAutorunModeChange={setAutorunMode}
/>
<StyledTab name="Sampling Settings" icon={CogIcon} />
<StyledTab name="View Settings" icon={ChartSquareBarIcon} />
<StyledTab name="Input Variables" icon={CurrencyDollarIcon} />
</Tab.List>
</div>
{vars.showEditor ? withEditor : withoutEditor}
</div>
</Tab.Group>
</StyledTab.Group>
</SquiggleContainer>
);
};

View File

@ -0,0 +1,24 @@
import React from "react";
import { Path, UseFormRegister } from "react-hook-form";
export function Checkbox<T>({
name,
label,
register,
}: {
name: Path<T>;
label: string;
register: UseFormRegister<T>;
}) {
return (
<label className="flex items-center">
<input
type="checkbox"
{...register(name)}
className="form-checkbox focus:ring-indigo-500 h-4 w-4 text-indigo-600 border-gray-300 rounded"
/>
{/* Clicking on the div makes the checkbox lose focus while mouse button is pressed, leading to annoying blinking; I couldn't figure out how to fix this. */}
<div className="ml-3 text-sm font-medium text-gray-700">{label}</div>
</label>
);
}

View File

@ -0,0 +1,60 @@
import React, { Fragment } from "react";
import { Tab } from "@headlessui/react";
import clsx from "clsx";
type StyledTabProps = {
name: string;
icon: (props: React.ComponentProps<"svg">) => JSX.Element;
};
type StyledTabType = React.FC<StyledTabProps> & {
List: React.FC<{ children: React.ReactNode }>;
Group: typeof Tab.Group;
Panels: typeof Tab.Panels;
Panel: typeof Tab.Panel;
};
export const StyledTab: StyledTabType = ({ name, icon: Icon }) => {
return (
<Tab as={Fragment}>
{({ selected }) => (
<button className="group flex rounded-md focus:outline-none focus-visible:ring-offset-gray-100">
<span
className={clsx(
"p-1 pl-2.5 pr-3.5 rounded-md flex items-center text-sm font-medium",
selected && "bg-white shadow-sm ring-1 ring-black ring-opacity-5"
)}
>
<Icon
className={clsx(
"-ml-0.5 mr-2 h-4 w-4",
selected
? "text-slate-500"
: "text-gray-400 group-hover:text-gray-900"
)}
/>
<span
className={clsx(
selected
? "text-gray-900"
: "text-gray-600 group-hover:text-gray-900"
)}
>
{name}
</span>
</span>
</button>
)}
</Tab>
);
};
StyledTab.List = ({ children }) => (
<Tab.List className="flex w-fit p-0.5 rounded-md bg-slate-100 hover:bg-slate-200">
{children}
</Tab.List>
);
StyledTab.Group = Tab.Group;
StyledTab.Panels = Tab.Panels;
StyledTab.Panel = Tab.Panel;

View File

@ -0,0 +1,41 @@
import clsx from "clsx";
import { motion } from "framer-motion";
import React from "react";
type IconType = (props: React.ComponentProps<"svg">) => JSX.Element;
type Props = {
status: boolean;
onChange: (status: boolean) => void;
texts: [string, string];
icons: [IconType, IconType];
};
export const Toggle: React.FC<Props> = ({
texts: [onText, offText],
icons: [OnIcon, OffIcon],
status,
onChange,
}) => {
const CurrentIcon = status ? OnIcon : OffIcon;
return (
<motion.button
layout
transition={{ duration: 0.2 }}
className={clsx(
"rounded-full py-1 bg-indigo-500 text-white text-xs font-semibold flex items-center space-x-1",
status ? "bg-indigo-500" : "bg-gray-400",
status ? "pl-1 pr-3" : "pl-3 pr-1",
!status && "flex-row-reverse space-x-reverse"
)}
onClick={() => onChange(!status)}
>
<motion.div layout transition={{ duration: 0.2 }}>
<CurrentIcon className="w-6 h-6" />
</motion.div>
<motion.span layout transition={{ duration: 0.2 }}>
{status ? onText : offText}
</motion.span>
</motion.button>
);
};

111
yarn.lock
View File

@ -1802,6 +1802,18 @@
url-loader "^4.1.1"
webpack "^5.72.1"
"@emotion/is-prop-valid@^0.8.2":
version "0.8.8"
resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz#db28b1c4368a259b60a97311d6a952d4fd01ac1a"
integrity sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==
dependencies:
"@emotion/memoize" "0.7.4"
"@emotion/memoize@0.7.4":
version "0.7.4"
resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.4.tgz#19bf0f5af19149111c40d98bb0cf82119f5d9eeb"
integrity sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==
"@eslint/eslintrc@^1.3.0":
version "1.3.0"
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.3.0.tgz#29f92c30bb3e771e4a2048c95fa6855392dfac4f"
@ -2237,6 +2249,59 @@
resolved "https://registry.yarnpkg.com/@mdx-js/util/-/util-1.6.22.tgz#219dfd89ae5b97a8801f015323ffa4b62f45718b"
integrity sha512-H1rQc1ZOHANWBvPcW+JpGwr+juXSxM8Q8YCkm3GhZd8REu1fHR3z99CErO1p9pkcfcxZnMdIZdIsXkOHY0NilA==
"@motionone/animation@^10.10.1":
version "10.10.1"
resolved "https://registry.yarnpkg.com/@motionone/animation/-/animation-10.10.1.tgz#d3d3ecb11c6d7507a104b080f31b07be9bc7056d"
integrity sha512-iX839/Ir5wT7hVX0yCZYjcDhHAOkVR5hIhVBTf37qEUD693uVwrxC2i1BI9vMVPc1rIoFtftYjOtwoO9Oq/aog==
dependencies:
"@motionone/easing" "^10.9.0"
"@motionone/types" "^10.9.0"
"@motionone/utils" "^10.9.0"
tslib "^2.3.1"
"@motionone/dom@^10.11.0":
version "10.11.0"
resolved "https://registry.yarnpkg.com/@motionone/dom/-/dom-10.11.0.tgz#dc2e46b0e21044cc9ae89a994cd6725eac09e48b"
integrity sha512-ST56HBslkeoeDwqRFWafkD+JT5FEwlHqB2K2KGaQh6wo6zfKQ9xwjQcqgDiBv2gg6s8ycjHAdFS6dnMHQ5hXKw==
dependencies:
"@motionone/animation" "^10.10.1"
"@motionone/generators" "^10.9.0"
"@motionone/types" "^10.9.0"
"@motionone/utils" "^10.9.0"
hey-listen "^1.0.8"
tslib "^2.3.1"
"@motionone/easing@^10.9.0":
version "10.9.0"
resolved "https://registry.yarnpkg.com/@motionone/easing/-/easing-10.9.0.tgz#42910c70dd30e1bbba27a9612ce03bca022d0da7"
integrity sha512-FYIr3HlQEb7aE5LOpY6BPQUaPyKeJt6VfGA+npy73+JIGqoVOjbrdZ1ZQxzTXqO76mG3UZvv1+twrDamRQsxFw==
dependencies:
"@motionone/utils" "^10.9.0"
tslib "^2.3.1"
"@motionone/generators@^10.9.0":
version "10.9.0"
resolved "https://registry.yarnpkg.com/@motionone/generators/-/generators-10.9.0.tgz#46f3e706f45a9566db7e0ce62d677c884813cdaa"
integrity sha512-BOkHx4qQswJV+z/6k05qdvRENE4hG606NI5cIPTsLtSuksnRn83utuj/15VTNoFeYHuTdhwzxvIPvlPVayIGTg==
dependencies:
"@motionone/types" "^10.9.0"
"@motionone/utils" "^10.9.0"
tslib "^2.3.1"
"@motionone/types@^10.9.0":
version "10.9.0"
resolved "https://registry.yarnpkg.com/@motionone/types/-/types-10.9.0.tgz#7caa9c1b746dd30bcf6104ce9c82a5a928385da2"
integrity sha512-ZcEDfsrS2ym9+vExV7+59avkzEO/PLkNj16uaDvbWhi0Q/vOZ72j2LQTrtDLWVyZRIeUaB/i8DJP017Gj6UYQw==
"@motionone/utils@^10.9.0":
version "10.9.0"
resolved "https://registry.yarnpkg.com/@motionone/utils/-/utils-10.9.0.tgz#a68fe41fe37e4365b07dfc676f014a0e43b8beb2"
integrity sha512-5IgmwQ8TdH1HsQ9d2QZeBCu9+HkqjoYRYItRpmusoyiedPMZaKdU3pr3qFP5nbAj68Ww2sTUxgEZEOF20qJA6w==
dependencies:
"@motionone/types" "^10.9.0"
hey-listen "^1.0.8"
tslib "^2.3.1"
"@mrmlnc/readdir-enhanced@^2.2.1":
version "2.2.1"
resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde"
@ -9149,6 +9214,27 @@ fragment-cache@^0.2.1:
dependencies:
map-cache "^0.2.2"
framer-motion@^6.4.1:
version "6.4.1"
resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-6.4.1.tgz#d351a96d80bea5736af4175e6307bc63bb4daed1"
integrity sha512-JiVnG3p0mO9yWxjNx3xxJaKP4hTWdm6oIbzfMJZEMtoOahSNoDTRZA/mFPzfVNj2foKwFPOLFv/lw8UUZjyUqA==
dependencies:
"@motionone/dom" "^10.11.0"
framesync "6.0.1"
hey-listen "^1.0.8"
popmotion "11.0.3"
style-value-types "5.0.0"
tslib "^2.1.0"
optionalDependencies:
"@emotion/is-prop-valid" "^0.8.2"
framesync@6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/framesync/-/framesync-6.0.1.tgz#5e32fc01f1c42b39c654c35b16440e07a25d6f20"
integrity sha512-fUY88kXvGiIItgNC7wcTOl0SNRCVXMKSWW2Yzfmn7EKNc+MpCzcz9DhdHcdjbrtN3c6R4H5dTY2jiCpPdysEjA==
dependencies:
tslib "^2.1.0"
fresh@0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
@ -9816,6 +9902,11 @@ he@^1.2.0:
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
hey-listen@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/hey-listen/-/hey-listen-1.0.8.tgz#8e59561ff724908de1aa924ed6ecc84a56a9aa68"
integrity sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==
highlight.js@^10.4.1, highlight.js@~10.7.0:
version "10.7.3"
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.3.tgz#697272e3991356e40c3cac566a74eef681756531"
@ -13390,6 +13481,16 @@ polished@^4.2.2:
dependencies:
"@babel/runtime" "^7.17.8"
popmotion@11.0.3:
version "11.0.3"
resolved "https://registry.yarnpkg.com/popmotion/-/popmotion-11.0.3.tgz#565c5f6590bbcddab7a33a074bb2ba97e24b0cc9"
integrity sha512-Y55FLdj3UxkR7Vl3s7Qr4e9m0onSnP8W7d/xQLsoJM40vs6UKHFdygs6SWryasTZYqugMjm3BepCF4CWXDiHgA==
dependencies:
framesync "6.0.1"
hey-listen "^1.0.8"
style-value-types "5.0.0"
tslib "^2.1.0"
posix-character-classes@^0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
@ -16410,6 +16511,14 @@ style-to-object@0.3.0, style-to-object@^0.3.0:
dependencies:
inline-style-parser "0.1.1"
style-value-types@5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/style-value-types/-/style-value-types-5.0.0.tgz#76c35f0e579843d523187989da866729411fc8ad"
integrity sha512-08yq36Ikn4kx4YU6RD7jWEv27v4V+PUsOGa4n/as8Et3CuODMJQ00ENeAVXAeydX4Z2j1XHZF1K2sX4mGl18fA==
dependencies:
hey-listen "^1.0.8"
tslib "^2.1.0"
stylehacks@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-5.1.0.tgz#a40066490ca0caca04e96c6b02153ddc39913520"
@ -16967,7 +17076,7 @@ tslib@^1.8.1:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.4.0:
tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.1, tslib@^2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3"
integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==