diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js index 2e7af2b..780e822 100644 --- a/frontend/postcss.config.js +++ b/frontend/postcss.config.js @@ -1,6 +1,29 @@ -export default { - plugins: { - tailwindcss: {}, - autoprefixer: {}, +import autoprefixer from 'autoprefixer' +import tailwindcss from 'tailwindcss' + +// Apply the same breakpoints to handwritten CSS and Tailwind utilities. +export const inlineBreakpoints = { + postcssPlugin: 'inline-breakpoints', + OnceExit (root) { + const queries = [] + root.walkAtRules ('media', query => { + if (/\b(?:min|max)-width\s*:/.test (query.params)) + queries.push (query) + }) + for (const query of queries) { + const vertical = query.cloneAfter ({ + params: query.params.replace (/\b(min|max)-width(?=\s*:)/g, '$1-height') }) + for (const [branch, scope] of [ + [query, ':where(html:not(:lang(mn-Mong)))'], + [vertical, ':where(html:lang(mn-Mong))']]) { + branch.walkRules (rule => { + rule.selectors = rule.selectors.map (selector => `${scope} ${selector}`) + }) + } + } }, } + +export default { + plugins: [tailwindcss (), inlineBreakpoints, autoprefixer ()], +} diff --git a/frontend/postcss.config.test.js b/frontend/postcss.config.test.js new file mode 100644 index 0000000..266b407 --- /dev/null +++ b/frontend/postcss.config.test.js @@ -0,0 +1,52 @@ +import postcss from 'postcss' +import tailwindcss from 'tailwindcss' +import { describe, expect, it } from 'vitest' + +import { inlineBreakpoints } from './postcss.config.js' + +describe ('inline breakpoints', () => { + it ('uses height for vertical menus and width for horizontal menus', async () => { + const source = ` + @media (min-width: 768px) { .menu, .account { display: flex } } + @media (min-width: 1024px) { .menu { font-size: 1rem } } + @media (max-width: 767px) { .mobile { display: block } } + @media (prefers-reduced-motion: reduce) { .menu { transition: none } } + ` + const { css } = await postcss ([inlineBreakpoints]).process ( + source, { from: 'menus.css' }) + const rules = [] + postcss.parse (css).walkRules (rule => { + rules.push ([rule.parent.params, rule.selector]) + }) + + expect (rules).toEqual ([ + ['(min-width: 768px)', + ':where(html:not(:lang(mn-Mong))) .menu, ' + + ':where(html:not(:lang(mn-Mong))) .account'], + ['(min-height: 768px)', + ':where(html:lang(mn-Mong)) .menu, :where(html:lang(mn-Mong)) .account'], + ['(min-width: 1024px)', ':where(html:not(:lang(mn-Mong))) .menu'], + ['(min-height: 1024px)', ':where(html:lang(mn-Mong)) .menu'], + ['(max-width: 767px)', ':where(html:not(:lang(mn-Mong))) .mobile'], + ['(max-height: 767px)', ':where(html:lang(mn-Mong)) .mobile'], + ['(prefers-reduced-motion: reduce)', '.menu']]) + }) + + it ('includes responsive utilities generated by Tailwind', async () => { + const { css } = await postcss ([tailwindcss ({ + content: [{ raw: '
' }], + corePlugins: { preflight: false } }), inlineBreakpoints]).process ( + '@tailwind utilities;', { from: 'utilities.css' }) + const rules = [] + postcss.parse (css).walkRules (rule => { + rules.push ([rule.parent.params, rule.selector]) + }) + + expect (rules).toContainEqual ([ + '(min-width: 768px)', ':where(html:not(:lang(mn-Mong))) .md\\:hidden']) + expect (rules).toContainEqual ([ + '(min-height: 768px)', ':where(html:lang(mn-Mong)) .md\\:hidden']) + expect (rules).toContainEqual ([ + '(min-height: 1024px)', ':where(html:lang(mn-Mong)) .lg\\:text-base']) + }) +}) diff --git a/frontend/src/components/ResponsiveMarqueeText.tsx b/frontend/src/components/ResponsiveMarqueeText.tsx index 0960730..a703d2a 100644 --- a/frontend/src/components/ResponsiveMarqueeText.tsx +++ b/frontend/src/components/ResponsiveMarqueeText.tsx @@ -1,7 +1,9 @@ -import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings' -import { cn } from '@/lib/utils' import { useEffect, useRef, useState } from 'react' +import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings' +import { useVerticalPreview } from '@/lib/useLocale' +import { cn } from '@/lib/utils' + import type { FC } from 'react' type Props = { @@ -11,7 +13,7 @@ type Props = { title?: string } const DESKTOP_MARQUEE_MEDIA = - '(min-width: 768px) and (hover: hover) and (pointer: fine) and (prefers-reduced-motion: no-preference)' + '(hover: hover) and (pointer: fine) and (prefers-reduced-motion: no-preference)' const MARQUEE_START_DELAY_MS = 1600 const MARQUEE_END_HOLD_MS = 2400 const MARQUEE_SCROLL_PX_PER_SECOND = 43 @@ -21,6 +23,7 @@ const MIN_MARQUEE_OVERFLOW_PX = 1 const ResponsiveMarqueeText: FC