From 72622f11d1cf9d5bf79692ff880ddc2c5f1557c3 Mon Sep 17 00:00:00 2001 From: Simon Relet Date: Sat, 16 May 2026 10:04:46 +0200 Subject: [PATCH 1/5] chore(show): Upgrade to Tailwind 4 --- .vscode/settings.json | 2 +- apps/show/Dockerfile | 2 + apps/show/eslint.config.js | 6 +- apps/show/package.json | 10 +- apps/show/postcss.config.js | 7 - apps/show/prettier.config.js | 6 +- .../public/fonts/caramel-mocacino/font.css | 7 - apps/show/public/fonts/fira-sans/font.css | 23 - apps/show/scripts/generate-theme.ts | 256 +++++- apps/show/src/core/actions/action.tsx | 38 +- .../src/core/actions/prose-inline-action.tsx | 8 +- apps/show/src/core/actions/social-link.tsx | 2 +- apps/show/src/core/controllers/tabs.tsx | 6 +- apps/show/src/core/data-display/chip.tsx | 6 +- .../core/data-display/email-html.server.tsx | 346 +++++--- .../show/src/core/data-display/error-page.tsx | 11 +- apps/show/src/core/data-display/file-item.tsx | 6 +- .../src/core/data-display/icon-inline.tsx | 2 +- apps/show/src/core/data-display/image.tsx | 28 +- apps/show/src/core/data-display/markdown.tsx | 2 +- apps/show/src/core/data-display/receipt.tsx | 4 +- apps/show/src/core/data-display/task-item.tsx | 6 +- .../form-elements/input-active-filter.tsx | 17 +- .../core/form-elements/input-file-image.tsx | 16 +- .../src/core/form-elements/input-file.tsx | 8 +- apps/show/src/core/illustration/bee.tsx | 8 +- apps/show/src/core/layout/board-card.tsx | 18 +- apps/show/src/core/layout/footer.tsx | 18 +- apps/show/src/core/layout/form-layout.tsx | 40 +- apps/show/src/core/layout/header.tsx | 26 +- apps/show/src/core/layout/helper-card.tsx | 4 +- .../src/core/layout/highlight-background.tsx | 2 +- apps/show/src/core/layout/lazy-element.tsx | 2 +- .../show/src/core/layout/legal-background.tsx | 2 +- .../show/src/core/layout/light-board-card.tsx | 16 +- apps/show/src/core/layout/page-background.tsx | 6 +- apps/show/src/core/layout/section.tsx | 5 +- apps/show/src/core/layout/separator.tsx | 9 +- apps/show/src/core/loaders/spinner.tsx | 4 +- .../animations/card-animations-on-stand.tsx | 2 +- .../animations/email.service.server.tsx | 15 +- .../application/email.service.server.tsx | 18 +- .../documents/email.service.server.tsx | 8 +- .../email.service.server.tsx | 8 +- .../exhibitors/dogs-configuration/helper.tsx | 17 +- apps/show/src/exhibitors/item.tsx | 4 +- .../exhibitors/perks/email.service.server.tsx | 8 +- apps/show/src/exhibitors/perks/helper.tsx | 6 +- .../profile/email.service.server.tsx | 32 +- .../email.service.server.tsx | 15 +- apps/show/src/faq/item.tsx | 16 +- .../show/src/invoice/email.service.server.tsx | 8 +- apps/show/src/providers/item.tsx | 4 +- apps/show/src/root.tsx | 37 +- apps/show/src/routes/[manifest.json].tsx | 6 +- .../section-documents.tsx | 4 +- .../section-helper.tsx | 2 +- .../section-helper.tsx | 2 +- .../section-helper.tsx | 16 +- .../section-on-stage-animations.tsx | 8 +- .../section-helper.tsx | 16 +- .../section-helper.tsx | 16 +- .../section-helper.tsx | 16 +- .../section-tabs.tsx | 2 +- .../section-more-questions.tsx | 2 +- .../section-title.tsx | 13 +- .../routes/_layout._index/route-online.tsx | 122 ++- .../routes/_layout._index/route-waiting.tsx | 39 +- .../routes/_layout.acces/section-carpool.tsx | 10 +- .../routes/_layout.acces/section-ecology.tsx | 2 +- .../_layout.acces/section-information.tsx | 10 +- .../routes/_layout.acces/section-title.tsx | 12 +- .../section-pictures.tsx | 6 +- .../section-title.tsx | 13 +- .../modal-filters.tsx | 36 +- .../_layout.exposants._index/section-list.tsx | 2 +- .../section-waiting-helper.tsx | 4 +- .../section-information.tsx | 6 +- .../fieldset-documents.tsx | 4 +- .../_layout.faq/section-more-questions.tsx | 2 +- .../src/routes/_layout.faq/section-title.tsx | 13 +- .../section-event-list.tsx | 48 +- .../section-waiting-helper.tsx | 2 +- apps/show/src/routes/_layout/footer.tsx | 8 +- ...ons-precedentes.$edition.$pictureIndex.tsx | 6 +- apps/show/src/sponsors/item.tsx | 4 +- apps/show/src/styles/animation.css | 35 + apps/show/src/styles/custom-scrollbar.css | 19 + .../src/{tailwind.css => styles/global.css} | 25 +- apps/show/src/styles/grid-column.css | 16 + apps/show/src/styles/icon-size.css | 15 + apps/show/src/styles/main.css | 12 + apps/show/src/styles/safe-area.css | 63 ++ apps/show/src/styles/state.css | 62 ++ apps/show/src/styles/text-style.css | 127 +++ apps/show/src/styles/theme.css | 131 +++ apps/show/tailwind.config.ts | 761 ------------------ apps/show/vite.config.ts | 3 + libs/tailwindcss-peekaboo/src/peekaboo.css | 10 +- .../src/peekaboo.stories.tsx | 31 +- pnpm-lock.yaml | 41 +- pnpm-workspace.yaml | 2 + 102 files changed, 1541 insertions(+), 1447 deletions(-) delete mode 100644 apps/show/postcss.config.js delete mode 100644 apps/show/public/fonts/caramel-mocacino/font.css delete mode 100644 apps/show/public/fonts/fira-sans/font.css create mode 100644 apps/show/src/styles/animation.css create mode 100644 apps/show/src/styles/custom-scrollbar.css rename apps/show/src/{tailwind.css => styles/global.css} (70%) create mode 100644 apps/show/src/styles/grid-column.css create mode 100644 apps/show/src/styles/icon-size.css create mode 100644 apps/show/src/styles/main.css create mode 100644 apps/show/src/styles/safe-area.css create mode 100644 apps/show/src/styles/state.css create mode 100644 apps/show/src/styles/text-style.css create mode 100644 apps/show/src/styles/theme.css delete mode 100644 apps/show/tailwind.config.ts 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/show/Dockerfile b/apps/show/Dockerfile index 4cd53b92..b683cb86 100644 --- a/apps/show/Dockerfile +++ b/apps/show/Dockerfile @@ -40,6 +40,7 @@ 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/tailwindcss-peekaboo/package.json libs/tailwindcss-peekaboo/ COPY libs/zod-utils/package.json libs/zod-utils/ COPY apps/show/package.json apps/show/ @@ -60,6 +61,7 @@ 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 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 (
      -