Merge pull request #777 from quantified-uncertainty/pause-autoplay
Manual play mode
This commit is contained in:
commit
f2ccf574ed
|
@ -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",
|
||||
|
|
|
@ -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?.(),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -48,7 +48,8 @@ export interface SquiggleChartProps {
|
|||
|
||||
const defaultOnChange = () => {};
|
||||
|
||||
export const SquiggleChart: React.FC<SquiggleChartProps> = ({
|
||||
export const SquiggleChart: React.FC<SquiggleChartProps> = React.memo(
|
||||
({
|
||||
code = "",
|
||||
environment,
|
||||
onChange = defaultOnChange, // defaultOnChange must be constant, don't move its definition here
|
||||
|
@ -101,4 +102,5 @@ export const SquiggleChart: React.FC<SquiggleChartProps> = ({
|
|||
environment={environment ?? defaultEnvironment}
|
||||
/>
|
||||
);
|
||||
};
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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,9 +50,7 @@ interface PlaygroundProps {
|
|||
showEditor?: boolean;
|
||||
}
|
||||
|
||||
const schema = yup
|
||||
.object()
|
||||
.shape({
|
||||
const schema = yup.object({}).shape({
|
||||
sampleCount: yup
|
||||
.number()
|
||||
.required()
|
||||
|
@ -72,76 +76,19 @@ const schema = yup
|
|||
.min(10)
|
||||
.max(100)
|
||||
.default(50),
|
||||
showTypes: yup.boolean(),
|
||||
showControls: yup.boolean(),
|
||||
showSummary: yup.boolean(),
|
||||
showEditor: yup.boolean(),
|
||||
logX: yup.boolean(),
|
||||
expY: yup.boolean(),
|
||||
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),
|
||||
})
|
||||
.required();
|
||||
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,91 +129,9 @@ 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"
|
||||
/>
|
||||
{/* 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>
|
||||
);
|
||||
}
|
||||
|
||||
export const SquigglePlayground: FC<PlaygroundProps> = ({
|
||||
defaultCode = "",
|
||||
height = 500,
|
||||
showTypes = false,
|
||||
showControls = false,
|
||||
showSummary = false,
|
||||
logX = false,
|
||||
expY = false,
|
||||
code: controlledCode,
|
||||
onCodeChange,
|
||||
onSettingsChange,
|
||||
showEditor = true,
|
||||
}) => {
|
||||
const [code, setCode] = useMaybeControlledValue({
|
||||
value: controlledCode,
|
||||
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: {
|
||||
sampleCount: 1000,
|
||||
xyPointLength: 1000,
|
||||
chartHeight: 150,
|
||||
showTypes,
|
||||
showControls,
|
||||
logX,
|
||||
expY,
|
||||
showSummary,
|
||||
showEditor,
|
||||
leftSizePercent: 50,
|
||||
showSettingsPage: false,
|
||||
diagramStart: 0,
|
||||
diagramStop: 10,
|
||||
diagramCount: 20,
|
||||
},
|
||||
});
|
||||
const vars = useWatch({
|
||||
control,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
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
|
||||
|
@ -291,15 +156,17 @@ export const SquigglePlayground: FC<PlaygroundProps> = ({
|
|||
/>
|
||||
<div className="mt-2">
|
||||
<Text>
|
||||
When distributions are converted into PointSet shapes, we need to
|
||||
know how many coordinates to use.
|
||||
When distributions are converted into PointSet shapes, we need to know
|
||||
how many coordinates to use.
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const viewSettings = (
|
||||
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">
|
||||
|
@ -353,10 +220,10 @@ export const SquigglePlayground: FC<PlaygroundProps> = ({
|
|||
<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.
|
||||
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
|
||||
|
@ -384,7 +251,28 @@ export const SquigglePlayground: FC<PlaygroundProps> = ({
|
|||
</div>
|
||||
);
|
||||
|
||||
const inputVariableSettings = (
|
||||
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">
|
||||
|
@ -396,7 +284,7 @@ export const SquigglePlayground: FC<PlaygroundProps> = ({
|
|||
<div className="border border-slate-200 mt-6 mb-2">
|
||||
<JsonEditor
|
||||
value={importString}
|
||||
onChange={getChangeJson}
|
||||
onChange={onChange}
|
||||
oneLine={false}
|
||||
showGutter={true}
|
||||
height={150}
|
||||
|
@ -415,10 +303,142 @@ export const SquigglePlayground: FC<PlaygroundProps> = ({
|
|||
</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 = "",
|
||||
height = 500,
|
||||
showTypes = false,
|
||||
showControls = false,
|
||||
showSummary = false,
|
||||
logX = false,
|
||||
expY = false,
|
||||
code: controlledCode,
|
||||
onCodeChange,
|
||||
onSettingsChange,
|
||||
showEditor = true,
|
||||
}) => {
|
||||
const [code, setCode] = useMaybeControlledValue({
|
||||
value: controlledCode,
|
||||
defaultValue: defaultCode,
|
||||
onChange: onCodeChange,
|
||||
});
|
||||
|
||||
const [imports, setImports] = useState({});
|
||||
|
||||
const { register, control } = useForm({
|
||||
resolver: yupResolver(schema),
|
||||
defaultValues: {
|
||||
sampleCount: 1000,
|
||||
xyPointLength: 1000,
|
||||
chartHeight: 150,
|
||||
showTypes,
|
||||
showControls,
|
||||
logX,
|
||||
expY,
|
||||
showSummary,
|
||||
showEditor,
|
||||
leftSizePercent: 50,
|
||||
showSettingsPage: false,
|
||||
diagramStart: 0,
|
||||
diagramStop: 10,
|
||||
diagramCount: 20,
|
||||
},
|
||||
});
|
||||
const vars = useWatch({
|
||||
control,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
onSettingsChange?.(vars);
|
||||
}, [vars, onSettingsChange]);
|
||||
|
||||
const env: environment = useMemo(
|
||||
() => ({
|
||||
sampleCount: Number(vars.sampleCount),
|
||||
xyPointLength: Number(vars.xyPointLength),
|
||||
}),
|
||||
[vars.sampleCount, vars.xyPointLength]
|
||||
);
|
||||
|
||||
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,9 +498,10 @@ 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">
|
||||
<div className="flex justify-between items-center mt-2">
|
||||
<StyledTab.List>
|
||||
<StyledTab
|
||||
name={vars.showEditor ? "Code" : "Display"}
|
||||
icon={vars.showEditor ? CodeIcon : EyeIcon}
|
||||
|
@ -478,10 +509,18 @@ export const SquigglePlayground: FC<PlaygroundProps> = ({
|
|||
<StyledTab name="Sampling Settings" icon={CogIcon} />
|
||||
<StyledTab name="View Settings" icon={ChartSquareBarIcon} />
|
||||
<StyledTab name="Input Variables" icon={CurrencyDollarIcon} />
|
||||
</Tab.List>
|
||||
</StyledTab.List>
|
||||
<RunControls
|
||||
autorunMode={autorunMode}
|
||||
isStale={renderedCode !== code}
|
||||
run={run}
|
||||
isRunning={isRunning}
|
||||
onAutorunModeChange={setAutorunMode}
|
||||
/>
|
||||
</div>
|
||||
{vars.showEditor ? withEditor : withoutEditor}
|
||||
</div>
|
||||
</Tab.Group>
|
||||
</StyledTab.Group>
|
||||
</SquiggleContainer>
|
||||
);
|
||||
};
|
||||
|
|
24
packages/components/src/components/ui/Checkbox.tsx
Normal file
24
packages/components/src/components/ui/Checkbox.tsx
Normal 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>
|
||||
);
|
||||
}
|
60
packages/components/src/components/ui/StyledTab.tsx
Normal file
60
packages/components/src/components/ui/StyledTab.tsx
Normal 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;
|
41
packages/components/src/components/ui/Toggle.tsx
Normal file
41
packages/components/src/components/ui/Toggle.tsx
Normal 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
111
yarn.lock
|
@ -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==
|
||||
|
|
Loading…
Reference in New Issue
Block a user