diff --git a/.codesandbox/ci.json b/.codesandbox/ci.json index 29b9c5891..12f6ddf24 100644 --- a/.codesandbox/ci.json +++ b/.codesandbox/ci.json @@ -5,7 +5,9 @@ "packages": [ "packages/pigment-css-core", "packages/pigment-css-nextjs-plugin", + "packages/pigment-css-plugin", "packages/pigment-css-react", + "packages/pigment-css-react-new", "packages/pigment-css-theme", "packages/pigment-css-unplugin", "packages/pigment-css-utils", @@ -14,7 +16,9 @@ "sandboxes": [ "/examples/pigment-css-nextjs-ts", "/examples/pigment-css-remix-ts", - "/examples/pigment-css-vite-ts" + "/examples/pigment-css-vite-ts", + "/v1-examples/pigment-css-v1-nextjs", + "/v1-examples/pigment-css-v1-vite" ], "silent": true } diff --git a/.eslintignore b/.eslintignore index 1a9209a1e..c784bf3cf 100644 --- a/.eslintignore +++ b/.eslintignore @@ -11,7 +11,10 @@ /packages/pigment-css-react/tests/**/fixtures /packages/pigment-css-core/exports/ /packages/pigment-css-core/tests/**/fixtures +/packages/pigment-css-react-new/exports/ +/packages/pigment-css-react-new/tests/**/fixtures /packages/pigment-css-nextjs-plugin/loader.js +/packages/pigment-css-plugin/exports/ # Ignore fixtures /packages-internal/scripts/typescript-to-proptypes/test/*/* /test/bundling/fixtures/**/*.fixture.js @@ -35,3 +38,5 @@ pnpm-lock.yaml # If we want to format these files we'd need to do it in crowdin docs/**/*-pt.md docs/**/*-zh.md +/tmpapps +/v1-examples diff --git a/.eslintrc.js b/.eslintrc.js index d36b0c245..f238ae40f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -57,12 +57,6 @@ module.exports = { // Airbnb use warn https://github.com/airbnb/javascript/blob/63098cbb6c05376dbefc9a91351f5727540c1ce1/packages/eslint-config-airbnb-base/rules/style.js#L97 // but eslint recommands error 'func-names': 'error', - 'no-restricted-imports': [ - 'error', - { - patterns: ['@mui/*/*/*'], - }, - ], 'no-continue': 'off', 'no-constant-condition': 'error', // Use the proptype inheritance chain @@ -83,6 +77,8 @@ module.exports = { ], 'no-use-before-define': 'off', + 'react/react-in-jsx-scope': 'off', + // disabled type-aware linting due to performance considerations '@typescript-eslint/dot-notation': 'off', 'dot-notation': 'error', @@ -201,6 +197,7 @@ module.exports = { 'react/jsx-no-useless-fragment': ['error', { allowExpressions: true }], 'lines-around-directive': 'off', + 'react/prop-types': 'off', }, overrides: [ { @@ -281,7 +278,7 @@ module.exports = { }, // Next.js entry points pages { - files: ['docs/pages/**/*.js'], + files: ['docs/apps/**/*{.tsx,.ts,.js}'], rules: { 'react/prop-types': 'off', }, diff --git a/.npmrc b/.npmrc index 8e012302a..4ee4951bb 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1,2 @@ enable-pre-post-scripts = true +link-workspace-packages = true \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 9656d60e0..5c60492e8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,5 +9,6 @@ "language": "markdown", "scheme": "file" } - ] + ], + "typescript.tsdk": "node_modules/typescript/lib" } diff --git a/apps/pigment-css-next-app/package.json b/apps/pigment-css-next-app/package.json index a449dde00..2f273273c 100644 --- a/apps/pigment-css-next-app/package.json +++ b/apps/pigment-css-next-app/package.json @@ -15,13 +15,13 @@ "local-ui-lib": "workspace:^", "react": "^19.0.0", "react-dom": "^19.0.0", - "next": "15.1.3" + "next": "15.1.6" }, "devDependencies": { "@pigment-css/nextjs-plugin": "workspace:^", - "@types/node": "^18.19.63", - "@types/react": "^19.0.2", - "@types/react-dom": "^19.0.2", + "@types/node": "^20", + "@types/react": "^19.0.8", + "@types/react-dom": "^19.0.3", "eslint": "^8.57.0", "typescript": "^5.4.4" }, diff --git a/apps/pigment-css-vite-app/package.json b/apps/pigment-css-vite-app/package.json index 65e81ecaa..47b4ca541 100644 --- a/apps/pigment-css-vite-app/package.json +++ b/apps/pigment-css-vite-app/package.json @@ -30,11 +30,11 @@ "@pigment-css/vite-plugin": "workspace:^", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", - "@vitejs/plugin-react": "^4.3.3", + "@vitejs/plugin-react": "^4.3.4", "postcss": "^8.4.47", "postcss-combine-media-query": "^1.0.1", - "vite": "5.4.10", - "vite-plugin-pages": "^0.32.3" + "vite": "6.2.1", + "vite-plugin-pages": "^0.32.5" }, "nx": { "targets": { diff --git a/docs/.env b/docs/.env index f302767e2..c3162dc23 100644 --- a/docs/.env +++ b/docs/.env @@ -1,3 +1,4 @@ -REPO_ROOT=https://github.com/mui/pigment-css -DEFAULT_BRANCH=master - +APP_NAME=Pigment CSS +GITHUB=https://github.com/mui/pigment-css +NPM=https://www.npmjs.com/package/@pigment-css/core +WEBSITE=https://pigment-css.com diff --git a/docs/.eslintrc.js b/docs/.eslintrc.js deleted file mode 100644 index 959aad4c4..000000000 --- a/docs/.eslintrc.js +++ /dev/null @@ -1,18 +0,0 @@ -module.exports = { - rules: { - 'react/prop-types': 'off', - 'react/react-in-jsx-scope': 'off', - 'react/no-unknown-property': ['error', { ignore: ['sx'] }], - 'import/extensions': [ - 'error', - 'ignorePackages', - { - '': 'never', - js: 'never', - jsx: 'never', - ts: 'never', - tsx: 'never', - }, - ], - }, -}; diff --git a/docs/.gitignore b/docs/.gitignore index f36e2cb9d..70a084a3a 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -28,6 +28,10 @@ npm-debug.log* yarn-debug.log* yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* # vercel .vercel @@ -35,3 +39,4 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts +export diff --git a/docs/README.md b/docs/README.md index a8d867734..f15b9c46d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,4 +1,15 @@ -# Pigment CSS Docs app +# Pigment CSS Docs -This is the Pigment CSS docs application bootstrapped with Next.js 15. It uses App Router and -Pigment CSS for styling. +This is a Next.js based app that uses the app router and exports a static build that is hosted on Netlify. + +It uses Pigment CSS itself for all the styling. The Pigment CSS config in the `next.config.ts` file includes an `include` filter to only transform files ending in `pigment.ts` or `pigment.tsx`. So any `styled` or `css` call should be done in a file ending with the same extension. Rest of the files won't go through Pigment CSS's transform making it comparatively faster than trying to transform all the files, even if they don't contain anything related to Pigment CSS. + +`pigment.tsx` has also been added to the `pageExtensions` option in the Next.js config so that we can write our layout/page files directly and that'll be part of Next.js routes. + +All the navigation items in the left sidebar of the docs is part of [src/nav.ts](./src/nav.ts) file. This also decides the pages that'll be generated during build. + +## Adding a new page + +To add a new mdx page, identify the correct category (directory) in the [content](./src/content/) directory and create a new mdx file. Make sure to add an appropriate navigation item in [src/nav.ts](./src/nav.ts) at the desired place. The sidebar should be updated now with the new item. Navigate to the new page. + +Any navigation item or parent category can be marked as `draft` by setting `draft: true`. This makes sure that the marked nav items and the corresponding pages are only rendered during development and won't be part of the final production build. diff --git a/docs/data/getting-started/overview/overview.mdx b/docs/data/getting-started/overview/overview.mdx deleted file mode 100644 index e0ac64163..000000000 --- a/docs/data/getting-started/overview/overview.mdx +++ /dev/null @@ -1,12 +0,0 @@ ---- -title: Quick start -description: Get started with Pigment CSS, a zero-runtime CSS-in-JS library. ---- - -# Quick start - - - -## Installation - -Pigment CSS has two category of packages. First one is to be imported and used in your source code. This includes the public API of the package. Second category is to be imported in your bundler config file to configure and actually be able to use Pigment CSS. We currently support [Next.js](https://nextjs.org/) (no Turbopack yet), [Vite](https://vite.dev/) and [webpack](https://webpack.js.org/). diff --git a/docs/data/pages.ts b/docs/data/pages.ts deleted file mode 100644 index 8b8a1435b..000000000 --- a/docs/data/pages.ts +++ /dev/null @@ -1,36 +0,0 @@ -export interface RouteMetadata { - pathname: string; - title?: string; - children?: readonly RouteMetadata[]; - planned?: boolean; - unstable?: boolean; -} - -const pages: readonly RouteMetadata[] = [ - { - pathname: '/getting-started', - title: 'Getting started', - children: [{ pathname: '/getting-started/overview', title: 'Overview' }], - }, -]; - -export default pages; - -function extractSlug(pathname: string) { - return pathname.split('/').pop()!; -} - -export function getSlugs(parentPath: string) { - const slugs: string[] = []; - - const categoryPages = pages.find((page) => page.pathname === parentPath); - categoryPages?.children?.forEach((level2Page) => { - if (level2Page.children) { - slugs.push(...level2Page.children.map((page) => extractSlug(page.pathname))); - } else { - slugs.push(extractSlug(level2Page.pathname)); - } - }); - - return slugs; -} diff --git a/docs/eslint.config.mjs b/docs/eslint.config.mjs new file mode 100644 index 000000000..5b67bd0ac --- /dev/null +++ b/docs/eslint.config.mjs @@ -0,0 +1,14 @@ +import { dirname } from 'path'; +import { fileURLToPath } from 'url'; +import { FlatCompat } from '@eslint/eslintrc'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const compat = new FlatCompat({ + baseDirectory: __dirname, +}); + +const eslintConfig = [...compat.extends('next/core-web-vitals', 'next/typescript')]; + +export default eslintConfig; diff --git a/docs/globals.d.ts b/docs/globals.d.ts deleted file mode 100644 index e7c116d41..000000000 --- a/docs/globals.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -declare global { - namespace NodeJS { - interface ProcessEnv { - NODE_ENV: string; - REPO_ROOT: string; - DATA_DIR: string; - DEFAULT_BRANCH: string; - } - } -} diff --git a/docs/next.config.ts b/docs/next.config.ts index 7a9997146..cd61fe2dd 100644 --- a/docs/next.config.ts +++ b/docs/next.config.ts @@ -1,54 +1,50 @@ -import * as url from 'url'; -import * as path from 'path'; +import * as path from 'node:path'; import type { NextConfig } from 'next'; -// @ts-ignore -// eslint-disable-next-line no-restricted-imports +import withPigment, { type PigmentCSSConfig } from '@pigment-css/plugin/nextjs'; +// @ts-expect-error This file doesn't have TS definitions. import withDocsInfra from '@mui/monorepo/docs/nextConfigDocsInfra.js'; -import { withPigment, extendTheme } from '@pigment-css/nextjs-plugin'; -import { theme as baseTheme } from './src/theme'; +import theme, { THEME_DARK } from './src/theme'; + import rootPackage from '../package.json'; -const currentDirectory = url.fileURLToPath(new URL('.', import.meta.url)); -const DATA_DIR = path.join(currentDirectory, 'data'); +const isProd = process.env.NODE_ENV === 'production'; +const CONTENT_DIR = path.join(__dirname, 'src', 'content'); const nextConfig: NextConfig = { + pageExtensions: ['tsx', 'pigment.tsx'], trailingSlash: false, env: { - DATA_DIR, - CURRENT_VERSION: rootPackage.version, - }, - distDir: 'export', - output: process.env.NODE_ENV === 'production' ? 'export' : undefined, - eslint: { - ignoreDuringBuilds: true, - }, - devIndicators: { - buildActivity: true, - buildActivityPosition: 'bottom-right', - appIsrStatus: false, + LIB_VERSION: rootPackage.version, + CONTENT_DIR, + CHANGELOG_FILE: path.join(__dirname, '../CHANGELOG.md'), }, + ...(isProd && { distDir: 'export', output: 'export' }), experimental: { esmExternals: true, workerThreads: false, - turbo: undefined, + useLightningcss: true, }, }; -const theme = extendTheme({ - colorSchemes: { - light: baseTheme, +const pigmentConfig: PigmentCSSConfig = { + theme: { + colorSchemes: { + light: theme, + dark: THEME_DARK, + }, + defaultScheme: 'light', + getSelector(mode) { + if (mode === 'light') { + return ':root, [data-theme="light"]'; + } + return `[data-theme="${mode}"]`; + }, }, -}); + transformSx: false, + displayName: !isProd, + sourceMap: !isProd, + include: /\.pigment\.tsx?$/, +}; -export default withPigment(withDocsInfra(nextConfig), { - theme, - displayName: true, - sourceMap: process.env.NODE_ENV !== 'production', - babelOptions: { - plugins: [ - '@babel/plugin-proposal-explicit-resource-management', - '@babel/plugin-transform-unicode-property-regex', - ], - }, -}); +export default withPigment(withDocsInfra(nextConfig), pigmentConfig); diff --git a/docs/package.json b/docs/package.json index 983e83f5e..59463ea85 100644 --- a/docs/package.json +++ b/docs/package.json @@ -1,43 +1,49 @@ { "name": "docs", - "version": "0.1.0", + "version": "0.0.0", "private": true, + "type": "module", "scripts": { + "clean": "rm -rf .next export", "dev": "next dev", - "build": "cross-env NODE_ENV=production next build", - "preview": "serve ./export", - "lint": "next lint", - "typescript": "tsc --noEmit -p ." + "build": "next build", + "start": "pnpm dlx serve export", + "lint": "next lint" }, "dependencies": { - "@base_ui/react": "^1.0.0-alpha.3", + "@base-ui-components/react": "^1.0.0-alpha.6", "@mdx-js/mdx": "^3.1.0", - "@pigment-css/react": "workspace:*", - "@stefanprobst/rehype-extract-toc": "^2.2.0", + "@pigment-css/react-new": "workspace:*", + "@stefanprobst/rehype-extract-toc": "^2.2.1", + "clipboard-copy": "4.0.1", "clsx": "^2.1.1", - "next": "15.0.2", - "react": "18.3.1", - "react-dom": "18.3.1", - "rehype-pretty-code": "0.14.0", + "estree-util-value-to-estree": "^3.3.2", + "hast-util-to-string": "^3.0.1", + "lucide-react": "^0.479.0", + "next": "15.2.3", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "rehype-autolink-headings": "^7.1.0", + "rehype-highlight": "^7.0.2", + "rehype-pretty-code": "^0.14.0", "rehype-slug": "^6.0.0", - "remark-frontmatter": "^5.0.0", - "remark-gfm": "^4.0.0", - "remark-mdx-frontmatter": "^5.0.0", - "shiki": "^1.22.2", - "to-vfile": "^8.0.0", - "vfile-matter": "^5.0.0" + "remark-gfm": "^4.0.1", + "remark-typography": "^0.6.21", + "scroll-into-view-if-needed": "^3.1.0", + "shiki": "^3.1.0", + "unist-util-visit-parents": "^6.0.1" }, "devDependencies": { - "@babel/plugin-proposal-explicit-resource-management": "^7.25.9", - "@babel/plugin-transform-unicode-property-regex": "^7.25.9", - "@mui/monorepo": "github:mui/material-ui#ae455647016fe5dee968b017aa191e176bc113dd", - "@pigment-css/nextjs-plugin": "workspace:*", + "@eslint/eslintrc": "^3", + "@mui/monorepo": "github:mui/material-ui#v6.4.7", + "@pigment-css/plugin": "workspace:*", + "@types/mdx": "^2.0.13", "@types/node": "^20", - "@types/react": "^18", - "@types/react-dom": "^18", - "eslint-config-next": "15.0.2", - "serve": "14.2.4", - "tailwindcss": "^3.4.14" + "@types/react": "^19.0.8", + "@types/react-dom": "^19.0.3", + "eslint": "^9", + "eslint-config-next": "15.2.3", + "typescript": "^5" }, "nx": { "targets": { diff --git a/docs/public/_headers b/docs/public/_headers new file mode 100644 index 000000000..c033664c2 --- /dev/null +++ b/docs/public/_headers @@ -0,0 +1,24 @@ +/_next/*.js + Cache-Control: public, max-age=31536000, immutable + +/static/*.ico + Content-Type: image/x-icon + +/performance/* + X-Robots-Tag: noindex + +/experiments/* + X-Robots-Tag: noindex + +/* + Strict-Transport-Security: max-age=31536000; includeSubDomains; preload + # Block usage in iframes. + X-Frame-Options: SAMEORIGIN + # Force the browser to trust the Content-Type header + # https://stackoverflow.com/questions/18337630/what-is-x-content-type-options-nosniff + X-Content-Type-Options: nosniff + X-XSS-Protection: 1; mode=block + Referrer-Policy: strict-origin-when-cross-origin + # TODO: progressively reduce the CSP scopes + # Start with a wildcard, using https://github.com/mui/toolpad/blob/f4c4eb046b352e4fc00729c3bed605e671b040c4/packages/toolpad-studio/src/server/index.ts#L241 + Content-Security-Policy: default-src * data: mediastream: blob: filesystem: about: ws: wss: 'unsafe-eval' 'wasm-unsafe-eval' 'unsafe-inline'; script-src * data: blob: 'unsafe-inline' 'unsafe-eval'; script-src-elem * data: blob: 'unsafe-inline'; connect-src * data: blob: 'unsafe-inline'; img-src * data: blob: 'unsafe-inline'; media-src * data: blob: 'unsafe-inline'; frame-src * data: blob: ; style-src * data: blob: 'unsafe-inline'; font-src * data: blob: 'unsafe-inline'; frame-ancestors *; diff --git a/docs/public/_redirects b/docs/public/_redirects new file mode 100644 index 000000000..b247d8e41 --- /dev/null +++ b/docs/public/_redirects @@ -0,0 +1,3 @@ +# For links that we can't edit later on, for example hosted in the code published on npm +/r/discord https://discord.gg/ZaAFvR9H 302 + diff --git a/docs/public/robots.txt b/docs/public/robots.txt new file mode 100644 index 000000000..8eb289a7a --- /dev/null +++ b/docs/public/robots.txt @@ -0,0 +1,4 @@ +# Algolia-Crawler-Verif: 98C49CAFF7AEED76 + +User-agent: * +Allow: / diff --git a/docs/public/static/apple-touch-icon.png b/docs/public/static/apple-touch-icon.png new file mode 100644 index 000000000..d321ff77e Binary files /dev/null and b/docs/public/static/apple-touch-icon.png differ diff --git a/docs/public/static/favicon-dev.ico b/docs/public/static/favicon-dev.ico new file mode 100644 index 000000000..ecc83d6ad Binary files /dev/null and b/docs/public/static/favicon-dev.ico differ diff --git a/docs/public/static/favicon-dev.svg b/docs/public/static/favicon-dev.svg new file mode 100644 index 000000000..7281b26ae --- /dev/null +++ b/docs/public/static/favicon-dev.svg @@ -0,0 +1,4 @@ + + + + diff --git a/docs/public/static/favicon.ico b/docs/public/static/favicon.ico new file mode 100644 index 000000000..bfe641239 Binary files /dev/null and b/docs/public/static/favicon.ico differ diff --git a/docs/public/static/favicon.svg b/docs/public/static/favicon.svg new file mode 100644 index 000000000..e0acedab3 --- /dev/null +++ b/docs/public/static/favicon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/docs/public/static/logo-dev.svg b/docs/public/static/logo-dev.svg new file mode 100644 index 000000000..908a98abb --- /dev/null +++ b/docs/public/static/logo-dev.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/docs/public/static/logo.png b/docs/public/static/logo.png new file mode 100644 index 000000000..80b66418e Binary files /dev/null and b/docs/public/static/logo.png differ diff --git a/docs/public/static/logo.svg b/docs/public/static/logo.svg new file mode 100644 index 000000000..50da83666 --- /dev/null +++ b/docs/public/static/logo.svg @@ -0,0 +1,4 @@ + + + + diff --git a/docs/src/app/(content)/getting-started/[slug]/not-found.tsx b/docs/src/app/(content)/getting-started/[slug]/not-found.tsx deleted file mode 100644 index 7e283ad66..000000000 --- a/docs/src/app/(content)/getting-started/[slug]/not-found.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import * as React from 'react'; -import { MainContent, MainContentContainer } from '@/components/MainContent'; -import { Description } from '@/components/mdx/Description'; - -export default function NotFoundPage() { - return ( - - -

Page not found

- - Apologies, but the page you were looking for wasn't found. Try reaching for the - search button on the nav bar above to look for another one. - -
-
- ); -} diff --git a/docs/src/app/(content)/getting-started/[slug]/page.tsx b/docs/src/app/(content)/getting-started/[slug]/page.tsx deleted file mode 100644 index bfd32dd6b..000000000 --- a/docs/src/app/(content)/getting-started/[slug]/page.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import * as React from 'react'; -import { notFound } from 'next/navigation'; -import { Metadata } from 'next'; -import routes, { getSlugs } from '@data/pages'; -import { EditPageOnGithub } from '@/components/EditPageOnGithub'; -import { SiblingPageLinks } from '@/components/SiblingPageLinks'; -import { TableOfContents } from '@/components/TableOfContents'; -import { Description } from '@/components/mdx/Description'; -import { components } from '@/components/mdx/MDXComponents'; -import { MainContentContainer, MainContent } from '@/components/MainContent'; - -interface Props { - params: Promise<{ slug: string }>; -} - -const SEGMENT = 'getting-started'; - -export default async function GettingStartedPage(props: Props) { - const { slug } = await props.params; - const { getMarkdownPage } = await import('@/utils/getMarkdownPage'); - try { - const { isMd, MDXContent, metadata, tableOfContents } = await getMarkdownPage(SEGMENT, slug); - const allComponents = { - ...components, - Description: () => , - Demo: () => null, - }; - - return ( - - - - - -
-
- -
-
-
- -
-
-
- -
- ); - } catch (ex) { - if ((ex as Error).message === '404') { - return notFound(); - } - throw ex; - } -} - -export function generateStaticParams() { - return getSlugs(`/${SEGMENT}`).map((slug) => ({ slug })); -} - -export async function generateMetadata({ params }: Props): Promise { - const { getMarkdownPageMetadata } = await import('@/utils/getMarkdownPage'); - const { slug } = await params; - const { title = 'Getting started', description } = await getMarkdownPageMetadata(SEGMENT, slug); - - return { - title: { - absolute: title, - template: '%s | Pigment CSS', - }, - description, - twitter: { - title, - description, - }, - openGraph: { - title, - description, - }, - }; -} diff --git a/docs/src/app/(content)/layout.tsx b/docs/src/app/(content)/layout.tsx deleted file mode 100644 index 388864a84..000000000 --- a/docs/src/app/(content)/layout.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import routes from '@data/pages'; -import { AppBar } from '@/components/AppBar'; -import { Navigation } from '@/components/Navigation'; - -export default function ContentLayout({ children }: React.PropsWithChildren<{}>) { - return ( -
- -
- - {children} -
-
- ); -} diff --git a/docs/src/app/(public)/(content)/[contentDir]/[slug]/page.tsx b/docs/src/app/(public)/(content)/[contentDir]/[slug]/page.tsx new file mode 100644 index 000000000..5bdb0ce2d --- /dev/null +++ b/docs/src/app/(public)/(content)/[contentDir]/[slug]/page.tsx @@ -0,0 +1,31 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import ContentPage from 'docs/components/ContentPage'; +import { filteredNav } from 'docs/nav'; + +export default async function GettingStarted({ + params, +}: { + params: Promise<{ slug: string; contentDir: string }>; +}) { + const { contentDir, slug } = await params; + return ( +
+ +
+ ); +} + +export function generateStaticParams() { + const items = filteredNav + .flatMap((section) => + section.links.map((link) => ({ + slug: link.href.split('/').pop() as string, + contentDir: section.dirname, + })), + ) + .filter(({ contentDir, slug }) => + fs.existsSync(path.join(process.env.CONTENT_DIR, contentDir, `${slug}.mdx`)), + ); + return items; +} diff --git a/docs/src/app/(public)/(content)/layout.pigment.tsx b/docs/src/app/(public)/(content)/layout.pigment.tsx new file mode 100644 index 000000000..b5b0c741b --- /dev/null +++ b/docs/src/app/(public)/(content)/layout.pigment.tsx @@ -0,0 +1,101 @@ +import * as React from 'react'; +import type { Metadata, Viewport } from 'next/types'; +import { styled, t } from '@pigment-css/react-new'; + +import { Header } from 'docs/components/Header'; +import * as SideNav from 'docs/components/SideNav'; +import { filteredNav } from 'docs/nav'; +import * as QuickNav from 'docs/components/QuickNav'; + +const Root = styled.div(({ theme }) => ({ + $sidebarWidth: '17.5rem', + display: 'grid', + alignItems: 'start', + paddingTop: t('$header.height'), + paddingInline: '1.5rem', + gridTemplateColumns: '1fr', + [theme.breakpoints.gt('sm')]: { + paddingInline: '2.5rem', + }, + [theme.breakpoints.gt('lg')]: { + paddingTop: 0, + paddingInline: 0, + gridTemplateColumns: '$sidebarWidth 1fr 3rem', + }, + [theme.breakpoints.gt('quickNav')]: { + gridTemplateColumns: '$sidebarWidth 1fr $sidebarWidth', + }, +})); + +const Main = styled.div(({ theme }) => ({ + minWidth: 0, + maxWidth: '48rem', + width: '100%', + paddingTop: '1.5rem', + paddingBottom: '5rem', + margin: '0 auto', + [theme.breakpoints.gt('sm')]: { + paddingTop: '2rem', + }, + [theme.breakpoints.gt('lg')]: { + margin: 0, + }, + [theme.breakpoints.gt('quickNav')]: { + margin: 0, + }, +})); + +export default function Layout({ children }: React.PropsWithChildren) { + return ( + +
+ + {filteredNav.map((section) => ( + + {section.label} + + {section.links.map((link) => ( + + {link.label} + + ))} + + + ))} + +
+ {children} +
+ + ); +} + +// Title and description are pulled from

and in the MDX. +export const metadata: Metadata = { + title: null, + description: null, +}; + +export const viewport: Viewport = { + themeColor: [ + // Desktop Safari header background + { + media: '(prefers-color-scheme: light) and (min-width: 1024px)', + color: 'oklch(95% 0.25% 264)', + }, + { + media: '(prefers-color-scheme: dark) and (min-width: 1024px)', + color: 'oklch(25% 1% 264)', + }, + + // Mobile Safari header background (match the site header) + { + media: '(prefers-color-scheme: light)', + color: 'oklch(98% 0.25% 264)', + }, + { + media: '(prefers-color-scheme: dark)', + color: 'oklch(17% 1% 264)', + }, + ], +}; diff --git a/docs/src/app/(public)/layout.pigment.tsx b/docs/src/app/(public)/layout.pigment.tsx new file mode 100644 index 000000000..8c80e6ae9 --- /dev/null +++ b/docs/src/app/(public)/layout.pigment.tsx @@ -0,0 +1,81 @@ +import * as React from 'react'; +import type { Metadata } from 'next/types'; +import { styled } from '@pigment-css/react-new'; + +const Root = styled.div(({ theme }) => ({ + $rootLayoutPaddingX: '0rem', + isolation: 'isolate', + zIndex: 0, + position: 'relative', + paddingInline: '$rootLayoutPaddingX', + [theme.breakpoints.gt('lg')]: { + $rootLayoutPaddingX: '3rem', + + '&::before, &::after': { + content: '""', + position: 'absolute', + backgroundColor: theme.color.gridline, + height: 1, + right: 0, + left: 0, + }, + '&::before': { + top: theme.header.height, + marginTop: -1, + }, + '&::after': { + bottom: theme.header.height, + marginBottom: -1, + }, + }, +})); + +const Container = styled.div(({ theme }) => ({ + position: 'relative', + display: 'flex', + flexDirection: 'column', + marginInline: 'auto', + minHeight: '100dvh', + maxWidth: `calc(${theme.breakpoint.maxLayoutWidth} - $rootLayoutPaddingX * 2)`, + [theme.breakpoints.gt('lg')]: { + paddingBlock: theme.header.height, + + '&::before, &::after': { + content: '""', + position: 'absolute', + top: 0, + bottom: 0, + width: 1, + backgroundColor: theme.color.gridline, + }, + '&::before': { + left: 0, + marginLeft: -1, + }, + '&::after': { + right: 0, + marginRight: -1, + }, + }, +})); + +const Content = styled.div({ + display: 'flex', + flexGrow: 1, + flexDirection: 'column', + backgroundColor: '$color.content', +}); + +export default function Layout({ children }: React.PropsWithChildren) { + return ( + + + {children} + + + ); +} + +export const metadata: Metadata = { + metadataBase: new URL('https://pigment-css.com'), +}; diff --git a/docs/src/app/(public)/page.pigment.tsx b/docs/src/app/(public)/page.pigment.tsx new file mode 100644 index 000000000..acfc121bb --- /dev/null +++ b/docs/src/app/(public)/page.pigment.tsx @@ -0,0 +1,115 @@ +import { Metadata, Viewport } from 'next'; +import Image from 'next/image'; +import { css, styled, t } from '@pigment-css/react-new'; + +import { applyText, spacing } from 'docs/utils/theme'; +import { Link } from 'docs/components/Link'; +import { ArrowRightIcon } from 'docs/icons/ArrowRightIcon'; +import { nav } from 'docs/nav'; + +export const Root = styled('div')({ + display: 'flex', + flexDirection: 'column', + flexGrow: 1, + paddingTop: '3rem', + paddingBottom: 'calc(3rem + 5vh)', + paddingInline: '2rem', + alignItems: 'center', + justifyContent: 'center', +}); + +export const Content = styled('div')({ + width: '100%', + maxWidth: '25rem', + textWrap: 'balance', +}); + +export const Heading = styled('h1')(({ theme }) => ({ + ...applyText(theme, 'xl'), + fontWeight: 500, + marginBottom: '0.5rem', +})); + +export const Caption = styled.p` + color: ${t('$color.gray.600')}; + margin-bottom: 2rem; +`; + +export const linkStyle = css( + ({ theme }) => ` + display: inline-flex; + align-items: center; + padding: ${spacing(theme, 1)}; + gap: ${spacing(theme, 1)}; + margin: ${spacing(theme, -1)}; + + [dir="rtl"] & .arrow-icon { + transform: rotate(180deg); + } +`, +); + +const logoStyle = css(({ theme }) => ({ + width: 17, + height: 28, + marginBottom: spacing(theme, 8), + marginLeft: 1, +})); + +const description = 'A Zero runtime CSS-in-JS styling engine.'; + +export default function HomePage() { + return ( + + + {`${process.env.APP_NAME} + {process.env.APP_NAME} + {description} + + Documentation + + + + ); +} + +export const metadata: Metadata = { + description, + twitter: { + description, + }, + openGraph: { + description, + }, +}; + +export const viewport: Viewport = { + themeColor: [ + // Desktop Safari page background + { + media: '(prefers-color-scheme: light) and (min-width: 1024px)', + color: 'oklch(95% 0.25% 264)', + }, + { + media: '(prefers-color-scheme: dark) and (min-width: 1024px)', + color: 'oklch(25% 1% 264)', + }, + + // Mobile Safari header background (match the page) + { + media: '(prefers-color-scheme: light)', + color: '#FFF', + }, + { + media: '(prefers-color-scheme: dark)', + color: '#000', + }, + ], +}; diff --git a/docs/src/app/favicon.ico b/docs/src/app/favicon.ico deleted file mode 100644 index 94a1f8d7f..000000000 Binary files a/docs/src/app/favicon.ico and /dev/null differ diff --git a/docs/src/app/fonts/GeistMonoVF.woff b/docs/src/app/fonts/GeistMonoVF.woff deleted file mode 100644 index f2ae185cb..000000000 Binary files a/docs/src/app/fonts/GeistMonoVF.woff and /dev/null differ diff --git a/docs/src/app/fonts/GeistVF.woff b/docs/src/app/fonts/GeistVF.woff deleted file mode 100644 index 1b62daacf..000000000 Binary files a/docs/src/app/fonts/GeistVF.woff and /dev/null differ diff --git a/docs/src/app/globals.css b/docs/src/app/globals.css index 86193b45e..2579a6fea 100644 --- a/docs/src/app/globals.css +++ b/docs/src/app/globals.css @@ -1,53 +1,429 @@ -html, -body { - max-width: 100vw; - overflow-x: hidden; -} +@layer pigment.globals { + /* + 1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) + 2. Remove default margins and padding + 3. Reset all borders. +*/ -::selection { - background: var(--gray-container-3); -} + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + box-sizing: border-box; /* 1 */ + margin: 0; /* 2 */ + padding: 0; /* 2 */ + border: 0 solid; /* 3 */ + } -body { - font-family: Arial, Helvetica, sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - padding-top: 49px; -} + /* + 1. Use a consistent sensible line-height in all browsers. + 2. Prevent adjustments of font size after orientation changes in iOS. + 3. Use a more readable tab size. + 4. Use the user's configured `sans` font-family by default. + 5. Use the user's configured `sans` font-feature-settings by default. + 6. Use the user's configured `sans` font-variation-settings by default. + 7. Disable tap highlights on iOS. +*/ -@layer global-reset { - * { - box-sizing: border-box; - padding: 0; - margin: 0; + html, + :host { + line-height: 1.5; /* 1 */ + /* stylelint-disable-next-line property-no-vendor-prefix */ + -webkit-text-size-adjust: 100%; /* 2 */ + tab-size: 4; /* 3 */ + font-family: var( + --default-font-family, + ui-sans-serif, + system-ui, + sans-serif, + 'Apple Color Emoji', + 'Segoe UI Emoji', + 'Segoe UI Symbol', + 'Noto Color Emoji' + ); /* 4 */ + font-feature-settings: var(--default-font-feature-settings, normal); /* 5 */ + font-variation-settings: var(--default-font-variation-settings, normal); /* 6 */ + -webkit-tap-highlight-color: transparent; /* 7 */ + } + + /* + Inherit line-height from `html` so users can set them as a class directly on the `html` element. +*/ + + body { + line-height: inherit; } + /* + 1. Add the correct height in Firefox. + 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) + 3. Reset the default border style to a 1px solid border. +*/ + + hr { + height: 0; /* 1 */ + color: inherit; /* 2 */ + border-top-width: 1px; /* 3 */ + } + + /* + Add the correct text decoration in Chrome, Edge, and Safari. +*/ + + abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; + } + + /* + Remove the default font size and weight for headings. +*/ + + h1, + h2, + h3, + h4, + h5, + h6 { + font-size: inherit; + font-weight: inherit; + } + + /* + Reset links to optimize for opt-in styling instead of opt-out. +*/ + a { color: inherit; - text-decoration: none; + -webkit-text-decoration: inherit; + text-decoration: inherit; } -} -.shiki, -.shiki span, -[data-theme], -[data-theme] span { - color: var(--shiki-light) !important; - background-color: var(--shiki-light-bg) !important; - /* Optional, if you also want font styles */ - font-style: var(--shiki-light-font-style) !important; - font-weight: var(--shiki-light-font-weight) !important; - text-decoration: var(--shiki-light-text-decoration) !important; -} + /* + Add the correct font weight in Edge and Safari. +*/ + + b, + strong { + font-weight: bolder; + } + + /* + 1. Use the user's configured `mono` font-family by default. + 2. Use the user's configured `mono` font-feature-settings by default. + 3. Use the user's configured `mono` font-variation-settings by default. + 4. Correct the odd `em` font sizing in all browsers. +*/ + + code, + kbd, + samp, + pre { + font-family: var( + --default-mono-font-family, + ui-monospace, + SFMono-Regular, + Menlo, + Monaco, + Consolas, + 'Liberation Mono', + 'Courier New', + monospace + ); /* 1 */ + font-feature-settings: var(--default-mono-font-feature-settings, normal); /* 2 */ + font-variation-settings: var(--default-mono-font-variation-settings, normal); /* 3 */ + font-size: 1em; /* 4 */ + } + + /* + Add the correct font size in all browsers. +*/ + + small { + font-size: 80%; + } + + /* + Prevent `sub` and `sup` elements from affecting the line height in all browsers. +*/ + + sub, + sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; + } + + sub { + bottom: -0.25em; + } + + sup { + top: -0.5em; + } + + /* + 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) + 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) + 3. Remove gaps between table borders by default. +*/ + + table { + text-indent: 0; /* 1 */ + border-color: inherit; /* 2 */ + border-collapse: collapse; /* 3 */ + } + + /* + Use the modern Firefox focus style for all focusable elements. +*/ + + :-moz-focusring { + outline: auto; + } + + /* + Add the correct vertical alignment in Chrome and Firefox. +*/ + + progress { + vertical-align: baseline; + } + + /* + Add the correct display in Chrome and Safari. +*/ + + summary { + display: list-item; + } + + /* + Make lists unstyled by default. +*/ + + ol, + ul, + menu { + list-style: none; + } + + /* + 1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) + 2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) + This can trigger a poorly considered lint error in some tools but is included by design. +*/ + + img, + svg, + video, + canvas, + audio, + iframe, + embed, + object { + display: block; /* 1 */ + vertical-align: middle; /* 2 */ + } + + /* + Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) +*/ + + img, + video { + max-width: 100%; + height: auto; + } + + /* + 1. Inherit font styles in all browsers. + 2. Remove border radius in all browsers. + 3. Remove background color in all browsers. + 4. Ensure consistent opacity for disabled states in all browsers. +*/ -html.dark .shiki, -html.dark [data-theme], -html.dark .shiki span, -html.dark [data-theme] span { - color: var(--shiki-dark) !important; - background-color: var(--shiki-dark-bg) !important; - /* Optional, if you also want font styles */ - font-style: var(--shiki-dark-font-style) !important; - font-weight: var(--shiki-dark-font-weight) !important; - text-decoration: var(--shiki-dark-text-decoration) !important; + button, + input, + select, + optgroup, + textarea, + ::file-selector-button { + font: inherit; /* 1 */ + font-feature-settings: inherit; /* 1 */ + font-variation-settings: inherit; /* 1 */ + letter-spacing: inherit; /* 1 */ + color: inherit; /* 1 */ + border-radius: 0; /* 2 */ + background-color: transparent; /* 3 */ + opacity: 1; /* 4 */ + } + + /* + Restore default font weight. +*/ + + :where(select:is([multiple], [size])) optgroup { + font-weight: bolder; + } + + /* + Restore indentation. +*/ + + :where(select:is([multiple], [size])) optgroup option { + padding-inline-start: 20px; + } + + /* + Restore space after button. +*/ + + ::file-selector-button { + margin-inline-end: 4px; + } + + /* + 1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) + 2. Set the default placeholder color to a semi-transparent version of the current text color. +*/ + + ::placeholder { + opacity: 1; /* 1 */ + color: color-mix(in oklab, currentColor 50%, transparent); /* 2 */ + } + + /* + Prevent resizing textareas horizontally by default. +*/ + + textarea { + resize: vertical; + } + + /* + Remove the inner padding in Chrome and Safari on macOS. +*/ + + ::-webkit-search-decoration { + -webkit-appearance: none; + } + + /* + 1. Ensure date/time inputs have the same height when empty in iOS Safari. + 2. Ensure text alignment can be changed on date/time inputs in iOS Safari. +*/ + + ::-webkit-date-and-time-value { + min-height: 1lh; /* 1 */ + text-align: inherit; /* 2 */ + } + + /* + Prevent height from changing on date/time inputs in macOS Safari when the input is set to `display: block`. +*/ + + ::-webkit-datetime-edit { + display: inline-flex; + } + + /* + Remove excess padding from pseudo-elements in date/time inputs to ensure consistent height across browsers. +*/ + + ::-webkit-datetime-edit-fields-wrapper { + padding: 0; + } + + ::-webkit-datetime-edit, + ::-webkit-datetime-edit-year-field, + ::-webkit-datetime-edit-month-field, + ::-webkit-datetime-edit-day-field, + ::-webkit-datetime-edit-hour-field, + ::-webkit-datetime-edit-minute-field, + ::-webkit-datetime-edit-second-field, + ::-webkit-datetime-edit-millisecond-field, + ::-webkit-datetime-edit-meridiem-field { + padding-block: 0; + } + + /* + Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) +*/ + + :-moz-ui-invalid { + box-shadow: none; + } + + /* + Correct the inability to style the border radius in iOS Safari. +*/ + + button, + input:where([type='button'], [type='reset'], [type='submit']), + ::file-selector-button { + appearance: button; + } + + /* + Correct the cursor style of increment and decrement buttons in Safari. +*/ + + ::-webkit-inner-spin-button, + ::-webkit-outer-spin-button { + height: auto; + } + + /* + Make elements with the HTML hidden attribute stay hidden by default. +*/ + + [hidden]:where(:not([hidden='until-found'])) { + display: none !important; + } + + [data-language='css'] { + --syntax-variable: var(--color-navy); + + /* Target CSS property names – ugly but works */ + & [style='color:var(--syntax-constant)']:first-child { + --syntax-constant: var(--color-foreground); + } + } + + /* Collapse most colors to blue for inline code */ + [data-inline], + [data-highlighted-chars] { + --syntax-default: var(--color-blue); + --syntax-entity: var(--color-blue); + --syntax-parameter: var(--color-blue); + --syntax-variable: var(--color-blue); + --syntax-keyword: var(--color-blue); + --syntax-string: var(--color-blue); + --syntax-nullish: var(--color-blue); + } + + /* Recover some of the syntax highlighting colors in tables */ + [data-table-code] { + --syntax-default: var(--color-foreground); + --syntax-entity: var(--color-violet); + --syntax-keyword: var(--color-red); + --syntax-string: var(--color-navy); + --syntax-nullish: var(--color-gray-500); + } + + [data-highlighted-line] { + background-color: var(--color-line-highlight); + } + + [data-highlighted-line-id='strong'] { + background-color: var(--color-line-highlight-strong); + } + + [data-highlighted-chars] { + color: var(--syntax-default); + background-color: var(--color-inline-highlight); + } } diff --git a/docs/src/app/layout.pigment.tsx b/docs/src/app/layout.pigment.tsx new file mode 100644 index 000000000..c492a85b9 --- /dev/null +++ b/docs/src/app/layout.pigment.tsx @@ -0,0 +1,119 @@ +import '@pigment-css/react-new/styles.css'; +import './globals.css'; + +import * as React from 'react'; +import { Metadata, Viewport } from 'next'; +import { Inter } from 'next/font/google'; + +import { css, t } from '@pigment-css/react-new'; + +import favicon from '~assets/favicon.ico'; +import faviconDev from '~assets/favicon-dev.ico'; +import faviconSvg from '~assets/favicon.svg'; +import faviconSvgDev from '~assets/favicon-dev.svg'; +import faviconApple from '~assets/apple-touch-icon.png'; + +const inter = Inter({ + variable: '--font-inter', + subsets: ['latin'], +}); + +const htmlCls = css` + word-break: break-word; + overflow-y: scroll; +`; + +const bodyCls = css` + font-family: system-ui; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + background-color: ${t('$color.background')}; + color: ${t('$color.foreground')}; + min-width: 320px; + line-height: 1.5; + font-synthesis: none; +`; + +export default async function Layout({ children }: React.PropsWithChildren) { + return ( + // suppressHydrationWarning is needed because we immediately modify the html on client + // to update the theme before React hydration happens. + + + + + diff --git a/v1-examples/pigment-css-vite-ts/package.json b/v1-examples/pigment-css-vite-ts/package.json new file mode 100644 index 000000000..b4a179f3f --- /dev/null +++ b/v1-examples/pigment-css-vite-ts/package.json @@ -0,0 +1,31 @@ +{ + "name": "@example/pigment-css-vite-ts", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "start": "vite preview" + }, + "dependencies": { + "@pigment-css/react-new": "*", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@eslint/js": "^9.21.0", + "@pigment-css/plugin": "*", + "@types/react": "^19.0.10", + "@types/react-dom": "^19.0.4", + "@vitejs/plugin-react": "^4.3.4", + "eslint": "^9.21.0", + "eslint-plugin-react-hooks": "^5.1.0", + "eslint-plugin-react-refresh": "^0.4.19", + "globals": "^15.15.0", + "typescript": "~5.7.2", + "typescript-eslint": "^8.24.1", + "vite": "^6.2.0" + } +} diff --git a/v1-examples/pigment-css-vite-ts/public/favicon.svg b/v1-examples/pigment-css-vite-ts/public/favicon.svg new file mode 100644 index 000000000..e0acedab3 --- /dev/null +++ b/v1-examples/pigment-css-vite-ts/public/favicon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/v1-examples/pigment-css-vite-ts/src/App.tsx b/v1-examples/pigment-css-vite-ts/src/App.tsx new file mode 100644 index 000000000..df6cb30d9 --- /dev/null +++ b/v1-examples/pigment-css-vite-ts/src/App.tsx @@ -0,0 +1,262 @@ +import { styled, keyframes } from '@pigment-css/react-new'; +import { ThemeSelector } from './ThemeSelector'; + +const gradientAnimation = keyframes({ + '0%': { + backgroundPosition: '0% 50%', + }, + '50%': { + backgroundPosition: '100% 50%', + }, + '100%': { + backgroundPosition: '0% 50%', + }, +}); + +const Container = styled.div(({ theme }) => ({ + width: '100%', + minHeight: '100vh', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + background: theme.colors.background.gradient, + backgroundSize: '400% 400%', + color: 'white', + padding: '2rem 0', + [theme.utils.reducedMotion('no-preference')]: { + animation: `${gradientAnimation} 15s ease infinite`, + }, + [theme.breakpoints.gt('md')]: { + padding: '1rem 0', + }, +})); + +const Header = styled.header(({ theme }) => ({ + width: '100%', + maxWidth: '1200px', + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + padding: '1rem', + borderRadius: '1rem', + marginBottom: '4rem', + backgroundColor: theme.colors.background.glass, + backdropFilter: 'blur(12px)', + boxShadow: `0 4px 30px ${theme.effects.glass.shadow}`, + border: `1px solid ${theme.colors.border.glass}`, + [theme.breakpoints.lt('md')]: { + flexDirection: 'column', + gap: '1rem', + textAlign: 'center', + marginBottom: '2rem', + }, +})); + +const Logo = styled.h1(({ theme }) => ({ + margin: 0, + fontSize: '2rem', + fontWeight: 700, + letterSpacing: '-0.05em', + [theme.breakpoints.lt('md')]: { + fontSize: '1.75rem', + }, +})); + +const Hero = styled.main(({ theme }) => ({ + maxWidth: '800px', + textAlign: 'center', + marginTop: '4rem', + padding: '0 1rem', + [theme.breakpoints.gt('md')]: { + marginTop: '2rem', + }, +})); + +const Title = styled.h2(({ theme }) => ({ + fontSize: '4rem', + fontWeight: 800, + marginBottom: '1.5rem', + lineHeight: 1.1, + [theme.breakpoints.gt('md')]: { + fontSize: '3rem', + }, + [theme.breakpoints.gt('sm')]: { + fontSize: '2.5rem', + }, +})); + +const Subtitle = styled.p(({ theme }) => ({ + fontSize: '1.5rem', + opacity: 0.9, + marginBottom: '3rem', + lineHeight: 1.6, + [theme.breakpoints.gt('md')]: { + fontSize: '1.25rem', + marginBottom: '2rem', + }, + [theme.breakpoints.gt('sm')]: { + fontSize: '1.1rem', + }, +})); + +const ButtonGroup = styled.div(({ theme }) => ({ + display: 'flex', + gap: '1rem', + justifyContent: 'center', + [theme.breakpoints.lt('md')]: { + flexDirection: 'column', + alignItems: 'stretch', + gap: '0.75rem', + }, +})); + +const Button = styled.button(({ theme }) => ({ + padding: '1rem 2rem', + fontSize: '1.1rem', + fontWeight: 600, + borderRadius: '0.5rem', + border: 'none', + cursor: 'pointer', + [theme.utils.reducedMotion('no-preference')]: { + transition: 'transform 0.2s ease, opacity 0.2s ease', + '&:hover': { + transform: 'translateY(-2px)', + opacity: 0.9, + }, + }, + [theme.utils.reducedMotion('reduce')]: { + '&:hover': { + opacity: 0.9, + }, + }, + [theme.breakpoints.gt('sm')]: { + padding: '0.875rem 1.5rem', + fontSize: '1rem', + }, + variants: { + variant: { + primary: { + backgroundColor: theme.colors.text.primary, + color: theme.colors.primary, + }, + secondary: { + backgroundColor: 'transparent', + border: `2px solid ${theme.colors.primary}`, + color: theme.colors.text.primary, + }, + }, + }, + defaultVariants: { + variant: 'primary', + }, +})); + +const FeatureGrid = styled.div(({ theme }) => ({ + display: 'grid', + gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))', + gap: '2rem', + width: '100%', + maxWidth: '1200px', + marginTop: '6rem', + padding: '0 1rem', + [theme.breakpoints.lt('md')]: { + marginTop: '4rem', + gap: '1.5rem', + }, + [theme.breakpoints.lt('sm')]: { + marginTop: '3rem', + gap: '1rem', + }, +})); + +const FeatureCard = styled.div(({ theme }) => ({ + color: theme.colors.text.primary, + backgroundColor: theme.colors.background.glass, + backdropFilter: 'blur(12px)', + boxShadow: `0 4px 30px ${theme.effects.glass.shadow}`, + border: `1px solid ${theme.colors.border.glass}`, + borderRadius: '1rem', + padding: '2rem', + textAlign: 'left', + [theme.utils.reducedMotion('no-preference')]: { + transition: 'transform 0.2s ease', + '&:hover': { + transform: 'translateY(-5px)', + }, + }, + [theme.breakpoints.lt('md')]: { + padding: '1.5rem', + }, + [theme.breakpoints.lt('sm')]: { + padding: '1.25rem', + }, +})); + +const FeatureTitle = styled.h3(({ theme }) => ({ + fontSize: '1.5rem', + marginBottom: '1rem', + fontWeight: 600, + [theme.breakpoints.gt('sm')]: { + fontSize: '1.25rem', + }, +})); + +const FeatureDescription = styled.p(({ theme }) => ({ + fontSize: '1.1rem', + opacity: 0.9, + lineHeight: 1.6, + [theme.breakpoints.gt('sm')]: { + fontSize: '1rem', + }, +})); + +function App() { + return ( + +
+ Pigment CSS + +
+ + + Build beautiful interfaces with Pigment CSS + + A modern CSS-in-JS solution that makes styling React components a breeze. Write + maintainable styles with the power of TypeScript. + + + + + + + + + + Type-Safe Styling + + Get complete TypeScript support with autocomplete and type checking for your styles. + + + + Zero Runtime + + Styles are processed at build time, resulting in minimal runtime overhead. + + + + Dynamic Theming + + Create and switch between themes with ease. Full support for dark mode and custom + themes. + + + +
+ ); +} + +export default App; diff --git a/v1-examples/pigment-css-vite-ts/src/ThemeSelector.tsx b/v1-examples/pigment-css-vite-ts/src/ThemeSelector.tsx new file mode 100644 index 000000000..7f963a63c --- /dev/null +++ b/v1-examples/pigment-css-vite-ts/src/ThemeSelector.tsx @@ -0,0 +1,30 @@ +import * as React from 'react'; + +type Mode = 'light' | 'dark' | 'system'; + +export function ThemeSelector() { + const [mode, setMode] = React.useState( + window.localStorage.getItem('mode') as Mode | null, + ); + + const handleChange = (e: React.ChangeEvent) => { + const newMode = e.target.value as Mode; + if (!newMode) { + return; + } + setMode(newMode); + window.localStorage.setItem('mode', newMode); + document.documentElement.dataset.theme = newMode; + }; + + return ( + + ); +} diff --git a/v1-examples/pigment-css-vite-ts/src/augment.d.ts b/v1-examples/pigment-css-vite-ts/src/augment.d.ts new file mode 100644 index 000000000..43ab05eb4 --- /dev/null +++ b/v1-examples/pigment-css-vite-ts/src/augment.d.ts @@ -0,0 +1,6 @@ +import type { Theme as UserTheme } from './theme'; + +declare module '@pigment-css/react-new' { + // eslint-disable-next-line @typescript-eslint/no-empty-object-type + export interface Theme extends UserTheme {} +} diff --git a/v1-examples/pigment-css-vite-ts/src/index.css b/v1-examples/pigment-css-vite-ts/src/index.css new file mode 100644 index 000000000..38646f1ad --- /dev/null +++ b/v1-examples/pigment-css-vite-ts/src/index.css @@ -0,0 +1,74 @@ +@layer pigment.globals { + :root { + font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + } + + a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; + } + a:hover { + color: #535bf2; + } + + body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; + } + + h1 { + font-size: 3.2em; + line-height: 1.1; + } + + button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; + } + button:hover { + border-color: #646cff; + } + button:focus, + button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; + } + + @media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } + } +} + +#root { + width: 100%; +} diff --git a/v1-examples/pigment-css-vite-ts/src/main.tsx b/v1-examples/pigment-css-vite-ts/src/main.tsx new file mode 100644 index 000000000..f376200c6 --- /dev/null +++ b/v1-examples/pigment-css-vite-ts/src/main.tsx @@ -0,0 +1,13 @@ +import '@pigment-css/react-new/styles.css'; +import './index.css'; + +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; + +import App from './App.tsx'; + +createRoot(document.getElementById('root')!).render( + + + , +); diff --git a/v1-examples/pigment-css-vite-ts/src/theme.ts b/v1-examples/pigment-css-vite-ts/src/theme.ts new file mode 100644 index 000000000..9a01434d1 --- /dev/null +++ b/v1-examples/pigment-css-vite-ts/src/theme.ts @@ -0,0 +1,189 @@ +const BREAKPOINTS = { + sm: '480px', + md: '768px', +} as const; + +const baseTokens = { + typography: { + fontFamily: 'system-ui, Avenir, Helvetica, Arial, sans-serif', + fontSizes: { + logo: '2rem', + nav: '1.1rem', + title: { + desktop: '4rem', + tablet: '3rem', + mobile: '2.5rem', + }, + subtitle: { + desktop: '1.5rem', + tablet: '1.25rem', + mobile: '1.1rem', + }, + button: { + desktop: '1.1rem', + mobile: '1rem', + }, + featureTitle: { + desktop: '1.5rem', + mobile: '1.25rem', + }, + featureDescription: { + desktop: '1.1rem', + mobile: '1rem', + }, + }, + fontWeights: { + regular: 400, + medium: 500, + semibold: 600, + bold: 700, + extrabold: 800, + }, + lineHeights: { + tight: 1.1, + normal: 1.5, + relaxed: 1.6, + }, + letterSpacing: { + tight: '-0.05em', + }, + }, + spacing: { + 0: '0', + 1: '0.75rem', + 2: '1rem', + 3: '1.5rem', + 4: '2rem', + 5: '3rem', + 6: '4rem', + 8: '6rem', + }, + sizes: { + container: '1200px', + content: '800px', + featureCard: '250px', + minWidth: '320px', + }, + radii: { + sm: '0.5rem', + md: '1rem', + button: '8px', + }, + animations: { + gradient: { + duration: '15s', + timing: 'ease', + iteration: 'infinite', + }, + transition: { + duration: '0.2s', + timing: 'ease', + }, + button: { + duration: '0.25s', + }, + }, + breakpoints: { + gt(key: keyof typeof BREAKPOINTS) { + return `@media (min-width: ${BREAKPOINTS[key]})`; + }, + lt(key: keyof typeof BREAKPOINTS) { + return `@media (max-width: ${BREAKPOINTS[key]})`; + }, + }, + utils: { + reducedMotion(val: 'no-preference' | 'reduce') { + return `@media (prefers-reduced-motion: ${val})`; + }, + colorScheme(val: 'light' | 'dark') { + return `@media (prefers-color-scheme: ${val})`; + }, + }, +} as const; + +export const lightTheme = { + ...baseTokens, + colors: { + primary: '#e73c7e', + text: { + primary: 'white', + secondary: 'rgba(255, 255, 255, 0.9)', + body: '#213547', + }, + background: { + gradient: 'linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab)', + glass: 'rgba(255, 255, 255, 0.25)', + body: '#ffffff', + button: '#f9f9f9', + }, + border: { + glass: 'rgba(255, 255, 255, 0.3)', + button: 'transparent', + }, + shadow: { + text: 'rgba(0, 0, 0, 0.2)', + glass: 'rgba(0, 0, 0, 0.1)', + }, + link: { + default: '#646cff', + hover: '#747bff', + }, + }, + effects: { + glass: { + blur: '12px', + background: 'rgba(255, 255, 255, 0.25)', + border: '1px solid rgba(255, 255, 255, 0.3)', + shadow: '0 4px 30px rgba(0, 0, 0, 0.1)', + }, + hover: { + lift: 'translateY(-2px)', + liftLarge: 'translateY(-5px)', + opacity: 0.9, + }, + }, +} as const; + +export const darkTheme = { + colors: { + primary: '#e73c7e', + text: { + primary: 'white', + secondary: 'rgba(255, 255, 255, 0.9)', + body: 'rgba(255, 255, 255, 0.87)', + }, + background: { + gradient: 'linear-gradient(-45deg, #2d1b4e, #1e0f3c, #0a0521, #02010a)', + glass: 'rgba(0, 0, 0, 0.3)', + body: '#242424', + button: '#1a1a1a', + }, + border: { + glass: 'rgba(255, 255, 255, 0.1)', + button: 'transparent', + }, + shadow: { + text: 'rgba(0, 0, 0, 0.4)', + glass: 'rgba(0, 0, 0, 0.2)', + }, + link: { + default: '#646cff', + hover: '#535bf2', + }, + }, + effects: { + glass: { + blur: '12px', + background: 'rgba(0, 0, 0, 0.3)', + border: '1px solid rgba(255, 255, 255, 0.1)', + shadow: '0 4px 30px rgba(0, 0, 0, 0.2)', + }, + hover: { + lift: 'translateY(-2px)', + liftLarge: 'translateY(-5px)', + opacity: 0.9, + }, + }, +} as const; + +export type Theme = typeof lightTheme; diff --git a/v1-examples/pigment-css-vite-ts/src/vite-env.d.ts b/v1-examples/pigment-css-vite-ts/src/vite-env.d.ts new file mode 100644 index 000000000..11f02fe2a --- /dev/null +++ b/v1-examples/pigment-css-vite-ts/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/v1-examples/pigment-css-vite-ts/tsconfig.app.json b/v1-examples/pigment-css-vite-ts/tsconfig.app.json new file mode 100644 index 000000000..358ca9ba9 --- /dev/null +++ b/v1-examples/pigment-css-vite-ts/tsconfig.app.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/v1-examples/pigment-css-vite-ts/tsconfig.json b/v1-examples/pigment-css-vite-ts/tsconfig.json new file mode 100644 index 000000000..d32ff6820 --- /dev/null +++ b/v1-examples/pigment-css-vite-ts/tsconfig.json @@ -0,0 +1,4 @@ +{ + "files": [], + "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }] +} diff --git a/v1-examples/pigment-css-vite-ts/tsconfig.node.json b/v1-examples/pigment-css-vite-ts/tsconfig.node.json new file mode 100644 index 000000000..db0becc8b --- /dev/null +++ b/v1-examples/pigment-css-vite-ts/tsconfig.node.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/v1-examples/pigment-css-vite-ts/vite.config.ts b/v1-examples/pigment-css-vite-ts/vite.config.ts new file mode 100644 index 000000000..647de1955 --- /dev/null +++ b/v1-examples/pigment-css-vite-ts/vite.config.ts @@ -0,0 +1,27 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import pigment from '@pigment-css/plugin/vite'; + +import { lightTheme, darkTheme } from './src/theme'; + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [ + react(), + pigment({ + theme: { + colorSchemes: { + light: lightTheme, + dark: darkTheme, + }, + defaultScheme: 'light', + getSelector(mode) { + if (mode === 'light') { + return ':root,[data-theme="light"]'; + } + return `[data-theme="${mode}"]`; + }, + }, + }), + ], +}); diff --git a/v1-examples/pigment-css-webpack-ts/.gitignore b/v1-examples/pigment-css-webpack-ts/.gitignore new file mode 100644 index 000000000..c925c21d5 --- /dev/null +++ b/v1-examples/pigment-css-webpack-ts/.gitignore @@ -0,0 +1,2 @@ +/dist +/node_modules diff --git a/v1-examples/pigment-css-webpack-ts/README.md b/v1-examples/pigment-css-webpack-ts/README.md new file mode 100644 index 000000000..8f7e8cac4 --- /dev/null +++ b/v1-examples/pigment-css-webpack-ts/README.md @@ -0,0 +1,23 @@ +# Pigment CSS Webpack example + +This is a minimal Webpack app using Pigment CSS as the styling solution. + +The `theme` is declared in `src/theme.ts` file and then imported in `webpack.config.ts` file. + +The Typescript type augmentation has been done in `src/augment.d.ts` file. + +## Running locally + +Use your favorite package manager to first install the dependencies and then starting the dev server. + +```bash +npm install +npm run dev +``` + +To build and then run the app, + +```bash +npm run build +npm run start +``` diff --git a/v1-examples/pigment-css-webpack-ts/package.json b/v1-examples/pigment-css-webpack-ts/package.json new file mode 100644 index 000000000..f9f348907 --- /dev/null +++ b/v1-examples/pigment-css-webpack-ts/package.json @@ -0,0 +1,41 @@ +{ + "name": "@example/pigment-css-webpack-ts", + "version": "1.0.0", + "description": "", + "private": true, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "dev": "webpack serve --mode development", + "build": "cross-env NODE_ENV=production webpack --mode production", + "start": "npx serve dist" + }, + "keywords": [], + "author": "", + "license": "MIT", + "dependencies": { + "@pigment-css/react-new": "*", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@pigment-css/plugin": "*", + "@types/react": "^19.0.10", + "@types/react-dom": "^19.0.4", + "copy-webpack-plugin": "^13.0.0", + "css-loader": "^7.1.2", + "clean-webpack-plugin": "^4.0.0", + "css-minimizer-webpack-plugin": "^7.0.2", + "fork-ts-checker-webpack-plugin": "^8.0.0", + "html-webpack-plugin": "^5.6.3", + "mini-css-extract-plugin": "^2.9.2", + "postcss-loader": "^8.1.1", + "style-loader": "^4.0.0", + "terser-webpack-plugin": "^5.3.14", + "ts-loader": "^9.5.2", + "ts-node": "^10.9.2", + "typescript": "^5.8.2", + "webpack": "^5.98.0", + "webpack-cli": "^6.0.1", + "webpack-dev-server": "^5.2.0" + } +} diff --git a/v1-examples/pigment-css-webpack-ts/public/favicon.svg b/v1-examples/pigment-css-webpack-ts/public/favicon.svg new file mode 100644 index 000000000..e0acedab3 --- /dev/null +++ b/v1-examples/pigment-css-webpack-ts/public/favicon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/v1-examples/pigment-css-webpack-ts/public/index.html b/v1-examples/pigment-css-webpack-ts/public/index.html new file mode 100644 index 000000000..0f7536058 --- /dev/null +++ b/v1-examples/pigment-css-webpack-ts/public/index.html @@ -0,0 +1,12 @@ + + + + + + Pigment CSS with Webpack and TypeScript + + + +
+ + diff --git a/v1-examples/pigment-css-webpack-ts/src/App.tsx b/v1-examples/pigment-css-webpack-ts/src/App.tsx new file mode 100644 index 000000000..df6cb30d9 --- /dev/null +++ b/v1-examples/pigment-css-webpack-ts/src/App.tsx @@ -0,0 +1,262 @@ +import { styled, keyframes } from '@pigment-css/react-new'; +import { ThemeSelector } from './ThemeSelector'; + +const gradientAnimation = keyframes({ + '0%': { + backgroundPosition: '0% 50%', + }, + '50%': { + backgroundPosition: '100% 50%', + }, + '100%': { + backgroundPosition: '0% 50%', + }, +}); + +const Container = styled.div(({ theme }) => ({ + width: '100%', + minHeight: '100vh', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + background: theme.colors.background.gradient, + backgroundSize: '400% 400%', + color: 'white', + padding: '2rem 0', + [theme.utils.reducedMotion('no-preference')]: { + animation: `${gradientAnimation} 15s ease infinite`, + }, + [theme.breakpoints.gt('md')]: { + padding: '1rem 0', + }, +})); + +const Header = styled.header(({ theme }) => ({ + width: '100%', + maxWidth: '1200px', + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + padding: '1rem', + borderRadius: '1rem', + marginBottom: '4rem', + backgroundColor: theme.colors.background.glass, + backdropFilter: 'blur(12px)', + boxShadow: `0 4px 30px ${theme.effects.glass.shadow}`, + border: `1px solid ${theme.colors.border.glass}`, + [theme.breakpoints.lt('md')]: { + flexDirection: 'column', + gap: '1rem', + textAlign: 'center', + marginBottom: '2rem', + }, +})); + +const Logo = styled.h1(({ theme }) => ({ + margin: 0, + fontSize: '2rem', + fontWeight: 700, + letterSpacing: '-0.05em', + [theme.breakpoints.lt('md')]: { + fontSize: '1.75rem', + }, +})); + +const Hero = styled.main(({ theme }) => ({ + maxWidth: '800px', + textAlign: 'center', + marginTop: '4rem', + padding: '0 1rem', + [theme.breakpoints.gt('md')]: { + marginTop: '2rem', + }, +})); + +const Title = styled.h2(({ theme }) => ({ + fontSize: '4rem', + fontWeight: 800, + marginBottom: '1.5rem', + lineHeight: 1.1, + [theme.breakpoints.gt('md')]: { + fontSize: '3rem', + }, + [theme.breakpoints.gt('sm')]: { + fontSize: '2.5rem', + }, +})); + +const Subtitle = styled.p(({ theme }) => ({ + fontSize: '1.5rem', + opacity: 0.9, + marginBottom: '3rem', + lineHeight: 1.6, + [theme.breakpoints.gt('md')]: { + fontSize: '1.25rem', + marginBottom: '2rem', + }, + [theme.breakpoints.gt('sm')]: { + fontSize: '1.1rem', + }, +})); + +const ButtonGroup = styled.div(({ theme }) => ({ + display: 'flex', + gap: '1rem', + justifyContent: 'center', + [theme.breakpoints.lt('md')]: { + flexDirection: 'column', + alignItems: 'stretch', + gap: '0.75rem', + }, +})); + +const Button = styled.button(({ theme }) => ({ + padding: '1rem 2rem', + fontSize: '1.1rem', + fontWeight: 600, + borderRadius: '0.5rem', + border: 'none', + cursor: 'pointer', + [theme.utils.reducedMotion('no-preference')]: { + transition: 'transform 0.2s ease, opacity 0.2s ease', + '&:hover': { + transform: 'translateY(-2px)', + opacity: 0.9, + }, + }, + [theme.utils.reducedMotion('reduce')]: { + '&:hover': { + opacity: 0.9, + }, + }, + [theme.breakpoints.gt('sm')]: { + padding: '0.875rem 1.5rem', + fontSize: '1rem', + }, + variants: { + variant: { + primary: { + backgroundColor: theme.colors.text.primary, + color: theme.colors.primary, + }, + secondary: { + backgroundColor: 'transparent', + border: `2px solid ${theme.colors.primary}`, + color: theme.colors.text.primary, + }, + }, + }, + defaultVariants: { + variant: 'primary', + }, +})); + +const FeatureGrid = styled.div(({ theme }) => ({ + display: 'grid', + gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))', + gap: '2rem', + width: '100%', + maxWidth: '1200px', + marginTop: '6rem', + padding: '0 1rem', + [theme.breakpoints.lt('md')]: { + marginTop: '4rem', + gap: '1.5rem', + }, + [theme.breakpoints.lt('sm')]: { + marginTop: '3rem', + gap: '1rem', + }, +})); + +const FeatureCard = styled.div(({ theme }) => ({ + color: theme.colors.text.primary, + backgroundColor: theme.colors.background.glass, + backdropFilter: 'blur(12px)', + boxShadow: `0 4px 30px ${theme.effects.glass.shadow}`, + border: `1px solid ${theme.colors.border.glass}`, + borderRadius: '1rem', + padding: '2rem', + textAlign: 'left', + [theme.utils.reducedMotion('no-preference')]: { + transition: 'transform 0.2s ease', + '&:hover': { + transform: 'translateY(-5px)', + }, + }, + [theme.breakpoints.lt('md')]: { + padding: '1.5rem', + }, + [theme.breakpoints.lt('sm')]: { + padding: '1.25rem', + }, +})); + +const FeatureTitle = styled.h3(({ theme }) => ({ + fontSize: '1.5rem', + marginBottom: '1rem', + fontWeight: 600, + [theme.breakpoints.gt('sm')]: { + fontSize: '1.25rem', + }, +})); + +const FeatureDescription = styled.p(({ theme }) => ({ + fontSize: '1.1rem', + opacity: 0.9, + lineHeight: 1.6, + [theme.breakpoints.gt('sm')]: { + fontSize: '1rem', + }, +})); + +function App() { + return ( + +
+ Pigment CSS + +
+ + + Build beautiful interfaces with Pigment CSS + + A modern CSS-in-JS solution that makes styling React components a breeze. Write + maintainable styles with the power of TypeScript. + + + + + + + + + + Type-Safe Styling + + Get complete TypeScript support with autocomplete and type checking for your styles. + + + + Zero Runtime + + Styles are processed at build time, resulting in minimal runtime overhead. + + + + Dynamic Theming + + Create and switch between themes with ease. Full support for dark mode and custom + themes. + + + +
+ ); +} + +export default App; diff --git a/v1-examples/pigment-css-webpack-ts/src/ThemeSelector.tsx b/v1-examples/pigment-css-webpack-ts/src/ThemeSelector.tsx new file mode 100644 index 000000000..7f963a63c --- /dev/null +++ b/v1-examples/pigment-css-webpack-ts/src/ThemeSelector.tsx @@ -0,0 +1,30 @@ +import * as React from 'react'; + +type Mode = 'light' | 'dark' | 'system'; + +export function ThemeSelector() { + const [mode, setMode] = React.useState( + window.localStorage.getItem('mode') as Mode | null, + ); + + const handleChange = (e: React.ChangeEvent) => { + const newMode = e.target.value as Mode; + if (!newMode) { + return; + } + setMode(newMode); + window.localStorage.setItem('mode', newMode); + document.documentElement.dataset.theme = newMode; + }; + + return ( + + ); +} diff --git a/v1-examples/pigment-css-webpack-ts/src/augment.d.ts b/v1-examples/pigment-css-webpack-ts/src/augment.d.ts new file mode 100644 index 000000000..4873062c7 --- /dev/null +++ b/v1-examples/pigment-css-webpack-ts/src/augment.d.ts @@ -0,0 +1,5 @@ +import type { Theme as UserTheme } from './theme'; + +declare module '@pigment-css/react-new' { + export interface Theme extends UserTheme {} +} diff --git a/v1-examples/pigment-css-webpack-ts/src/index.css b/v1-examples/pigment-css-webpack-ts/src/index.css new file mode 100644 index 000000000..38646f1ad --- /dev/null +++ b/v1-examples/pigment-css-webpack-ts/src/index.css @@ -0,0 +1,74 @@ +@layer pigment.globals { + :root { + font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + } + + a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; + } + a:hover { + color: #535bf2; + } + + body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; + } + + h1 { + font-size: 3.2em; + line-height: 1.1; + } + + button { + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; + } + button:hover { + border-color: #646cff; + } + button:focus, + button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; + } + + @media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } + } +} + +#root { + width: 100%; +} diff --git a/v1-examples/pigment-css-webpack-ts/src/main.tsx b/v1-examples/pigment-css-webpack-ts/src/main.tsx new file mode 100644 index 000000000..b4762b5bc --- /dev/null +++ b/v1-examples/pigment-css-webpack-ts/src/main.tsx @@ -0,0 +1,13 @@ +import '@pigment-css/react-new/styles.css'; +import './index.css'; + +import * as React from 'react'; +import * as ReactDOM from 'react-dom/client'; + +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + , +); diff --git a/v1-examples/pigment-css-webpack-ts/src/theme.ts b/v1-examples/pigment-css-webpack-ts/src/theme.ts new file mode 100644 index 000000000..34273190f --- /dev/null +++ b/v1-examples/pigment-css-webpack-ts/src/theme.ts @@ -0,0 +1,189 @@ +const BREAKPOINTS = { + sm: '480px', + md: '768px', +} as const; + +const baseTokens = { + typography: { + fontFamily: 'system-ui, Avenir, Helvetica, Arial, sans-serif', + fontSizes: { + logo: '2rem', + nav: '1.1rem', + title: { + desktop: '4rem', + tablet: '3rem', + mobile: '2.5rem', + }, + subtitle: { + desktop: '1.5rem', + tablet: '1.25rem', + mobile: '1.1rem', + }, + button: { + desktop: '1.1rem', + mobile: '1rem', + }, + featureTitle: { + desktop: '1.5rem', + mobile: '1.25rem', + }, + featureDescription: { + desktop: '1.1rem', + mobile: '1rem', + }, + }, + fontWeights: { + regular: 400, + medium: 500, + semibold: 600, + bold: 700, + extrabold: 800, + }, + lineHeights: { + tight: 1.1, + normal: 1.5, + relaxed: 1.6, + }, + letterSpacing: { + tight: '-0.05em', + }, + }, + spacing: { + 0: '0', + 1: '0.75rem', + 2: '1rem', + 3: '1.5rem', + 4: '2rem', + 5: '3rem', + 6: '4rem', + 8: '6rem', + }, + sizes: { + container: '1200px', + content: '800px', + featureCard: '250px', + minWidth: '320px', + }, + radii: { + sm: '0.5rem', + md: '1rem', + button: '8px', + }, + animations: { + gradient: { + duration: '15s', + timing: 'ease', + iteration: 'infinite', + }, + transition: { + duration: '0.2s', + timing: 'ease', + }, + button: { + duration: '0.25s', + }, + }, + breakpoints: { + gt(key: keyof typeof BREAKPOINTS) { + return `@media (min-width: ${BREAKPOINTS[key]})`; + }, + lt(key: keyof typeof BREAKPOINTS) { + return `@media (max-width: ${BREAKPOINTS[key]})`; + }, + }, + utils: { + reducedMotion(val: 'no-preference' | 'reduce') { + return `@media (prefers-reduced-motion: ${val})`; + }, + colorScheme(val: 'light' | 'dark') { + return `@media (prefers-color-scheme: ${val})`; + }, + }, +} as const; + +export const lightTheme = { + ...baseTokens, + colors: { + primary: '#e73c7e', + text: { + primary: 'white', + secondary: 'rgba(255, 255, 255, 0.9)', + body: '#213547', + }, + background: { + gradient: 'linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab)', + glass: 'rgba(255, 255, 255, 0.25)', + body: '#ffffff', + button: '#f9f9f9', + }, + border: { + glass: 'rgba(255, 255, 255, 0.3)', + button: 'transparent', + }, + shadow: { + text: 'rgba(0, 0, 0, 0.2)', + glass: 'rgba(0, 0, 0, 0.1)', + }, + link: { + default: '#646cff', + hover: '#747bff', + }, + }, + effects: { + glass: { + blur: '12px', + background: 'rgba(255, 255, 255, 0.25)', + border: '1px solid rgba(255, 255, 255, 0.3)', + shadow: '0 4px 30px rgba(0, 0, 0, 0.1)', + }, + hover: { + lift: 'translateY(-2px)', + liftLarge: 'translateY(-5px)', + opacity: 0.9, + }, + }, +}; + +export type Theme = typeof lightTheme; + +export const darkTheme = { + colors: { + primary: '#e73c7e', + text: { + primary: 'white', + secondary: 'rgba(255, 255, 255, 0.9)', + body: 'rgba(255, 255, 255, 0.87)', + }, + background: { + gradient: 'linear-gradient(-45deg, #2d1b4e, #1e0f3c, #0a0521, #02010a)', + glass: 'rgba(0, 0, 0, 0.3)', + body: '#242424', + button: '#1a1a1a', + }, + border: { + glass: 'rgba(255, 255, 255, 0.1)', + button: 'transparent', + }, + shadow: { + text: 'rgba(0, 0, 0, 0.4)', + glass: 'rgba(0, 0, 0, 0.2)', + }, + link: { + default: '#646cff', + hover: '#535bf2', + }, + }, + effects: { + glass: { + blur: '12px', + background: 'rgba(0, 0, 0, 0.3)', + border: '1px solid rgba(255, 255, 255, 0.1)', + shadow: '0 4px 30px rgba(0, 0, 0, 0.2)', + }, + hover: { + lift: 'translateY(-2px)', + liftLarge: 'translateY(-5px)', + opacity: 0.9, + }, + }, +} as const; diff --git a/v1-examples/pigment-css-webpack-ts/tsconfig.json b/v1-examples/pigment-css-webpack-ts/tsconfig.json new file mode 100644 index 000000000..7a2fa2816 --- /dev/null +++ b/v1-examples/pigment-css-webpack-ts/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "skipLibCheck": true, + "esModuleInterop": true, + "target": "ES6", + "moduleResolution": "bundler", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + + /* Bundler mode */ + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx" + }, + "include": ["src/**/*.ts", "src/**/*.tsx", "webpack.config.ts"] +} diff --git a/v1-examples/pigment-css-webpack-ts/webpack.config.ts b/v1-examples/pigment-css-webpack-ts/webpack.config.ts new file mode 100644 index 000000000..7416cbf35 --- /dev/null +++ b/v1-examples/pigment-css-webpack-ts/webpack.config.ts @@ -0,0 +1,183 @@ +import path from 'path'; +import webpack from 'webpack'; +import HtmlWebpackPlugin from 'html-webpack-plugin'; +import MiniCssExtractPlugin from 'mini-css-extract-plugin'; +import CssMinimizerPlugin from 'css-minimizer-webpack-plugin'; +import TerserPlugin from 'terser-webpack-plugin'; +import { CleanWebpackPlugin } from 'clean-webpack-plugin'; +import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin'; +import CopyWebpackPlugin from 'copy-webpack-plugin'; +import pigmentCssPlugin from '@pigment-css/plugin/webpack'; + +import 'webpack-dev-server'; + +import { lightTheme, darkTheme } from './src/theme'; + +// Define environment type +const isProduction = process.env.NODE_ENV === 'production'; + +// Create webpack configuration +const config: webpack.Configuration = { + target: 'web', + mode: isProduction ? 'production' : 'development', + entry: './src/main.tsx', + output: { + path: path.resolve(__dirname, 'dist'), + filename: isProduction ? 'js/[name].[contenthash].js' : 'js/[name].js', + publicPath: '/', + }, + devtool: isProduction ? 'source-map' : 'eval-source-map', + module: { + rules: [ + { + test: /\.tsx?$/, + exclude: /node_modules/, + type: 'javascript/esm', + use: [ + { + loader: 'ts-loader', + options: { + // disable type checker - we will use it in fork plugin + transpileOnly: true, + }, + }, + ], + }, + { + test: /\.css$/, + use: [ + isProduction ? MiniCssExtractPlugin.loader : 'style-loader', + 'css-loader', + 'postcss-loader', + ], + }, + { + test: /\.(png|svg|jpg|jpeg|gif)$/i, + type: 'asset', + parser: { + dataUrlCondition: { + maxSize: 10 * 1024, // 10kb + }, + }, + generator: { + filename: 'images/[hash][ext][query]', + }, + }, + { + test: /\.(woff|woff2|eot|ttf|otf)$/i, + type: 'asset', + generator: { + filename: 'fonts/[hash][ext][query]', + }, + }, + ], + }, + resolve: { + extensions: ['.tsx', '.ts', '.js', '.jsx'], + }, + optimization: { + minimizer: [ + new TerserPlugin({ + terserOptions: { + compress: { + drop_console: isProduction, + }, + }, + }), + new CssMinimizerPlugin(), + ], + splitChunks: { + chunks: 'all', + name: false, + }, + }, + plugins: [ + pigmentCssPlugin({ + theme: { + colorSchemes: { + light: lightTheme, + dark: darkTheme, + }, + defaultScheme: 'light', + getSelector(mode) { + if (mode === 'light') { + return ':root,[data-theme="light"]'; + } + return `[data-theme="${mode}"]`; + }, + }, + }), + new CleanWebpackPlugin(), + new HtmlWebpackPlugin({ + template: './public/index.html', + minify: isProduction + ? { + removeComments: true, + collapseWhitespace: true, + removeRedundantAttributes: true, + useShortDoctype: true, + removeEmptyAttributes: true, + removeStyleLinkTypeAttributes: true, + keepClosingSlash: true, + minifyJS: true, + minifyCSS: true, + minifyURLs: true, + } + : false, + }), + new ForkTsCheckerWebpackPlugin({ + typescript: { + diagnosticOptions: { + semantic: true, + syntactic: true, + }, + }, + }), + ...(isProduction + ? [ + new MiniCssExtractPlugin({ + filename: 'css/[name].[contenthash].css', + chunkFilename: 'css/[id].[contenthash].css', + }), + new CopyWebpackPlugin({ + patterns: [ + { + from: 'public', + to: '', + globOptions: { + ignore: ['**/index.html'], + }, + }, + ], + }), + ] + : []), + ], + devServer: { + static: { + directory: path.join(__dirname, 'public'), + }, + historyApiFallback: true, + hot: true, + port: 3000, + open: true, + compress: true, + client: { + overlay: { + errors: true, + warnings: false, + }, + }, + }, + stats: { + colors: true, + errorDetails: true, + }, + performance: { + hints: isProduction ? 'warning' : false, + maxEntrypointSize: 512000, + maxAssetSize: 512000, + }, +}; + +export default config;