diff --git a/.vscode/settings.json b/.vscode/settings.json index 629745e6..5a605532 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -20,7 +20,7 @@ "tailwindCSS.classFunctions": ["cn"], "tailwindCSS.experimental.configFile": { "apps/admin/src/styles/main.css": "apps/admin/**", - "apps/website/tailwind.config.ts": "apps/website/**", + "apps/website/src/styles/main.css": "apps/website/**", "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 3460a502..8adb3409 100644 --- a/apps/admin/Dockerfile +++ b/apps/admin/Dockerfile @@ -47,7 +47,6 @@ 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 scripts/package.json scripts/ diff --git a/apps/show/Dockerfile b/apps/show/Dockerfile index ca95f547..74319b12 100644 --- a/apps/show/Dockerfile +++ b/apps/show/Dockerfile @@ -47,7 +47,6 @@ 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 scripts/package.json scripts/ diff --git a/apps/website/Dockerfile b/apps/website/Dockerfile index bae3f24b..41c18e5e 100644 --- a/apps/website/Dockerfile +++ b/apps/website/Dockerfile @@ -47,7 +47,6 @@ 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 scripts/package.json scripts/ diff --git a/apps/website/eslint.config.js b/apps/website/eslint.config.js index c28c06bb..b6765fe1 100644 --- a/apps/website/eslint.config.js +++ b/apps/website/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/website/package.json b/apps/website/package.json index a1212811..36cd3d9f 100644 --- a/apps/website/package.json +++ b/apps/website/package.json @@ -39,10 +39,10 @@ "@remix-run/react": "catalog:", "@remix-run/serve": "catalog:", "body-scroll-lock": "catalog:", + "es-toolkit": "catalog:", "focus-trap": "catalog:", "history": "catalog:", "isbot": "catalog:", - "es-toolkit": "catalog:", "luxon": "catalog:", "react": "catalog:", "react-dom": "catalog:", @@ -57,7 +57,10 @@ "devDependencies": { "@animeaux/dev-tools": "workspace:*", "@remix-run/dev": "catalog:", + "@tailwindcss/node": "catalog:tailwindcss4", + "@tailwindcss/vite": "catalog:tailwindcss4", "@types/body-scroll-lock": "catalog:", + "@types/culori": "catalog:", "@types/luxon": "catalog:", "@types/node": "catalog:", "@types/react": "catalog:", @@ -65,14 +68,14 @@ "@types/react-transition-group": "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:", - "tailwindcss": "catalog:", + "tailwindcss": "catalog:tailwindcss4", "tsx": "catalog:", "typescript": "catalog:", "vite": "catalog:", diff --git a/apps/website/postcss.config.js b/apps/website/postcss.config.js deleted file mode 100644 index d9bc7d09..00000000 --- a/apps/website/postcss.config.js +++ /dev/null @@ -1,7 +0,0 @@ -export default { - plugins: { - "tailwindcss/nesting": {}, - tailwindcss: {}, - autoprefixer: {}, - }, -} diff --git a/apps/website/prettier.config.js b/apps/website/prettier.config.js index 9bcbf4e4..1a6f250b 100644 --- a/apps/website/prettier.config.js +++ b/apps/website/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/website/scripts/generate-theme.ts b/apps/website/scripts/generate-theme.ts index 1bac73e0..ced211a4 100644 --- a/apps/website/scripts/generate-theme.ts +++ b/apps/website/scripts/generate-theme.ts @@ -1,28 +1,42 @@ -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, screens } from "../tailwind.config.js" +import { __unstable__loadDesignSystem as loadDesignSystem } from "@tailwindcss/node" +import { formatHex } from "culori" +import { orderBy } from "es-toolkit" +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 DEST_DIRECTORY = resolve(DIRNAME, "../src/generated") const DEST_FILE = resolve(DEST_DIRECTORY, "theme.ts") console.log("Building...") +const mainCss = await readFile(MAIN_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, colors } - 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()} + +${getColors()} +` const currentContent = await safelyReadFile(DEST_FILE) if (currentContent === content) { @@ -35,3 +49,98 @@ if (currentContent === content) { await writeFile(DEST_FILE, content) console.info(`Built theme (${relativeToCwd(DEST_FILE)})`) + +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 brandBlue = colors.get("brand-blue-800") + const gray50 = colors.get("gray-50") + + return `export namespace Color { + export const brandBlue = ${JSON.stringify(brandBlue)} + export const brandBlueHex = ${JSON.stringify(formatHex(brandBlue))} + export const gray50 = ${JSON.stringify(gray50)} + export const gray50Hex = ${JSON.stringify(formatHex(gray50))} + + export const background = Color.gray50 + export const backgroundHex = Color.gray50Hex + export const primary = Color.brandBlue + export const primaryHex = Color.brandBlueHex +}` +} + +/** + * 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/website/src/animals/item.tsx b/apps/website/src/animals/item.tsx index 2d446893..f2c679d7 100644 --- a/apps/website/src/animals/item.tsx +++ b/apps/website/src/animals/item.tsx @@ -55,9 +55,9 @@ export function AnimalItem({

@@ -65,21 +65,18 @@ export function AnimalItem({ {animal.name}

-

+

{speciesLabels} {formatAge(animal.birthdate)}

diff --git a/apps/website/src/blog/item.tsx b/apps/website/src/blog/item.tsx index e3b1c696..584c84c4 100644 --- a/apps/website/src/blog/item.tsx +++ b/apps/website/src/blog/item.tsx @@ -30,14 +30,14 @@ export function ArticleItem({ />
-

+

{DateTime.fromISO(article.publicationDate).toLocaleString( DateTime.DATE_MED, )}{" "} par {article.authorName}

-

+

{article.title}

{article.description}

diff --git a/apps/website/src/core/actions.ts b/apps/website/src/core/actions.ts index 30471797..8ef7a7b0 100644 --- a/apps/website/src/core/actions.ts +++ b/apps/website/src/core/actions.ts @@ -3,17 +3,17 @@ import { cn } from "@animeaux/core" type ActionColor = "blue" | "yellow" | "gray" const STANDALONE_ACTION_COLOR_CLASS_NAME: Record = { - blue: "bg-brandBlue text-white hover:bg-brandBlue-lighter", - yellow: "bg-brandYellow text-black hover:bg-brandYellow-darker", - gray: "bg-gray-100 hover:bg-gray-200", + blue: cn("bg-brand-blue text-white hover:bg-brand-blue-lighter"), + yellow: cn("bg-brand-yellow text-black hover:bg-brand-yellow-darker"), + gray: cn("bg-gray-100 hover:bg-gray-200"), } export const actionClassNames = { standalone: ({ color = "blue" }: { color?: ActionColor } = {}) => cn( - "flex min-w-max flex-none items-center justify-center px-6 py-2 transition-[background-color,transform] duration-100 ease-in-out rounded-bubble-sm text-body-emphasis active:scale-95", + "flex min-w-max flex-none items-center justify-center rounded-bubble-sm px-6 py-2 text-body-emphasis transition-[background-color,scale] active:scale-95", STANDALONE_ACTION_COLOR_CLASS_NAME[color], ), proseInline: () => - "border-b text-body-emphasis border-b-brandBlue hover:border-b-2", + cn("border-b border-b-brand-blue text-body-emphasis hover:border-b-2"), } diff --git a/apps/website/src/core/controllers/paginator.tsx b/apps/website/src/core/controllers/paginator.tsx index f4929719..e34f1846 100644 --- a/apps/website/src/core/controllers/paginator.tsx +++ b/apps/website/src/core/controllers/paginator.tsx @@ -77,9 +77,9 @@ function PaginatorItem({ { "w-[40px] rounded-bubble-sm": !isEllipsis, }, - "flex items-center justify-center transition-colors duration-100 ease-in-out text-body-emphasis", + "flex items-center justify-center text-body-emphasis transition-colors", { - "bg-brandBlue text-white": isActive, + "bg-brand-blue text-white": isActive, "hover:bg-gray-200": !isActive && to != null, }, )} diff --git a/apps/website/src/core/controllers/searchForm.tsx b/apps/website/src/core/controllers/searchForm.tsx index 24f9d951..37fcb504 100644 --- a/apps/website/src/core/controllers/searchForm.tsx +++ b/apps/website/src/core/controllers/searchForm.tsx @@ -89,7 +89,7 @@ export function SearchForm({
@@ -135,7 +135,7 @@ export function SearchForm({ @@ -161,7 +161,7 @@ function Select({ onChange(event.target.value as ValueType) }} className={cn( - "min-w-0 flex-1 cursor-pointer appearance-none bg-transparent px-6 py-2 transition-colors duration-100 ease-in-out rounded-bubble-sm hover:bg-gray-50", + "min-w-0 flex-1 cursor-pointer appearance-none rounded-bubble-sm bg-transparent px-6 py-2 transition-colors hover:bg-gray-50", { "text-gray-500": value == null }, )} > diff --git a/apps/website/src/core/controllers/tabs.tsx b/apps/website/src/core/controllers/tabs.tsx index 238d59a2..a33904a7 100644 --- a/apps/website/src/core/controllers/tabs.tsx +++ b/apps/website/src/core/controllers/tabs.tsx @@ -21,9 +21,9 @@ export function Tab({ type="button" onClick={onSelect} className={cn( - "relative flex px-3 py-2 transition-colors duration-150 ease-in-out text-body-emphasis", + "relative flex px-3 py-2 text-body-emphasis transition-colors duration-slow", { - "text-brandBlue": isActive, + "text-brand-blue": isActive, "text-gray-600 hover:text-black": !isActive, }, )} @@ -34,7 +34,7 @@ export function Tab({ {(transitionState) => ( ({ name, width: Number(width.replace("px", "")) }), - ), - [({ width }) => width], - ["desc"], -) - .map(({ name }) => name) - .concat("default") +const BREAKPOINT_NAMES = [...Breakpoint.names, "default"] as const export type StaticImageProps = { image: ImageDescriptor - sizes: Partial> & { + sizes: Partial> & { // `default` is mandatory. default: string } @@ -47,14 +37,14 @@ export function StaticImage({ }: StaticImageProps) { invariant(fallbackSize != null, "At least one size should be provided.") - 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" + breakpoint === "default" ? width - : `(min-width: ${theme.screens[screen]}) ${width}`, + : `(min-width: ${Breakpoint.value[breakpoint]}px) ${width}`, ) } @@ -78,8 +68,8 @@ export function StaticImage({ type ImageBackground = "none" | "gray" const IMAGE_BACKGROUND_CLASS_NAME: Record = { - gray: "bg-gray-100", - none: "", + gray: cn("bg-gray-100"), + none: cn(), } export function DynamicImage({ diff --git a/apps/website/src/core/data-display/markdown.tsx b/apps/website/src/core/data-display/markdown.tsx index f72aac42..372f7f22 100644 --- a/apps/website/src/core/data-display/markdown.tsx +++ b/apps/website/src/core/data-display/markdown.tsx @@ -91,7 +91,7 @@ export const ARTICLE_COMPONENTS: MarkdownProps["components"] = {
- +
{children}
), @@ -107,8 +107,8 @@ export const ARTICLE_COMPONENTS: MarkdownProps["components"] = { h2: ({ children }) => (

{children} diff --git a/apps/website/src/core/data-display/options.tsx b/apps/website/src/core/data-display/options.tsx index 0dbd675c..3050c9e0 100644 --- a/apps/website/src/core/data-display/options.tsx +++ b/apps/website/src/core/data-display/options.tsx @@ -9,7 +9,7 @@ export function OptionList({ children }: { children: React.ReactNode }) { // Ensure the list takes the entire viewport width for better overflow // indicators. "-mx-page w-screen min-w-0", - "flex snap-x snap-mandatory items-start gap-3 overflow-x-auto scroll-smooth scrollbars-none", + "scrollbars-none flex snap-x snap-mandatory items-start gap-3 overflow-x-auto scroll-smooth", // Because of `overflow-x-auto`, we need to ensure card shadows are // visible. "-my-6 py-6", @@ -37,7 +37,7 @@ export function OptionCard({ children }: { children: React.ReactNode }) { "md:m-0 md:w-auto md:max-w-sm md:flex-1 md:p-0", )} > -
+
{children}
@@ -45,7 +45,7 @@ export function OptionCard({ children }: { children: React.ReactNode }) { } export function OptionTitle({ children }: { children: React.ReactNode }) { - return

{children}

+ return

{children}

} export function OptionDescription({ children }: { children: React.ReactNode }) { @@ -63,8 +63,8 @@ export function OptionPrice({

{children} @@ -91,7 +91,7 @@ export function OptionFeature({ diff --git a/apps/website/src/core/data-display/timeline.tsx b/apps/website/src/core/data-display/timeline.tsx index dec3d393..0bccca51 100644 --- a/apps/website/src/core/data-display/timeline.tsx +++ b/apps/website/src/core/data-display/timeline.tsx @@ -34,15 +34,15 @@ export function TimelineItem({

-

{title}

+

{title}

{children}

diff --git a/apps/website/src/core/layout/bubble-section.tsx b/apps/website/src/core/layout/bubble-section.tsx index 26ca084f..25780b13 100644 --- a/apps/website/src/core/layout/bubble-section.tsx +++ b/apps/website/src/core/layout/bubble-section.tsx @@ -1,13 +1,13 @@ import { cn } from "@animeaux/core" export const bubbleSectionClassNames = { - root: () => "relative flex", + root: () => cn("relative flex"), // Wrap the shape because it looks like SVG can only be sized with width and // height. But we don't want the width class to be a complexe arbitrary value - // with hard coded size in px: `w-[calc(100%_-_16px)]`. + // with hard coded size in px: `w-[calc(100%-16px)]`. bubbleContainer: () => - cn("absolute bottom-0 left-2 right-2 top-0 -z-10", "md:left-4 md:right-4"), - content: () => "w-full", + cn("absolute inset-x-2 inset-y-0 -z-10", "md:inset-x-4"), + content: () => cn("w-full"), } export function BubbleShape({ @@ -24,8 +24,8 @@ export function BubbleShape({ // Allow the shape to stretch. preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg" - className={cn("h-full w-full overflow-visible", { - "stroke-brandYellow": isDouble, + className={cn("size-full overflow-visible", { + "stroke-brand-yellow": isDouble, "stroke-gray-300": !isDouble, })} style={style} diff --git a/apps/website/src/core/layout/footer.tsx b/apps/website/src/core/layout/footer.tsx index d39ba8c0..46966785 100644 --- a/apps/website/src/core/layout/footer.tsx +++ b/apps/website/src/core/layout/footer.tsx @@ -12,7 +12,7 @@ export function Footer() { return (