Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions apps/blog/src/app/(blog)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { WebNavigation } from "@prisma-docs/ui/components/web-navigation";
import { Footer } from "@prisma-docs/ui/components/footer";
import { ThemeProvider } from "@prisma-docs/ui/components/theme-provider";
export function baseOptions() {
return {
nav: {
Expand Down Expand Up @@ -104,9 +106,10 @@ export function baseOptions() {

export default function Layout({ children }: { children: React.ReactNode }) {
return (
<>
<ThemeProvider defaultTheme="system" storageKey="blog-theme">
<WebNavigation links={baseOptions().links} />
{children}
</>
<Footer />
</ThemeProvider>
);
}
30 changes: 15 additions & 15 deletions apps/docs/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,30 @@
import { Provider } from '@/components/provider';
import { getBaseUrl } from '@/lib/urls';
import './global.css';
import { Inter, Barlow } from 'next/font/google';
import type { Metadata } from 'next';
import type { ReactNode } from 'react';
import Script from 'next/script';
import { Provider } from "@/components/provider";
import { getBaseUrl } from "@/lib/urls";
import "./global.css";
import { Inter, Barlow } from "next/font/google";
import type { Metadata } from "next";
import type { ReactNode } from "react";
import Script from "next/script";

const inter = Inter({
subsets: ['latin'],
variable: '--font-inter',
subsets: ["latin"],
variable: "--font-inter",
});

const barlow = Barlow({
subsets: ['latin'],
weight: ['400', '500', '600', '700'],
variable: '--font-barlow',
subsets: ["latin"],
weight: ["400", "500", "600", "700"],
variable: "--font-barlow",
});

export const metadata: Metadata = {
metadataBase: new URL(getBaseUrl()),
title: {
default: 'Prisma Documentation',
template: '%s | Prisma Documentation',
default: "Prisma Documentation",
template: "%s | Prisma Documentation",
},
description:
'Documentation for Prisma ORM, Prisma Postgres, Prisma Accelerate, and the Prisma ecosystem. Build type-safe database applications with ease.',
"Documentation for Prisma ORM, Prisma Postgres, Prisma Accelerate, and the Prisma ecosystem. Build type-safe database applications with ease.",
};

export default function Layout({ children }: { children: ReactNode }) {
Expand Down
127 changes: 127 additions & 0 deletions packages/ui/THEME.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# Theme Provider Usage

This package includes a custom React-based theme provider that works independently of Next.js.

## Setup

### 1. Wrap your app with the ThemeProvider

```tsx
import { ThemeProvider } from '@prisma-docs/ui/components/theme-provider';

function App() {
return (
<ThemeProvider
defaultTheme="system"
storageKey="app-theme"
attribute="data-theme"
>
{/* Your app content */}
</ThemeProvider>
);
}
```

### 2. Use the ThemeToggle component

```tsx
import { ThemeToggle } from '@prisma-docs/ui/components/theme-toggle';

function Header() {
return (
<header>
<ThemeToggle mode="light-dark" />
{/* or */}
<ThemeToggle mode="light-dark-system" />
</header>
);
}
```

### 3. Access theme in any component

```tsx
import { useTheme } from '@prisma-docs/ui/components/theme-provider';

function MyComponent() {
const { theme, resolvedTheme, setTheme } = useTheme();

return (
<div>
<p>Current theme: {theme}</p>
<p>Resolved theme: {resolvedTheme}</p>
<button onClick={() => setTheme('dark')}>Dark</button>
<button onClick={() => setTheme('light')}>Light</button>
<button onClick={() => setTheme('system')}>System</button>
</div>
);
}
```

## ThemeProvider Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `children` | `React.ReactNode` | required | Your app content |
| `defaultTheme` | `'light' \| 'dark' \| 'system'` | `'system'` | Default theme on first load |
| `storageKey` | `string` | `'theme'` | localStorage key for persisting theme |
| `attribute` | `string` | `'data-theme'` | HTML attribute to set on document root |

## ThemeToggle Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `mode` | `'light-dark' \| 'light-dark-system'` | `'light-dark'` | Toggle mode |
| `className` | `string` | - | Additional CSS classes |

### Modes

- **`light-dark`**: Simple toggle between light and dark (2 icons)
- **`light-dark-system`**: Toggle between light, dark, and system (3 icons)

## How it works

1. **Persistence**: Theme choice is saved to localStorage
2. **System preference**: Automatically detects system dark mode preference
3. **Attribute setting**: Sets `data-theme` attribute on `<html>` element
4. **Dark class**: Adds/removes `dark` class for Tailwind dark mode
5. **Reactive**: Listens to system theme changes when theme is set to 'system'

## Tailwind Configuration

Make sure your Tailwind config uses the `class` or `selector` strategy:

```js
// tailwind.config.js
module.exports = {
darkMode: 'class', // or 'selector'
// ... rest of config
}
```

Or use the attribute selector:

```js
// tailwind.config.js
module.exports = {
darkMode: ['selector', '[data-theme="dark"]'],
// ... rest of config
}
```

## CSS Variables

You can define theme-specific CSS variables:

```css
:root {
--background: white;
--foreground: black;
}

[data-theme="dark"],
.dark {
--background: black;
--foreground: white;
}
```
222 changes: 222 additions & 0 deletions packages/ui/src/components/footer-badges.tsx

Large diffs are not rendered by default.

205 changes: 205 additions & 0 deletions packages/ui/src/components/footer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
import footerData from "../data/footer";
import clsx from "clsx";
import { AnchorHTMLAttributes } from "react";
import { getRedirectableLink, isAbsoluteUrl } from "../lib/is-absolute-url";
import {
Action,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@prisma-docs/eclipse";
import { Logo } from "./navigation-menu";
import { gdpr, hipaa, iso27, soc2 } from "./footer-badges";
import PDPStatus from "./pdp-status";
import { ThemeToggle } from "./theme-toggle";

type ColorType = "indigo" | "teal" | "white" | undefined;

type LinkProps = AnchorHTMLAttributes<HTMLAnchorElement> & {
external?: boolean;
color?: ColorType;
};

const Link = ({ external, color, children, href, ...rest }: LinkProps) => {
const className = clsx(
"text-foreground-neutral-weak text-md font-semibold leading-md flex items-center cursor-pointer font-medium box-border no-underline px-2.5 -ml-2.5 py-1.5 transition-colors hover:bg-background-ppg-strong rounded-square transition-all",
);

if (external || !href || href.startsWith("http") || href.startsWith("#")) {
return (
<a
{...rest}
href={href}
className={className}
target={external ? "_blank" : "_self"}
>
{children}
{external && (
<i className="fa-regular fa-arrow-up-right text-foreground-neutral-weaker text-xs ml-1" />
)}
</a>
);
}

return (
<a {...rest} href={href} className={className}>
{children}
</a>
);
};

interface FooterProps {
className?: string;
style?: any;
color?: ColorType;
darker?: boolean;
hideNewsletter?: boolean;
lightTheme?: boolean;
absoluteLinks?: boolean;
newsletterComponent?: any;
}

const Footer = ({
style,
darker = false,
absoluteLinks = false,
}: FooterProps) => {
return (
<footer className="bg-background-default w-screen overflow-x-hidden overflow-y-visible max-w-full">
<div
className={clsx(
"px-8 pt-[72px] pb-8 md:px-6 md:pt-[46px] md:pb-[100px] max-w-[1288px] mx-auto",
darker && "bg-[#090A15]",
)}
style={style}
>
<div className="font-inter print:hidden relative">
{/* Logo and Social Links Column */}
<div className="mb-8 flex-1 lg:mb-0 w-full top-0 flex justify-between items-center lg:flex-col lg:items-start lg:absolute lg:max-w-fit lg:h-72">
<div className="flex flex-col justify-center">
<div className="text-stroke-neutral-stronger [&>svg]:h-10!">
{Logo}
</div>
</div>
<div className="flex justify-start gap-2 md:max-w-[190px]">
{footerData.socialIcons.map((socialLink: any, idx: number) => (
<a
href={socialLink.url}
target="_blank"
rel="noopener"
key={idx}
aria-label={socialLink.title}
className={clsx(
"text-[1.375rem] transition-colors hover:[&>div]:bg-background-ppg-strong",
)}
>
<Action color="neutral" size="2xl">
<i
className={`fa-brands fa-${socialLink.icon} text-current text-foreground-neutral-weak transition-colors`}
/>
</Action>
</a>
))}
</div>
</div>
{/* Main Grid Row */}
<div className="grid max-sm:gap-8 max-sm:grid-cols-2 grid-cols-[repeat(4,auto)] lg:gap-8 xl:gap-12 relative lg:w-fit ml-auto">
{/* Footer Columns */}
{footerData.footerItems.map((footerItem: any, idx: number) => (
<div
className="flex-1 lg:mb-0 lg:px-2 min-w-40"
key={`footer-${idx}`}
>
<span className="uppercase stretch-display font-mona-sans inline-block font-bold text-base text-foreground-neutral tracking-[0.1em] mt-0 mb-2.5 lg:mb-3">
{footerItem.title}
</span>
{footerItem.links.map((link: any, idx: number) =>
link._type === "footerLinkType" ? (
<Link
key={idx}
href={getRedirectableLink(link.url, absoluteLinks)}
external={isAbsoluteUrl(link.url)}
referrerPolicy={`${link.url ? "no-referrer" : ""}`}
>
{link.title}
</Link>
) : (
<DropdownMenu key={idx}>
<DropdownMenuTrigger className="focus-visible:outline-none">
<span className="text-foreground-neutral-weak text-lg flex cursor-pointer font-medium box-border no-underline leading-[1.39] px-8 -ml-8 py-1.5 transition-colors relative w-max items-center hover:bg-background-ppg-strong rounded-square transition-all">
{link.title}
<i className="fa-regular fa-chevron-down text-foreground-neutral-weaker ml-2 text-base text-inherit" />
</span>
</DropdownMenuTrigger>
<DropdownMenuContent>
{link.links.map(
(dropLink: { title: string; url: string }) => (
<DropdownMenuItem
key={dropLink.title}
className="hover:bg-background-ppg-strong!"
>
<a
href={dropLink.url}
target="_blank"
className="text-left capitalize text-foreground-neutral-weak text-md font-semibold"
>
{dropLink.title}
</a>
</DropdownMenuItem>
),
)}
</DropdownMenuContent>
</DropdownMenu>
),
)}
</div>
))}
</div>
<div className="h-px w-full bg-stroke-neutral-weak my-6" />
{/* Compliance Footer */}
<div className="gap-6 md:items-center justify-between flex w-full md:pb-0 pb-11">
<PDPStatus className="justify-start" />
<div className="md:max-w-78 flex justify-between w-full items-center absolute md:relative bottom-0 left-0">
<a
href="https://trust.prisma.io/"
target="__blank"
rel="noopener noreferrer"
aria-label="Prisma Trust"
>
{gdpr}
</a>
<a
href="https://trust.prisma.io/"
target="__blank"
rel="noopener noreferrer"
aria-label="Prisma Trust"
>
{hipaa}
</a>
<a
href="https://trust.prisma.io/"
target="__blank"
rel="noopener noreferrer"
aria-label="Prisma Trust"
>
{iso27}
</a>
<a
href="https://trust.prisma.io/"
target="__blank"
rel="noopener noreferrer"
aria-label="Prisma Trust"
>
{soc2}
</a>
</div>
<ThemeToggle mode="light-dark-system" className="w-fit" />
</div>
</div>
</div>
</footer>
);
};

export { Footer };
Loading
Loading