Skip to content

Commit

Permalink
Global settings dropdown
Browse files Browse the repository at this point in the history
  • Loading branch information
BenjaBobs committed May 4, 2024
1 parent 16ff76c commit 8d4dcfc
Show file tree
Hide file tree
Showing 8 changed files with 231 additions and 39 deletions.
8 changes: 7 additions & 1 deletion src/common/flex/flex.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { MouseEventHandler, PropsWithChildren } from "react";
import { CSSProperties, MouseEventHandler, PropsWithChildren } from "react";
import "@src/common/flex/flex.scss";

export function Flex(
Expand All @@ -17,9 +17,12 @@ export function Flex(
justify?: React.CSSProperties["justifyContent"];
itemsSizing?: "even";
itemsPlacement?: "center";
pad?: CSSProperties["padding"];
gap?: number | `${number}%`;
width?: number | `${number}%`;
color?: string;
bg?: CSSProperties["background"];
border?: CSSProperties["border"];
}>
) {
const classes = [
Expand All @@ -40,6 +43,9 @@ export function Flex(
gap: props.gap,
width: props.width,
color: props.color,
padding: props.pad,
background: props.bg,
border: props.border,
...props.style,
}}
onClick={props.onClick}
Expand Down
21 changes: 21 additions & 0 deletions src/common/input/checkbox/CheckBox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Flex } from "@src/common/flex/flex";
import { PropsWithChildren } from "react";

export function CheckBox(
props: PropsWithChildren<{
checked?: boolean;
onChange?: (checked: boolean) => void;
}>
) {
return (
<Flex
right
slim
justify="center"
onClick={() => props.onChange?.(!props.checked)}
>
<input readOnly checked={!!props.checked} type="checkbox" />
{props.children}
</Flex>
);
}
13 changes: 13 additions & 0 deletions src/common/windows/dropdown/Dropdown.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.dropdown {
position: relative;

.dropdown-content {
position: absolute;
transform-origin: top left;
z-index: 2;

// place center of the dropdown button
// left: 50%;
// translate: -50%;
}
}
93 changes: 93 additions & 0 deletions src/common/windows/dropdown/Dropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import React, { PropsWithChildren, useRef, useState } from "react";
import "@src/common/windows/dropdown/Dropdown.scss";
import { useEffectRefsPopulated } from "@src/common/hooks/useEffectRefsPopulated";

export function Dropdown(
props: PropsWithChildren<{
content: React.ReactNode;
trigger?: "click" | "hover";
}>
) {
const trigger = props.trigger ?? "click";
const [isOpen, setIsOpen] = useState(false);

const popupRef = useRef<HTMLDivElement>(null);
const animationRef = useRef<Animation>();

useEffectRefsPopulated(() => {
animationRef.current = popupRef.current!.animate(
[
{ transform: "scale(0, 0)", offset: 0 },
{ transform: "scale(0.1, 0.5)", offset: 0.3 },
{ transform: "scale(0.1, 1)", offset: 0.7 },
{ transform: "scale(1, 1)", offset: 1 },
],
{ duration: 200, easing: "ease", fill: "both" }
);
animationRef.current.pause();
animationRef.current.currentTime = 0;

animationRef.current!.onfinish = () => {
if (animationRef.current!.playbackRate > 0) {
setIsOpen(true);
} else {
setIsOpen(false);
}
};
}, [popupRef.current]);

const openDropdown = () => {
if (isOpen) return;
setIsOpen(true);

animationRef.current!.playbackRate = 1;
if (animationRef.current?.playState !== "running")
animationRef.current!.play();
};

const closeDropdown = () => {
if (!isOpen) return;
animationRef.current!.playbackRate = -1;
if (animationRef.current?.playState !== "running")
animationRef.current!.play();
};

const toggleDropdown = () => {
if (isOpen) closeDropdown();
else openDropdown();
};

return (
<div
onClick={(evt) => {
evt.preventDefault();
evt.stopPropagation();
}}
className={`dropdown`}
onMouseEnter={() => {
if (trigger === "hover") {
openDropdown();
}
}}
onMouseLeave={() => {
if (trigger === "hover") {
closeDropdown();
}
}}
>
<div
className="dropdown-trigger"
onClick={() => {
if (trigger === "click") {
toggleDropdown();
}
}}
>
{props.children}
</div>
<div ref={popupRef} className="dropdown-content">
{isOpen && props.content}
</div>
</div>
);
}
80 changes: 78 additions & 2 deletions src/japanese/japanese-sitemap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,24 @@ import { RouteDefinition } from "@src/sitemap";
import { VerbTeForm } from "@src/japanese/systems/verbs/forms/VerbTeForm";
import { SystemsOverview } from "@src/japanese/systems/SystemsOverview";
import { VerbsOverview } from "@src/japanese/systems/verbs/VerbsOverview";
import { proxy } from "valtio";
import { proxy, useSnapshot } from "valtio";
import { VerbPresentForm } from "@src/japanese/systems/verbs/forms/VerbPresentForm";
import "@src/japanese/japanese-style.scss";
import { Flex } from "@src/common/flex/flex";
import { Dropdown } from "@src/common/windows/dropdown/Dropdown";
import { CheckBox } from "@src/common/input/checkbox/CheckBox";

export const JapaneseSiteMap = {
menu: { name: "Japanese" },
menu: {
name: (
<Flex gap={8} justify="space-around">
<span>Japanese</span>
<Dropdown trigger="click" content={<JapaneseSettingsCard />}>
<span className="clickable">&#128736;</span>
</Dropdown>
</Flex>
),
},
nested: {
kana: {
menu: { name: "Kana table" },
Expand Down Expand Up @@ -37,8 +49,72 @@ export const JapaneseSiteMap = {
},
} satisfies RouteDefinition;

export function JapaneseSettingsCard() {
const snap = useSnapshot(JapaneseSettings);

return (
<Flex down bg="white" border="1px solid black">
<h3>Settings</h3>
<Flex center gap={40}>
<Flex down>
<CheckBox
checked={snap.japanese}
onChange={(value) => {
return (JapaneseSettings.japanese =
JapaneseSettings.hiragana =
JapaneseSettings.katakana =
value);
}}
>
Japanese
</CheckBox>
<CheckBox
checked={snap.hiragana}
onChange={(value) => {
return (JapaneseSettings.hiragana = JapaneseSettings.japanese =
value);
}}
>
Hiragana
</CheckBox>
<CheckBox
checked={snap.katakana}
onChange={(value) =>
(JapaneseSettings.katakana = JapaneseSettings.japanese = value)
}
>
Katakana
</CheckBox>
<CheckBox
checked={snap.romaji}
onChange={(value) => (JapaneseSettings.romaji = value)}
>
Romaji
</CheckBox>
</Flex>
<Flex down>
<CheckBox
checked={snap.casual}
onChange={(value) => (JapaneseSettings.casual = value)}
>
Casual
</CheckBox>
<CheckBox
checked={snap.keigo}
onChange={(value) => (JapaneseSettings.keigo = value)}
>
Keigo
</CheckBox>
</Flex>
</Flex>
</Flex>
);
}

export const JapaneseSettings = proxy({
japanese: true,
hiragana: true,
katakana: true,
romaji: true,
keigo: true,
casual: true,
Expand Down
43 changes: 8 additions & 35 deletions src/japanese/kana-table/KanaTable.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { ContentBox } from "@src/common/context-box/ContextBox";
import { Flex } from "@src/common/flex/flex";
import { JapaneseSettings } from "@src/japanese/japanese-sitemap";
import {
Kana,
KanaRow,
kanaRows,
} from "@src/japanese/kana-table/KanaTable.data";
import "@src/japanese/kana-table/KanaTable.scss";
import React, { CSSProperties } from "react";
import { useSnapshot } from "valtio";

const vowels = ["a", "i", "u", "e", "o"] as const;

Expand All @@ -29,44 +31,15 @@ function positionToGridArea(
}

export function KanaTablePage() {
const [hiragana, setHiragana] = React.useState(true);
const [romaji, setRomaji] = React.useState(true);
const [katakana, setKatakana] = React.useState(true);
const snap = useSnapshot(JapaneseSettings);

return (
<ContentBox>
<div>
<Flex right justify="center" gap={40}>
<div>
<input
id="hiragana-checkbox"
checked={hiragana}
onChange={() => setHiragana(!hiragana)}
type="checkbox"
/>
<label htmlFor="hiragana-checkbox">Hiragana</label>
</div>
<div>
<input
id="katakana-checkbox"
checked={katakana}
onChange={() => setKatakana(!katakana)}
type="checkbox"
/>
<label htmlFor="katakana-checkbox">Katakana</label>
</div>
<div>
<input
id="romaji-checkbox"
checked={romaji}
onChange={() => setRomaji(!romaji)}
type="checkbox"
/>
<label htmlFor="romaji-checkbox">Romaji</label>
</div>
</Flex>
<KanaTable hiragana={hiragana} romaji={romaji} katakana={katakana} />
</div>
<KanaTable
hiragana={snap.hiragana}
romaji={snap.romaji}
katakana={snap.katakana}
/>
</ContentBox>
);
}
Expand Down
10 changes: 10 additions & 0 deletions src/program.scss
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,13 @@ mark {
p {
margin: 0;
}

.clickable {
cursor: pointer;
transition: text-decoration 0.2s, filter 0.2s;

&:hover {
text-decoration: underline;
filter: brightness(2);
}
}
2 changes: 1 addition & 1 deletion src/sitemap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export type RouteDefinition = {
fullPath?: string;
element?: JSX.Element;
menu?: {
name: string;
name: React.ReactNode;
};
nested?: RouteBranch;
};
Expand Down

0 comments on commit 8d4dcfc

Please sign in to comment.