diff --git a/app/components/Catcher.tsx b/app/components/Catcher.tsx index 7f25a3692..dd0dbe749 100644 --- a/app/components/Catcher.tsx +++ b/app/components/Catcher.tsx @@ -1,8 +1,8 @@ import { useCatch, useLocation } from "@remix-run/react"; -import { DISCORD_URL } from "~/constants"; import { getLogInUrl } from "~/utils"; import { useUser } from "~/hooks/common"; import { Button } from "./Button"; +import { discordUrl } from "~/utils/urls"; // TODO: some nice art export function Catcher() { @@ -18,7 +18,7 @@ export function Catcher() { {user ? (

If you need assistance you can ask for help on{" "} - + our Discord

diff --git a/app/components/Layout/DrawingSection.tsx b/app/components/Layout/DrawingSection.tsx new file mode 100644 index 000000000..28aa22749 --- /dev/null +++ b/app/components/Layout/DrawingSection.tsx @@ -0,0 +1,373 @@ +import clsx from "clsx"; +import randomColor from "randomcolor"; +import { useState } from "react"; + +interface HSL { + h: number; + s: number; + l: number; +} + +class Color { + public r: number; + public g: number; + public b: number; + constructor(r: number, g: number, b: number) { + this.r = this.clamp(r); + this.g = this.clamp(g); + this.b = this.clamp(b); + } + + toString() { + return `rgb(${Math.round(this.r)}, ${Math.round(this.g)}, ${Math.round( + this.b + )})`; + } + + set(r: number, g: number, b: number) { + this.r = this.clamp(r); + this.g = this.clamp(g); + this.b = this.clamp(b); + } + + hueRotate(angle = 0) { + angle = (angle / 180) * Math.PI; + const sin = Math.sin(angle); + const cos = Math.cos(angle); + + this.multiply([ + 0.213 + cos * 0.787 - sin * 0.213, + 0.715 - cos * 0.715 - sin * 0.715, + 0.072 - cos * 0.072 + sin * 0.928, + 0.213 - cos * 0.213 + sin * 0.143, + 0.715 + cos * 0.285 + sin * 0.14, + 0.072 - cos * 0.072 - sin * 0.283, + 0.213 - cos * 0.213 - sin * 0.787, + 0.715 - cos * 0.715 + sin * 0.715, + 0.072 + cos * 0.928 + sin * 0.072, + ]); + } + + grayscale(value = 1) { + this.multiply([ + 0.2126 + 0.7874 * (1 - value), + 0.7152 - 0.7152 * (1 - value), + 0.0722 - 0.0722 * (1 - value), + 0.2126 - 0.2126 * (1 - value), + 0.7152 + 0.2848 * (1 - value), + 0.0722 - 0.0722 * (1 - value), + 0.2126 - 0.2126 * (1 - value), + 0.7152 - 0.7152 * (1 - value), + 0.0722 + 0.9278 * (1 - value), + ]); + } + + sepia(value = 1) { + this.multiply([ + 0.393 + 0.607 * (1 - value), + 0.769 - 0.769 * (1 - value), + 0.189 - 0.189 * (1 - value), + 0.349 - 0.349 * (1 - value), + 0.686 + 0.314 * (1 - value), + 0.168 - 0.168 * (1 - value), + 0.272 - 0.272 * (1 - value), + 0.534 - 0.534 * (1 - value), + 0.131 + 0.869 * (1 - value), + ]); + } + + saturate(value = 1) { + this.multiply([ + 0.213 + 0.787 * value, + 0.715 - 0.715 * value, + 0.072 - 0.072 * value, + 0.213 - 0.213 * value, + 0.715 + 0.285 * value, + 0.072 - 0.072 * value, + 0.213 - 0.213 * value, + 0.715 - 0.715 * value, + 0.072 + 0.928 * value, + ]); + } + + multiply(matrix: number[]) { + const newR = this.clamp( + this.r * matrix[0] + this.g * matrix[1] + this.b * matrix[2] + ); + const newG = this.clamp( + this.r * matrix[3] + this.g * matrix[4] + this.b * matrix[5] + ); + const newB = this.clamp( + this.r * matrix[6] + this.g * matrix[7] + this.b * matrix[8] + ); + this.r = newR; + this.g = newG; + this.b = newB; + } + + brightness(value = 1) { + this.linear(value); + } + contrast(value = 1) { + this.linear(value, -(0.5 * value) + 0.5); + } + + linear(slope = 1, intercept = 0) { + this.r = this.clamp(this.r * slope + intercept * 255); + this.g = this.clamp(this.g * slope + intercept * 255); + this.b = this.clamp(this.b * slope + intercept * 255); + } + + invert(value = 1) { + this.r = this.clamp((value + (this.r / 255) * (1 - 2 * value)) * 255); + this.g = this.clamp((value + (this.g / 255) * (1 - 2 * value)) * 255); + this.b = this.clamp((value + (this.b / 255) * (1 - 2 * value)) * 255); + } + + hsl(): HSL { + // Code taken from https://stackoverflow.com/a/9493060/2688027, licensed under CC BY-SA. + const r = this.r / 255; + const g = this.g / 255; + const b = this.b / 255; + const max = Math.max(r, g, b); + const min = Math.min(r, g, b); + + let h = 0; + let s = 0; + const l = (max + min) / 2; + + if (max === min) { + h = s = 0; + } else { + const d = max - min; + s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + switch (max) { + case r: + h = (g - b) / d + (g < b ? 6 : 0); + break; + + case g: + h = (b - r) / d + 2; + break; + + case b: + h = (r - g) / d + 4; + break; + } + h /= 6; + } + + return { + h: h * 100, + s: s * 100, + l: l * 100, + }; + } + + clamp(value: number): number { + if (value > 255) { + value = 255; + } else if (value < 0) { + value = 0; + } + return value; + } +} +interface Solution { + loss: number; + values: number[]; +} +class Solver { + private target: Color; + private targetHSL: HSL; + private reusedColor: Color; + constructor(target: Color) { + this.target = target; + this.targetHSL = target.hsl(); + this.reusedColor = new Color(0, 0, 0); + } + + solve() { + const result = this.solveNarrow(this.solveWide()); + return { + values: result.values, + loss: result.loss, + filter: this.css(result.values), + }; + } + + solveWide(): Solution { + const A = 5; + const c = 15; + const a = [60, 180, 18000, 600, 1.2, 1.2]; + + let best = { loss: Infinity, values: [] as number[] }; + for (let i = 0; best.loss > 25 && i < 3; i++) { + const initial = [50, 20, 3750, 50, 100, 100]; + const result = this.spsa(A, a, c, initial, 1000); + if (result.loss < best.loss) { + best = result; + } + } + return best; + } + + solveNarrow(wide: Solution) { + const A = wide.loss; + const c = 2; + const A1 = A + 1; + const a = [0.25 * A1, 0.25 * A1, A1, 0.25 * A1, 0.2 * A1, 0.2 * A1]; + return this.spsa(A, a, c, wide.values, 500); + } + + spsa( + A: number, + a: number[], + c: number, + values: number[], + iters: number + ): Solution { + const alpha = 1; + const gamma = 0.16666666666666666; + + let best = [] as number[]; + let bestLoss = Infinity; + const deltas = new Array(6); + const highArgs = new Array(6); + const lowArgs = new Array(6); + + for (let k = 0; k < iters; k++) { + const ck = c / Math.pow(k + 1, gamma); + for (let i = 0; i < 6; i++) { + deltas[i] = Math.random() > 0.5 ? 1 : -1; + highArgs[i] = values[i] + ck * deltas[i]; + lowArgs[i] = values[i] - ck * deltas[i]; + } + + const lossDiff = this.loss(highArgs) - this.loss(lowArgs); + for (let i = 0; i < 6; i++) { + const g = (lossDiff / (2 * ck)) * deltas[i]; + const ak = a[i] / Math.pow(A + k + 1, alpha); + values[i] = fix(values[i] - ak * g, i); + } + + const loss = this.loss(values); + if (loss < bestLoss) { + best = values.slice(0); + bestLoss = loss; + } + } + return { values: best, loss: bestLoss }; + + function fix(value: number, idx: number): number { + let max = 100; + if (idx === 2 /* saturate */) { + max = 7500; + } else if (idx === 4 /* brightness */ || idx === 5 /* contrast */) { + max = 200; + } + + if (idx === 3 /* hue-rotate */) { + if (value > max) { + value %= max; + } else if (value < 0) { + value = max + (value % max); + } + } else if (value < 0) { + value = 0; + } else if (value > max) { + value = max; + } + return value; + } + } + + loss(filters: number[]) { + // Argument is array of percentages. + const color = this.reusedColor; + color.set(0, 0, 0); + + color.invert(filters[0] / 100); + color.sepia(filters[1] / 100); + color.saturate(filters[2] / 100); + color.hueRotate(filters[3] * 3.6); + color.brightness(filters[4] / 100); + color.contrast(filters[5] / 100); + + const colorHSL = color.hsl(); + return ( + Math.abs(color.r - this.target.r) + + Math.abs(color.g - this.target.g) + + Math.abs(color.b - this.target.b) + + Math.abs(colorHSL.h - this.targetHSL.h) + + Math.abs(colorHSL.s - this.targetHSL.s) + + Math.abs(colorHSL.l - this.targetHSL.l) + ); + } + + css(filters: number[]) { + function fmt(idx: number, multiplier = 1) { + return Math.round(filters[idx] * multiplier); + } + return `invert(${fmt(0)}%) sepia(${fmt(1)}%) saturate(${fmt( + 2 + )}%) hue-rotate(${fmt(3, 3.6)}deg) brightness(${fmt(4)}%) contrast(${fmt( + 5 + )}%)`; + } +} + +type RGB = [number, number, number]; +function hexToRgb(hex: string): RGB { + // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF") + const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i; + hex = hex.replace(shorthandRegex, (_m, r: string, g: string, b: string) => { + return r + r + g + g + b + b; + }); + + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + if (result) { + return [ + parseInt(result[1], 16), + parseInt(result[2], 16), + parseInt(result[3], 16), + ]; + } + + throw new Error("Error parsing hex: " + hex); +} + +function getFilters(hex: string) { + let rgb = [255, 255, 255]; + rgb = hexToRgb(hex); + const color = new Color(rgb[0], rgb[1], rgb[2]); + const solver = new Solver(color); + const result = solver.solve(); + return result.filter; +} + +export function DrawingSection({ type }: { type: "girl" | "boy" }) { + const [hexCode, setHexCode] = useState(randomColor()); + + const handleColorChange = () => setHexCode(randomColor()); + + return ( +
+ + +
+ ); +} + +export default DrawingSection; diff --git a/app/components/Layout/Menu.tsx b/app/components/Layout/Menu.tsx new file mode 100644 index 000000000..abb7eb40d --- /dev/null +++ b/app/components/Layout/Menu.tsx @@ -0,0 +1,70 @@ +import { Link } from "@remix-run/react"; +import { navItemsGrouped } from "~/constants"; +import { useOnClickOutside } from "~/hooks/common"; +import { layoutIcon } from "~/utils"; +import { discordUrl, gitHubUrl, patreonUrl, twitterUrl } from "~/utils/urls"; +import { Button } from "../Button"; +import { CrossIcon } from "../icons/Cross"; +import { DiscordIcon } from "../icons/Discord"; +import { GitHubIcon } from "../icons/Github"; +import { PatreonIcon } from "../icons/Patreon"; +import { TwitterIcon } from "../icons/Twitter"; +import DrawingSection from "./DrawingSection"; +import * as React from "react"; + +export function Menu({ close }: { close: () => void }) { + const ref = React.useRef(null); + useOnClickOutside(ref, close); + + return ( +
+
+ +
+
+ + sendou.ink +
+ +
+ + + +
+
+ ); +} diff --git a/app/components/Layout/MobileNav.tsx b/app/components/Layout/MobileMenu.tsx similarity index 89% rename from app/components/Layout/MobileNav.tsx rename to app/components/Layout/MobileMenu.tsx index 3524af899..9031fdbed 100644 --- a/app/components/Layout/MobileNav.tsx +++ b/app/components/Layout/MobileMenu.tsx @@ -1,10 +1,9 @@ import clsx from "clsx"; import { Fragment } from "react"; import { Link } from "@remix-run/react"; -import { navItems } from "~/constants"; -import { SearchInput } from "./SearchInput"; +import { navItemsGrouped } from "~/constants"; -export function MobileNav({ +export function MobileMenu({ expanded, closeMenu, }: { @@ -14,10 +13,10 @@ export function MobileNav({ return (
- + {/* */}
- {navItems.map((navGroup) => ( + {navItemsGrouped.map((navGroup) => (
{navGroup.title} diff --git a/app/components/Layout/SearchInput.tsx b/app/components/Layout/SearchInput.tsx index 4acb25337..4dc1eef9d 100644 --- a/app/components/Layout/SearchInput.tsx +++ b/app/components/Layout/SearchInput.tsx @@ -1,6 +1,5 @@ -import * as React from "react"; import clsx from "clsx"; -import { SearchIcon } from "../../components/icons/Search"; +import * as React from "react"; export function SearchInput() { // TODO: search input that searches @@ -69,7 +68,7 @@ export function DumbSearchInput({ setValue(e.target.value)} onKeyDown={(event) => { @@ -78,7 +77,6 @@ export function DumbSearchInput({ } }} /> -
); } diff --git a/app/components/Layout/index.tsx b/app/components/Layout/index.tsx index e3160b3d7..8ffce72cb 100644 --- a/app/components/Layout/index.tsx +++ b/app/components/Layout/index.tsx @@ -1,79 +1,76 @@ +import { useMatches } from "@remix-run/react"; import * as React from "react"; -import { useState } from "react"; +import { PAGE_TITLE_KEY } from "~/constants"; +import { useWindowSize } from "~/hooks/common"; import { HamburgerButton } from "./HamburgerButton"; -import { MobileNav } from "./MobileNav"; +import { Menu } from "./Menu"; +import { MobileMenu } from "./MobileMenu"; import { SearchInput } from "./SearchInput"; import { UserItem } from "./UserItem"; -import { Link } from "@remix-run/react"; -import { navItems } from "~/constants"; -import { layoutIcon } from "~/utils"; -import clsx from "clsx"; + +interface LayoutProps { + children: React.ReactNode; + menuOpen: boolean; + setMenuOpen: (open: boolean) => void; +} export const Layout = React.memo(function Layout({ children, -}: { - children: React.ReactNode; -}) { - const [menuExpanded, setMenuExpanded] = useState(false); + menuOpen, + setMenuOpen, +}: LayoutProps) { + const matches = useMatches(); + + // you can set this page title from any loader + // deeper routes take precedence + const pageTitle = matches + .map((match) => match.data) + .filter(Boolean) + .reduceRight((acc: Nullable, routeData) => { + if (!acc && typeof routeData[PAGE_TITLE_KEY] === "string") { + return routeData[PAGE_TITLE_KEY] as string; + } + + return acc; + }, null); return ( <>
-
- - - -
+
{pageTitle}
setMenuExpanded((e) => !e)} + expanded={menuOpen} + onClick={() => setMenuOpen(!menuOpen)} />
- setMenuExpanded(false)} - /> - - -
- {new Array(50).fill(null).map((_, i) => ( - BETA - ))} -
- +
{children}
); }); + +function ScreenWidthSensitiveMenu({ + menuOpen, + setMenuOpen, +}: Pick) { + const { width } = useWindowSize(); + + const closeMenu = () => setMenuOpen(false); + + if (typeof width === "undefined") return null; + + // render it on mobile even if menuOpen = false for the sliding animation + if (width < 900) { + return ; + } + + if (!menuOpen) return null; + + return ; +} diff --git a/app/components/SubNav.tsx b/app/components/SubNav.tsx new file mode 100644 index 000000000..4967d8f12 --- /dev/null +++ b/app/components/SubNav.tsx @@ -0,0 +1,24 @@ +import { NavLink } from "@remix-run/react"; +import { RemixNavLinkProps } from "@remix-run/react/components"; +import clsx from "clsx"; +import React from "react"; +import { ArrowUpIcon } from "./icons/ArrowUp"; + +export function SubNav({ children }: { children: React.ReactNode }) { + return ; +} + +export function SubNavLink({ + children, + className, + ...props +}: RemixNavLinkProps & { + children: React.ReactNode; +}) { + return ( + + {children} + + + ); +} diff --git a/app/components/icons/ArrowUp.tsx b/app/components/icons/ArrowUp.tsx index f191ac646..97abef7fc 100644 --- a/app/components/icons/ArrowUp.tsx +++ b/app/components/icons/ArrowUp.tsx @@ -5,7 +5,7 @@ export function ArrowUpIcon({ style, }: { className?: string; - style: CSSProperties; + style?: CSSProperties; }) { return ( + + + ); +} diff --git a/app/components/icons/Patreon.tsx b/app/components/icons/Patreon.tsx new file mode 100644 index 000000000..ef5e9a940 --- /dev/null +++ b/app/components/icons/Patreon.tsx @@ -0,0 +1,16 @@ +export function PatreonIcon({ className }: { className?: string }) { + return ( + + + + + ); +} diff --git a/app/components/tournament/InfoBanner.tsx b/app/components/tournament/InfoBanner.tsx index a32c266a6..60d96cc73 100644 --- a/app/components/tournament/InfoBanner.tsx +++ b/app/components/tournament/InfoBanner.tsx @@ -1,14 +1,12 @@ -import { Link, useLoaderData, useLocation } from "@remix-run/react"; +import { Link, useLocation, useMatches } from "@remix-run/react"; import { DiscordIcon } from "~/components/icons/Discord"; import { TwitterIcon } from "~/components/icons/Twitter"; import { resolveTournamentFormatString } from "~/core/tournament/bracket"; -import { tournamentHasStarted } from "~/core/tournament/utils"; import { FindTournamentByNameForUrlI } from "~/services/tournament"; -import { getLogInUrl } from "~/utils"; -import { useUser } from "~/hooks/common"; export function InfoBanner() { - const data = useLoaderData(); + const [, parentRoute] = useMatches(); + const data = parentRoute.data as FindTournamentByNameForUrlI; const location = useLocation(); const urlToTournamentFrontPage = location.pathname @@ -78,7 +76,6 @@ export function InfoBanner() {
{data.organizer.name}
- @@ -104,57 +101,3 @@ function dayNumber(date: string) { function dateYYYYMMDD(date: string) { return new Date(date).toISOString().split("T")[0]; } - -function InfoBannerActionButton() { - const data = useLoaderData(); - const user = useUser(); - const location = useLocation(); - - const isAlreadyInATeamButNotCaptain = data.teams - .flatMap((team) => team.members) - .filter(({ captain }) => !captain) - .some(({ member }) => member.id === user?.id); - if (isAlreadyInATeamButNotCaptain) return null; - - const alreadyRegistered = data.teams - .flatMap((team) => team.members) - .some(({ member }) => member.id === user?.id); - if (alreadyRegistered) { - return ( - - Add players - - ); - } - - if (tournamentHasStarted(data.brackets)) { - return null; - } - - if (!user) { - return ( -
- -
- ); - } - - return ( - - Register - - ); -} diff --git a/app/constants.ts b/app/constants.ts index 1f0c10216..43c1bd9c9 100644 --- a/app/constants.ts +++ b/app/constants.ts @@ -1,7 +1,5 @@ import { Ability } from "@prisma/client"; -export const DISCORD_URL = "https://discord.gg/sendou"; - export const ADMIN_UUID = "ee2d82dd-624f-4b07-9d8d-ddee1f8fb36f"; export const ADMIN_TEST_DISCORD_ID = "79237403620945920"; @@ -10,6 +8,8 @@ export const NZAP_UUID = "6cd9d01d-b724-498a-b706-eb70edd8a773"; export const NZAP_TEST_DISCORD_ID = "455039198672453645"; export const NZAP_TEST_AVATAR = "f809176af93132c3db5f0a5019e96339"; +export const PAGE_TITLE_KEY = "pageTitle"; + export const ROOM_PASS_LENGTH = 4; export const LFG_GROUP_FULL_SIZE = 4; export const TOURNAMENT_TEAM_ROSTER_MIN_SIZE = 4; @@ -39,7 +39,7 @@ export const checkInClosesDate = (startTime: string): Date => { return new Date(new Date(startTime).getTime() - 1000 * 10); }; -export const navItems: { +export const navItemsGrouped: { title: string; items: { name: string; @@ -51,7 +51,7 @@ export const navItems: { { title: "builds", items: [ - { name: "browse", disabled: true }, + { name: "builds", disabled: true }, { name: "gear", disabled: true }, { name: "analyzer", disabled: true }, ], diff --git a/app/hooks/common.ts b/app/hooks/common.ts index 80a00827b..fb018b8b8 100644 --- a/app/hooks/common.ts +++ b/app/hooks/common.ts @@ -95,3 +95,33 @@ export function usePolling(pollingActive = true) { return lastUpdated; } + +// https://usehooks.com/useWindowSize/ +export function useWindowSize() { + // Initialize state with undefined width/height so server and client renders match + // Learn more here: https://joshwcomeau.com/react/the-perils-of-rehydration/ + const [windowSize, setWindowSize] = React.useState<{ + width?: number; + height?: number; + }>({ + width: undefined, + height: undefined, + }); + React.useEffect(() => { + // Handler to call on window resize + function handleResize() { + // Set window width/height to state + setWindowSize({ + width: window.innerWidth, + height: window.innerHeight, + }); + } + // Add event listener + window.addEventListener("resize", handleResize); + // Call handler right away so state gets updated with initial window size + handleResize(); + // Remove event listener on cleanup + return () => window.removeEventListener("resize", handleResize); + }, []); // Empty array ensures that effect is only run on mount + return windowSize; +} diff --git a/app/root.tsx b/app/root.tsx index 193ee6e29..6d5780e8a 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -1,4 +1,3 @@ -import * as React from "react"; import type { LinksFunction, LoaderFunction } from "@remix-run/node"; import { json } from "@remix-run/node"; import { @@ -8,17 +7,18 @@ import { Outlet, Scripts, useCatch, - useLoaderData, } from "@remix-run/react"; -import { LoggedInUser, LoggedInUserSchema } from "~/utils/schemas"; -import { Layout } from "./components/Layout"; -import { Catcher } from "./components/Catcher"; -import resetStyles from "~/styles/reset.css"; +import clsx from "clsx"; +import * as React from "react"; +import { io, Socket } from "socket.io-client"; import globalStyles from "~/styles/global.css"; import layoutStyles from "~/styles/layout.css"; -import { DISCORD_URL } from "./constants"; -import { io, Socket } from "socket.io-client"; +import resetStyles from "~/styles/reset.css"; +import { LoggedInUser, LoggedInUserSchema } from "~/utils/schemas"; +import { Catcher } from "./components/Catcher"; +import { Layout } from "./components/Layout"; import { SocketProvider } from "./utils/socketContext"; +import { discordUrl } from "./utils/urls"; export const links: LinksFunction = () => { return [ @@ -28,14 +28,14 @@ export const links: LinksFunction = () => { ]; }; -export interface EnvironmentVariables { - FF_ENABLE_CHAT?: "true" | "admin" | string; -} +// export interface EnvironmentVariables { +// FF_ENABLE_CHAT?: "true" | "admin" | string; +// } export interface RootLoaderData { user?: LoggedInUser; baseURL: string; - ENV: EnvironmentVariables; + // ENV: EnvironmentVariables; } export const loader: LoaderFunction = ({ context }) => { @@ -45,19 +45,20 @@ export const loader: LoaderFunction = ({ context }) => { return json({ user: data?.user, baseURL, - ENV: { - FF_ENABLE_CHAT: process.env.FF_ENABLE_CHAT, - }, + // ENV: { + // FF_ENABLE_CHAT: process.env.FF_ENABLE_CHAT, + // }, }); }; export const unstable_shouldReload = () => false; export default function App() { + const [menuOpen, setMenuOpen] = React.useState(false); const [socket, setSocket] = React.useState(); const children = React.useMemo(() => , []); - const data = useLoaderData(); + // const data = useLoaderData(); // TODO: for future optimization could only connect socket on sendouq/tournament pages React.useEffect(() => { @@ -69,9 +70,11 @@ export default function App() { }, []); return ( - + - {children} + + {children} + ); @@ -80,11 +83,13 @@ export default function App() { function Document({ children, title, - ENV, -}: { + disableBodyScroll = false, +}: // ENV, +{ children: React.ReactNode; title?: string; - ENV?: EnvironmentVariables; + disableBodyScroll?: boolean; + // ENV?: EnvironmentVariables; }) { return ( @@ -95,9 +100,9 @@ function Document({ - + {children} -