diff --git a/.github/workflows/admin.deploy.yml b/.github/workflows/admin.deploy.yml index 3b8013f0..23e21a85 100644 --- a/.github/workflows/admin.deploy.yml +++ b/.github/workflows/admin.deploy.yml @@ -63,7 +63,7 @@ jobs: - name: βŽ” Setup node uses: actions/setup-node@v4 with: - node-version: 22.12 + node-version: 22.13 cache: "pnpm" - name: πŸ“₯ Download deps diff --git a/.github/workflows/show.deploy.yml b/.github/workflows/show.deploy.yml index 6ad3cecd..797934ff 100644 --- a/.github/workflows/show.deploy.yml +++ b/.github/workflows/show.deploy.yml @@ -63,7 +63,7 @@ jobs: - name: βŽ” Setup node uses: actions/setup-node@v4 with: - node-version: 22.12 + node-version: 22.13 cache: "pnpm" - name: πŸ“₯ Download deps diff --git a/.github/workflows/website.deploy.yml b/.github/workflows/website.deploy.yml index 559b6996..f3ca7aca 100644 --- a/.github/workflows/website.deploy.yml +++ b/.github/workflows/website.deploy.yml @@ -63,7 +63,7 @@ jobs: - name: βŽ” Setup node uses: actions/setup-node@v4 with: - node-version: 22.12 + node-version: 22.13 cache: "pnpm" - name: πŸ“₯ Download deps diff --git a/.github/workflows/workspace.checks.yml b/.github/workflows/workspace.checks.yml index b5a3c7bf..f36586c2 100644 --- a/.github/workflows/workspace.checks.yml +++ b/.github/workflows/workspace.checks.yml @@ -25,7 +25,7 @@ jobs: - name: βŽ” Setup node uses: actions/setup-node@v4 with: - node-version: 22.12 + node-version: 22.13 cache: "pnpm" - name: πŸ“₯ Download deps diff --git a/.nvmrc b/.nvmrc index 35d2d08e..fb0a1355 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -22.12 +22.13 diff --git a/.vscode/settings.json b/.vscode/settings.json index 8b8f5799..629745e6 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -21,7 +21,7 @@ "tailwindCSS.experimental.configFile": { "apps/admin/src/styles/main.css": "apps/admin/**", "apps/website/tailwind.config.ts": "apps/website/**", - "apps/show/tailwind.config.ts": "apps/show/**", + "apps/show/src/styles/main.css": "apps/show/**", "libs/tailwindcss-peekaboo/storybook/styles.css": "libs/tailwindcss-peekaboo/**" }, diff --git a/apps/admin/Dockerfile b/apps/admin/Dockerfile index c29b43a9..3460a502 100644 --- a/apps/admin/Dockerfile +++ b/apps/admin/Dockerfile @@ -1,5 +1,5 @@ # Base stage for all subsequent stages. -FROM node:22.12-bullseye-slim AS base +FROM node:22.13-bullseye-slim AS base # Needed for Prisma generation and runtime. RUN apt-get update && apt-get install -y openssl @@ -15,6 +15,9 @@ RUN corepack enable # See https://github.com/nodejs/corepack/issues/612#issuecomment-2630469508 RUN npm i -g corepack@latest +# Pin the pnpm version to match `packageManager` in package.json. +RUN corepack prepare pnpm@11.1.2 --activate + WORKDIR /workspace # Populate pnpm store with all dependencies (prod and dev). @@ -32,17 +35,22 @@ RUN rm -rf node_modules FROM base AS packages-json COPY package.json ./ +COPY apps/admin/package.json apps/admin/ +COPY apps/show/package.json apps/show/ +COPY apps/website/package.json apps/website/ COPY libs/core/package.json libs/core/ COPY libs/dev-tools/package.json libs/dev-tools/ COPY libs/file-storage/package.json libs/file-storage/ +COPY libs/files-io/package.json libs/files-io/ COPY libs/form-data/package.json libs/form-data/ COPY libs/password/package.json libs/password/ COPY libs/prisma/package.json libs/prisma/ COPY libs/react-primitives/package.json libs/react-primitives/ COPY libs/search-params-io/package.json libs/search-params-io/ +COPY libs/tailwind-animation/package.json libs/tailwind-animation/ COPY libs/tailwindcss-peekaboo/package.json libs/tailwindcss-peekaboo/ COPY libs/zod-utils/package.json libs/zod-utils/ -COPY apps/admin/package.json apps/admin/ +COPY scripts/package.json scripts/ # App production build. FROM base AS build @@ -67,7 +75,7 @@ COPY apps/admin apps/admin # Synchronise injected workspace packages after copying source code. # See: docs/decisions/002-workspace-package-injection.md -RUN pnpm install --frozen-lockfile --offline +RUN pnpm -r sync-workspace # Build libs and app. RUN pnpm --filter admin... build @@ -105,6 +113,6 @@ COPY libs/prisma/schema.prisma libs/prisma/schema.prisma # Synchronise injected workspace packages after copying build artifacts. # See: docs/decisions/002-workspace-package-injection.md -RUN pnpm install --prod --frozen-lockfile --offline +RUN pnpm -r sync-workspace CMD ["pnpm", "--filter", "admin", "start"] diff --git a/apps/admin/src/animals/agreements.tsx b/apps/admin/src/animals/agreements.tsx index dd9daf8c..5bb0eadd 100644 --- a/apps/admin/src/animals/agreements.tsx +++ b/apps/admin/src/animals/agreements.tsx @@ -48,9 +48,9 @@ export function AgreementItem({ } const AGREEMENT_CLASS_NAMES: Record = { - [AgreementValue.TRUE]: "bg-green-50 text-green-600", - [AgreementValue.FALSE]: "bg-red-50 text-red-500", - [AgreementValue.UNKNOWN]: "bg-gray-100 text-gray-700", + [AgreementValue.TRUE]: cn("bg-green-50 text-green-600"), + [AgreementValue.FALSE]: cn("bg-red-50 text-red-500"), + [AgreementValue.UNKNOWN]: cn("bg-gray-100 text-gray-700"), } const ICONS: Record> = { diff --git a/apps/admin/src/core/data-display/image.tsx b/apps/admin/src/core/data-display/image.tsx index d65a062b..8dac5145 100644 --- a/apps/admin/src/core/data-display/image.tsx +++ b/apps/admin/src/core/data-display/image.tsx @@ -26,23 +26,23 @@ const BREAKPOINT_NAMES = [...Breakpoint.names, "default"] as const type AspectRatio = "none" | "1:1" | "4:3" const ASPECT_RATIO_CLASS_NAME: Record = { - "1:1": "aspect-square", - "4:3": "aspect-4/3", - none: "", + "1:1": cn("aspect-square"), + "4:3": cn("aspect-4/3"), + none: cn(""), } type ImageBackground = "none" | "gray" const IMAGE_BACKGROUND_CLASS_NAME: Record = { - gray: "bg-gray-100", - none: "", + gray: cn("bg-gray-100"), + none: cn(""), } type ObjectFit = "cover" | "contain" const OBJECT_FIT_CLASS_NAME: Record = { - contain: "object-contain", - cover: "object-cover", + contain: cn("object-contain"), + cover: cn("object-cover"), } export type DynamicImageProps = React.ComponentPropsWithoutRef< diff --git a/apps/admin/src/root.tsx b/apps/admin/src/root.tsx index d8a86681..6772bd2d 100644 --- a/apps/admin/src/root.tsx +++ b/apps/admin/src/root.tsx @@ -138,6 +138,10 @@ function Document({ children }: { children: React.ReactNode }) { isFullHeight ? "h-full" : undefined, "flex flex-col text-body-default text-gray-800", + + // Make line breaks are added mid-word if needed. + // See https://tailwindcss.com/docs/overflow-wrap#wrapping-mid-word + "wrap-break-word", )} > {children} diff --git a/apps/admin/src/styles/animations.css b/apps/admin/src/styles/animation.css similarity index 100% rename from apps/admin/src/styles/animations.css rename to apps/admin/src/styles/animation.css diff --git a/apps/admin/src/styles/custom-scrollbars.css b/apps/admin/src/styles/custom-scrollbar.css similarity index 82% rename from apps/admin/src/styles/custom-scrollbars.css rename to apps/admin/src/styles/custom-scrollbar.css index 308c7cef..0730559e 100644 --- a/apps/admin/src/styles/custom-scrollbars.css +++ b/apps/admin/src/styles/custom-scrollbar.css @@ -1,6 +1,6 @@ @utility scrollbars-none { &::-webkit-scrollbar { - @apply hidden h-0 w-0; + @apply hidden size-0; } &::-webkit-scrollbar-track-piece { @@ -10,7 +10,7 @@ @utility scrollbars-custom { &::-webkit-scrollbar { - @apply h-0.5 w-0.5; + @apply size-0.5; } &::-webkit-scrollbar-thumb { diff --git a/apps/admin/src/styles/globals.css b/apps/admin/src/styles/global.css similarity index 88% rename from apps/admin/src/styles/globals.css rename to apps/admin/src/styles/global.css index c36f1747..a6846fbe 100644 --- a/apps/admin/src/styles/globals.css +++ b/apps/admin/src/styles/global.css @@ -1,6 +1,6 @@ @layer base { /* - * Make sure focused elements without focus-visible don't have an outline. + * Make sure focused elements without `:focus-visible` don't have an outline. */ :focus:not(:focus-visible) { @apply outline-hidden; @@ -35,12 +35,6 @@ } body { - /* - * Make line breaks are added mid-word if needed. - * See https://tailwindcss.com/docs/overflow-wrap#wrapping-mid-word - */ - @apply wrap-break-word; - text-size-adjust: 100%; text-rendering: optimizeLegibility; } diff --git a/apps/admin/src/styles/grid-columns.css b/apps/admin/src/styles/grid-column.css similarity index 100% rename from apps/admin/src/styles/grid-columns.css rename to apps/admin/src/styles/grid-column.css diff --git a/apps/admin/src/styles/icon-sizes.css b/apps/admin/src/styles/icon-size.css similarity index 100% rename from apps/admin/src/styles/icon-sizes.css rename to apps/admin/src/styles/icon-size.css diff --git a/apps/admin/src/styles/main.css b/apps/admin/src/styles/main.css index 5ccde95c..df538396 100644 --- a/apps/admin/src/styles/main.css +++ b/apps/admin/src/styles/main.css @@ -1,13 +1,13 @@ @import "tailwindcss"; @import "@animeaux/tailwindcss-peekaboo"; -@import "./animations.css"; -@import "./custom-scrollbars.css"; -@import "./globals.css"; -@import "./grid-columns.css"; -@import "./icon-sizes.css"; +@import "./animation.css"; +@import "./custom-scrollbar.css"; +@import "./global.css"; +@import "./grid-column.css"; +@import "./icon-size.css"; @import "./inherit-background.css"; -@import "./safe-areas.css"; -@import "./states.css"; -@import "./text-styles.css"; +@import "./safe-area.css"; +@import "./state.css"; +@import "./text-style.css"; @import "./theme.css"; diff --git a/apps/admin/src/styles/safe-areas.css b/apps/admin/src/styles/safe-area.css similarity index 100% rename from apps/admin/src/styles/safe-areas.css rename to apps/admin/src/styles/safe-area.css diff --git a/apps/admin/src/styles/states.css b/apps/admin/src/styles/state.css similarity index 93% rename from apps/admin/src/styles/states.css rename to apps/admin/src/styles/state.css index 4b12aa6b..36563aad 100644 --- a/apps/admin/src/styles/states.css +++ b/apps/admin/src/styles/state.css @@ -33,6 +33,8 @@ * visible focus. * They usally don't have input mechanism that can hover over elements so we * check that. + * This override will also be used for `peer-focus-visible` and + * `group-focus-visible`. */ @custom-variant focus-visible { @media (hover: hover) { diff --git a/apps/admin/src/styles/text-styles.css b/apps/admin/src/styles/text-style.css similarity index 100% rename from apps/admin/src/styles/text-styles.css rename to apps/admin/src/styles/text-style.css diff --git a/apps/show/Dockerfile b/apps/show/Dockerfile index 4cd53b92..ca95f547 100644 --- a/apps/show/Dockerfile +++ b/apps/show/Dockerfile @@ -1,5 +1,5 @@ # Base stage for all subsequent stages. -FROM node:22.12-bullseye-slim AS base +FROM node:22.13-bullseye-slim AS base # Needed for Prisma generation and runtime. RUN apt-get update && apt-get install -y openssl @@ -15,6 +15,9 @@ RUN corepack enable # See https://github.com/nodejs/corepack/issues/612#issuecomment-2630469508 RUN npm i -g corepack@latest +# Pin the pnpm version to match `packageManager` in package.json. +RUN corepack prepare pnpm@11.1.2 --activate + WORKDIR /workspace # Populate pnpm store with all dependencies (prod and dev). @@ -32,16 +35,22 @@ RUN rm -rf node_modules FROM base AS packages-json COPY package.json ./ +COPY apps/admin/package.json apps/admin/ +COPY apps/show/package.json apps/show/ +COPY apps/website/package.json apps/website/ COPY libs/core/package.json libs/core/ COPY libs/dev-tools/package.json libs/dev-tools/ COPY libs/file-storage/package.json libs/file-storage/ COPY libs/files-io/package.json libs/files-io/ +COPY libs/form-data/package.json libs/form-data/ COPY libs/password/package.json libs/password/ COPY libs/prisma/package.json libs/prisma/ COPY libs/react-primitives/package.json libs/react-primitives/ COPY libs/search-params-io/package.json libs/search-params-io/ +COPY libs/tailwind-animation/package.json libs/tailwind-animation/ +COPY libs/tailwindcss-peekaboo/package.json libs/tailwindcss-peekaboo/ COPY libs/zod-utils/package.json libs/zod-utils/ -COPY apps/show/package.json apps/show/ +COPY scripts/package.json scripts/ # App production build. FROM base AS build @@ -60,12 +69,13 @@ COPY libs/password libs/password COPY libs/prisma libs/prisma COPY libs/react-primitives libs/react-primitives COPY libs/search-params-io libs/search-params-io +COPY libs/tailwindcss-peekaboo libs/tailwindcss-peekaboo COPY libs/zod-utils libs/zod-utils COPY apps/show apps/show # Synchronise injected workspace packages after copying source code. # See: docs/decisions/002-workspace-package-injection.md -RUN pnpm install --frozen-lockfile --offline +RUN pnpm -r sync-workspace # Build libs and app. RUN pnpm --filter show... build @@ -103,6 +113,6 @@ COPY libs/prisma/schema.prisma libs/prisma/schema.prisma # Synchronise injected workspace packages after copying build artifacts. # See: docs/decisions/002-workspace-package-injection.md -RUN pnpm install --prod --frozen-lockfile --offline +RUN pnpm -r sync-workspace CMD ["pnpm", "--filter", "show", "start"] diff --git a/apps/show/eslint.config.js b/apps/show/eslint.config.js index c28c06bb..b6765fe1 100644 --- a/apps/show/eslint.config.js +++ b/apps/show/eslint.config.js @@ -1 +1,5 @@ -export { default } from "@animeaux/dev-tools/eslint" +import { createConfig } from "@animeaux/dev-tools/eslint" + +export default createConfig({ + tailwindStylesheet: "./src/styles/main.css", +}) diff --git a/apps/show/package.json b/apps/show/package.json index e621528f..3285d09f 100644 --- a/apps/show/package.json +++ b/apps/show/package.json @@ -63,10 +63,10 @@ "blurhash": "catalog:", "cloudinary": "catalog:", "compression": "catalog:", + "es-toolkit": "catalog:", "express": "catalog:", "get-port": "catalog:", "isbot": "catalog:", - "es-toolkit": "catalog:", "lru-cache": "catalog:", "luxon": "catalog:", "morgan": "catalog:", @@ -79,13 +79,17 @@ "resend": "catalog:", "scroll-into-view-if-needed": "catalog:", "source-map-support": "catalog:", - "tailwindcss": "catalog:", + "tailwindcss": "catalog:tailwindcss4", "tiny-invariant": "catalog:" }, "devDependencies": { "@animeaux/dev-tools": "workspace:*", + "@animeaux/tailwindcss-peekaboo": "workspace:*", "@remix-run/dev": "catalog:", + "@tailwindcss/node": "catalog:tailwindcss4", + "@tailwindcss/vite": "catalog:tailwindcss4", "@types/compression": "catalog:", + "@types/culori": "catalog:", "@types/express": "catalog:", "@types/luxon": "catalog:", "@types/morgan": "catalog:", @@ -95,13 +99,13 @@ "@types/source-map-support": "catalog:", "autoprefixer": "catalog:", "chokidar": "catalog:", + "culori": "catalog:", "dotenv-flow": "catalog:", "eslint": "catalog:", "msw": "catalog:", "node-html-parser": "catalog:", "npm-run-all": "catalog:", "prettier": "catalog:", - "prettier-plugin-tailwindcss": "catalog:", "tsx": "catalog:", "type-fest": "catalog:", "typescript": "catalog:", diff --git a/apps/show/postcss.config.js b/apps/show/postcss.config.js deleted file mode 100644 index d9bc7d09..00000000 --- a/apps/show/postcss.config.js +++ /dev/null @@ -1,7 +0,0 @@ -export default { - plugins: { - "tailwindcss/nesting": {}, - tailwindcss: {}, - autoprefixer: {}, - }, -} diff --git a/apps/show/prettier.config.js b/apps/show/prettier.config.js index 9bcbf4e4..1a6f250b 100644 --- a/apps/show/prettier.config.js +++ b/apps/show/prettier.config.js @@ -1,5 +1 @@ -import { createConfig } from "@animeaux/dev-tools/prettier" - -export default createConfig({ - tailwindConfig: "./tailwind.config.ts", -}) +export { default } from "@animeaux/dev-tools/prettier" diff --git a/apps/show/public/fonts/caramel-mocacino/font.css b/apps/show/public/fonts/caramel-mocacino/font.css deleted file mode 100644 index b5781627..00000000 --- a/apps/show/public/fonts/caramel-mocacino/font.css +++ /dev/null @@ -1,7 +0,0 @@ -@font-face { - font-family: "CARAMEL MOCACINO"; - font-style: normal; - font-weight: 400; - font-display: swap; - src: url(/fonts/caramel-mocacino/caramel-mocacino.otf) format("opentype"); -} diff --git a/apps/show/public/fonts/fira-sans/font.css b/apps/show/public/fonts/fira-sans/font.css deleted file mode 100644 index 65025996..00000000 --- a/apps/show/public/fonts/fira-sans/font.css +++ /dev/null @@ -1,23 +0,0 @@ -@font-face { - font-family: "Fira Sans"; - font-style: normal; - font-weight: 400; - font-display: swap; - src: url(/fonts/fira-sans/fira-sans-regular.ttf) format("truetype"); -} - -@font-face { - font-family: "Fira Sans"; - font-style: normal; - font-weight: 500; - font-display: swap; - src: url(/fonts/fira-sans/fira-sans-medium.ttf) format("truetype"); -} - -@font-face { - font-family: "Fira Sans"; - font-style: normal; - font-weight: 600; - font-display: swap; - src: url(/fonts/fira-sans/fira-sans-semi-bold.ttf) format("truetype"); -} diff --git a/apps/show/scripts/generate-theme.ts b/apps/show/scripts/generate-theme.ts index 1414b96a..75fd4c56 100644 --- a/apps/show/scripts/generate-theme.ts +++ b/apps/show/scripts/generate-theme.ts @@ -1,34 +1,50 @@ -import { mkdir, writeFile } from "node:fs/promises" +import { mkdir, readFile, writeFile } from "node:fs/promises" import { dirname, resolve } from "node:path" import { fileURLToPath } from "node:url" -import { colors, fonts, screens, spacing } from "../tailwind.config.js" +import { __unstable__loadDesignSystem as loadDesignSystem } from "@tailwindcss/node" +import { formatHex } from "culori" +import { orderBy } from "es-toolkit/array" +import type { __unstable__loadDesignSystem as loadDesignSystemBase } from "tailwindcss" +import invariant from "tiny-invariant" + import { relativeToCwd, safelyReadFile } from "./shared.js" const FILENAME = fileURLToPath(import.meta.url) const DIRNAME = dirname(FILENAME) const SCRIPT_NAME = relativeToCwd(FILENAME) +const STYLES_DIRECTORY = resolve(DIRNAME, "../src/styles") +const MAIN_STYLE_FILE = resolve(STYLES_DIRECTORY, "main.css") +const TEXT_STYLE_FILE = resolve(STYLES_DIRECTORY, "text-style.css") const DEST_DIRECTORY = resolve(DIRNAME, "../src/generated") const DEST_FILE = resolve(DEST_DIRECTORY, "theme.ts") console.log("Building...") +const [mainCss, textStyleCss] = await Promise.all([ + readFile(MAIN_STYLE_FILE, "utf-8"), + readFile(TEXT_STYLE_FILE, "utf-8"), +]) + +// The type cast is required because @tailwindcss/node's `loadDesignSystem` +// types are broken (`design.theme: any`). We use tailwindcss's +// `loadDesignSystem` types because it's used internally and its types are fine. +const design = (await loadDesignSystem(mainCss, { + base: STYLES_DIRECTORY, +})) as Awaited> + // Ensure the destination directory exists. await mkdir(DEST_DIRECTORY, { recursive: true }) -// Add here values we need in code. -// Only add what is actualy needed to keep a minimal size. -const theme = { - screens, - screensPx: toNumberRecord(screens), - colors, - spacing: toNumberRecord(spacing), - fonts, -} - const content = `// This file is generated by ${SCRIPT_NAME} -export type ScreenSize = keyof typeof theme.screens -export const theme = ${JSON.stringify(theme, null, 2)}` +${getBreakpoints()} + +${getFonts()} + +${getColors()} + +${getSpacing()} +` const currentContent = await safelyReadFile(DEST_FILE) if (currentContent === content) { @@ -42,11 +58,211 @@ if (currentContent === content) { await writeFile(DEST_FILE, content) console.info(`Built theme (${relativeToCwd(DEST_FILE)})`) -function toNumberRecord(object: TObject) { - return Object.fromEntries( - Object.entries(object) - .filter(([_, value]) => typeof value === "string") - .map(([key, value]) => [key, Number(value.replace("px", ""))] as const) - .filter(([_, value]) => !isNaN(value)), +function getBreakpoints() { + const breakpoints = design.theme.namespace("--breakpoint") + + const sortedEntries = orderBy( + Array.from(breakpoints.entries()) + // `null` is used as key for a property with the namespace's name (e.g. + // `--breakpoint: `). + // For breakpoints this shouldn't happen. + .filter((entry): entry is [string, string] => entry[0] != null) + .map( + ([breakpoint, length]) => + [breakpoint, getLengthValuePx(length)] as const, + ), + [([_breakpoint, valuePx]) => valuePx], + ["desc"], + ) + + return `export type Breakpoint = typeof Breakpoint.names[number] + +export namespace Breakpoint { + /** Breakpoint names sorted by descending value. */ + export const names = ${JSON.stringify( + sortedEntries.map(([breakpoint]) => breakpoint), + )} as const + + /** Breakpoint values in pixels. */ + export const value = ${JSON.stringify(Object.fromEntries(sortedEntries), null, 2)} as const +}` +} + +function getColors() { + const colors = design.theme.namespace("--color") + + const alabaster = colors.get("alabaster-100") + const mystic = colors.get("mystic-500") + const prussianBlue = colors.get("prussian-blue-950") + const white = colors.get("white") + + return `export namespace Color { + export const alabaster = ${JSON.stringify(alabaster)} + export const alabasterHex = ${JSON.stringify(formatHex(alabaster))} + export const mystic = ${JSON.stringify(mystic)} + export const mysticHex = ${JSON.stringify(formatHex(mystic))} + export const prussianBlue = ${JSON.stringify(prussianBlue)} + export const prussianBlueHex = ${JSON.stringify(formatHex(prussianBlue))} + export const white = ${JSON.stringify(white)} + export const whiteHex = ${JSON.stringify(formatHex(white))} + export const primary = Color.mystic + export const primaryHex = Color.mysticHex +}` +} + +function getFonts() { + const fontFaces = parseFontFaces() + + const fonts = design.theme.namespace("--font") + const sans = parseFontFamily("sans", fonts, fontFaces) + const serif = parseFontFamily("serif", fonts, fontFaces) + + return `export namespace Font { + export const sans = ${JSON.stringify(sans, null, 2)} as const + + export const serif = ${JSON.stringify(serif, null, 2)} as const + + export const all = [sans, serif] +}` +} + +type FontVariant = { + style: string + weight: number + url: string + format: string +} + +function parseFontFaces() { + const variantsByFamily = new Map() + + for (const match of textStyleCss.matchAll( + /@font-face\s*\{(?[^}]+)\}/g, + )) { + const content = match.groups?.content?.trim() + + invariant(content, "Empty @font-face in theme") + + const { familyName, variant } = parseFontFace(content) + + const variants = variantsByFamily.get(familyName) ?? [] + variants.push(variant) + variantsByFamily.set(familyName, variants) + } + + invariant(variantsByFamily.size > 0, "No @font-face found in theme") + + return variantsByFamily +} + +function parseFontFace(content: string) { + const familyName = parseFontProperty(content, /font-family:\s*["]([^"]+)["]/) + const style = parseFontProperty(content, /font-style:\s*(\w+)/) + const weight = Number(parseFontProperty(content, /font-weight:\s*(\d+)/)) + const url = parseFontProperty(content, /url\(["]([^")]+)["]\)/) + const format = parseFontProperty(content, /format\(["]([^")]+)["]\)/) + + const variant: FontVariant = { style, weight, url, format } + + return { familyName, variant } +} + +function parseFontProperty(content: string, regExp: RegExp) { + const match = content.match(regExp) + const value = match?.[1]?.trim() + + invariant(value, `Unsupported @font-face from theme:\n${content}\n`) + + return value +} + +function parseFontFamily( + familyType: string, + themeFonts: Map, + fontFaces: Map, +) { + const content = themeFonts.get(familyType) + + invariant( + content != null, + `A \`--font-${familyType}\` definition is missing in the theme`, + ) + + const [name, ...fallbacks] = content + .split(",") + .map((name) => name.trim().replaceAll(/(^"|"$)/g, "")) + + invariant( + name != null && fallbacks.length > 0, + `Invalid \`--font-${familyType}\` definition in the theme`, ) + + const variants = fontFaces.get(name) + + invariant( + variants != null && variants.length > 0, + `No variant found for font family "${name}"`, + ) + + return { name, fallbacks, variants } +} + +function getSpacing() { + const spacings = design.theme.namespace("--spacing") + + // A property with the namespace's name has `null` as key. (e.g. + // `--spacing: `). + const spacing = spacings.get(null) + + invariant(spacing != null, "A `--spacing` definition is missing in the theme") + + return `export namespace Spacing { + export const unitPx = ${getLengthValuePx(spacing)} +}` +} + +/** + * Returns the given CSS length in pixels. + * + * @param length expressed with the `` CSS type. + * @returns The value in pixels + * @see https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/length + */ +function getLengthValuePx(length: string) { + length = length.trim() + + /** + * Matches values for the `` CSS type. + * We only support unitless `0`, rem and px. + * + * @see https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/length + */ + const CSS_LENGTH_REGEXP = /(?-?\d*\.?\d+)(?.*)/ + + const match = length.match(CSS_LENGTH_REGEXP) + + invariant( + match?.groups?.value != null, + `Invalid CSS length from theme: "${length}"`, + ) + + const value = Number(match.groups.value) + + // The unit is optional when the value is `0`. + // See https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/length#syntax + const unit = match.groups.unit || "px" + + switch (unit) { + case "px": { + return value + } + + case "rem": { + return 16 * value + } + + default: { + throw new Error(`Unsupported length unit: "${unit}" in "${length}"`) + } + } } diff --git a/apps/show/src/core/actions/action.tsx b/apps/show/src/core/actions/action.tsx index a7ada06b..c91393ed 100644 --- a/apps/show/src/core/actions/action.tsx +++ b/apps/show/src/core/actions/action.tsx @@ -20,7 +20,7 @@ export const Action = Object.assign( {...props} ref={ref} className={cn( - "relative flex flex-none rounded-0.5 px-2 py-0.5 transition-colors duration-normal text-body-lowercase-emphasis disabled:opacity-disabled can-hover:focus-visible:focus-spaced", + "relative flex flex-none rounded-0.5 px-2 py-0.5 text-body-emphasis transition-colors focus-ring-spaced focus-visible:focus-ring disabled:opacity-disabled", CLASS_NAMES_BY_COLOR[variant][color], className, )} @@ -32,7 +32,7 @@ export const Action = Object.assign( return ( @@ -58,7 +58,7 @@ export const ActionIcon = forwardRef< {...props} ref={ref} className={cn( - "flex flex-none rounded-0.5 px-1 py-0.5 transition-colors duration-normal icon-24 disabled:opacity-disabled can-hover:focus-visible:focus-spaced", + "flex flex-none rounded-0.5 px-1 py-0.5 icon-24 transition-colors focus-ring-spaced focus-visible:focus-ring disabled:opacity-disabled", CLASS_NAMES_BY_COLOR[variant][color], className, )} @@ -74,65 +74,65 @@ type Color = "alabaster" | "mystic" | "prussianBlue" const CLASS_NAMES_BY_COLOR: Record> = { button: { alabaster: cn( - "bg-alabaster text-prussianBlue", + "bg-alabaster text-prussian-blue", // @@ -169,10 +268,13 @@ export const EmailHtml = { return (
    ) }, @@ -184,10 +286,13 @@ export const EmailHtml = { return (
      ) }, @@ -200,7 +305,7 @@ export const EmailHtml = { ) return ( -
      +
      {childrenElements.map((child, index) => cloneElement(child, { _isFirst: index === 0 }), )} @@ -216,10 +321,12 @@ export const EmailHtml = { ) }, @@ -241,12 +348,14 @@ export const EmailHtml = { cellPadding="0" cellSpacing="0" role="presentation" - className={cn( - "m-0 w-full border-none", - _isFirst ? undefined : "mt-2", - )} + style={{ + margin: 0, + width: "100%", + borderStyle: "none", + ...(_isFirst ? {} : { marginTop: Spacing.unitPx * 2 }), + }} > - + {childrenElements.map((child, index) => cloneElement(child, { _isFirst: index === 0 }), )} @@ -257,20 +366,22 @@ export const EmailHtml = { RowSeparator: function EmailHtmlOutputRowSeparator() { return ( - - + +
      - +
      @@ -287,7 +398,7 @@ export const EmailHtml = { ) return ( - + {childrenElements.map((child) => cloneElement(child, { _isFirst }))} ) @@ -299,10 +410,13 @@ export const EmailHtml = { }: React.PropsWithChildren) { return ( {children} @@ -315,10 +429,12 @@ export const EmailHtml = { }: React.PropsWithChildren) { return ( {Children.toArray(children).map((child, index) => { if (!isValidElement(child)) { @@ -341,26 +457,18 @@ export type IsFirstProps = { _isFirst?: boolean } * Not exported from @react-email/components Font. */ type FallbackFont = KeepString< - React.ComponentProps["fallbackFontFamily"] + React.ComponentProps["fallbackFontFamily"] > type KeepString = T extends string ? T : never function Strong(props: React.PropsWithChildren) { - // Wrap in a span with the class because the font weight is not well - // rendered. - return ( - - - - ) + return } -export const EMAIL_SENTENCE_COMPONENTS: MarkdownComponents = { +const EMAIL_SENTENCE_COMPONENTS: MarkdownComponents = { ...SENTENCE_COMPONENTS, - strong: Strong, - a: ({ children, href }) => { if (href == null) { return children @@ -368,19 +476,18 @@ export const EMAIL_SENTENCE_COMPONENTS: MarkdownComponents = { return {children} }, + + em: (props) => ( + + ), + + strong: Strong, } -export const EMAIL_PARAGRAPH_COMPONENTS: MarkdownComponents = { +const EMAIL_PARAGRAPH_COMPONENTS: MarkdownComponents = { ...PARAGRAPH_COMPONENTS, ...EMAIL_SENTENCE_COMPONENTS, - p: (props) => ( - - ), - ul: (props) => ( ( -
    • +
    • + ), + + p: (props) => ( + ), } diff --git a/apps/show/src/core/data-display/error-page.tsx b/apps/show/src/core/data-display/error-page.tsx index 9f380afc..9b4dad04 100644 --- a/apps/show/src/core/data-display/error-page.tsx +++ b/apps/show/src/core/data-display/error-page.tsx @@ -37,7 +37,12 @@ export function ErrorPage({ isRoot = false }: { isRoot?: boolean }) { {!isRoot ? ( - + ) : null} - +

      {meta.title}

      diff --git a/apps/show/src/core/data-display/file-item.tsx b/apps/show/src/core/data-display/file-item.tsx index 0d8d1938..94a5ef95 100644 --- a/apps/show/src/core/data-display/file-item.tsx +++ b/apps/show/src/core/data-display/file-item.tsx @@ -39,7 +39,7 @@ export const FileItem = { return (
      @@ -64,7 +64,7 @@ export const FileItem = { } return ( -
      +
      diff --git a/apps/show/src/core/data-display/icon-inline.tsx b/apps/show/src/core/data-display/icon-inline.tsx index 45548a9d..2263b223 100644 --- a/apps/show/src/core/data-display/icon-inline.tsx +++ b/apps/show/src/core/data-display/icon-inline.tsx @@ -14,7 +14,7 @@ export const IconInline = forwardRef< ) diff --git a/apps/show/src/core/data-display/image.tsx b/apps/show/src/core/data-display/image.tsx index f46faa61..69a38cd9 100644 --- a/apps/show/src/core/data-display/image.tsx +++ b/apps/show/src/core/data-display/image.tsx @@ -1,11 +1,9 @@ import { cn } from "@animeaux/core" import { blurhashToDataUri } from "@unpic/placeholder" -import { orderBy } from "es-toolkit/array" import { forwardRef } from "react" import type { ImageData } from "#i/core/image/data.js" -import type { ScreenSize } from "#i/generated/theme.js" -import { theme } from "#i/generated/theme.js" +import { Breakpoint } from "#i/generated/theme.js" export const DynamicImage = forwardRef< React.ComponentRef<"img">, @@ -16,7 +14,7 @@ export const DynamicImage = forwardRef< fillTransparentBackground?: boolean image: ImageData objectFit?: ObjectFit - sizes: Partial> & { + sizes: Partial> & { // `default` is mandatory. default: string } @@ -48,11 +46,13 @@ export const DynamicImage = forwardRef< return `${url} ${size}w` }).join(",") - const sizes = SCREEN_SIZES.reduce((sizes, screen) => { - const width = sizesProp[screen] + const sizes = BREAKPOINT_NAMES.reduce((sizes, breakpoint) => { + const width = sizesProp[breakpoint] if (width != null) { - sizes.push(screen === "default" ? width : createImageMedia(screen, width)) + sizes.push( + breakpoint === "default" ? width : createImageMedia(breakpoint, width), + ) } return sizes @@ -98,8 +98,8 @@ type ImageSize = (typeof IMAGE_SIZES)[number] type AspectRatio = "none" | "1:1" | "4:3" | "16:9" | "16:10" type ObjectFit = "cover" | "contain" -export function createImageMedia(screenSize: ScreenSize, width?: string) { - return [`(min-width: ${theme.screens[screenSize]})`, width] +export function createImageMedia(breakpoint: Breakpoint, width?: string) { + return [`(min-width: ${Breakpoint.value[breakpoint]}px)`, width] .filter(Boolean) .join(" ") } @@ -161,15 +161,7 @@ export function createImageUrl( } // Larger to smaller. -const SCREEN_SIZES = orderBy( - (Object.entries(theme.screens) as [ScreenSize | "default", string][]).map( - ([name, width]) => ({ name, width: Number(width.replace("px", "")) }), - ), - [({ width }) => width], - ["desc"], -) - .map(({ name }) => name) - .concat("default") +const BREAKPOINT_NAMES = [...Breakpoint.names, "default"] as const const ASPECT_RATIO_CLASS_NAME: Record = { "1:1": cn("aspect-square"), diff --git a/apps/show/src/core/data-display/markdown.tsx b/apps/show/src/core/data-display/markdown.tsx index dea25322..c50385d3 100644 --- a/apps/show/src/core/data-display/markdown.tsx +++ b/apps/show/src/core/data-display/markdown.tsx @@ -59,7 +59,7 @@ export const SENTENCE_COMPONENTS: MarkdownComponents = { em: (props) => , strong: (props) => ( - + ), } diff --git a/apps/show/src/core/data-display/receipt.tsx b/apps/show/src/core/data-display/receipt.tsx index 3f33bbe0..06597ead 100644 --- a/apps/show/src/core/data-display/receipt.tsx +++ b/apps/show/src/core/data-display/receipt.tsx @@ -9,7 +9,7 @@ export namespace Receipt { return (
      ) } @@ -45,7 +45,7 @@ export namespace Receipt { export function Total({ ...props }: React.PropsWithChildren) { return ( - + ) } diff --git a/apps/show/src/core/data-display/task-item.tsx b/apps/show/src/core/data-display/task-item.tsx index f54a3fc8..6d0417e8 100644 --- a/apps/show/src/core/data-display/task-item.tsx +++ b/apps/show/src/core/data-display/task-item.tsx @@ -15,10 +15,10 @@ export const TaskItem = { {...props} ref={ref} className={cn( - "grid auto-cols-auto grid-flow-col grid-cols-auto-fr items-center gap-1 rounded-2 border border-mystic-200 px-2 py-1 transition-colors duration-normal can-hover:focus-visible:focus-compact md:gap-2", + "grid auto-cols-auto grid-flow-col grid-cols-auto-fr items-center gap-1 rounded-2 border border-mystic-200 px-2 py-1 transition-colors focus-visible:focus-ring md:gap-2", // Background. - "bg-transparent active:bg-mystic-100 can-hover:hover:bg-mystic-50 active:can-hover:hover:bg-mystic-100", + "bg-transparent hover:bg-mystic-50 active:bg-mystic-100 active:hover:bg-mystic-100", className, )} > @@ -61,7 +61,7 @@ export const TaskItem = { ) }), diff --git a/apps/show/src/core/form-elements/input-active-filter.tsx b/apps/show/src/core/form-elements/input-active-filter.tsx index 6009be74..f80e6004 100644 --- a/apps/show/src/core/form-elements/input-active-filter.tsx +++ b/apps/show/src/core/form-elements/input-active-filter.tsx @@ -33,16 +33,16 @@ export const InputActiveFilter = { type="checkbox" defaultChecked className={cn( - "peer/input absolute inset-0 -z-just-above h-full w-full cursor-pointer appearance-none rounded-0.5 transition-colors duration-normal", + "peer/input absolute inset-0 -z-just-above size-full cursor-pointer appearance-none rounded-0.5 transition-colors", // Background. - "bg-mystic active:bg-mystic-700 can-hover:hover:bg-mystic-600 active:can-hover:hover:bg-mystic-700", + "bg-mystic hover:bg-mystic-600 active:bg-mystic-700 active:hover:bg-mystic-700", // Border. "border border-mystic", // Focus outline. - "can-hover:focus-visible:focus-spaced", + "focus-ring-spaced focus-visible:focus-ring", className, )} @@ -60,14 +60,14 @@ export const InputActiveFilter = { {...props} ref={ref} className={cn( - "flex-none text-white icon-24 can-hover:group-hover/selector:hidden", + "flex-none icon-24 text-white group-hover/selector:hidden", className, )} /> ) @@ -81,17 +81,14 @@ export const InputActiveFilter = { ) }), RemoveIcon: function InputActiveFilterRemoveIcon() { return ( - + ) }, } diff --git a/apps/show/src/core/form-elements/input-file-image.tsx b/apps/show/src/core/form-elements/input-file-image.tsx index 35b95776..a6c84313 100644 --- a/apps/show/src/core/form-elements/input-file-image.tsx +++ b/apps/show/src/core/form-elements/input-file-image.tsx @@ -69,7 +69,7 @@ export const InputFileImage = { ref={ref} onChange={handleChange} className={cn( - "peer/input col-start-1 row-start-1 aspect-4/3 w-full cursor-pointer rounded-[inherit] opacity-0", + "peer/input col-start-1 row-start-1 aspect-4/3 w-full cursor-pointer rounded-inherit opacity-0", className, )} /> @@ -87,14 +87,14 @@ export const InputFileImage = { } return ( -
      +
      {state.data.file.name} @@ -115,7 +115,7 @@ export const InputFileImage = { if (defaultLogo != null) { return ( -
      +
      ) @@ -134,13 +134,13 @@ export const InputFileImage = { return (
      - +
      ) }, diff --git a/apps/show/src/core/form-elements/input-file.tsx b/apps/show/src/core/form-elements/input-file.tsx index 5682f361..e3f21a2f 100644 --- a/apps/show/src/core/form-elements/input-file.tsx +++ b/apps/show/src/core/form-elements/input-file.tsx @@ -42,7 +42,7 @@ export const InputFile = { ref={ref} onChange={handleChange} className={cn( - "peer/input col-start-1 row-start-1 h-full w-full cursor-pointer rounded-[inherit] opacity-0", + "peer/input col-start-1 row-start-1 size-full cursor-pointer rounded-inherit opacity-0", className, )} /> @@ -62,10 +62,10 @@ export const InputFile = { id={state.type === "empty" ? "upload-light" : undefined} mimeType={state.type === "filled" ? state.file.type : undefined} className={cn( - "pointer-events-none transition-colors duration-normal can-hover:peer-focus-visible/input:focus-spaced", + "pointer-events-none transition-colors focus-ring-spaced peer-focus-visible/input:focus-ring", // Background. - "bg-transparent peer-active/input:bg-mystic-100 can-hover:peer-hover/input:bg-mystic-50 peer-active/input:can-hover:peer-hover/input:bg-mystic-100", + "bg-transparent peer-hover/input:bg-mystic-50 peer-active/input:bg-mystic-100 peer-active/input:peer-hover/input:bg-mystic-100", className, )} /> @@ -87,7 +87,7 @@ export const InputFile = { {...props} ref={ref} className={cn( - "pointer-events-none transition-transform duration-slow can-hover:group-hover/item:scale-105", + "pointer-events-none transition-transform duration-slow group-hover/item:scale-105", className, )} /> diff --git a/apps/show/src/core/illustration/bee.tsx b/apps/show/src/core/illustration/bee.tsx index 7e77a8d4..8ad42a0b 100644 --- a/apps/show/src/core/illustration/bee.tsx +++ b/apps/show/src/core/illustration/bee.tsx @@ -42,7 +42,7 @@ const DIRECTION_PATHS: Record = { ), @@ -58,7 +58,7 @@ const DIRECTION_PATHS: Record = { ), @@ -74,7 +74,7 @@ const DIRECTION_PATHS: Record = { ), @@ -90,7 +90,7 @@ const DIRECTION_PATHS: Record = { ), diff --git a/apps/show/src/core/layout/board-card.tsx b/apps/show/src/core/layout/board-card.tsx index f4d5c831..fbecbae6 100644 --- a/apps/show/src/core/layout/board-card.tsx +++ b/apps/show/src/core/layout/board-card.tsx @@ -4,7 +4,7 @@ import { forwardRef } from "react" import { useElementSize } from "#i/core/elements.js" import { BeeIllustration } from "#i/core/illustration/bee.js" import { useScreenSizeCondition } from "#i/core/screen-size.js" -import { theme } from "#i/generated/theme.js" +import { Breakpoint, Spacing } from "#i/generated/theme.js" export const BoardCard = forwardRef< React.ComponentRef<"div">, @@ -20,7 +20,7 @@ export const BoardCard = forwardRef< {children} @@ -31,7 +31,7 @@ export const BoardCard = forwardRef< function BoardBackground() { const { ref, size } = useElementSize() const isMedium = useScreenSizeCondition( - (screenSize) => screenSize >= theme.screensPx.md, + (screenSize) => screenSize >= Breakpoint.value.md, ) return ( @@ -39,7 +39,7 @@ function BoardBackground() { // https://stackoverflow.com/questions/65565149/how-to-apply-resizeobserver-to-svg-element
      {size != null ? ( <> @@ -74,8 +74,8 @@ function Background({ height: number isMedium: boolean }) { - const spacing = isMedium ? theme.spacing[2] : theme.spacing[1] - const radius = isMedium ? theme.spacing[4] : theme.spacing[2] + const spacing = Spacing.unitPx * (isMedium ? 2 : 1) + const radius = Spacing.unitPx * (isMedium ? 4 : 2) return ( {children} - +
    • ) }, @@ -39,9 +39,9 @@ export const Footer = { LegalSection: function FooterLegalSection() { return (
      - + -

      +

      Copyright Β© {new Date().getFullYear()} Ani’Meaux

      @@ -56,7 +56,7 @@ export const Footer = {
      @@ -80,7 +80,7 @@ export const Footer = { alt="Association Ani’Meaux" aspectRatio="1:1" className={cn( - "w-[150px] justify-self-center rounded-full", + "w-12.5 justify-self-center rounded-full", isLarge ? "md:w-[200px]" : undefined, )} /> @@ -153,10 +153,10 @@ function ContactItem({ to={to} prefetch="intent" className={cn( - "grid grid-cols-2-auto items-start gap-1 rounded-0.5 can-hover:focus-visible:focus-spaced", + "grid grid-cols-auto-auto items-start gap-1 rounded-0.5 focus-ring-spaced focus-visible:focus-ring", // Text color. - "text-prussianBlue transition-colors duration-normal can-hover:hover:text-prussianBlue-900 active:can-hover:hover:text-prussianBlue-800", + "text-prussian-blue transition-colors hover:text-prussian-blue-900 active:hover:text-prussian-blue-800", )} > @@ -182,7 +182,7 @@ function Wave({ className }: { className?: string }) { > @@ -196,7 +196,7 @@ function Wave({ className }: { className?: string }) { > diff --git a/apps/show/src/core/layout/form-layout.tsx b/apps/show/src/core/layout/form-layout.tsx index af169314..c7239ce6 100644 --- a/apps/show/src/core/layout/form-layout.tsx +++ b/apps/show/src/core/layout/form-layout.tsx @@ -20,7 +20,7 @@ export const FormLayout = { {...props} ref={ref} className={cn( - "grid grid-cols-1 gap-4 md:grid-cols-[minmax(0,1fr)_auto] md:items-start lg:gap-8", + "grid grid-cols-1 gap-4 md:grid-cols-fr-auto md:items-start lg:gap-8", className, )} /> @@ -58,7 +58,7 @@ export const FormLayout = { {...props} ref={ref} className={cn( - "row-start-1 md:sticky md:top-4 md:col-start-2 md:w-[300px]", + "row-start-1 md:sticky md:top-4 md:col-start-2 md:w-25", hideOnSmallScreens ? "hidden md:grid" : undefined, className, )} @@ -94,7 +94,7 @@ export const FormLayout = { to={{ hash: `#${sectionId}` }} replace className={cn( - "grid grid-cols-fr-auto items-center gap-1 rounded-0.5 px-1 py-0.5 can-hover:focus-visible:focus-compact", + "grid grid-cols-fr-auto items-center gap-1 rounded-0.5 px-1 py-0.5 focus-visible:focus-ring", )} > {children} @@ -152,7 +152,7 @@ export const FormLayout = { ) }), @@ -248,7 +248,7 @@ export const FormLayout = { ref={ref} htmlFor={htmlFor} className={cn( - "text-caption-lowercase-default", + "text-caption", htmlFor != null ? "cursor-pointer" : undefined, className, )} @@ -270,7 +270,7 @@ export const FormLayout = { {...props} ref={ref} className={cn( - "col-span-full text-caption-lowercase-default", + "col-span-full text-caption", variant === "error" ? "text-mystic" : undefined, className, )} @@ -287,7 +287,7 @@ export const FormLayout = { {...props} ref={ref} className={cn( - "rounded-0.5 bg-transparent px-1 py-0.5 ring-1 ring-inset ring-mystic-200 placeholder:text-prussianBlue/50 can-hover:focus-visible:focus-compact", + "rounded-0.5 bg-transparent px-1 py-0.5 ring-1 ring-mystic-200 ring-inset placeholder:text-prussian-blue/50 focus-visible:focus-ring", className, )} /> @@ -304,19 +304,19 @@ export const FormLayout = { ref={ref} type="checkbox" className={cn( - "inline-flex h-2 w-3 appearance-none rounded-full p-[2px] transition-colors duration-normal enabled:cursor-pointer enabled:[&[readonly]]:cursor-default", + "inline-flex h-2 w-3 appearance-none rounded-full p-[2px] transition-colors enabled:cursor-pointer enabled:[[readonly]]:cursor-default", // Background. - "bg-transparent checked:bg-mystic active:bg-mystic-100 checked:active:bg-mystic-700 can-hover:hover:bg-mystic-50 checked:can-hover:hover:bg-mystic-600 active:can-hover:hover:bg-mystic-100 checked:active:can-hover:hover:bg-mystic-700", + "bg-transparent checked:bg-mystic hover:bg-mystic-50 checked:hover:bg-mystic-600 active:bg-mystic-100 checked:active:bg-mystic-700 active:hover:bg-mystic-100 checked:active:hover:bg-mystic-700", // Border. "border border-mystic-200 checked:border-mystic", // Focus outline. - "can-hover:focus-visible:focus-compact checked:can-hover:focus-visible:focus-spaced", + "focus-ring-spaced focus-visible:focus-ring checked:focus-visible:focus-ring", // Thumb. - "after:pointer-events-none after:aspect-square after:w-[18px] after:rounded-full after:border after:border-mystic after:transition-[background-color,border-color,transform] after:duration-normal checked:border-white checked:after:translate-x-1 checked:after:bg-white", + "after:pointer-events-none after:aspect-square after:w-1.5 after:rounded-full after:border after:border-mystic after:transition-[background-color,border-color,translate] checked:border-white checked:after:translate-x-1 checked:after:bg-white", className, )} @@ -333,7 +333,7 @@ export const FormLayout = { {...props} ref={ref} className={cn( - "rounded-0.5 px-1 py-0.5 ring-1 ring-inset ring-mystic-200 can-hover:focus-visible:focus-compact", + "rounded-0.5 px-1 py-0.5 ring-1 ring-mystic-200 ring-inset placeholder:text-prussian-blue/50 focus-visible:focus-ring", className, )} /> @@ -420,7 +420,7 @@ export const FormLayout = { {...props} ref={ref} className={cn( - "group/selector relative flex items-start gap-1 rounded-0.5 px-1 py-0.5 has-[input:enabled]:cursor-pointer has-[input:disabled]:opacity-disabled", + "group/selector relative flex items-start gap-1 rounded-0.5 px-1 py-0.5 has-[input:disabled]:opacity-disabled has-[input:enabled]:cursor-pointer", className, )} /> @@ -436,16 +436,16 @@ export const FormLayout = { {...props} ref={ref} className={cn( - "peer/input absolute inset-0 -z-just-above h-full w-full appearance-none rounded-0.5 transition-colors duration-normal", + "peer/input absolute inset-0 -z-just-above size-full appearance-none rounded-0.5 transition-colors", // Background. - "bg-transparent enabled:checked:bg-mystic enabled:active:bg-mystic-100 enabled:checked:active:bg-mystic-700 enabled:can-hover:hover:bg-mystic-50 enabled:checked:can-hover:hover:bg-mystic-600 enabled:active:can-hover:hover:bg-mystic-100 enabled:checked:active:can-hover:hover:bg-mystic-700", + "bg-transparent enabled:checked:bg-mystic enabled:hover:bg-mystic-50 enabled:checked:hover:bg-mystic-600 enabled:active:bg-mystic-100 enabled:checked:active:bg-mystic-700 enabled:active:hover:bg-mystic-100 enabled:checked:active:hover:bg-mystic-700", // Border. "border border-mystic-200 checked:border-mystic", // Focus outline. - "can-hover:focus-visible:focus-compact checked:can-hover:focus-visible:focus-spaced", + "focus-ring-spaced focus-visible:focus-ring checked:focus-visible:focus-ring", className, )} @@ -478,7 +478,7 @@ export const FormLayout = { {...props} ref={ref} className={cn( - "hidden flex-none text-white icon-24 peer-checked/input:block", + "hidden flex-none icon-24 text-white peer-checked/input:block", className, )} /> @@ -494,7 +494,7 @@ export const FormLayout = { {...props} ref={ref} className={cn( - "min-w-0 flex-1 transition-colors duration-normal text-body-lowercase-default peer-checked/input:text-white peer-checked/input:text-body-lowercase-emphasis", + "min-w-0 flex-1 text-body transition-colors peer-checked/input:text-body-emphasis peer-checked/input:text-white", className, )} /> @@ -503,7 +503,7 @@ export const FormLayout = { CheckboxIcon: function FormLayoutSelectorCheckboxIcon() { return ( - + ) @@ -511,7 +511,7 @@ export const FormLayout = { RadioIcon: function FormLayoutSelectorRadioIcon() { return ( - + ) }, }, diff --git a/apps/show/src/core/layout/header.tsx b/apps/show/src/core/layout/header.tsx index 47fff4ff..8cfb9758 100644 --- a/apps/show/src/core/layout/header.tsx +++ b/apps/show/src/core/layout/header.tsx @@ -61,11 +61,9 @@ export const Header = { role="menuitem" className={(props) => cn( - "flex justify-center px-1 transition-[color,transform] duration-normal text-body-uppercase-emphasis active:scale-95 can-hover:focus-visible:focus-compact lg:px-2", + "flex justify-center px-1 text-body-uppercase-emphasis transition-[color,scale] focus-visible:focus-ring active:scale-95 lg:px-2", - isReallyActive(props) - ? "text-mystic" - : "can-hover:hover:text-mystic", + isReallyActive(props) ? "text-mystic" : "hover:text-mystic", ) } > @@ -76,7 +74,7 @@ export const Header = { @@ -102,7 +100,7 @@ function HeaderRootSmall({ children }: React.PropsWithChildren) { return (
      - + @@ -114,15 +112,15 @@ function HeaderRootSmall({ children }: React.PropsWithChildren) { // https://github.com/radix-ui/primitives/issues/1630 onPointerMove={(event) => event.preventDefault()} onPointerLeave={(event) => event.preventDefault()} - className="duration-100 focus-visible:focus-compact-mystic group/menu-trigger flex aspect-square w-4 items-center justify-center text-[24px] transition-transform active:scale-95" + className="group/menu-trigger flex aspect-square w-4 items-center justify-center icon-24 transition-transform duration-100 focus-visible:focus-ring active:scale-95" > @@ -145,12 +143,14 @@ function HeaderRootSmall({ children }: React.PropsWithChildren) { event.preventDefault()} onPointerLeave={(event) => event.preventDefault()} - className="duration-150 absolute left-0 top-0 -z-just-above grid w-full grid-cols-1 bg-white/70 pb-4 backdrop-blur-2xl transition-[transform,opacity] pt-safe-[84px] px-safe-page-narrow data-[state=closed]:-translate-y-full data-[state=open]:translate-y-0 data-[state=closed]:opacity-0 data-[state=open]:opacity-100 data-[state=closed]:ease-in data-[state=open]:ease-out" + className={cn( + "absolute top-0 left-0 -z-just-above grid w-full grid-cols-1 bg-white/70 pt-safe-7 px-safe-page-narrow pb-4 backdrop-blur-2xl", + "-out-translate-y-full animation-duration-slow out-opacity-0 data-opened:animate-enter data-closed:animate-exit", + )} />
      @@ -160,7 +160,7 @@ function HeaderRootSmall({ children }: React.PropsWithChildren) { function HeaderRootLarge({ children }: React.PropsWithChildren) { return (
      -