diff --git a/src/app/conf/2025/components/testimonials/index.tsx b/src/app/conf/2025/components/testimonials/index.tsx index f34c4b8690..110228b31f 100644 --- a/src/app/conf/2025/components/testimonials/index.tsx +++ b/src/app/conf/2025/components/testimonials/index.tsx @@ -85,7 +85,7 @@ export function TestimonialsList({ className="flex shrink-0 snap-start flex-row-reverse items-center gap-6 max-md:flex-col md:px-10" >
-

+

{testimonial.quote}

+ + diff --git a/src/app/conf/_design-system/pixelarticons/modem.svg b/src/app/conf/_design-system/pixelarticons/modem.svg new file mode 100644 index 0000000000..7264407b31 --- /dev/null +++ b/src/app/conf/_design-system/pixelarticons/modem.svg @@ -0,0 +1,12 @@ + + + + diff --git a/src/app/conf/_design-system/utils/useOnClickOutside.tsx b/src/app/conf/_design-system/utils/useOnClickOutside.tsx new file mode 100644 index 0000000000..ad348f7e1c --- /dev/null +++ b/src/app/conf/_design-system/utils/useOnClickOutside.tsx @@ -0,0 +1,19 @@ +import { useEffect } from "react" + +export function useOnClickOutside( + ref: React.RefObject, + handler: (event: MouseEvent) => void, +) { + useEffect(() => { + const listener = (event: MouseEvent) => { + if (ref.current && event.composedPath().includes(ref.current)) { + return + } + handler(event) + } + + document.addEventListener("click", listener) + return () => document.removeEventListener("click", listener) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) +} diff --git a/src/app/env.d.ts b/src/app/env.d.ts index 9692f82cd7..57835bb125 100644 --- a/src/app/env.d.ts +++ b/src/app/env.d.ts @@ -12,7 +12,7 @@ declare module "*.svg?svgr" { export default content } -declare module "*.mdx?raw" { +declare module "*?raw" { const content: string export default content } diff --git a/src/components/footer/conference-footer-box.tsx b/src/components/footer/conference-footer-box.tsx index 8f1caa4f93..69016a8c25 100644 --- a/src/components/footer/conference-footer-box.tsx +++ b/src/components/footer/conference-footer-box.tsx @@ -16,7 +16,7 @@ export function ConferenceFooterBox({ diff --git a/src/components/footer/index.tsx b/src/components/footer/index.tsx index 8911cda8bf..fb1a596dc1 100644 --- a/src/components/footer/index.tsx +++ b/src/components/footer/index.tsx @@ -51,7 +51,7 @@ export function Footer({ extraLinks }: { extraLinks: FooterLink[] }) {

{section.route ? ( {section.title} @@ -61,11 +61,11 @@ export function Footer({ extraLinks }: { extraLinks: FooterLink[] }) { )}

)} - {section.links.map(link => ( + {section.links.map((link, i) => ( {link.title} @@ -81,7 +81,7 @@ export function Footer({ extraLinks }: { extraLinks: FooterLink[] }) { )}
diff --git a/src/components/index-page/data-colocation/component-tree.tsx b/src/components/index-page/data-colocation/component-tree.tsx new file mode 100644 index 0000000000..79208a199a --- /dev/null +++ b/src/components/index-page/data-colocation/component-tree.tsx @@ -0,0 +1,183 @@ +import ModemIcon from "@/app/conf/_design-system/pixelarticons/modem.svg?svgr" +import clsx from "clsx" + +const INNER_BOX_SIZE = 16 + +interface ComponentTreeProps extends React.HTMLAttributes { + names: [string, string, string, string] +} + +export function ComponentTree({ + names, + className, + ...rest +}: ComponentTreeProps) { + return ( +
+
+
+ + {names[0]} + +
+ +
+ +
+ + {names[1]} + +
+ +
+ +
+ + {names[2]} + +
+ +
+ +
+ + {names[3]} + +
+
+ +
+
+ +
+ +
+ + + + + +
+
+ + +
+ + +
+
+
+ + +
+ + +
+
+
+
+
+ ) +} + +interface NestedBoxProps extends React.HTMLAttributes { + bgColor: string + middleColor?: string + innerColor: string +} + +function NestedBox({ + bgColor, + middleColor, + innerColor, + ...rest +}: NestedBoxProps) { + const padding = INNER_BOX_SIZE / 2 + + return ( +
+
+
+
+
+ ) +} + +interface ComponentLabelProps extends React.HTMLAttributes { + children: React.ReactNode + className: string +} + +function ComponentLabel({ children, className, ...rest }: ComponentLabelProps) { + return ( +
+ {children} +
+ ) +} + +function LeafBox(props: React.HTMLAttributes) { + return ( + + ) +} + +function Fork(props: React.SVGProps) { + return ( + + + + + ) +} diff --git a/src/components/index-page/data-colocation/data-colocation.css b/src/components/index-page/data-colocation/data-colocation.css new file mode 100644 index 0000000000..27da4fb9c3 --- /dev/null +++ b/src/components/index-page/data-colocation/data-colocation.css @@ -0,0 +1,32 @@ +.sector-opacity { + & [data-sector] { + transition: opacity 0.2s ease-in; + transition-delay: 0.1s; + } + + [data-active-sector] & [data-sector] { + opacity: 0.4; + transition-delay: 0s; + transition-timing-function: ease-out; + } + + [data-active-sector="1"] & [data-sector="1"], + [data-active-sector="2"] & [data-sector="2"], + [data-active-sector="3"] & [data-sector="3"], + [data-active-sector="4"] & [data-sector="4"] { + opacity: 1; + } +} + +.sector-ring { + & [data-sector] { + @apply ring-2 ring-transparent ring-offset-2 ring-offset-transparent transition-shadow duration-200 ease-in; + } + + [data-active-sector="1"] & [data-sector="1"], + [data-active-sector="2"] & [data-sector="2"], + [data-active-sector="3"] & [data-sector="3"], + [data-active-sector="4"] & [data-sector="4"] { + @apply ring-2 ring-neu-500 duration-200 ease-out dark:ring-neu-400 dark:ring-offset-neu-0; + } +} diff --git a/src/components/index-page/data-colocation/data-colocation.json b/src/components/index-page/data-colocation/data-colocation.json new file mode 100644 index 0000000000..10e69f2c2a --- /dev/null +++ b/src/components/index-page/data-colocation/data-colocation.json @@ -0,0 +1,31 @@ +{ + "friends": [ + { + "name": "Trudie", + "profilePic": "trudie.webp", + "mutualFriendsCount": 120, + "isSubscribed": true, + "username": "funkytrudster89", + "email": "trudie@mail.com", + "location": "New York, USA" + }, + { + "name": "Frances", + "profilePic": "frances.webp", + "mutualFriendsCount": 42, + "isSubscribed": false, + "username": "frannyfran12", + "email": "frances@mail.com", + "location": "Madrid, Spain" + }, + { + "name": "Fernando", + "profilePic": "fernando.webp", + "mutualFriendsCount": 114, + "isSubscribed": true, + "username": "fern_whirlwind", + "email": "fernando@mail.com", + "location": "Amsterdam, Netherlands" + } + ] +} diff --git a/src/components/index-page/data-colocation/data-colocation.mdx b/src/components/index-page/data-colocation/data-colocation.mdx new file mode 100644 index 0000000000..c59e61735b --- /dev/null +++ b/src/components/index-page/data-colocation/data-colocation.mdx @@ -0,0 +1,25 @@ +```graphql +query GetFriendList { + ...FriendList +} + +fragment FriendList on Query { + friends { + ...FriendListItem + } +} + +fragment FriendListItem on Friend { + name + profilePic + mutualFriendsCount + isSubscribed + ...FriendInfo +} + +fragment FriendInfo on Friend { + username + email + location +} +``` diff --git a/src/components/index-page/data-colocation/friend-avatars/fernando.webp b/src/components/index-page/data-colocation/friend-avatars/fernando.webp new file mode 100644 index 0000000000..f8491caf15 Binary files /dev/null and b/src/components/index-page/data-colocation/friend-avatars/fernando.webp differ diff --git a/src/components/index-page/data-colocation/friend-avatars/frances.webp b/src/components/index-page/data-colocation/friend-avatars/frances.webp new file mode 100644 index 0000000000..22729367da Binary files /dev/null and b/src/components/index-page/data-colocation/friend-avatars/frances.webp differ diff --git a/src/components/index-page/data-colocation/friend-avatars/index.tsx b/src/components/index-page/data-colocation/friend-avatars/index.tsx new file mode 100644 index 0000000000..3bfa2890f0 --- /dev/null +++ b/src/components/index-page/data-colocation/friend-avatars/index.tsx @@ -0,0 +1,9 @@ +import trudie from "./trudie.webp" +import frances from "./frances.webp" +import fernando from "./fernando.webp" + +export const friendAvatars = { + trudie, + frances, + fernando, +} diff --git a/src/components/index-page/data-colocation/friend-avatars/trudie.webp b/src/components/index-page/data-colocation/friend-avatars/trudie.webp new file mode 100644 index 0000000000..fd467d89b3 Binary files /dev/null and b/src/components/index-page/data-colocation/friend-avatars/trudie.webp differ diff --git a/src/components/index-page/data-colocation/friend-list.tsx b/src/components/index-page/data-colocation/friend-list.tsx new file mode 100644 index 0000000000..364a760bd7 --- /dev/null +++ b/src/components/index-page/data-colocation/friend-list.tsx @@ -0,0 +1,114 @@ +import { friendAvatars } from "./friend-avatars" + +interface FriendList { + friends: FriendListItem[] +} + +export function FriendList({ friends }: FriendList) { + return ( +
+

+ Friends +

+
    + {friends.map(friend => ( + + ))} +
+
+ ) +} + +interface FriendListItem extends FriendInfo { + name: string + profilePic: string + mutualFriendsCount: number + isSubscribed: boolean +} + +export function FriendListItem({ + isSubscribed, + mutualFriendsCount, + name, + profilePic, + ...friendInfo +}: FriendListItem) { + const avatar = + friendAvatars[profilePic.replace(".webp", "") as keyof typeof friendAvatars] + + return ( +
  • + +
    +
    +
    + {name} +
    + {mutualFriendsCount} mutual friends +
    +
    + +
    + +
    +
  • + ) +} + +interface FriendInfo { + username: string + email: string + location: string +} + +const friendInfoKeys: (keyof FriendInfo)[] = ["username", "email", "location"] + +export function FriendInfo(props: FriendInfo) { + return ( +
    + {friendInfoKeys.map(key => ( +
    +
    {key}
    +
    {props[key]}
    +
    + ))} +
    + ) +} + +function SubscribeIconButton() { + return ( +
    + + + + +
    + ) +} diff --git a/src/components/index-page/data-colocation/index.tsx b/src/components/index-page/data-colocation/index.tsx new file mode 100644 index 0000000000..fc0fa61b56 --- /dev/null +++ b/src/components/index-page/data-colocation/index.tsx @@ -0,0 +1,207 @@ +import * as React from "react" +import { clsx } from "clsx" +import { Code } from "nextra/components" +import { + cloneElement, + ComponentPropsWithoutRef, + ReactElement, + useRef, +} from "react" + +import { Pre } from "@/components/pre" +import { SectionLabel } from "@/app/conf/_design-system/section-label" +import InfoIcon from "@/app/conf/_design-system/pixelarticons/info.svg?svgr" + +import { ComponentTree } from "./component-tree" +import { FriendList } from "./friend-list" + +import json from "./data-colocation.json" +import Query from "./data-colocation.mdx" +import "./data-colocation.css" +import { useOnClickOutside } from "@/app/conf/_design-system/utils/useOnClickOutside" + +const highlightedFragments = { + GetFriendList: 1, + FriendList: 2, + FriendListItem: 3, + FriendInfo: 4, +} + +const components = { + pre: (props: ComponentPropsWithoutRef) => ( +
    +  ),
    +  code: ({ children, ...rest }: ComponentPropsWithoutRef) => {
    +    let sectorIndex: number | undefined
    +    let depth = 0
    +
    +    if (children) {
    +      children = React.Children.map(children, child => {
    +        if (isSpanElement(child)) {
    +          let children = (child as ReactElement).props.children
    +
    +          if (isSpanElement(children)) {
    +            children = children.props.children
    +          } else if (Array.isArray(children)) {
    +            children = children
    +              .map(child => {
    +                if (isSpanElement(child)) {
    +                  return child.props.children
    +                }
    +                return child
    +              })
    +              .join("")
    +          }
    +
    +          if (/query|fragment/.test(children)) {
    +            for (const [name, index] of Object.entries(highlightedFragments)) {
    +              if (children.includes(` ${name} `)) sectorIndex = index
    +              depth++
    +            }
    +          }
    +
    +          if (children.includes("{")) depth++
    +          if (children.includes("}")) {
    +            depth--
    +            if (depth === 0) sectorIndex = undefined
    +          }
    +
    +          if (sectorIndex) {
    +            return cloneElement(child, {
    +              ...child.props,
    +              "data-sector": sectorIndex,
    +            } as React.HTMLAttributes)
    +          }
    +
    +          return child
    +        }
    +
    +        return child
    +      })
    +    }
    +    return {children}
    +  },
    +}
    +
    +function isSpanElement(
    +  child: React.ReactNode,
    +): child is ReactElement<{ children: React.ReactNode }> {
    +  return (
    +    typeof child === "object" &&
    +    !!child &&
    +    (child as ReactElement).type === "span"
    +  )
    +}
    +
    +export function DataColocation() {
    +  const markSector = (event: React.MouseEvent) => {
    +    const target =
    +      event.target && event.target instanceof HTMLElement ? event.target : null
    +
    +    const sectorElement = target?.closest("[data-sector]") as HTMLElement | null
    +    const sector = sectorElement?.dataset.sector
    +
    +    if (!sector) return
    +
    +    const currentTarget = event.currentTarget
    +
    +    if (currentTarget.dataset.activeSector !== sector) {
    +      currentTarget.dataset.activeSector = sector
    +    }
    +  }
    +
    +  const unmarkSector = (event: React.MouseEvent) => {
    +    console.log("unmarkSector", window.matchMedia("(hover: none)").matches)
    +    if (window.matchMedia("(hover: none)").matches) return
    +
    +    const target =
    +      event.relatedTarget && event.relatedTarget instanceof HTMLElement
    +        ? event.relatedTarget
    +        : null
    +
    +    const currentTarget = event.currentTarget
    +
    +    const sectorElement = target?.closest("[data-sector]") as HTMLElement | null
    +    const targetSector = sectorElement?.dataset.sector
    +    const currentActiveSector = currentTarget.dataset.activeSector
    +
    +    if (!targetSector) {
    +      delete currentTarget.dataset.activeSector
    +      return
    +    }
    +
    +    if (!currentActiveSector) return
    +
    +    currentTarget.dataset.activeSector = targetSector
    +  }
    +
    +  const ref = useRef(null)
    +  useOnClickOutside(ref, event => {
    +    console.log("clicked outside")
    +    if (event.currentTarget && event.currentTarget instanceof HTMLElement) {
    +      delete event.currentTarget.dataset.activeSector
    +    }
    +  })
    +
    +  return (
    +    
    +
    +
    + Data Colocation +

    Data Colocation

    +

    + GraphQL fragments let you reuse common field selections across + queries, making your code more maintainable and consistent. +

    +
    + ", + "", + "", + ]} + /> +
    +
    +
    + +
    + +
    +
    +
    + +
    +
    +
    + ) +} + +function FigureInfo() { + return ( +
    + +

    + Click on + Hover over the components to + see their GraphQL fragments. +

    +
    + ) +} diff --git a/src/components/index-page/graphql-advantages/integration.tsx b/src/components/index-page/graphql-advantages/integration.tsx index 032a7faebf..473cd12668 100644 --- a/src/components/index-page/graphql-advantages/integration.tsx +++ b/src/components/index-page/graphql-advantages/integration.tsx @@ -10,7 +10,7 @@ export function IntegrationFigure() { return (
    diff --git a/src/components/index-page/graphql-advantages/precision.tsx b/src/components/index-page/graphql-advantages/precision.tsx index a88b17f9aa..c50558ca45 100644 --- a/src/components/index-page/graphql-advantages/precision.tsx +++ b/src/components/index-page/graphql-advantages/precision.tsx @@ -93,7 +93,7 @@ export function PrecisionFigure() {
    diff --git a/src/components/index-page/graphql-advantages/versionless.tsx b/src/components/index-page/graphql-advantages/versionless.tsx
    index 95ea24a76e..97e3d78352 100644
    --- a/src/components/index-page/graphql-advantages/versionless.tsx
    +++ b/src/components/index-page/graphql-advantages/versionless.tsx
    @@ -32,7 +32,7 @@ export function VersionlessFigure() {
     
       return (
         
    +
    @@ -18,6 +19,7 @@ export function IndexPage() { +
    diff --git a/src/components/index-page/powered-by-community.tsx b/src/components/index-page/powered-by-community.tsx index 128e1da1ef..66005b1047 100644 --- a/src/components/index-page/powered-by-community.tsx +++ b/src/components/index-page/powered-by-community.tsx @@ -5,7 +5,7 @@ import ArrowDownIcon from "@/app/conf/_design-system/pixelarticons/arrow-down.sv export function PoweredByCommunity() { return (
    -
    +

    Powered by the community @@ -20,21 +20,21 @@ export function PoweredByCommunity() {
    Browse libraries Explore events & meetups Learn about GraphQL Foundation diff --git a/src/components/index-page/quotes-from-the-industry.tsx b/src/components/index-page/quotes-from-the-industry.tsx index e336be9b52..dffa2c0698 100644 --- a/src/components/index-page/quotes-from-the-industry.tsx +++ b/src/components/index-page/quotes-from-the-industry.tsx @@ -66,7 +66,7 @@ export function QuotesFromTheIndustry() {

    ) diff --git a/src/components/index-page/trusted-by/index.tsx b/src/components/index-page/trusted-by/index.tsx index b0148cd379..3459171471 100644 --- a/src/components/index-page/trusted-by/index.tsx +++ b/src/components/index-page/trusted-by/index.tsx @@ -32,7 +32,6 @@ const logos: LogoListItem[] = [ /> ), }, - // todo: Netflix? // { // href: "https://netflix.com", @@ -125,7 +124,7 @@ export function TrustedBy() { href={href} target="_blank" rel="noreferrer" - className="group relative flex shrink-0 items-center justify-center bg-neu-0 p-10 *:z-[1] before:absolute before:inset-2 hover:before:bg-neu-100/50 dark:hover:before:bg-[#202219]" + className="group relative flex shrink-0 items-center justify-center bg-neu-0 p-10 *:z-[1] before:absolute before:inset-2 hover:before:bg-neu-100/50 focus-visible:z-[1] dark:hover:before:bg-[#202219]" > diff --git a/tailwind.config.ts b/tailwind.config.ts index b33ded3675..3f1f24f171 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -198,7 +198,16 @@ const config: Config = { }, }) }), + tailwindMediaHover(), ], darkMode: ["class", 'html[class~="dark"]'], } + export default config + +function tailwindMediaHover() { + return plugin(({ addVariant }) => { + addVariant("hover-hover", "@media (hover: hover)") + addVariant("hover-none", "@media (hover: none)") + }) +}