diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 6dc76c423b..ecb1c1e79c 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -109,6 +109,7 @@ module.exports = { rules: { "mdx/remark": "error", "no-unused-expressions": "off", + "react/jsx-no-undef": "off", }, }, { diff --git a/next.config.js b/next.config.js index 24baa11c51..10d19a2d85 100644 --- a/next.config.js +++ b/next.config.js @@ -27,6 +27,25 @@ export default withLess( withNextra({ // reactStrictMode: true, provoke duplicated codemirror editors webpack(config) { + // #region MDX + const mdxRule = config.module.rules.find(rule => + rule.test?.test?.(".mdx"), + ) + if (mdxRule) { + mdxRule.resourceQuery = { + not: /raw/, + } + } + // Instead of transforming MDX, with ?source we can get + // the raw content to process in a Server Component. + config.module.rules.push({ + test: /\.mdx$/i, + resourceQuery: /raw/, + type: "asset/source", + }) + // #endregion MDX + + // #region SVGs const fileLoaderRule = config.module.rules.find(rule => rule.test?.test?.(".svg"), ) @@ -62,6 +81,7 @@ export default withLess( }, }, ) + // #endregion SVGs return config }, diff --git a/package.json b/package.json index 50ba6118aa..780d69c7f0 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "codemirror-graphql": "1.3.2", "date-fns": "^2.30.0", "fast-glob": "^3.3.3", + "github-slugger": "2.0.0", "graphql": "16.10.0", "gray-matter": "^4.0.3", "hast-util-to-string": "3.0.1", diff --git a/patches/nextra@3.0.0-alpha.22.patch b/patches/nextra@3.0.0-alpha.22.patch index 09f7508769..6f4a58ebf6 100644 --- a/patches/nextra@3.0.0-alpha.22.patch +++ b/patches/nextra@3.0.0-alpha.22.patch @@ -9,3 +9,18 @@ index af0891e3c2084edd326f40e6e18107b193640e0b..885858fa17285fb75649cf0ebd2576d1 import { createElement } from "react"; const DEFAULT_COMPONENTS = { img: (props) => createElement( +diff --git a/package.json b/package.json +index 0625691ebc1d79d7b7d22bb3b001f00a57519772..53d4b0c951a762ae056004789c9aca819e0ade62 100644 +--- a/package.json ++++ b/package.json +@@ -20,6 +20,10 @@ + "import": "./dist/server/remark-plugins/index.js", + "types": "./dist/server/remark-plugins/index.d.ts" + }, ++ "./rehype-plugins": { ++ "import": "./dist/server/rehype-plugins/index.js", ++ "types": "./dist/server/rehype-plugins/index.d.ts" ++ }, + "./hooks": { + "import": "./dist/client/hooks/index.js", + "types": "./dist/client/hooks/index.d.ts" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6a3151d375..aaa2cb45d3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,7 +6,7 @@ settings: patchedDependencies: nextra@3.0.0-alpha.28: - hash: rggcgtpcrgz5zyoxmhwje4p6rm + hash: rlrllt7ydx3t6f2hccqi2i2pf4 path: patches/nextra@3.0.0-alpha.22.patch importers: @@ -52,6 +52,9 @@ importers: fast-glob: specifier: ^3.3.3 version: 3.3.3 + github-slugger: + specifier: 2.0.0 + version: 2.0.0 graphql: specifier: 16.10.0 version: 16.10.0 @@ -96,10 +99,10 @@ importers: version: 3.0.1(less-loader@12.2.0(less@4.2.1))(less@4.2.1)(next@14.2.29(@babel/core@7.26.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) nextra: specifier: 3.0.0-alpha.28 - version: 3.0.0-alpha.28(patch_hash=rggcgtpcrgz5zyoxmhwje4p6rm)(@types/react@18.3.23)(next@14.2.29(@babel/core@7.26.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3) + version: 3.0.0-alpha.28(patch_hash=rlrllt7ydx3t6f2hccqi2i2pf4)(@types/react@18.3.23)(next@14.2.29(@babel/core@7.26.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3) nextra-theme-docs: specifier: 3.0.0-alpha.28 - version: 3.0.0-alpha.28(next@14.2.29(@babel/core@7.26.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nextra@3.0.0-alpha.28(patch_hash=rggcgtpcrgz5zyoxmhwje4p6rm)(@types/react@18.3.23)(next@14.2.29(@babel/core@7.26.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 3.0.0-alpha.28(next@14.2.29(@babel/core@7.26.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nextra@3.0.0-alpha.28(patch_hash=rlrllt7ydx3t6f2hccqi2i2pf4)(@types/react@18.3.23)(next@14.2.29(@babel/core@7.26.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) numbro: specifier: 2.5.0 version: 2.5.0 @@ -10126,7 +10129,7 @@ snapshots: - '@babel/core' - babel-plugin-macros - nextra-theme-docs@3.0.0-alpha.28(next@14.2.29(@babel/core@7.26.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nextra@3.0.0-alpha.28(patch_hash=rggcgtpcrgz5zyoxmhwje4p6rm)(@types/react@18.3.23)(next@14.2.29(@babel/core@7.26.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + nextra-theme-docs@3.0.0-alpha.28(next@14.2.29(@babel/core@7.26.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(nextra@3.0.0-alpha.28(patch_hash=rlrllt7ydx3t6f2hccqi2i2pf4)(@types/react@18.3.23)(next@14.2.29(@babel/core@7.26.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@headlessui/react': 1.7.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@popperjs/core': 2.11.8 @@ -10137,13 +10140,13 @@ snapshots: intersection-observer: 0.12.2 next: 14.2.29(@babel/core@7.26.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) next-themes: 0.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - nextra: 3.0.0-alpha.28(patch_hash=rggcgtpcrgz5zyoxmhwje4p6rm)(@types/react@18.3.23)(next@14.2.29(@babel/core@7.26.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3) + nextra: 3.0.0-alpha.28(patch_hash=rlrllt7ydx3t6f2hccqi2i2pf4)(@types/react@18.3.23)(next@14.2.29(@babel/core@7.26.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) scroll-into-view-if-needed: 3.1.0 zod: 3.22.4 - nextra@3.0.0-alpha.28(patch_hash=rggcgtpcrgz5zyoxmhwje4p6rm)(@types/react@18.3.23)(next@14.2.29(@babel/core@7.26.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3): + nextra@3.0.0-alpha.28(patch_hash=rlrllt7ydx3t6f2hccqi2i2pf4)(@types/react@18.3.23)(next@14.2.29(@babel/core@7.26.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3): dependencies: '@headlessui/react': 1.7.19(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@mdx-js/mdx': 3.0.1 diff --git a/src/app/conf/2024/page.tsx b/src/app/conf/2024/page.tsx index f968564339..e8dcc80f94 100644 --- a/src/app/conf/2024/page.tsx +++ b/src/app/conf/2024/page.tsx @@ -54,7 +54,7 @@ export default function Page() { </div> <div className="flex items-center gap-10"> <svg - className="w-[50%]" + className="w-1/2" viewBox="0 0 1239 142" fill="currentColor" xmlns="http://www.w3.org/2000/svg" diff --git a/src/app/conf/2025/code-of-conduct/code-of-conduct.mdx b/src/app/conf/2025/code-of-conduct/code-of-conduct.mdx new file mode 100644 index 0000000000..5909350faa --- /dev/null +++ b/src/app/conf/2025/code-of-conduct/code-of-conduct.mdx @@ -0,0 +1,53 @@ +The Linux Foundation and its project communities are dedicated to providing a harassment-free experience for participants at all of our events, whether they are held in person or virtually. Linux Foundation events are working conferences intended for professional networking and collaboration within the open source community. They exist to encourage the open exchange of ideas and expression and require an environment that recognizes the inherent worth of every person and group. While at Linux Foundation events or related ancillary or social events, any participants, including members, speakers, attendees, volunteers, sponsors, exhibitors, booth staff and anyone else, must not engage in harassment in any form. + +This Code of Conduct may be revised at any time by The Linux Foundation and the terms are non-negotiable. Your registration for or attendance at any Linux Foundation event, whether it's held in person or virtually, indicates your agreement to abide by this policy and its terms. + +<Callout> +## Expected Behavior + +All event participants, whether they are attending an in-person event or a virtual event, are expected to behave in accordance with professional standards, with both this Code of Conduct as well as their respective employer's policies governing appropriate workplace behavior and applicable laws. +</Callout> + +## Unacceptable Behavior + +Harassment will not be tolerated in any form, whether in person or virtually, including, but not limited to, harassment based on gender, gender identity and expression, sexual orientation, disability, physical appearance, body size, race, age, religion or any other status protected by laws in which the conference or program is being held. Harassment includes the use of abusive, offensive or degrading language, intimidation, stalking, harassing photography or recording, inappropriate physical contact, sexual imagery and unwelcome sexual advances or requests for sexual favors. Any report of harassment at one of our events, whether in person or virtual, will be addressed immediately. Participants asked to stop any harassing behavior are expected to comply immediately. Anyone who witnesses or is subjected to unacceptable behavior should notify a conference organizer at once. + +Individuals who participate (or plan to participate) in Linux Foundation events, whether its an in-person event or a virtual event, should conduct themselves at all times in a manner that comports with both the letter and spirit of this policy prohibiting harassment and abusive behavior, whether before, during or after the event. This includes statements made in social media postings, on-line publications, text messages, and all other forms of electronic communication. + +Speakers should not use sexual language, images, or any language or images that would constitute harassment as defined above in their talks. Exhibitor booths serve as a platform for presenting businesses and/or projects and should maintain a professional and inclusive presence; therefore, the use of sexualized images, activities, materials, or attire, including costumes and uniforms that contribute to a sexualized environment, is strictly prohibited. Additionally, booths must not be utilized for political campaigning or promoting political causes, including the display or engagement in activities or materials that support such endeavors. + +<Callout> +## Consequences of Unacceptable Behavior + +If a participant engages in harassing behavior, whether in person or virtually, the conference organizers may take any action they deem appropriate depending on the circumstances, ranging from issuance of a warning to the offending individual to expulsion from the conference with no refund. The Linux Foundation reserves the right to exclude any participant found to be engaging in harassing behavior from participating in any further Linux Foundation events, trainings or other activities. + +If a participant (or individual wishing to participate in a Linux Foundation event, in-person and/or virtual), through postings on social media or other online publications or another form of electronic communication, engages in conduct that violates this policy, whether before, during or after a Linux Foundation event, The Linux Foundation may take appropriate corrective action, which could include imposing a temporary or permanent ban on an individual's participation in future Linux Foundation events. +</Callout> + +## What To Do If You Witness or Are Subject To Unacceptable Behavior + +If you are being harassed, notice that someone else is being harassed, or have any other concerns relating to harassment, please contact a member of the conference staff immediately. You are also encouraged to contact Angela Brown, Senior VP & General Manager of Events, at [angela@linuxfoundation.org](mailto:angela@linuxfoundation.org). + +<Callout> +## Incident Response + +Our staff has taken incident response training and responds to harassment reports quickly and thoroughly. As referenced above, if a participant engages in harassing behavior, whether in-person or virtually, the conference organizers may take any action they deem appropriate, ranging from issuance of a warning to the offending individual to expulsion from the conference with no refund, depending on the circumstances. The Linux Foundation reserves the right to exclude any participant found to be engaging in harassing behavior from participating in any further Linux Foundation events, trainings or other activities. + +Conference staff will also provide support to victims, including, but not limited to: +- Providing an Escort +- Contacting Hotel/Venue Security or Local Law Enforcement +- Briefing Key Event Staff For Response/Victim Assistance +- And otherwise assisting those experiencing harassment to ensure that they feel safe for the duration of the conference. +</Callout> + +## Health and Safety Requirements + +It is necessary for all attendees to cooperate and protect one another. For this reason, The Linux Foundation's events may have health and safety requirements (the "Health and Safety Requirements"). The specific requirements may vary from event to event, and will be communicated in writing prior to and during the event. + +If an attendee fails to comply with any of the Health and Safety Requirements, The Linux Foundation may (but is not obligated to) take appropriate corrective action, which could include immediate removal from the event and venue without a refund, and/or imposing a temporary or permanent ban on an individual's participation in future Linux Foundation events. + +<Callout> +## Pre-Event Concerns + +If you are planning to attend an upcoming event, whether in-person or virtually and have concerns regarding another individual who may be present, please contact Angela Brown ([angela@linuxfoundation.org](mailto:angela@linuxfoundation.org)). Precautions will be taken to ensure your comfort and safety, including, but not limited to providing an escort, prepping onsite event staff, keeping victim and harasser from attending the same talks/social events and providing onsite contact cell phone numbers for immediate contact. +</Callout> diff --git a/src/app/conf/2025/code-of-conduct/page.tsx b/src/app/conf/2025/code-of-conduct/page.tsx new file mode 100644 index 0000000000..619061fc1f --- /dev/null +++ b/src/app/conf/2025/code-of-conduct/page.tsx @@ -0,0 +1,99 @@ +import type { Metadata } from "next" +import clsx from "clsx" + +import { Anchor } from "@/app/conf/_design-system/anchor" +import { ServerComponentMarkdown } from "@/app/conf/_components/server-component-markdown" +import { Button } from "@/app/conf/_design-system/button" + +import { NavbarPlaceholder } from "../components/navbar" +import { Hero, HeroStripes } from "../components/hero" +import "../resources/prose.css" + +import markdown from "./code-of-conduct.mdx?raw" + +export const metadata: Metadata = { + title: "Code of Conduct | GraphQLConf 2025", +} + +const components = { + a: (props: React.AnchorHTMLAttributes<HTMLAnchorElement>) => { + return ( + <Anchor + {...props} + href={props.href ?? ""} + className={clsx(props.className, "typography-link")} + /> + ) + }, + ul: (props: React.HTMLAttributes<HTMLUListElement>) => { + return <ul {...props} className={clsx(props.className, "-mt-6")} /> + }, + Callout: (props: React.HTMLAttributes<HTMLDivElement>) => { + return ( + <div + {...props} + className={clsx( + props.className, + "gql-prose-inner -mx-4 w-fit border border-neu-300 bg-neu-50 p-4 dark:border-neu-100 dark:bg-neu-50/50 max-md:border-x-0 xl:my-4", + )} + /> + ) + }, +} + +export default function ResourcesPage() { + return ( + <> + <NavbarPlaceholder className="top-0 bg-neu-100 before:bg-white/30 dark:bg-[#181A12] dark:before:bg-blk/40" /> + <Hero + pageName="Code of conduct" + subtitle="The Linux Foundation" + colorScheme="neutral" + stripes={ + <HeroStripes + className="-scale-x-100 dark:data-[loaded=true]:opacity-80" + evenClassName="bg-[linear-gradient(180deg,hsl(var(--color-sec-light))_0%,hsl(319deg_100%_90%_/_0.2)_100%)] dark:bg-[linear-gradient(180deg,hsl(var(--color-sec-dark))_0%,hsl(var(--color-neu-100))_100%)]" + oddClassName="bg-[linear-gradient(180deg,hsl(319deg_100%_90%_/_0.2)_0%,hsl(var(--color-sec-base))_100%)] dark:bg-[linear-gradient(180deg,hsl(var(--color-sec-dark))_0%,hsl(var(--color-neu-0))_100%)]" + /> + } + > + <Button + href="https://events.linuxfoundation.org/about/code-of-conduct/" + className="mt-[18px] w-fit" + > + See on The Linux Foundation + </Button> + </Hero> + <main className="gql-all-anchors-focusable gql-conf-navbar-strip text-neu-900 before:bg-white/40 before:dark:bg-blk/30"> + <div className="gql-conf-container gql-conf-section xl:mb-16 xl:mt-8"> + <ServerComponentMarkdown + markdown={markdown} + extractToc + render={({ mdx, data }) => { + return ( + <div className="gql-prose md:max-lg:[&>:not(:first-child)]:mx-4"> + <aside className="gql-sticky-aside row-span-8 -mt-1 w-fit sm:max-xl:grid sm:max-xl:grid-cols-2 sm:max-xl:bg-neu-100 sm:max-xl:p-4 dark:sm:max-xl:bg-neu-50/50 xl:max-w-[284px]"> + {data.toc.map(({ value, id, depth }) => ( + <a + key={id} + data-depth={depth} + className="raw typography-menu block p-4 py-2 text-neu-800 hover:bg-neu-100 hover:text-neu-900 dark:hover:bg-neu-50 max-xl:-ml-4" + style={{ + paddingLeft: (depth - 2) * 16 + 16, + }} + href={`#${id}`} + > + {value} + </a> + ))} + </aside> + {mdx({ components })} + </div> + ) + }} + /> + </div> + </main> + </> + ) +} diff --git a/src/app/conf/2025/components/call-for-proposals.tsx b/src/app/conf/2025/components/call-for-proposals.tsx index 7725b2001c..b7084564ff 100644 --- a/src/app/conf/2025/components/call-for-proposals.tsx +++ b/src/app/conf/2025/components/call-for-proposals.tsx @@ -104,7 +104,7 @@ function NotesTab() { All speakers are required to adhere to our{" "} <Link className="typography-link dark:text-neu-50" - href="/conf/2025/resources/#code-of-conduct" + href="/conf/2025/code-of-conduct" > Code of Conduct </Link> diff --git a/src/app/conf/2025/components/hero/index.tsx b/src/app/conf/2025/components/hero/index.tsx index 747fdd7363..1c2ac61332 100644 --- a/src/app/conf/2025/components/hero/index.tsx +++ b/src/app/conf/2025/components/hero/index.tsx @@ -1,3 +1,5 @@ +import { ReactNode } from "react" + import { CalendarIcon } from "@/app/conf/_design-system/pixelarticons/calendar-icon" import { PinIcon } from "@/app/conf/_design-system/pixelarticons/pin-icon" @@ -5,49 +7,79 @@ import GraphQLFoundationWordmark from "../../assets/graphql-foundation-wordmark. import { ImageLoaded } from "../image-loaded" import blurBean from "./blur-bean-cropped.webp" +import clsx from "clsx" +import { + StripesDecoration, + StripesDecorationProps, +} from "@/app/conf/_design-system/stripes-decoration" -export function Hero({ - pageName, - year, - children, - bottom, -}: { +export type HeroProps = { pageName?: string - year: string children: React.ReactNode bottom?: React.ReactNode -}) { + colorScheme?: "primary" | "neutral" + stripes?: ReactNode +} & ( + | { year: string | number; subtitle?: never } + | { year?: never; subtitle: string } +) + +export function Hero(props: HeroProps) { + const colorScheme = props.colorScheme || "primary" + return ( - <article className="gql-conf-navbar-strip relative isolate flex flex-col justify-center bg-pri-base text-neu-0 selection:bg-blk/40 before:bg-white/30 dark:bg-pri-darker dark:text-neu-900 dark:selection:bg-white/40 before:dark:bg-blk/40"> + <article + className={clsx( + "gql-conf-navbar-strip relative isolate flex flex-col justify-center selection:bg-blk/40 before:bg-white/30 before:dark:bg-blk/40", + colorScheme === "primary" + ? "bg-pri-base text-neu-0 dark:bg-pri-darker dark:text-neu-900 dark:selection:bg-white/40" + : "bg-neu-100 dark:bg-neu-50/25", + )} + > <article className="relative"> - <HeroStripes /> + {props.stripes || <HeroStripes />} <div className="gql-conf-container mx-auto flex max-w-full flex-col gap-12 overflow-hidden p-4 pt-6 sm:p-8 sm:pt-12 md:gap-12 md:bg-left md:p-12 lg:px-24 lg:pb-16 lg:pt-24"> <div className="flex gap-10 max-md:flex-col md:justify-between"> - {pageName ? ( + {props.pageName ? ( <div> - <span className="typography-h3 text-sec-base"> - GraphQLConf {year} + <span + className={clsx( + "typography-body-lg lg:typography-h3", + colorScheme === "primary" + ? "text-sec-base" + : "text-pri-base", + )} + > + {props.year ? `GraphQLConf ${props.year}` : props.subtitle} </span> - <h1 className="typography-d1">{pageName}</h1> + <h1 className="typography-d1">{props.pageName}</h1> </div> ) : ( <h1 className="typography-d1 flex flex-wrap gap-2"> <span>GraphQLConf</span> - <span className="text-sec-base">{year}</span> + <span className="text-sec-base"> + {props.year || props.subtitle} + </span> </h1> )} <div className="flex h-min items-center gap-4"> <span className="typography-body-sm whitespace-pre"> hosted by </span> - <GraphQLFoundationWordmark width={128} height={34.877} /> + <GraphQLFoundationWordmark + width={128} + height={34.877} + className={ + colorScheme === "neutral" ? "[&_path]:fill-primary" : "" + } + /> </div> </div> - <div className="flex flex-col gap-8">{children}</div> + <div className="flex flex-col gap-8">{props.children}</div> </div> </article> - {bottom} + {props.bottom} </article> ) } @@ -69,40 +101,31 @@ export function HeroDateAndLocation() { ) } -const maskEven = - "repeating-linear-gradient(to right, transparent, transparent 12px, black 12px, black 24px)" -const maskOdd = - "repeating-linear-gradient(to right, black, black 12px, transparent 12px, transparent 24px)" - -export function HeroStripes() { +export interface HeroStripesProps extends StripesDecorationProps { + className?: string +} +export function HeroStripes({ className, ...rest }: HeroStripesProps) { return ( <ImageLoaded role="presentation" image={blurBean} - className="pointer-events-none absolute inset-x-0 bottom-[-385px] top-[-203px] -z-10 translate-y-12 opacity-0 transition duration-[400ms] ease-linear [mask-size:100%_50%] data-[loaded=true]:translate-y-0 data-[loaded=true]:opacity-100 sm:[mask-size:125%] xl:[mask-size:100%]" + className={clsx( + "pointer-events-none absolute inset-x-0 bottom-[-385px] top-[-203px] -z-10 translate-y-12 opacity-0 transition duration-[400ms] ease-linear [mask-size:100%_50%] data-[loaded=true]:translate-y-0 data-[loaded=true]:opacity-100 sm:[mask-size:125%] xl:[mask-size:100%]", + className, + )} style={{ maskImage: `url(${blurBean.src})`, WebkitMaskImage: `url(${blurBean.src})`, - // maskSize: "100%", // todo: (very low priority) need the newly exported full blur bean with rotation to match the mobile design 1-1 maskRepeat: "no-repeat", WebkitMaskRepeat: "no-repeat", maskPosition: "center", WebkitMaskPosition: "center", }} > - <div - className="absolute inset-0 bg-[linear-gradient(180deg,hsl(var(--color-pri-light))_0%,hsl(319deg_100%_90%_/_0.2)_100%)] dark:bg-[linear-gradient(180deg,hsl(var(--color-pri-dark))_0%,hsl(319_100%_20%_/_1)_100%)]" - style={{ - maskImage: maskEven, - WebkitMaskImage: maskEven, - }} - /> - <div - className="absolute inset-0 bg-[linear-gradient(180deg,hsl(319deg_100%_90%_/_0.2)_0%,hsl(var(--color-pri-base))_100%)] dark:bg-[linear-gradient(180deg,hsl(319_100%_30%_/_1)_0%,hsl(var(--color-pri-dark))_100%)]" - style={{ - maskImage: maskOdd, - WebkitMaskImage: maskOdd, - }} + <StripesDecoration + evenClassName="bg-[linear-gradient(180deg,hsl(var(--color-pri-light))_0%,hsl(319deg_100%_90%_/_0.2)_100%)] dark:bg-[linear-gradient(180deg,hsl(var(--color-pri-dark))_0%,hsl(319_100%_20%_/_1)_100%)]" + oddClassName="bg-[linear-gradient(180deg,hsl(319deg_100%_90%_/_0.2)_0%,hsl(var(--color-pri-base))_100%)] dark:bg-[linear-gradient(180deg,hsl(319_100%_30%_/_1)_0%,hsl(var(--color-pri-dark))_100%)]" + {...rest} /> </ImageLoaded> ) diff --git a/src/app/conf/2025/layout.tsx b/src/app/conf/2025/layout.tsx index 4bb4bfeb77..bc6b002f9e 100644 --- a/src/app/conf/2025/layout.tsx +++ b/src/app/conf/2025/layout.tsx @@ -64,7 +64,7 @@ export default function Layout({ [ { children: "Code of Conduct", - href: "/conf/2025/resources/#code-of-conduct", + href: "/conf/2025/code-of-conduct", }, { children: "Inclusion & Accessibility", diff --git a/src/app/conf/2025/resources/page.tsx b/src/app/conf/2025/resources/page.tsx index 4105506677..9790a6e736 100644 --- a/src/app/conf/2025/resources/page.tsx +++ b/src/app/conf/2025/resources/page.tsx @@ -1,6 +1,9 @@ import { Metadata } from "next" +import { Button } from "@/app/conf/_design-system/button" + import { NavbarPlaceholder } from "../components/navbar" +import { Hero, HeroStripes } from "../components/hero" import Resources from "./client-mdx" import "./prose.css" @@ -12,9 +15,28 @@ export const metadata: Metadata = { export default function ResourcesPage() { return ( <> - <NavbarPlaceholder className="top-0 bg-neu-0 before:bg-white/30 dark:bg-neu-0 dark:before:bg-blk/40" /> + <NavbarPlaceholder className="top-0 bg-neu-100 before:bg-white/30 dark:bg-[#181A12] dark:before:bg-blk/40" /> + <Hero + pageName="Resources" + year="2025" + colorScheme="neutral" + stripes={ + <HeroStripes + className="-scale-x-100 dark:data-[loaded=true]:opacity-80" + evenClassName="bg-[linear-gradient(180deg,hsl(var(--color-sec-light))_0%,hsl(319deg_100%_90%_/_0.2)_100%)] dark:bg-[linear-gradient(180deg,hsl(var(--color-sec-dark))_0%,hsl(var(--color-neu-100))_100%)]" + oddClassName="bg-[linear-gradient(180deg,hsl(319deg_100%_90%_/_0.2)_0%,hsl(var(--color-sec-base))_100%)] dark:bg-[linear-gradient(180deg,hsl(var(--color-sec-dark))_0%,hsl(var(--color-neu-0))_100%)]" + /> + } + > + <Button + href="mailto:graphql_events@linuxfoundation.org" + className="w-fit" + > + Talk to us + </Button> + </Hero> <main className="gql-all-anchors-focusable gql-conf-navbar-strip text-neu-900 before:bg-white/40 before:dark:bg-blk/30"> - <div className="gql-conf-container gql-conf-section gql-prose"> + <div className="gql-conf-container gql-conf-section gql-prose xl:mb-16 xl:mt-8"> <Resources /> </div> </main> diff --git a/src/app/conf/2025/resources/prose.css b/src/app/conf/2025/resources/prose.css index a0379ecdaa..3927294808 100644 --- a/src/app/conf/2025/resources/prose.css +++ b/src/app/conf/2025/resources/prose.css @@ -4,12 +4,15 @@ --grid-col-content: 1; @apply xl:[--grid-col-content:3]; - @apply typography-body-md grid grid-cols-1 gap-6 xl:my-12 xl:grid-cols-[auto_1fr_var(--prose-width)_1fr]; + @apply typography-body-md grid grid-cols-1 gap-6 xl:grid-cols-[auto_1fr_var(--prose-width)_1fr]; & > * { grid-column: var(--grid-col-content); } +} +.gql-prose, +.gql-prose-inner { & h1 { @apply typography-h2 lg:col-span-full xl:pb-16; } @@ -23,12 +26,24 @@ & > p, & > ul, & > ol { - @apply max-w-[var(--prose-width)]; + @apply max-w-[var(--prose-width)] text-neu-800; } & > p { @apply text-pretty; } + + & > ul { + @apply list-disc pl-4; + } + + & > ol { + @apply list-decimal pl-4; + } +} + +.gql-prose-inner { + @apply grid grid-cols-1 gap-6; } .gql-sticky-aside { diff --git a/src/app/conf/2025/resources/resources.mdx b/src/app/conf/2025/resources/resources.mdx index 8b67bcb1d4..3ca0d8155b 100644 --- a/src/app/conf/2025/resources/resources.mdx +++ b/src/app/conf/2025/resources/resources.mdx @@ -1,5 +1,3 @@ -# GraphQLConf 2025 Resource Hub - <aside className='gql-sticky-aside w-fit row-span-8 -mt-1 sm:max-xl:grid-cols-2 sm:max-xl:grid sm:max-xl:bg-neu-100 dark:sm:max-xl:bg-neu-50/50 sm:max-xl:p-4'> {toc.map(({value, id, depth}) => <a key={id} data-depth={depth} className='block raw hover:bg-neu-100 dark:hover:bg-neu-50 py-2 p-4 max-xl:-ml-4 hover:text-neu-900 text-neu-800 typography-menu xl:data-[depth=2]:text-lg data-[depth=2]:font-semibold' style={{ paddingLeft: (depth - 2) * 16 + 16, @@ -12,11 +10,11 @@ The GraphQL Foundation and the Linux Foundation are dedicated to providing a har This Code of Conduct may be revised at any time by The GraphQL Foundation or The Linux Foundation and the terms are non-negotiable. Your registration for or attendance at GraphQL, whether in person or virtually, indicates your agreement to abide by this policy and its terms. -Please read the full [Code of Conduct](https://events.linuxfoundation.org/about/code-of-conduct/) for the complete policy and terms. +Please read the full [Code of Conduct](/conf/2025/code-of-conduct) for the complete policy and terms. ## Inclusion & Accessibility -Education and collaboration are vital to the future of the open source ecosystem, and it is imperative to us that everyone in the community that wants to participate feels welcome to do so regardless of gender, gender identity, sexual orientation, disability, race, ethnicity, age, religion or economic status. Our [code of conduct](#code-of-conduct) outlines our expectations for all those who participate in our community, as well as the consequences for unacceptable behavior. +Education and collaboration are vital to the future of the open source ecosystem, and it is imperative to us that everyone in the community that wants to participate feels welcome to do so regardless of gender, gender identity, sexual orientation, disability, race, ethnicity, age, religion or economic status. Our [code of conduct](/conf/2025/code-of-conduct) outlines our expectations for all those who participate in our community, as well as the consequences for unacceptable behavior. We have a broad range of both onsite resources and emergency resources, as well as a health & safety policy. diff --git a/src/app/conf/_components/server-component-markdown.tsx b/src/app/conf/_components/server-component-markdown.tsx new file mode 100644 index 0000000000..7b4111bd99 --- /dev/null +++ b/src/app/conf/_components/server-component-markdown.tsx @@ -0,0 +1,89 @@ +import "server-only" + +// @ts-expect-error - we already have it in transitive deps and I want to avoid having a duplicate +import { compile, run } from "@mdx-js/mdx" +import * as runtime from "react/jsx-runtime" +import { Root, Element } from "hast" +import { visit } from "unist-util-visit" +import { toString } from "hast-util-to-string" +import Slugger from "github-slugger" + +/** + * Nextra's builtin MDX support requires jumping out to a Client component + * (and creating new file) and we don't really need that for everything. + * + * Sometimes we want to grab some markdown without all the remark and rehype + * plugins configured by Nextra. + */ +export async function ServerComponentMarkdown({ + markdown, + extractToc = false, + render = ({ mdx }) => mdx({}), +}: { + markdown: string + extractToc?: boolean + render?: (props: { + mdx: ({ + components, + }: { + components?: MdxComponents + }) => React.AwaitedReactNode + data: { toc: TableOfContents } + }) => React.AwaitedReactNode +}) { + try { + const rehypePlugins = extractToc ? [rehypeExtractTableOfContents] : [] + + const vfile = await compile(markdown, { + outputFormat: "function-body", + remarkPlugins: [], + rehypePlugins, + recmaPlugins: [], + }) + + const { default: mdx } = await run(vfile, { + ...runtime, + baseUrl: import.meta.url, + }) + + return render({ data: vfile.data, mdx }) + } catch (error) { + console.error(error) + if (process.env.NODE_ENV === "production") throw error + return ( + <div>{error instanceof Error ? error.message : "Error loading MDX"}</div> + ) + } +} + +type TableOfContents = Array<{ value: string; id: string; depth: number }> +type MdxComponents = Record<string, React.ComponentType> + +/** + * Nextra has a built-in plugin like this, but it also steals the heading contents + * as is tightly coupled with other Nextra features. + */ +function rehypeExtractTableOfContents() { + const slugger = new Slugger() + + return function (tree: Root, file: any) { + const toc: TableOfContents = [] + + visit(tree, "element", (node: Element) => { + if (node.tagName && /^h[1-6]$/.test(node.tagName)) { + const depth = parseInt(node.tagName.charAt(1), 10) + const value = toString(node) + const slug = slugger.slug(value) + + node.properties ||= node.properties || {} + // add id to the heading element if it's not already set + node.properties.id ||= slug + + toc.push({ value, id: slug, depth }) + } + }) + + file.data = file.data || {} + file.data.toc = toc + } +} diff --git a/src/app/env.d.ts b/src/app/env.d.ts index a0ee3ba5f3..9692f82cd7 100644 --- a/src/app/env.d.ts +++ b/src/app/env.d.ts @@ -11,3 +11,8 @@ declare module "*.svg?svgr" { const content: React.FC<React.SVGProps<SVGElement>> export default content } + +declare module "*.mdx?raw" { + const content: string + export default content +}