diff --git a/next.config.js b/next.config.js index 946f1b693..7b44c6282 100644 --- a/next.config.js +++ b/next.config.js @@ -25,11 +25,14 @@ const dlv = require('dlv') const Prism = require('prismjs') const fallbackLayouts = { - 'src/pages/docs/**/*': ['@/layouts/DocumentationLayout', 'DocumentationLayout'], + // 'src/pages/docs/**/*': ['@/layouts/DocumentationLayout', 'DocumentationLayout'], + 'src/pages/zh/docs/**/*': ['@/layouts/.ZH/DocumentationLayout', 'DocumentationLayout'], } const fallbackDefaultExports = { - 'src/pages/{docs,components}/**/*': ['@/layouts/ContentsLayout', 'ContentsLayout'], + // 'src/pages/{docs,components}/**/*': ['@/layouts/ContentsLayout', 'ContentsLayout'], + 'src/pages/components/.ZH/**/*': ['@/layouts/.ZH/ContentsLayout', 'ContentsLayout'], + 'src/pages/zh/docs/**/*': ['@/layouts/.ZH/ContentsLayout', 'ContentsLayout'], 'src/pages/blog/**/*': ['@/layouts/BlogPostLayout', 'BlogPostLayout'], } diff --git a/src/components/.ZH/ChineseCategory.js b/src/components/.ZH/ChineseCategory.js new file mode 100644 index 000000000..1de819a55 --- /dev/null +++ b/src/components/.ZH/ChineseCategory.js @@ -0,0 +1,21 @@ +export default { + 'Getting Started': "起手式", + 'Core Concepts': "核心概念", + 'Customization': "客製化", + 'Community': "社群", + 'Base Styles':"基底樣式", + 'Flexbox & Grid':'Flexbox & Grid', + 'Spacing':'Spacing', + 'Sizing':'Sizing', + 'Typography':'Typography', + 'Backgrounds':'Backgrounds', + 'Borders':'Borders', + 'Effects':'Effects', + 'Filters':'Filters', + 'Transitions & Animation':'Transitions & Animation', + 'Transforms':'Transforms', + 'Interactivity':'Interactivity', + 'SVG':'SVG', + 'Accessibility':'Accessibility', + 'Official Plugins':'Official Plugins', +} \ No newline at end of file diff --git a/src/components/.ZH/Footer.js b/src/components/.ZH/Footer.js new file mode 100644 index 000000000..150bd6f8c --- /dev/null +++ b/src/components/.ZH/Footer.js @@ -0,0 +1,91 @@ +import Link from 'next/link' +import clsx from 'clsx' + +export function Footer({ children, previous, next }) { + return ( + + ) +} diff --git a/src/components/.ZH/Header.js b/src/components/.ZH/Header.js new file mode 100644 index 000000000..0980bd057 --- /dev/null +++ b/src/components/.ZH/Header.js @@ -0,0 +1,277 @@ +import Link from 'next/link' +import { VersionSwitcher } from '@/components/VersionSwitcher' +import { SearchButton } from '@/components/Search' +// import { SearchButton } from '@/components/.ZH/Search' +import Router from 'next/router' +import { Logo } from '@/components/Logo' +import { Dialog } from '@headlessui/react' +import { useEffect, useState } from 'react' +import clsx from 'clsx' +import chineseCategory from '@/components/.ZH/ChineseCategory' + +function Featured() { + return ( + + + Tailwind CSS v3.0 + + + {/* Just-in-Time all the time, colored shadows, scroll snap and more */} + 全時段的 JIT 模式,還有陰影色彩、捲動吸附以及更多新功能 + + + + + ) +} + +export function NavPopover() { + let [isOpen, setIsOpen] = useState(false) + + useEffect(() => { + if (!isOpen) return + function handleRouteChange() { + setIsOpen(false) + } + Router.events.on('routeChangeComplete', handleRouteChange) + return () => { + Router.events.off('routeChangeComplete', handleRouteChange) + } + }, [isOpen]) + + return ( + <> + + + +
+ +
    + +
+
+
+ + ) +} + +export function NavItems() { + return ( + <> +
  • + + {/* Docs */} + 技術文件 + +
  • +
  • + + {/* Components */} + 元件庫 + +
  • +
  • + + {/* Blog */} + 部落格 + +
  • +
  • + + Tailwind CSS on GitHub + + +
  • + + ) +} + +export function Header({ hasNav = false, navIsOpen, onNavToggle, title, section }) { + let [isOpaque, setIsOpaque] = useState(false) + + useEffect(() => { + let offset = 50 + function onScroll() { + if (!isOpaque && window.scrollY > offset) { + setIsOpaque(true) + } else if (isOpaque && window.scrollY <= offset) { + setIsOpaque(false) + } + } + window.addEventListener('scroll', onScroll, { passive: true }) + return () => { + window.removeEventListener('scroll', onScroll, { passive: true }) + } + }, [isOpaque]) + + return ( + <> +
    +
    + + + + +
    +
    +
    +
    +
    +
    + + { + e.preventDefault() + Router.push('/zh/brand') + }} + > + {/* Tailwind CSS home page */} + Tailwind CSS 首頁 + + + + + + + + {/* Search */} + 搜尋 + + +
    + +
    +
    +
    + {hasNav && ( +
    + + {title && ( +
      + {section && ( +
    1. + {/* {section} */} + {chineseCategory[section]} + +
    2. + )} +
    3. {title}
    4. +
    + )} +
    + )} +
    +
    + + ) +} diff --git a/src/components/.ZH/Heading.js b/src/components/.ZH/Heading.js new file mode 100644 index 000000000..aae845bad --- /dev/null +++ b/src/components/.ZH/Heading.js @@ -0,0 +1,80 @@ +import { useEffect, useContext, useRef } from 'react' +import { ContentsContext } from '@/layouts/.ZH/ContentsLayout' +import { useTop } from '@/hooks/useTop' +import clsx from 'clsx' + +export function Heading({ + level, + id, + children, + number, + badge, + className = '', + hidden = false, + ignore = false, + style = {}, + nextElement, + ...props +}) { + let Component = `h${level}` + const context = useContext(ContentsContext) + + let ref = useRef() + let top = useTop(ref) + + useEffect(() => { + if (!context) return + if (typeof top !== 'undefined') { + context.registerHeading(id, top) + } + return () => { + context.unregisterHeading(id) + } + }, [top, id, context?.registerHeading, context?.unregisterHeading]) + + return ( + + {!hidden && ( + + ​ +
    + +
    +
    + )} + {number && ( + + {number} + + )} + {children} + {badge && ( + + {badge} + + )} +
    + ) +} diff --git a/src/components/.ZH/HtmlZenGarden.js b/src/components/.ZH/HtmlZenGarden.js new file mode 100644 index 000000000..74dc80b89 --- /dev/null +++ b/src/components/.ZH/HtmlZenGarden.js @@ -0,0 +1,815 @@ +import { AnimateSharedLayout, motion } from 'framer-motion' +// import { font as pallyVariable } from '../fonts/generated/Pally-Variable.module.css' +// import { font as synonymVariable } from '../fonts/generated/Synonym-Variable.module.css' +// import { font as sourceSerifProRegular } from '../fonts/generated/SourceSerifPro-Regular.module.css' +// import { font as ibmPlexMonoRegular } from '../fonts/generated/IBMPlexMono-Regular.module.css' +// import { font as ibmPlexMonoSemiBold } from '../fonts/generated/IBMPlexMono-SemiBold.module.css' +import { font as pallyVariable } from '../../fonts/generated/Pally-Variable.module.css' +import { font as synonymVariable } from '../../fonts/generated/Synonym-Variable.module.css' +import { font as sourceSerifProRegular } from '../../fonts/generated/SourceSerifPro-Regular.module.css' +import { font as ibmPlexMonoRegular } from '../../fonts/generated/IBMPlexMono-Regular.module.css' +import { font as ibmPlexMonoSemiBold } from '../../fonts/generated/IBMPlexMono-SemiBold.module.css' +import { usePrevious } from '@/hooks/usePrevious' +import { useCallback, useEffect, useRef, useState } from 'react' +import debounce from 'debounce' +import dlv from 'dlv' +import { fit } from '@/utils/fit' +import clsx from 'clsx' +import colors from 'tailwindcss/colors' + +const themes = { + Simple: { + wrapper: { borderRadius: 12 }, + container: '', + image: { + width({ containerWidth, col }, css = false) { + if (!containerWidth) return 192 + if (css) { + return col ? '100%' : '192px' + } else { + return col ? containerWidth : 192 + } + }, + height({ containerWidth, col }) { + if (!containerWidth) return 305 + return col ? 191 : 305 + }, + borderRadius: [ + [8, 8, 0, 0], + [8, 8, 0, 0], + [8, 0, 0, 8], + ], + src: require('@/img/classic-utility-jacket.jpg').default, + originalWidth: 1200, + originalHeight: 1600, + }, + contentContainer: 'p-6', + header: '-mt-6 pt-6 pb-6', + heading: 'flex-auto', + stock: 'flex-none w-full mt-2', + button: { + grid: ['1fr auto', '1fr 1fr auto', 'auto auto 1fr'], + height: 42, + borderRadius: 8, + className: 'px-6', + primary: { + class: ['col-span-2', '', ''], + backgroundColor: colors.gray[900], + text: 'text-white font-semibold', + }, + secondary: { + backgroundColor: colors.white, + borderColor: colors.gray[200], + text: 'text-gray-900 font-semibold', + }, + like: { + color: colors.gray[300], + }, + }, + size: { + container: '', + list: 'space-x-3', + button: { + activeFont: 'font-semibold', + size: 38, + borderRadius: 8, + color: colors.gray[700], + activeBackgroundColor: colors.gray[900], + activeColor: colors.white, + }, + }, + smallprint: { + container: ['mt-6', 'mt-6', 'mt-6 mb-1'], + inner: 'text-sm text-gray-700', + }, + }, + Playful: { + wrapper: { borderRadius: 12 }, + // container: ['p-6', 'p-6', ''], + image: { + width({ containerWidth, col }, css = false) { + if (!containerWidth) return 224 + if (css) { + return col ? 'calc(100% + 1rem)' : '224px' + } else { + return col ? containerWidth + 16 : 224 + } + }, + height({ containerWidth, col }) { + if (!containerWidth) return 305 + 16 + return col ? 191 : 305 + 16 + }, + borderRadius: 8, + src: require('@/img/kids-jumper.jpg').default, + originalWidth: 1200, + originalHeight: 1700, + className: ['-mt-2 -mx-2', '-mt-2 -mx-2', '-my-2 -ml-2'], + }, + contentContainer: 'p-6', + header: ['pb-4', 'pb-4', '-mt-6 pt-6 pb-4'], + heading: 'flex-auto', + price: 'mt-2 w-full flex-none order-1', + stock: 'flex-none ml-3', + button: { + grid: ['1fr auto', '1fr 1fr auto', 'auto auto 1fr'], + height: 46, + borderRadius: 46 / 2, + className: 'px-6', + primary: { + class: ['col-span-2', '', ''], + backgroundColor: colors.violet[600], + text: `text-base text-white font-medium ${pallyVariable}`, + }, + secondary: { + backgroundColor: colors.white, + borderColor: colors.gray[200], + text: `text-base text-gray-900 font-medium ${pallyVariable}`, + }, + like: { + color: colors.violet[600], + backgroundColor: colors.violet[50], + borderColor: colors.violet[50], + }, + }, + size: { + container: '', + list: 'space-x-3', + button: { + font: `font-bold ${pallyVariable}`, + size: 38, + borderRadius: 38 / 2, + borderColor: colors.white, + color: colors.violet[400], + activeBackgroundColor: colors.violet[600], + activeBorderColor: colors.violet[600], + activeColor: colors.white, + }, + }, + smallprint: { container: 'mt-5', inner: `text-sm ${pallyVariable}` }, + }, + Elegant: { + wrapper: { borderRadius: 0 }, + container: 'p-1.5', + image: { + width({ containerWidth, col }, css = false) { + if (!containerWidth) return 210 + if (css) { + return col ? '100%' : '210px' + } else { + return col ? containerWidth - 12 : 210 + } + }, + height({ containerWidth, col }) { + if (!containerWidth) return 305 - 12 + return col ? 177 : 305 - 12 + }, + borderRadius: 0, + src: require('@/img/fancy-suit-jacket.jpg').default, + originalWidth: 1200, + originalHeight: 2128, + }, + contentContainer: ['p-6 pt-0 -mx-1.5 -mb-1.5', 'p-6 pt-0 -mx-1.5 -mb-1.5', 'p-6 pt-0 -my-1.5'], + header: 'py-6', + heading: 'w-full flex-none mb-3', + stock: 'flex-none ml-auto', + button: { + grid: ['1fr auto', '1fr 1fr auto', '1fr 1fr auto'], + height: 46, + borderRadius: 0, + primary: { + class: ['col-span-2', '', ''], + backgroundColor: colors.gray[900], + text: `text-white font-medium tracking-wide uppercase ${synonymVariable}`, + }, + secondary: { + backgroundColor: colors.white, + borderColor: colors.gray[200], + text: `text-gray-900 font-medium tracking-wide uppercase ${synonymVariable}`, + }, + like: { + color: colors.gray[300], + }, + }, + size: { + container: '', + button: { + font: `font-medium ${synonymVariable}`, + size: 32, + borderRadius: 32 / 2, + color: colors.gray[500], + activeBackgroundColor: colors.gray[100], + activeColor: colors.gray[900], + }, + }, + smallprint: { + container: 'mt-[1.375rem]', + inner: `text-sm ${synonymVariable}`, + }, + }, + Brutalist: { + wrapper: { borderRadius: 0 }, + container: ['p-4 pb-6', 'p-4 pb-6', 'p-6 pb-[1.0625rem]'], + image: { + width({ containerWidth, col }, css = false) { + if (!containerWidth) return 184 + if (css) { + return col ? '100%' : '184px' + } else { + return col ? containerWidth - 32 : 184 + } + }, + height({ containerWidth, col }) { + if (!containerWidth) return 224 + return col ? 160 : 224 + }, + borderRadius: 0, + src: require('@/img/retro-shoe.jpg').default, + originalWidth: 1200, + originalHeight: 1772, + }, + contentContainer: ['px-2', 'px-2', 'pl-8'], + header: ['py-6', 'py-6', '-mt-6 py-6'], + heading: 'w-full flex-none mb-2', + stock: 'flex-auto ml-3', + button: { + grid: ['1fr auto', '1fr 1fr auto', 'auto auto 1fr'], + width: 30, + height: 46, + borderRadius: 0, + className: 'px-6', + primary: { + class: ['col-span-2', '', ''], + backgroundColor: colors.teal[400], + borderColor: colors.black, + text: `text-base text-black uppercase ${ibmPlexMonoSemiBold}`, + }, + secondary: { + backgroundColor: colors.white, + borderColor: colors.gray[200], + text: `text-base text-black uppercase ${ibmPlexMonoSemiBold}`, + }, + like: { + container: ' ', + className: 'justify-center', + color: colors.black, + borderColor: colors.white, + }, + }, + size: { + container: 'my-6', + list: 'space-x-3', + button: { + font: ibmPlexMonoRegular, + size: 42, + borderRadius: 0, + color: colors.black, + activeBackgroundColor: colors.black, + activeColor: colors.white, + }, + }, + smallprint: { + container: 'mt-4', + inner: `${ibmPlexMonoRegular} text-xs leading-6`, + }, + }, +} + +const imageAnimationVariants = { + visible: { opacity: [0, 1], zIndex: 2 }, + prev: { zIndex: 1 }, + hidden: { zIndex: 0 }, +} + +export function HtmlZenGarden({ theme }) { + const prevTheme = usePrevious(theme) + const [{ width: containerWidth, col, above }, setContainerState] = useState({ + width: 0, + col: false, + }) + const containerRef = useRef() + + const updateWidth = useCallback( + debounce(() => { + if (!containerRef.current) return + const newWidth = Math.round(containerRef.current.getBoundingClientRect().width) + const newCol = + window.innerWidth < 640 + ? 'sm' + : window.innerWidth >= 1024 && window.innerWidth < 1280 + ? 'lg' + : false + const newAbove = window.innerWidth < 1024 + if (newWidth !== containerWidth || newCol !== col || newAbove !== above) { + setContainerState({ width: newWidth, col: newCol, above: newAbove }) + } + }, 300) + ) + + useEffect(() => { + const observer = new window.ResizeObserver(updateWidth) + observer.observe(containerRef.current) + updateWidth() + return () => { + observer.disconnect() + } + }, [containerWidth, col, updateWidth]) + + const getThemeValue = (key, defaultValue) => { + const value = dlv(themes[theme], key, defaultValue) + return Array.isArray(value) ? value[col === 'sm' ? 0 : col === 'lg' ? 1 : 2] : value + } + + const getImageRadius = (key) => { + let radius = themes[theme].image.borderRadius + if (!Array.isArray(radius)) { + return { + borderTopLeftRadius: radius, + borderTopRightRadius: radius, + borderBottomRightRadius: radius, + borderBottomLeftRadius: radius, + } + } + if (Array.isArray(radius[0])) { + radius = radius[col === 'sm' ? 0 : col === 'lg' ? 1 : 2] + } + if (!Array.isArray(radius)) { + return { + borderTopLeftRadius: radius, + borderTopRightRadius: radius, + borderBottomRightRadius: radius, + borderBottomLeftRadius: radius, + } + } + return { + borderTopLeftRadius: radius[0], + borderTopRightRadius: radius[1], + borderBottomRightRadius: radius[2], + borderBottomLeftRadius: radius[3], + } + } + + return ( + +
    + {!containerWidth ? ( +
    + ) : ( + + +
    + + {Object.keys(themes).map((name, i) => ( + + ))} + + +
    +
    +
    + +
    + + {/* Classic Utility + Jacket */} + 經典機能性外套 + + + {/* Kids Jumpsuit */} + 兒童連身裝 + + + {/* Dogtooth Style Jacket */} + 韓版西裝外套 + + + {/* Retro Shoe */} + NIKE 復古版球鞋 + +
    +
    + + $110.00 + + + $39.00 + + + $350.00 + + + $89.00 + +
    +
    + + {/* In stock */} + 有現貨 + + + {/* In stock */} + 有現貨 + + + {/* In stock */} + 有現貨 + + + {/* In stock */} + 有現貨 + +
    +
    +
    + + {['XS', 'S', 'M', 'L', 'XL'].map((size) => ( + + {size === 'XS' && ( + + )} + + {Object.keys(themes).map((name) => ( + + {size === 'XS' && name === 'Brutalist' ? ( + <> + {/* + */} + {size} + + ) : ( + size + )} + + ))} + + ))} + +
    + +
    +
    + + {Object.keys(themes).map((name, i) => ( + + {/* Buy now */} + 直接購買 + + ))} + +
    +
    + + {Object.keys(themes).map((name, i) => ( + + {/* Add to bag */} + 加到購物車 + + ))} + +
    +
    + + + + + +
    +
    +
    + {Object.keys(themes).map((name) => ( + + + {/* Free shipping on all + continental US orders. */} + 全台保證 24h 到貨 + ,台北市 6h 到貨 + + + ))} +
    +
    +
    +
    + )} +
    + + ) +} diff --git a/src/components/.ZH/Tabs.js b/src/components/.ZH/Tabs.js new file mode 100644 index 000000000..b6c339802 --- /dev/null +++ b/src/components/.ZH/Tabs.js @@ -0,0 +1,57 @@ +export function Tabs({ tabs, selected, onChange, className, iconClassName }) { + return ( +
    +
      + {Object.entries(tabs).map(([name, icon]) => ( +
    • + +
    • + ))} +
    +
    + ) +} + +function chineseName(name) { + return { + // Utilities + 'Sizing': "尺寸", + 'Colors': "色彩", + 'Typography': "文字版式", + 'Shadows': "陰影", + + // Layouts + 'Simple': "簡約", + 'Playful': "生動", + 'Elegant': "典雅", + 'Brutalist': "狂野", + + // Features + 'CSS Grid': "CSS 網格", + 'Transforms': "變形", + 'Filters': "濾鏡", + }[name] +} \ No newline at end of file diff --git a/src/components/.ZH/Testimonials.js b/src/components/.ZH/Testimonials.js new file mode 100644 index 000000000..ea2f691e2 --- /dev/null +++ b/src/components/.ZH/Testimonials.js @@ -0,0 +1,988 @@ +import { useIsomorphicLayoutEffect } from '@/hooks/useIsomorphicLayoutEffect' +import clsx from 'clsx' +import { useEffect, useRef, useState } from 'react' +import { useInView } from 'react-intersection-observer' + +let testimonials = [ + // Column 1 + [ + { + content: 'I feel like an idiot for not using Tailwind CSS until now.', + url: 'https://twitter.com/ryanflorence/status/1187951799442886656', + author: { + name: 'Ryan Florence', + role: 'Remix & React Training', + avatar: require('@/img/avatars/ryan-florence.jpg').default, + }, + }, + { + content: + 'If I had to recommend a way of getting into programming today, it would be HTML + CSS with Tailwind CSS.', + url: 'https://twitter.com/rauchg/status/1225611926320738304', + author: { + name: 'Guillermo Rauch', + role: 'Vercel', + avatar: require('@/img/avatars/guillermo-rauch.jpg').default, + }, + }, + { + content: `I have no design skills and with Tailwind I can actually make good looking websites with ease and it's everything I ever wanted in a CSS framework.`, + author: { + name: 'Sara Vieira', + role: 'CodeSandbox', + avatar: require('@/img/avatars/sara-vieira.jpg').default, + }, + }, + { + content: `Tailwind CSS is the greatest CSS framework on the planet.`, + url: 'https://twitter.com/taylorotwell/status/1106539049202999296', + author: { + name: 'Bret "The Hitman" Hart', + role: 'Former WWE Champion', + avatar: require('@/img/avatars/bret-hart.jpg').default, + }, + }, + { + content: `I started using @tailwindcss. I instantly fell in love with their responsive modifiers, thorough documentation, and how easy it was customizing color palettes.`, + url: 'https://twitter.com/dacey_nolan/status/1303744545587441666', + author: { + name: 'Dacey Nolan', + role: 'Software Engineer', + avatar: require('@/img/avatars/dacey-nolan.jpg').default, + }, + }, + + { + content: 'Loved it the very moment I used it.', + url: 'https://twitter.com/GTsurwa/status/1304226774491033601', + author: { + name: 'Gilbert Rabut Tsurwa', + role: 'Web Developer', + avatar: require('@/img/avatars/gilbert-rabut-tsurwa.jpg').default, + }, + }, + { + content: + 'There’s one thing that sucks about @tailwindcss - once you’ve used it on a handful of projects it is a real pain in the ass to write normal CSS again.', + url: 'https://twitter.com/iamgraem_e/status/1322861404781748228', + author: { + name: 'Graeme Houston', + role: 'JavaScript Developer', + avatar: require('@/img/avatars/graeme-houston.jpg').default, + }, + }, + + { + content: `Okay, I’m officially *all* in on the @tailwindcss hype train. Never thought building websites could be so ridiculously fast and flexible.`, + url: 'https://twitter.com/lukeredpath/status/1316543571684663298', + author: { + name: 'Aaron Bushnell', + role: 'Programmer @ TrendyMinds', + avatar: require('@/img/avatars/aaron-bushnell.jpg').default, + }, + }, + { + content: 'Okay, @tailwindcss just clicked for me and now I feel like a #!@%&$% idiot.', + url: 'https://twitter.com/ken_wheeler/status/1225373231139475458', + author: { + name: 'Ken Wheeler', + role: `React Engineer`, + avatar: require('@/img/avatars/ken-wheeler.jpg').default, + }, + }, + { + content: `I've been using @tailwindcss the past few months and it's amazing. I already used some utility classes before, but going utility-first... this is the way.`, + url: 'https://twitter.com/JadLimcaco/status/1327417021915561984', + author: { + name: 'Jad Limcaco', + role: 'Designer', + avatar: require('@/img/avatars/jad-limcaco.jpg').default, + }, + }, + { + content: `After finally getting to use @tailwindcss on a real client project in the last two weeks I never want to write CSS by hand again. I was a skeptic, but the hype is real.`, + url: 'https://twitter.com/lukeredpath/status/1316543571684663298', + author: { + name: 'Luke Redpath', + role: 'Ruby & iOS Developer', + avatar: require('@/img/avatars/luke-redpath.jpg').default, + }, + }, + { + content: + "I didn't think I was going to like @tailwindcss... spent a day using it for a POC, love it! I wish this had been around when we started our company design system, seriously considering a complete rebuild", + url: 'https://twitter.com/JonBloomer/status/1300923818622377984', + author: { + name: 'Jon Bloomer', + role: 'Front-End Developer', + avatar: require('@/img/avatars/jon-bloomer.jpg').default, + }, + }, + { + content: '@tailwindcss looked unpleasant at first, but now I’m hooked on it.', + url: 'https://twitter.com/droidgilliland/status/1222733372855848961', + author: { + name: 'Andrew Gilliland', + role: 'Front-End Developer', + avatar: require('@/img/avatars/andrew-gilliland.jpg').default, + }, + }, + + // New 1 + { + content: 'Once you start using tailwind, there is no going back.', + url: 'https://twitter.com/pkspyder007/status/1463126688301158400', + author: { + name: 'Praveen Kumar', + avatar: require('@/img/avatars/pkspyder007.jpg').default, + }, + }, + { + content: + 'I use @tailwindcss for every single project because it removes most of the annoyances of css and is multiple times quicker', + url: 'https://twitter.com/fanduvasu/status/1443396529558011904', + author: { + name: 'Vasu Bansal', + avatar: require('@/img/avatars/fanduvasu.jpg').default, + }, + }, + { + content: + "It's changed the trajectory of my business. I'm able to design better looking, better performing, and more accessible components in 1/3 of the time.", + url: 'https://twitter.com/lawjolla/status/1443295146959728643', + author: { + name: 'Dennis Walsh', + avatar: require('@/img/avatars/lawjolla.jpg').default, + }, + }, + { + content: + 'My first tailwind project worked great but what really kicked ass was going back to it months later and saving so much time making new changes. I knew how everything fit together instantly.', + url: 'https://twitter.com/ericlbarnes/status/1303814860879343618', + author: { + name: 'Eric L. Barnes', + avatar: require('@/img/avatars/ericlbarnes.jpg').default, + }, + }, + { + content: + "Tailwind looked like pure spaghetti until I used it in a real project. Now it's the only way I make websites. Simple, fast, scalable.", + url: 'https://twitter.com/nicksaraev/status/1304200875758428160', + author: { + name: 'Nick Saraev', + avatar: require('@/img/avatars/nicksaraev.jpg').default, + }, + }, + { + content: + 'Tailwind is a classic example of why you need to put preconceptions aside when evaluating tech. The experience and productivity is streets ahead of what you might have believed based on old school CSS thinking!', + url: 'https://twitter.com/_lukebennett/status/1303744015943204867', + author: { + name: 'Luke Bennett', + avatar: require('@/img/avatars/_lukebennett.jpg').default, + }, + }, + { + content: + 'TailwindCSS is a framework like no other. Rather than constraining you to a set design, it gives you the tools and the standardization to build exactly what you want.', + url: 'https://twitter.com/carre_sam/status/1303750185663770625', + author: { + name: 'Sam Carré', + avatar: require('@/img/avatars/carre_sam.jpg').default, + }, + }, + { + content: + 'I remember being horrified the first time I saw utility first css. But these past months using Tailwind on an increasing number of projects has just been a joyful new way to build things on the web.', + url: 'https://twitter.com/evanfuture/status/1303743551583514637', + author: { + name: 'Evan Payne', + avatar: require('@/img/avatars/evanfuture.jpg').default, + }, + }, + { + content: + "I was initially skeptical as I began using @tailwindcss, until I now needed to copy a @sveltejs component to a different location and I didn't need to worry about any of my styles breaking.", + url: 'https://twitter.com/rotimi_best/status/1407010180760539136', + author: { + name: 'Rotimi Best', + avatar: require('@/img/avatars/rotimi_best.jpg').default, + }, + }, + { + content: '@tailwindcss makes you better at CSS. Change my mind.', + url: 'https://twitter.com/Dominus_Kelvin/status/1362891692634963973', + author: { + name: 'Kelvin Omereshone', + avatar: require('@/img/avatars/Dominus_Kelvin.jpg').default, + }, + }, + { + content: + "Awesome stuff! I'm no designer or front-end developer; until I found Tailwind last year I hadn't done any CSS since the early nineties. Tailwind, and Tailwind UI mean I can now create good looking front ends quickly, which is super empowering. Crazy impressive project.", + url: 'https://twitter.com/JCMagoo/status/1443334891706454018', + author: { + name: 'John W Clarke', + avatar: require('@/img/avatars/JCMagoo.jpg').default, + }, + }, + { + content: + 'I admit I was a big skeptic of @tailwindcss until last year. I thought "why would I ever type a million classes that just abstract single CSS properties?" By now, I feel like I\'m twice as productive when building UIs. It\'s really amazing.', + url: 'https://twitter.com/tweetsofsumit/status/1460171778337083394', + author: { + name: 'Sumit Kumar', + avatar: require('@/img/avatars/tweetsofsumit.jpg').default, + }, + }, + { + content: + 'I\'m nearing completion on my months-long project of rewriting my company\'s frontend in TypeScript and @tailwindcss. Still, every time I re-implement a component, I think, "Wow, that was way easier this time." Tailwind rocks.', + url: 'https://twitter.com/mannieschumpert/status/1445868384869134336', + author: { + name: 'Mannie Schumpert', + role: 'Co-Founder/CTO @LaunchPathInc', + avatar: require('@/img/avatars/mannieschumpert.jpg').default, + }, + }, + + // New 2 + // { + // content: + // 'As a lazy developer, I love that I can copy/paste HTML examples that use Tailwind CSS and it just works in my app.', + // url: 'https://twitter.com/adamwathan/status/1468648955240230918', + // author: { + // name: 'Mark Funk', + // role: 'UI Engineer at Netflix', + // avatar: require('@/img/avatars/.jpg').default, + // }, + // }, + { + content: + 'With the amount of shipping we have to do, skipping the conversion of brainwaves to CSS, and being able to implement at the speed of thought using Tailwind, my life as a fullstack developer has never been more blissful.', + url: 'https://twitter.com/0xholman/status/1468691614453227523', + author: { + name: 'Christian Holman', + avatar: require('@/img/avatars/0xholman.jpg').default, + }, + }, + { + content: + 'Tailwind makes it easy to bring new developers into the frontend project without having to worry about the mental exercise of understanding ‘some’ developer’s class hierarchy and thought process behind it.', + url: 'https://twitter.com/jilsonthomas/status/1468678743644327942', + author: { + name: 'Jilson Thomas', + role: 'UI Designer/Developer', + avatar: require('@/img/avatars/jilsonthomas.jpg').default, + }, + }, + { + content: + 'Tailwind has been a total game-changer for our dev team. It allows us to move faster, keep our UI consistent, and focus on the work we want to do instead of writing CSS.', + url: 'https://twitter.com/jakeryansmith/status/1468668828041293831', + author: { + name: 'Jake Ryan Smith', + role: 'Full-Stack Developer', + avatar: require('@/img/avatars/jakeryansmith.jpg').default, + }, + }, + ], + // Column 2 + [ + { + content: + 'Have been working with CSS for over ten years and Tailwind just makes my life easier. It is still CSS and you use flex, grid, etc. but just quicker to write and maintain.', + url: 'https://twitter.com/debs_obrien/status/1243255468241420288', + author: { + name: `Debbie O'Brien`, + role: 'Head of Learning @ Nuxt.js', + avatar: require('@/img/avatars/debbie-obrien.jpg').default, + }, + }, + { + content: + 'I’ve been writing CSS for over 20 years, and up until 2017, the way I wrote it changed frequently. It’s not a coincidence Tailwind was released the same year. It might look wrong, but spend time with it and you’ll realize semantic CSS was a 20 year mistake.', + url: 'https://twitter.com/frontendben/status/1468687028036452363', + author: { + name: 'Ben Furfie', + role: 'Developer', + avatar: require('@/img/avatars/frontendben.jpg').default, + }, + }, + { + content: 'Tailwind makes writing code feel like I’m using a design tool.', + url: 'https://twitter.com/didiercatz/status/1468657403382181901', + author: { + name: 'Didier Catz', + role: 'Co-Founder @StyptApp', + avatar: require('@/img/avatars/didiercatz.jpg').default, + }, + }, + { + content: + 'Tailwind CSS is bridging the gap between design systems and products. It’s becoming the defacto API for styling.', + url: 'https://twitter.com/frontstuff_io/status/1468667263532339204', + author: { + name: 'Sarah Dayan', + role: 'Staff Software Engineer @Algolia', + avatar: require('@/img/avatars/frontstuff_io.jpg').default, + }, + }, + { + content: 'I never want to write regular CSS again. Only @tailwindcss.', + url: 'https://twitter.com/trey/status/1457854984020762626', + author: { + name: 'Trey Piepmeier', + role: 'Web Developer', + avatar: require('@/img/avatars/trey.jpg').default, + }, + }, + { + content: + 'I came into my job wondering why the last dev would ever implement Tailwind into our projects, within days I was a Tailwind convert and use it for all of my personal projects.', + url: 'https://twitter.com/maddiexcampbell/status/1303752658029740032', + author: { + name: 'Madeline Campbell', + role: 'Full-Stack Developer', + avatar: require('@/img/avatars/madeline-campbell.jpg').default, + }, + }, + { + content: + 'Tailwind made me enjoy frontend development again and gave me the confidence that I can realize any design - no matter how complex it may be.', + url: 'https://twitter.com/marcelpociot/status/1468664587146956803', + author: { + name: 'Marcel Pociot', + role: 'CTO at @beyondcode', + avatar: require('@/img/avatars/marcelpociot.jpg').default, + }, + }, + { + content: 'Tailwind turned me into a complete stack developer.', + url: 'https://twitter.com/lepikhinb/status/1468665237155074056', + author: { + name: 'Boris Lepikhin', + role: 'Full-Stack Developer', + avatar: require('@/img/avatars/lepikhinb.jpg').default, + }, + }, + { + content: + "Tailwind is the easiest and simplest part of any project I work on. I can't imagine I'll build anything big without it.", + url: 'https://twitter.com/assertchris/status/1468651427664908292', + author: { + name: 'Christopher Pitt', + role: 'Developer', + avatar: require('@/img/avatars/assertchris.png').default, + }, + }, + { + content: + "Tailwind CSS has alleviated so many problems we've all become accustomed to with traditional CSS that it makes you wonder how you ever developed websites without it.", + url: 'https://twitter.com/ChaseGiunta/status/1468658689569665026', + author: { + name: 'Chase Giunta', + role: 'Developer', + avatar: require('@/img/avatars/ChaseGiunta.jpg').default, + }, + }, + { + content: + 'Having used other CSS frameworks, I always come back to Tailwind CSS as it gives me the ability to create a consistent and easy to use design system in my projects. Thanks to Tailwind CSS I only need one cup of coffee to get started on a new project.', + url: 'https://twitter.com/zaku_dev/status/1468666521895325697', + author: { + name: 'Ivan Guzman', + role: 'Software Engineer', + avatar: require('@/img/avatars/zaku_dev.png').default, + }, + }, + { + content: + 'I’ve been using TailwindCSS for many years, and yet they seem to still amaze us every year with the updates. It’s aided me in building websites super quickly, I could never go back to boring old CSS classes!', + url: 'https://twitter.com/heychazza', + author: { + name: 'Charlie Joseph', + role: 'Developer', + avatar: require('@/img/avatars/heychazza.jpg').default, + }, + }, + { + content: + 'Tailwind CSS is a design system implementation in pure CSS. It is also configurable. It gives developers super powers. It allows them to build websites with a clean consistent UI out of the box. It integrates well with any web dev framework because it‘s just CSS! Genius.', + url: 'https://twitter.com/kahliltweets/status/1468654856617476097', + author: { + name: 'Kahlil Lechelt', + role: 'JavaScript Developer', + avatar: require('@/img/avatars/kahliltweets.jpg').default, + }, + }, + { + content: + 'It’s super simple to slowly migrate to Tailwind from e.g. Bootstrap by using its prefix feature. Benefiting from its features while not having to spend a lot of time upfront is amazing!', + url: 'https://twitter.com/MarcoSinghof/status/1468654001772244993', + author: { + name: 'Marco Singhof', + role: 'Full-Stack Developer', + avatar: require('@/img/avatars/MarcoSinghof.jpg').default, + }, + }, + { + content: + 'I wasn’t comfortable using CSS until I met Tailwind. Its easy to use abstraction combined with excellent documentation are a game changer!', + url: 'https://twitter.com/joostmeijles/status/1468650757876555778', + author: { + name: 'Joost Meijles', + role: 'Head of Unplatform @avivasolutions', + avatar: require('@/img/avatars/joostmeijles.jpg').default, + }, + }, + { + content: + "Tailwind turns implementing designs from a chore to a joy. You'll fall in love with building for the web all over again.", + url: 'https://twitter.com/_swanson/status/1468653854199853057', + author: { + name: 'Matt Swanson', + role: 'Developer', + avatar: require('@/img/avatars/_swanson.jpg').default, + }, + }, + { + content: + 'Tailwind CSS helps you eject from the complexity of abstracting styles away. Having styles right there in your HTML is powerful, which gets even more obvious when using products like Tailwind UI.', + url: 'https://twitter.com/silvenon/status/1468676092504551433', + author: { + name: 'Matija Marohnić', + role: 'Front-End Developer', + avatar: require('@/img/avatars/silvenon.jpg').default, + }, + }, + { + content: + 'If Tailwind is like Tachyons on steroids, Tailwind UI is like Lance Armstrong winning the Tour de France (seven times). Without, of course, the scandal and shame.', + url: 'https://twitter.com/hughdurkin/status/1468658970848079872', + author: { + name: 'Hugh Durkin', + role: 'Developer', + avatar: require('@/img/avatars/hughdurkin.jpg').default, + }, + }, + { + content: + 'Being burned by other abandoned CSS frameworks, my biggest fear was to bet on yet another framework that may disappear. However, I gave it a try and couldn’t be happier. They keep improving the framework in meaningful ways on a regular basis. It feels very much alive.', + url: 'https://twitter.com/wolax/status/1468653118443470848', + author: { + name: 'Matthias Schmidt', + role: 'Programmer', + avatar: require('@/img/avatars/wolax.jpg').default, + }, + }, + { + content: + 'Getting buy-in on TailwindCSS from our entire team of developers took some time and discussion, but once we implemented company wide, it has made it a breeze for any developer to jump into any project and quickly make changes/enhancements.', + url: 'https://twitter.com/jerredchurst/status/1468657838494998530', + author: { + name: 'Jerred Hurst', + role: 'CTO Primitive', + avatar: require('@/img/avatars/jerredchurst.jpg').default, + }, + }, + { + content: + "Tailwind CSS has at the same time made CSS enjoyable and drastically changed how I build out products. It's rapid, efficient and an absolute joy to work with.", + url: 'https://twitter.com/braunshizzle/status/1468676003941830666', + author: { + name: 'Braunson Yager', + role: 'Full Stack Developer & Designer', + avatar: require('@/img/avatars/braunshizzle.jpg').default, + }, + }, + { + content: + 'Using any CSS framework other than Tailwind seems like a step backward in web development at this point. Absolutely nothing else comes close to making me as productive during the design phase of development than Tailwind.', + url: 'https://twitter.com/zac_zajdel/status/1468662057079914499', + author: { + name: 'Zac Jordan Zajdel', + role: 'Developer', + avatar: require('@/img/avatars/zac_zajdel.jpg').default, + }, + }, + { + content: + 'Tailwind has completely revolutionized our devops pipeline. The CLI works consistently no matter what framework is in place.', + url: 'https://twitter.com/joelvarty/status/1468671752356126728', + author: { + name: 'Joel Varty', + role: 'President & CTO @agilitycms', + avatar: require('@/img/avatars/joelvarty.jpg').default, + }, + }, + { + content: + 'Tailwind is like a really nice pair of socks. You think, “okay, how good can a pair of socks be”. Then you put socks on and you are like “%@#! these are socks”.', + url: 'https://twitter.com/NeilDocherty/status/1468668979698937859', + author: { + name: 'Neil Docherty', + role: 'Software Engineer', + avatar: require('@/img/avatars/NeilDocherty.jpg').default, + }, + }, + { + content: + 'Tailwind unified our css work across different client projects more than any other methodology, while letting us keep our bespoke designs, and even improved performance and stability of our sites.', + url: 'https://twitter.com/skttl/status/1468669231864725514', + author: { + name: 'Søren Kottal', + role: 'Developer', + avatar: require('@/img/avatars/skttl.jpg').default, + }, + }, + { + content: 'Tailwind is the only way to work with CSS at scale. ', + url: 'https://twitter.com/aarondfrancis/status/1468696321607544840', + author: { + name: 'Aaron Francis', + role: 'Developer', + avatar: require('@/img/avatars/aarondfrancis.jpg').default, + }, + }, + { + content: + "TailwindCSS has single-handedly been the biggest and most impactful change for our team's development workflow. I'm glad I live in a universe where Tailwind exists.", + url: 'https://twitter.com/Megasanjay/status/1468674483099557890', + author: { + name: 'Sanjay Soundarajan', + role: 'Front-End Developer', + avatar: require('@/img/avatars/Megasanjay.jpg').default, + }, + }, + { + content: + 'Tailwind solves a complex problem in an elegant way. It provides a ready-to-use UI, all while not compromising on enabling developers to quickly build anything imaginable.', + url: 'https://twitter.com/brentgarner/status/1468676369143926789', + author: { + name: 'Brent Garner', + role: 'Developer', + avatar: require('@/img/avatars/brentgarner.jpg').default, + }, + }, + ], + // Column 3 + [ + { + content: 'Skip to the end. Use @tailwindcss.', + url: 'https://twitter.com/kentcdodds/status/1468692023158796289', + author: { + name: 'Kent C. Dodds', + role: 'Developer and Educator', + avatar: require('@/img/avatars/kentcdodds.jpg').default, + }, + }, + { + content: + 'I was bad at front-end until I discovered Tailwind CSS. I have learnt a lot more about design and CSS itself after I started working with Tailwind. Creating web pages is 5x faster now.', + url: 'https://twitter.com/shrutibalasa', + author: { + name: 'Shruti Balasa', + role: 'Full Stack Web Developer & Tech Educator', + avatar: require('@/img/avatars/shrutibalasa.jpg').default, + }, + }, + { + content: "I don't use it but if I would use something I'd use Tailwind!", + url: 'https://twitter.com/levelsio/status/1288542608390856705', + author: { + name: 'Pieter Levels', + role: 'Maker', + avatar: require('@/img/avatars/levelsio.jpg').default, + }, + }, + { + content: + 'With Tailwind I can offer my clients faster turnaround times on custom WordPress themes, both for initial builds and for future revisions.', + url: 'https://twitter.com/gregsvn/status/1468667690042617857', + author: { + name: 'Greg Sullivan', + role: 'WordPress Developer', + avatar: require('@/img/avatars/gregsvn.jpg').default, + }, + }, + { + content: 'Thanks to @tailwindcss, CSS started to make sense to me.', + url: 'https://twitter.com/enunomaduro/status/1468650695104647170', + author: { + name: 'Nuno Maduro', + role: 'Core Team Member @laravelphp', + avatar: require('@/img/avatars/enunomaduro.jpg').default, + }, + }, + { + content: + "Tailwind clicked for me almost immediately. I can't picture myself writing another BEM class ever again. Happy user since the first public release! Productivity is at an all time high, thanks to @tailwindcss.", + url: 'https://twitter.com/igor_randj/status/1468654576576380930', + author: { + name: 'Igor Randjelovic', + role: 'Developer', + avatar: require('@/img/avatars/igor_randj.jpg').default, + }, + }, + { + content: + 'CSS has always been the hardest part of offering a digital service. It made me feel like a bad developer. Tailwind gives me confidence in web development again. Their docs are my first port of call.', + url: 'https://twitter.com/ohhdanm/status/1468653056988528643', + author: { + name: 'Dan Malone', + role: 'Founder of @mawla_io', + avatar: require('@/img/avatars/ohhdanm.jpg').default, + }, + }, + { + content: + 'I thought "Why would I need Tailwind CSS? I already know CSS and use Bootstrap", but after giving it a try I decided to switch all my projects to Tailwind.', + url: 'https://twitter.com/sertxudev/status/1468660429715030019', + author: { + name: 'Sergio Peris', + role: 'DevOps Engineer & Network Administrator', + avatar: require('@/img/avatars/sertxudev.jpg').default, + }, + }, + { + content: + 'The Tailwind docs are its real magic. It is actually better documented than CSS itself. It’s such a pleasure to use.', + url: 'https://twitter.com/zachknicker/status/1468662554658443264', + author: { + name: 'Zach Knickerbocker', + role: 'Developer', + avatar: require('@/img/avatars/zachknicker.jpg').default, + }, + }, + { + content: + "I've never felt more confident designing and styling websites and web apps than when I've used TailwindCSS.", + url: 'https://twitter.com/grossmeyer/status/1468671286415089666', + author: { + name: 'Glenn Meyer', + role: 'Developer', + avatar: require('@/img/avatars/grossmeyer.jpg').default, + }, + }, + { + content: + 'Tried it once, never looked back. Tailwindcss convert since 0.7 and it just keeps getting better and better.', + url: 'https://twitter.com/Jan_DHollander/status/1468653579405770754', + author: { + name: "Jan D'Hollander", + role: 'Front-End Developer', + avatar: require('@/img/avatars/Jan_DHollander.jpg').default, + }, + }, + { + content: + 'If you work at an agency and deal with hundreds of unique sites, each of them having their own custom CSS is a nightmare. Do your future self a favor and use Tailwind!', + url: 'https://twitter.com/waunakeesoccer1/status/1468736369757466625', + author: { + name: 'Andrew Brown', + avatar: require('@/img/avatars/waunakeesoccer1.jpg').default, + }, + }, + { + content: + 'Before Tailwind CSS I was banging my head against the wall trying to make sense of my CSS spaghetti. Now I am banging my head against the wall wondering why I didn’t try it earlier. My head hurts and my wall has a big hole in it. But at least using CSS is pleasant again!', + url: 'https://twitter.com/marckohlbrugge/status/1468731283400536071', + author: { + name: 'Marc Köhlbrugge', + avatar: require('@/img/avatars/marckohlbrugge.jpg').default, + }, + }, + { + content: + 'I was skeptical at first and resisted for a long time but after doing the first projects with Tailwind CSS this year, normal CSS just feels wrong and slow.', + url: 'https://twitter.com/davidhellmann/status/1468703979232272398', + author: { + name: 'David Hellmann', + role: 'Digital Designer & Developer', + avatar: require('@/img/avatars/davidhellmann.jpg').default, + }, + }, + { + content: + "After using Tailwind for the first time, I wondered why I used anything else. It's now my go-to CSS framework for any application, production or prototype.", + url: 'https://twitter.com/all_about_code/status/1468651643210240000', + author: { + name: 'Joshua Lowe', + role: 'Developer', + avatar: require('@/img/avatars/all_about_code.jpg').default, + }, + }, + { + content: + 'Tailwind not only made me able to focus on building great UI’s but it also improved my overall CSS skills by having such a wonderful docs site when I needed to handwrite CSS.', + url: 'https://twitter.com/joshmanders/status/1468710484396359681', + author: { + name: 'Josh Manders', + role: 'Developer', + avatar: require('@/img/avatars/joshmanders.jpg').default, + }, + }, + { + content: + 'Using Tailwind is an accelerant for rapid prototyping design systems. Strong documentation, helpful community, and instant results.', + url: 'https://twitter.com/igaenssley/status/1468674047328370690', + author: { + name: 'Ian Gaenssley', + role: 'Design Operations Lead at BetterCloud', + avatar: require('@/img/avatars/igaenssley.jpg').default, + }, + }, + { + content: + 'I instinctively hated utility CSS, but Tailwind completely converted me. It reignited my excitement for front-end development and implementing custom designs!', + url: 'https://twitter.com/jessarchercodes/status/1468743738545434626', + author: { + name: 'Jess Archer', + role: 'Full-Stack Developer', + avatar: require('@/img/avatars/jessarchercodes.png').default, + }, + }, + { + content: + 'Tailwind CSS bridges the gap between design and dev more than anything else. It reintroduces context to development, limits cognitive load with choice architecture, grants access to a token library out of the box and is incredibly easy to pickup. It helped my design career so much.', + url: 'https://twitter.com/CoreyGinnivan/status/1468698985435041794', + author: { + name: 'Corey Ginnivan', + role: 'Co-Founder of FeatureBoard', + avatar: require('@/img/avatars/CoreyGinnivan.jpg').default, + }, + }, + { + content: + "When I'm working on a project that isn't using Tailwind, first I yell, then I take a deep breath, then I run npm install tailwindcss.", + url: 'https://twitter.com/ryanchenkie/status/1468675898559840263', + author: { + name: 'Ryan Chenkie', + avatar: require('@/img/avatars/ryanchenkie.jpg').default, + }, + }, + { + content: + "Going back to a large website that doesn't use Tailwind is like hopping out of a Tesla and into my dad's rusted Minnesota farm truck. Sure, it works, but the clutch is slipping, the brakes barely work, and it's filled with old tires we're not even using anymore.", + url: 'https://twitter.com/dangayle/status/1468738215431467008', + author: { + name: 'Dan Gayle', + role: 'Senior Front-End Developer @CrateandBarrel', + avatar: require('@/img/avatars/dangayle.jpg').default, + }, + }, + { + content: + 'I pushed back hard at the mention of Tailwind initially due to the number of classes in my code however within 5 minutes or using it I was hooked and now am the annoying guy pushing Tailwind on anyone who will listen. It has simplified my dev workflow beyond measurement.', + url: 'https://twitter.com/dbrooking/status/1468718511040126982', + author: { + name: 'Dan Brooking', + role: 'Head Engineer @SubscriptionBox', + avatar: require('@/img/avatars/dbrooking.jpg').default, + }, + }, + { + content: + 'I never bothered to learn vanilla CSS because it’s a waste of time — why bother when I have Tailwind instead? Turns out I learned a ton of CSS anyway just by using Tailwind. It’s such a perfect middleground between thoughtful abstraction, while still letting you break free.', + url: 'https://twitter.com/TrevorCampbell_/status/1468739918662930432', + author: { + name: 'Trevor Campbell', + avatar: require('@/img/avatars/TrevorCampbell_.jpg').default, + }, + }, + { + content: + "Tailwind and the ecosystem around it is like a giant turbocharger for web agencies. It helps teams of developers and designers develop a shared language and system of constraints that speeds up the entire process. It's a game-changer for efficient teamwork.", + url: 'https://twitter.com/sagalbot/status/1468727120218103809', + author: { + name: 'Jeff Sagal', + role: 'Full-Stack Developer', + avatar: require('@/img/avatars/sagalbot.jpg').default, + }, + }, + { + content: + 'Tailwind provides the style of bespoke design, the constraint of a design system, and the flexibility to make it infinitely customizable, without being shoehorned into making every website look like it was cut from the same cloth.', + url: 'https://twitter.com/michaeldyrynda/status/1468720374657392645', + author: { + name: 'Michael Dyrynda', + role: 'Australian', + avatar: require('@/img/avatars/michaeldyrynda.jpg').default, + }, + }, + { + content: + 'Tailwind completely changed my freelance career by allowing me to build out completely custom designs really fast without writing any CSS.', + url: 'https://twitter.com/jasonlbeggs/status/1468666464911736835', + author: { + name: 'Jason Beggs', + role: 'Front-End Developer', + avatar: require('@/img/avatars/jasonlbeggs.jpg').default, + }, + }, + { + content: 'Using TailwindCSS will make you feel like you just unlocked a cheat code.', + url: 'https://twitter.com/dpaluy/status/1468678245327454211', + author: { + name: 'David Paluy', + role: 'CTO @Quartix', + avatar: require('@/img/avatars/dpaluy.png').default, + }, + }, + { + content: + 'Every developer I’ve convinced to give Tailwind a try has come back and said they are never going back. Every. Single. One.', + url: 'https://twitter.com/jacobgraf/status/1468931374245687298', + author: { + name: 'Jacob Graf', + role: 'Web Developer', + avatar: require('@/img/avatars/jacobgraf.jpg').default, + }, + }, + ], +] + +function Testimonial({ author, content, url, expanded }) { + let [focusable, setFocusable] = useState(true) + let ref = useRef() + + useEffect(() => { + if (ref.current.offsetTop !== 0) { + setFocusable(false) + } + }, []) + + return ( +
  • +
    +
    + {typeof content === 'string' ?

    {content}

    : content} +
    +
    + +
    +
    + {url ? ( + + + {author.name} + + ) : ( + author.name + )} +
    +
    {author.role}
    +
    +
    +
    +
  • + ) +} + +export function Testimonials() { + let ref = useRef() + let [expanded, setExpanded] = useState(false) + let [showCollapseButton, setShowCollapseButton] = useState(false) + let [transition, setTransition] = useState(false) + let { ref: inViewRef, inView } = useInView({ threshold: 0 }) + let initial = useRef(true) + + useIsomorphicLayoutEffect(() => { + if (initial.current) { + initial.current = false + return + } + if (expanded) { + ref.current.focus({ preventScroll: expanded }) + } else { + ref.current.focus() + ref.current.scrollIntoView() + } + if (expanded) { + setShowCollapseButton(false) + } + }, [expanded]) + + useEffect(() => { + setTimeout(() => setTransition(expanded), 0) + }, [expanded]) + + useEffect(() => { + if (!expanded || !inView) return + function onScroll() { + let bodyRect = document.body.getBoundingClientRect() + let rect = ref.current.getBoundingClientRect() + let middle = rect.top + rect.height / 4 - bodyRect.top - window.innerHeight / 2 + let isHalfWay = window.scrollY > middle + if (showCollapseButton && !isHalfWay) { + setShowCollapseButton(false) + } else if (!showCollapseButton && isHalfWay) { + setShowCollapseButton(true) + } + } + window.addEventListener('scroll', onScroll, { passive: true }) + return () => { + window.removeEventListener('scroll', onScroll, { passive: true }) + } + }, [expanded, showCollapseButton, inView]) + + return ( +
    +

    Testimonials

    +
    + {testimonials.map((column, i) => ( +
      + {column.map((testimonial) => ( + + ))} +
    + ))} +
    +
    + +
    +
    + ) +} \ No newline at end of file diff --git a/src/components/.ZH/home/BuildAnything.js b/src/components/.ZH/home/BuildAnything.js new file mode 100644 index 000000000..ae5590a06 --- /dev/null +++ b/src/components/.ZH/home/BuildAnything.js @@ -0,0 +1,356 @@ +import { Fragment, useEffect, useRef, useState } from 'react' +import { + IconContainer, + Caption, + BigText, + Paragraph, + Link, + Widont, + themeTabs, +} from '@/components/home/common' +import { Tabs } from '@/components/.ZH/Tabs' +import { CodeWindow, getClassNameForToken } from '@/components/CodeWindow' +import iconUrl from '@/img/icons/home/build-anything.png' +// import { HtmlZenGarden } from '@/components/HtmlZenGarden' +import { HtmlZenGarden } from '@/components/.ZH/HtmlZenGarden' +import clsx from 'clsx' +// import { GridLockup } from '../GridLockup' +import { GridLockup } from '@/components/GridLockup' +// import { lines } from '../../samples/build-anything.html?highlight' +import { lines } from '../../../samples/.ZH/build-anything.html?highlight' + +const code = { + /*html*/ + Simple: `
    +
    + +
    +
    +
    +

    Classic Utility Jacket

    +
    $110.00
    + +
    有現貨
    +
    +
    +
    + + + + + +
    +
    +
    +
    + + +
    + +
    + +

    全台灣保證24h到貨, 台北市 6h到貨

    +
    +
    `, + /*html*/ + Playful: `
    +
    + +
    +
    +
    +

    Classic Utility Jacket

    +
    $110.00
    + +
    有現貨
    +
    +
    +
    + + + + + +
    +
    +
    +
    + + +
    + +
    + +

    全台灣保證24h到貨, 台北市 6h到貨

    +
    +
    `, + /*html*/ + Elegant: `
    +
    + +
    +
    +
    +

    Classic Utility Jacket

    +
    $350.00
    + +
    有現貨
    +
    +
    +
    + + + + + +
    +
    +
    +
    + + +
    + +
    + +

    全台灣保證24h到貨, 台北市 6h到貨

    +
    +
    `, + /*html*/ + Brutalist: `
    +
    + +
    +
    +
    +

    Retro Shoe

    +
    $89.00
    + +
    有現貨
    +
    +
    +
    + + + + + +
    +
    +
    +
    + + +
    + +
    + +

    全台灣保證24h到貨, 台北市 6h到貨

    +
    +
    `, +} + +function extractClasses(code) { + return code.match(/class="[^"]+"/g).map((attr) => attr.substring(7, attr.length - 1)) +} + +const classes = { + Simple: extractClasses(code.Simple), + Playful: extractClasses(code.Playful), + Elegant: extractClasses(code.Elegant), + Brutalist: extractClasses(code.Brutalist), +} + +const content = { + // Simple: ['/classic-utility-jacket.jpg', 'Classic Utility Jacket', '$110.00'], + // Playful: ['/kids-jumpsuit.jpg', 'Kids Jumpsuit', '$39.00'], + // Elegant: ['/dogtooth-style-jacket.jpg', 'DogTooth Style Jacket', '$350.00'], + // Brutalist: ['/retro-shoe.jpg', 'Retro Shoe', '$89.00'], + Simple: ['/classic-utility-jacket.jpg', '經典機能性夾克', '$110.00'], + Playful: ['/kids-jumpsuit.jpg', '兒童連身裝', '$39.00'], + Elegant: ['/dogtooth-style-jacket.jpg', '韓版西裝外套', '$350.00'], + Brutalist: ['/retro-shoe.jpg', 'NIKE 復古版球鞋', '$89.00'], +} + +export function BuildAnything() { + const [theme, setTheme] = useState('Simple') + let classIndex = 0 + let contentIndex = 0 + + const initial = useRef(true) + + useEffect(() => { + initial.current = false + }, []) + + return ( +
    +
    + + + + {/* Build anything */} + 打造萬物 + + {/* Build whatever you want, seriously. */} + 你能創造你想要的一切,沒唬你 + + + {/* Because Tailwind is so low-level, it never encourages you to design the same site twice. + Even with the same color palette and sizing scale, it's easy to build the same component + with a completely different look in the next project. */} + 因為 Tailwind 就是這麼的好上手,所以它絕不鼓勵你設計出兩次同樣的網站。 + 即使用了同個色板和尺寸,還是可以很輕易的讓你在下個專案中用同樣的元件,做出完全不同的設計。 + + + {/* Get started, installation */} + 快點開始,安裝吧 + +
    + +
    +
    + } + right={ + + + {lines.map((tokens, lineIndex) => ( + + {tokens.map((token, tokenIndex) => { + if (token.content === '_') { + let cls = classes[theme][classIndex++] + return ( + + {cls} + + ) + } + + if (token.content.includes('__content__')) { + let text = content[theme][contentIndex++] + return ( + + {token.content.split(/(__content__)/).map((part, i) => + i === 1 ? ( + + {text} + + ) : ( + part + ) + )} + + ) + } + + return ( + + {token.content} + + ) + })} + {'\n'} + + ))} + + + } + /> +
    + ) +} diff --git a/src/components/.ZH/home/ComponentDriven.js b/src/components/.ZH/home/ComponentDriven.js new file mode 100644 index 000000000..485e48ac4 --- /dev/null +++ b/src/components/.ZH/home/ComponentDriven.js @@ -0,0 +1,696 @@ +import { + IconContainer, + Caption, + BigText, + Paragraph, + Link, + Widont, + InlineCode, +} from '@/components/home/common' +import { GridLockup } from '@/components/GridLockup' +import { CodeWindow, getClassNameForToken } from '@/components/CodeWindow' +import { Fragment, useEffect, useState } from 'react' +import { useIsomorphicLayoutEffect } from '@/hooks/useIsomorphicLayoutEffect' +import iconUrl from '@/img/icons/home/component-driven.png' +import { Tabs } from '@/components/.ZH/Tabs' +import { AnimatePresence, motion } from 'framer-motion' +import clsx from 'clsx' +import { useInView } from 'react-intersection-observer' + +import { lines as reactMoviesSample } from '../../../samples/.ZH/react/movies.jsx?highlight' +import { lines as reactNavSample } from '../../../samples/react/nav.jsx?highlight' +import { lines as reactNavItemSample } from '../../../samples/react/nav-item.jsx?highlight' +import { lines as reactListSample } from '../../../samples/react/list.jsx?highlight' +import { lines as reactListItemSample } from '../../../samples/.ZH/react/list-item.jsx?highlight' + +import { lines as vueMoviesSample } from '../../../samples/.ZH/vue/movies.html?highlight' +import { lines as vueNavSample } from '../../../samples/vue/nav.html?highlight' +import { lines as vueNavItemSample } from '../../../samples/vue/nav-item.html?highlight' +import { lines as vueListSample } from '../../../samples/vue/list.html?highlight' +import { lines as vueListItemSample } from '../../../samples/.ZH/vue/list-item.html?highlight' + +import { lines as angularMoviesSample } from '../../../samples/.ZH/angular/movies.js?highlight' +import { lines as angularNavSample } from '../../../samples/angular/nav.js?highlight' +import { lines as angularNavItemSample } from '../../../samples/angular/nav-item.js?highlight' +import { lines as angularListSample } from '../../../samples/angular/list.js?highlight' +import { lines as angularListItemSample } from '../../../samples/.ZH/angular/list-item.js?highlight' + +import { lines as bladeMoviesSample } from '../../../samples/.ZH/blade/movies.html?highlight' +import { lines as bladeNavSample } from '../../../samples/blade/nav.html?highlight' +import { lines as bladeNavItemSample } from '../../../samples/blade/nav-item.html?highlight' +import { lines as bladeListSample } from '../../../samples/blade/list.html?highlight' +import { lines as bladeListItemSample } from '../../../samples/.ZH/blade/list-item.html?highlight' + +import { lines as css } from '../../../samples/apply.txt?highlight=css' +// import { lines as html } from '../../samples/apply.html?highlight' +import { lines as html } from '../../../samples/.ZH/apply.html?highlight' + +function highlightDecorators(lines) { + for (let i = 0; i < lines.length; i++) { + for (let j = 0; j < lines[i].length; j++) { + if (lines[i][j].types.includes('function') && lines[i][j - 1].content.trim() === '@') { + lines[i][j - 1].types = ['function'] + } + } + } + return lines +} + +const movies = [ + { + title: 'Prognosis Negative', + starRating: '2.66', + rating: 'PG-13', + year: '2021', + // genre: 'Comedy', + genre: '喜劇', + // runtime: '1h 46m', + runtime: '106 分鐘', + cast: 'Simon Pegg, Zach Galifianakis', + image: require('@/img/prognosis-negative.jpg').default, + }, + { + title: 'Rochelle, Rochelle', + starRating: '3.25', + rating: 'R', + year: '2020', + // genre: 'Romance', + genre: '浪漫', + // runtime: '1h 56m', + runtime: '116 分鐘', + cast: 'Emilia Clarke', + image: require('@/img/rochelle-rochelle.jpg').default, + }, + { + title: 'Death Blow', + starRating: '4.95', + rating: '18A', + year: '2020', + // genre: 'Action', + genre: '動作', + // runtime: '2h 5m', + runtime: '125 分鐘', + cast: 'Idris Elba, John Cena, Thandiwe Newton', + image: require('@/img/death-blow.jpg').default, + }, +] + +const tabs = { + React: { + 'Movies.js': reactMoviesSample, + 'Nav.js': reactNavSample, + 'NavItem.js': reactNavItemSample, + 'List.js': reactListSample, + 'ListItem.js': reactListItemSample, + }, + Vue: { + 'Movies.vue': vueMoviesSample, + 'Nav.vue': vueNavSample, + 'NavItem.vue': vueNavItemSample, + 'List.vue': vueListSample, + 'ListItem.vue': vueListItemSample, + }, + Angular: { + 'movies.component.ts': highlightDecorators(angularMoviesSample), + 'nav.component.ts': highlightDecorators(angularNavSample), + 'nav-item.component.ts': highlightDecorators(angularNavItemSample), + 'list.component.ts': highlightDecorators(angularListSample), + 'list-item.component.ts': highlightDecorators(angularListItemSample), + }, + Blade: { + 'movies.blade.php': bladeMoviesSample, + 'nav.blade.php': bladeNavSample, + 'nav-item.blade.php': bladeNavItemSample, + 'list.blade.php': bladeListSample, + 'list-item.blade.php': bladeListItemSample, + }, +} + +function ComponentLink({ onClick, children }) { + const [active, setActive] = useState(false) + + useEffect(() => { + function onKey(e) { + const modifier = e.ctrlKey || e.shiftKey || e.altKey || e.metaKey + if (!active && modifier) { + setActive(true) + } else if (active && !modifier) { + setActive(false) + } + } + window.addEventListener('keydown', onKey) + window.addEventListener('keyup', onKey) + return () => { + window.removeEventListener('keydown', onKey) + window.removeEventListener('keyup', onKey) + } + }, [active]) + + return active ? ( + + ) : ( + children + ) +} + +function ComponentExample({ framework }) { + const [activeTab, setActiveTab] = useState(0) + const lines = tabs[framework][Object.keys(tabs[framework])[activeTab]] + + useIsomorphicLayoutEffect(() => { + setActiveTab(0) + }, [framework]) + + return ( + + + +
    +
      + {Object.keys(tabs[framework]).map((tab, tabIndex) => ( +
    • + +
    • + ))} +
    +
    +
    + + + + + + {lines.map((tokens, lineIndex) => ( + + {tokens.map((token, tokenIndex) => { + if ( + (token.types[token.types.length - 1] === 'class-name' || + (token.types[token.types.length - 1] === 'tag' && + /^([A-Z]|x-)/.test(token.content))) && + tokens[tokenIndex - 1]?.types[tokens[tokenIndex - 1].types.length - 1] === + 'punctuation' && + (tokens[tokenIndex - 1]?.content === '<' || + tokens[tokenIndex - 1].content === ' + + setActiveTab( + Object.keys(tabs[framework]).findIndex((x) => + x.startsWith(`${token.content.replace(/^x-/, '')}.`) + ) + ) + } + > + {token.content} + + + ) + } + + if ( + token.types[token.types.length - 1] === 'string' && + /^(['"`])\.\/.*?\.(js|vue)\1$/.test(token.content) + ) { + const tab = token.content.substr(3, token.content.length - 4) + return ( + + {token.content.substr(0, 1)} + + {token.content.substr(0, 1)} + + ) + } + + return ( + + {token.content} + + ) + })} + {'\n'} + + ))} + + + + + ) +} + +function ApplyExample({ inView }) { + return ( + +

    + styles.css +

    +
    + + {css.map((tokens, lineIndex) => ( + + {tokens.map((token, tokenIndex) => { + let className = getClassNameForToken(token) + if (className) { + className = className + .replace(/\bclass\b/, 'selector') + .replace(/\b(number|color)\b/, '') + } + return ( + + {token.content} + + ) + })} + {'\n'} + + ))} + +
    +

    + index.html +

    +
    + +
    + {html.map((tokens, lineIndex) => ( +
    = 4 && lineIndex <= 5 ? 'not-mono' : undefined} + > + {tokens.map((token, tokenIndex) => { + return ( + + {token.content} + + ) + })} +
    + ))} +
    +
    +
    +
    + ) +} + +function AtApplySection() { + const { inView, ref } = useInView({ threshold: 0.5, triggerOnce: true }) + + return ( +
    +
    + {/*

    Not into component frameworks?

    */} +

    沒有在使用元件框架?

    + + {/* If you like to keep it old school use Tailwind's @apply directive + to extract repeated utility patterns into custom CSS classes just by copying and pasting */} + 想要保持老派格調,不想加入元件框架的世界中嗎?那用 Tailwind 的 @apply 指令把你重複的功能樣式集中到自訂的 CSS class 裡吧。 + + + 閱讀更多關於樣式重新使用的內容 + +
    + +
    +
    +
    +

    + {/* Weekly one-on-one */} + 每周 1 對 1 教學 +

    +
    +
    + {/*
    Date and time
    */} +
    日期與時間
    +
    + {/* -{' '} */} + -{' '} + +
    +
    +
    + {/*
    Location
    */} +
    地點
    +
    + Kitchener, ON +
    +
    +
    + {/*
    Description
    */} +
    案件描述
    + {/*
    No meeting description
    */} +
    無任何描述
    +
    +
    + {/*
    Attendees
    */} +
    參與者
    +
    + Andrew McDonald +
    +
    +
    +
    +
    + {/* Decline */} + 拒絕 +
    +
    + {/* Accept */} + 接案 +
    +
    +
    +
    +
    +
    + +
    +
    + ) +} + +const tabItems = { + React: (selected) => ( + <> + + + + + + ), + Vue: (selected) => ( + <> + + + + ), + Angular: (selected) => ( + <> + + + + + ), + Blade: (selected) => ( + <> + + + + + + + ), +} + +export function ComponentDriven() { + const [framework, setFramework] = useState('React') + + return ( +
    +
    + + + + {/* Component-driven */} + 元件驅動 + + {/* Worried about duplication? Don’t be. */} + 擔心複用性的問題? 免了啦 + + + {/* If you're repeating the same utilities over and over and over again, all you have to do is + extract them into a component or template partial and boom — you've got a single source of + truth so you can make changes in one place. */} + 如果你想要一直、一直、一直地重複使用你的功能或設計, + 那你需要的是把它們做成元件或樣板,這樣你只要改變一次,就能應用在所有地方。 + + + 閱讀更多關於樣式重新使用的內容 + +
    + +
    +
    + + + + {movies.map(({ title, starRating, rating, year, genre, runtime, cast, image }, i) => ( +
    + +
    +

    {title}

    +
    +
    +
    + {/* Star rating */} + 評分 + + + +
    +
    {starRating}
    +
    +
    + {/*
    Rating
    */} +
    分級
    +
    {rating}
    +
    +
    + {/*
    Year
    */} +
    上映年份
    +
    {year}
    +
    +
    + {/*
    Genre
    */} +
    類型
    +
    + + {genre} +
    +
    +
    + {/*
    Runtime
    */} +
    片長
    +
    + + {runtime} +
    +
    +
    + {/*
    Cast
    */} +
    演員陣容
    +
    {cast}
    +
    +
    +
    +
    + ))} +
    + } + right={} + /> + + + + ) +} \ No newline at end of file diff --git a/src/components/.ZH/home/ConstraintBased.js b/src/components/.ZH/home/ConstraintBased.js new file mode 100644 index 000000000..56c339820 --- /dev/null +++ b/src/components/.ZH/home/ConstraintBased.js @@ -0,0 +1,377 @@ +import { IconContainer, Caption, BigText, Paragraph, Link, Widont } from '@/components/home/common' +import { Tabs } from '@/components/.ZH/Tabs' +import { CodeWindow, getClassNameForToken } from '@/components/CodeWindow' +import iconUrl from '@/img/icons/home/constraint-based.png' +import defaultConfig from 'defaultConfig' +import { AnimatePresence, motion } from 'framer-motion' +import { useState } from 'react' +// import { GridLockup } from '../GridLockup' +import { GridLockup } from '../../GridLockup' +import clsx from 'clsx' +// import { lines as sizingSample } from '../../samples/sizing.html?highlight' +// import { lines as colorsSample } from '../../samples/colors.html?highlight' +// import { lines as typographySample } from '../../samples/typography.html?highlight' +// import { lines as shadowsSample } from '../../samples/shadows.html?highlight' +import { lines as sizingSample } from '../../../samples/sizing.html?highlight' +import { lines as colorsSample } from '../../../samples/colors.html?highlight' +import { lines as typographySample } from '../../../samples/typography.html?highlight' +import { lines as shadowsSample } from '../../../samples/shadows.html?highlight' + +const tokens = { + Sizing: sizingSample, + Colors: colorsSample, + Typography: typographySample, + Shadows: shadowsSample, +} + +let tabs = { + Sizing: (selected) => ( + <> + + + + ), + Colors: (selected) => ( + <> + + + + + + ), + Typography: (selected) => ( + <> + + + + + ), + Shadows: (selected) => ( + <> + + + + + ), +} + +function Bars({ sizes, className }) { + return ( + + {sizes.map((key, i) => ( +
  • + +
    + w-{key} +
    + +
  • + ))} +
    + ) +} + +function Sizing() { + return ( + <> + + + + ) +} + +function Colors() { + return ( + + {['sky', 'blue', 'indigo', 'purple'].map((color, i) => ( + +
      + {Object.keys(defaultConfig.theme.colors[color]).map((key) => ( +
    • + ))} +
    +
    + {color}-50 + + + + + + + + {color}-900 +
    +
    + ))} +
    + ) +} + +function Typography() { + return ( + + {[ + [ + 'font-sans', + 'text-sm leading-6 sm:text-base sm:leading-6 lg:text-sm lg:leading-6 xl:text-base xl:leading-6', + ], + ['font-serif', 'text-sm leading-6 sm:text-lg lg:text-sm lg:leading-6 xl:text-lg'], + ['font-mono', 'text-sm leading-6 sm:leading-7 lg:leading-6 xl:leading-7'], + ].map((font, i) => ( + +

    {font[0]}

    +
    + The quick brown fox jumps over the lazy dog. +
    +
    + ))} +
    + ) +} + +function Shadows() { + return ( + +
      + {['shadow-sm', 'shadow', 'shadow-md', 'shadow-lg', 'shadow-xl', 'shadow-2xl'].map( + (shadow, i) => ( + + {shadow} + + ) + )} +
    +
    + ) +} + +export function ConstraintBased() { + const [tab, setTab] = useState('Sizing') + + return ( +
    +
    + + + + {/* Constraint-based */} + 以約束 (Constraint) 為基底 + + 為您設計系統而生的 API + + + {/* Utility classes help you work within the constraints of a system instead of littering your + stylesheets with arbitrary values. They make it easy to be consistent with color choices, + spacing, typography, shadows, and everything else that makes up a well-engineered design + system. */} + 功能性 class 可以約束您的系統,而不是在樣式表中填上亂七八糟的數值。 + 它們讓顏色、間距、文字版式、陰影,以及其他的一切都能井然有序的保持一致性, + 締造精良的設計系統。 + + + {/* Learn more, utility-first fundamentals */} + 了解詳情,功能優先的基本原則 + +
    + setTab(tab)} + className="text-indigo-600" + iconClassName="text-indigo-500" + /> +
    +
    + +
    +
    +
    +
    + + {tab === 'Sizing' && } + {tab === 'Colors' && } + {tab === 'Typography' && } + {tab === 'Shadows' && } + +
    +
    +
    + } + right={ + + + + + {tokens[tab].map((tokens, lineIndex) => ( +
    + {tokens.map((token, tokenIndex) => { + if (token.types[token.types.length - 1] === 'attr-value') { + return ( + + {token.content.split(/\[([^\]]+)\]/).map((part, i) => + i % 2 === 0 ? ( + part + ) : ( + + {part} + + ) + )} + + ) + } + return ( + + {token.content} + + ) + })} +
    + ))} +
    +
    +
    +
    + } + /> +
    + ) +} \ No newline at end of file diff --git a/src/components/.ZH/home/Customization.js b/src/components/.ZH/home/Customization.js new file mode 100644 index 000000000..c599c5476 --- /dev/null +++ b/src/components/.ZH/home/Customization.js @@ -0,0 +1,364 @@ +import { + IconContainer, + Caption, + BigText, + Paragraph, + Link, + Widont, + themeTabs, +} from '@/components/home/common' +import { Tabs } from '@/components/.ZH/Tabs' +import { CodeWindow } from '@/components/CodeWindow' +import iconUrl from '@/img/icons/home/customization.png' +import { useEffect, useRef, useState } from 'react' +import tailwindColors from 'tailwindcss/colors' +import { AnimatePresence, motion } from 'framer-motion' +// import { font as pallyVariable } from '../../fonts/generated/Pally-Variable.module.css' +// import { font as sourceSerifProRegular } from '../../fonts/generated/SourceSerifPro-Regular.module.css' +// import { font as ibmPlexMonoRegular } from '../../fonts/generated/IBMPlexMono-Regular.module.css' +// import { font as synonymVariable } from '../../fonts/generated/Synonym-Variable.module.css' +import { font as pallyVariable } from '../../../fonts/generated/Pally-Variable.module.css' +import { font as sourceSerifProRegular } from '../../../fonts/generated/SourceSerifPro-Regular.module.css' +import { font as ibmPlexMonoRegular } from '../../../fonts/generated/IBMPlexMono-Regular.module.css' +import { font as synonymVariable } from '../../../fonts/generated/Synonym-Variable.module.css' +// import { Token } from '../Code' +import { Token } from '@/components/Code' +import clsx from 'clsx' +// import { GridLockup } from '../GridLockup' +import { GridLockup } from '../../GridLockup' +// import { tokens } from '../../samples/customization.js?highlight' +import { tokens } from '../../../samples/customization.js?highlight' + +const defaultSampleBody = + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut augue gravida cras quis ac duis pretium ullamcorper consequat. Integer pellentesque eu.' + +const themes = { + Simple: { + font: 'Inter', + fontStacks: [ + ['Inter', 'system-ui', 'sans-serif'], + ['Inter', 'system-ui', 'sans-serif'], + ], + bodySize: '14pt', + colors: { + primary: 'blue', + secondary: 'gray', + }, + }, + Playful: { + font: 'Pally', + fontStacks: [ + ['Pally', 'Comic Sans MS', 'sans-serif'], + ['Pally', 'Comic Sans MS', 'sans-serif'], + ], + bodySize: '14pt', + classNameDisplay: `${pallyVariable} font-medium`, + classNameBody: pallyVariable, + colors: { + primary: 'rose', + secondary: 'violet', + }, + }, + Elegant: { + font: 'Source Serif Pro', + fontStacks: [ + ['Source Serif Pro', 'Georgia', 'serif'], + ['Synonym', 'system-ui', 'sans-serif'], + ], + bodySize: '14pt', + classNameDisplay: sourceSerifProRegular, + classNameBody: synonymVariable, + colors: { + primary: 'gray', + secondary: 'emerald', + }, + }, + Brutalist: { + font: 'IBM Plex Mono', + fontStacks: [ + ['IBM Plex Mono', 'Menlo', 'monospace'], + ['IBM Plex Mono', 'Menlo', 'monospace'], + ], + bodySize: '14pt', + classNameDisplay: ibmPlexMonoRegular, + classNameBody: ibmPlexMonoRegular, + sampleBody: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut augue gravida cras quis ac duis pretium ullamcorper consequat.', + colors: { + primary: 'gray', + secondary: 'teal', + }, + }, +} + +export function Customization() { + const [theme, setTheme] = useState('Simple') + + return ( +
    +
    + + + + {/* Customization */} + 客製化 + + {/* Extend it, tweak it, change it. */} + 延伸、微調和改變, 隨你喜好。 + + +

    + {/* Tailwind includes an expertly crafted set of defaults out-of-the-box, but literally + everything can be customized — from the color palette to the spacing scale to the box + shadows to the mouse cursor. */} + Tailwind 包含了一組專門設計、可以直接使用的預設值, + 但是其實從色票、間距、陰影到滑鼠游標,任何東西,都是可以自訂的。 +

    +

    + {/* Use the tailwind.config.js file to craft your own design system, then let Tailwind + transform it into your own custom CSS framework. */} + 用 tailwind.config.js 設定檔來創造自己的設計系統, + 讓 Tailwind 來把它轉換成屬於你客製化的 CSS 框架。 +

    +
    + + {/* Learn more, configuration */} + 了解詳情,關於配置的部分 + +
    + +
    +
    + +
    +
    +
    +
    +
    +
    +

    Typography

    +
      +
    • +
      +
      +
      CSS class
      +
      font-display
      +
      +
      +
      Font name
      + + + {themes[theme].font} + + +
      +
      +
      Sample
      + + + AaBbCc + + +
      +
      +
    • +
    • +
      +
      +
      CSS class
      +
      font-body
      +
      +
      +
      Font size
      + + + {themes[theme].bodySize} + + +
      +
      +
      Sample
      + + + {themes[theme].sampleBody || defaultSampleBody} + + +
      +
      +
    • +
    +
    + +
    +
    +

    Colors

    +
      + {Object.entries(themes[theme].colors).map(([name, color], index) => ( +
    • +
      +
      +
      CSS class prefix
      +
      bg-{name}
      +
      +
      +
      Range
      +
      50-900
      +
      +
      +
      Sample
      +
      +
        + {[50, 100, 200, 300, 400, 500, 600, 700, 800, 900].map((key) => ( + + ))} +
      +
      +
      +
      +
    • + ))} +
    +
    +
    +
    +
    + } + right={ + + { + if (typeof token === 'string' && token.includes('__SECONDARY_COLOR__')) { + return ['__SECONDARY_COLOR__', token] + } + return token + }} + /> + + } + /> +
    + ) +} + +function CustomizationToken({ theme, ...props }) { + const { token } = props + const initial = useRef(true) + + useEffect(() => { + initial.current = false + }, []) + + if (token[0] === 'string' && token[1].startsWith("'font-")) { + let [i, j] = token[1].match(/[0-9]+/g).map((x) => parseInt(x, 10)) + + return ( + + {"'"} + + {themes[theme].fontStacks[i][j]} + + {"'"} + + ) + } + + if (token[0] === 'string' && token[1].startsWith("'color-")) { + const [, name, shade] = token[1].substr(1, token[1].length - 2).split('-') + const color = tailwindColors[themes[theme].colors[name]][shade] + + return ( + + {"'"} + + {color} + + {"'"} + + ) + } + + if (token[0] === '__SECONDARY_COLOR__') { + let name = Object.keys(themes[theme].colors)[1] + return token[1].split('__SECONDARY_COLOR__').map((part, i) => + i % 2 === 0 ? ( + part + ) : ( + + {name} + + ) + ) + } + + return +} \ No newline at end of file diff --git a/src/components/.ZH/home/DarkMode.js b/src/components/.ZH/home/DarkMode.js new file mode 100644 index 000000000..c0e603c11 --- /dev/null +++ b/src/components/.ZH/home/DarkMode.js @@ -0,0 +1,204 @@ +import { useState } from 'react' +import { Switch } from '@headlessui/react' +import { + Paragraph, + IconContainer, + Caption, + BigText, + Link, + Widont, + InlineCode, +} from '@/components/home/common' +import { CodeWindow } from '@/components/CodeWindow' +import iconUrl from '@/img/icons/home/dark-mode.png' +import { addClassTokens } from '@/utils/addClassTokens' +import { Token } from '@/components/Code' +import clsx from 'clsx' +// import { GridLockup } from '../GridLockup' +import { GridLockup } from '@/components/GridLockup' +// import { code, tokens } from '../../samples/dark-mode-zh.html?highlight' +import { code, tokens } from '../../../samples/.ZH/dark-mode.html?highlight' + +function Sun(props) { + return ( + + ) +} + +function Moon(props) { + return ( + + ) +} + +function DarkModeSwitch({ enabled, onChange }) { + return ( + + {/* {enabled ? 'Enable' : 'Disable'} dark mode */} + {enabled ? '開啟' : '關閉'}深色模式 + + + + + + + + ) +} + +export function DarkMode() { + const [enabled, setEnabled] = useState(false) + + return ( +
    +
    + + + + {/* Dark mode */} + 深色模式 + + {/* Now with Dark Mode. */} + 現在,有深色模式了 + + + {/* Don’t want to be one of those websites that blinds people when they open it on their phone + at 2am? Enable dark mode in your configuration file then throw{' '} + dark: in front of any color utility to apply it when dark mode is + active. Works for background colors, text colors, border colors, and even gradients. */} + 不想讓在凌晨兩點用手機打開你網站時的用戶眼睛被閃瞎嗎? + 在設定文件中開啟深色模式並在任何顏色功能前加上{' '} + dark: ,這麼一來,當深色模式開啟時, + 背景顏色、文字顏色、邊框顏色甚至是漸層色都能發生變化。 + + + {/* Learn more, dark mode */} + 了解更多關於深色模式的內容 + +
    + + +
    /g, '
    '), + }} + /> +
    + } + right={ + + + + } + /> + + ) +} + +function DarkModeToken({ token, parentTypes, enabled, children }) { + if (token[0] === 'class') { + if (token[1].startsWith('dark:')) { + return ( + + {children} + + ) + } + if (token[1].startsWith('(light)')) { + return ( + + {token[1].replace(/^\(light\)/, '')} + + ) + } + } + + return ( + + {children} + + ) +} \ No newline at end of file diff --git a/src/components/.ZH/home/EditorTools.js b/src/components/.ZH/home/EditorTools.js new file mode 100644 index 000000000..b4eab4b85 --- /dev/null +++ b/src/components/.ZH/home/EditorTools.js @@ -0,0 +1,433 @@ +import { IconContainer, Caption, BigText, Paragraph, Link } from '@/components/home/common' +import { CodeWindow, getClassNameForToken } from '@/components/CodeWindow' +import { motion } from 'framer-motion' +import { Fragment, useEffect, useState } from 'react' +import iconUrl from '@/img/icons/home/editor-tools.png' +import { useInView } from 'react-intersection-observer' +import colors from 'tailwindcss/colors' +import dlv from 'dlv' +// import { GridLockup } from '../GridLockup' +import { GridLockup } from '@/components/GridLockup' +// import { lines } from '../../samples/editor-tools.html?highlight' +import { lines } from '../../../samples/editor-tools.html?highlight' +import clsx from 'clsx' + +const problems = [ + ["'flex' applies the same CSS property as 'block'.", 'cssConflict [1, 20]'], + ["'block' applies the same CSS property as 'flex'.", 'cssConflict [1, 54]'], +] + +const completions = [ + // + ['sm:', '@media (min-width: 640px)'], + ['md:'], + ['lg:'], + ['xl:'], + ['focus:'], + ['group-hover:'], + ['hover:'], + ['container'], + ['space-y-0'], + ['space-x-0'], + ['space-y-1'], + ['space-x-1'], + // + ['bg-fixed', 'background-position: fixed;'], + ['bg-local'], + ['bg-scroll'], + ['bg-clip-border'], + ['bg-clip-padding'], + ['bg-clip-content'], + ['bg-clip-text'], + ['bg-transparent', 'background-color: transparent;'], + ['bg-current'], + ['bg-black', '#000'], + ['bg-white', '#fff'], + ['bg-gray-50', colors.gray[50]], + // + ['bg-teal-50', `background-color: ${colors.teal[50]};`, colors.teal[50]], + ['bg-teal-100', `background-color: ${colors.teal[100]};`, colors.teal[100]], + ['bg-teal-200', `background-color: ${colors.teal[200]};`, colors.teal[200]], + ['bg-teal-300', `background-color: ${colors.teal[300]};`, colors.teal[300]], + ['bg-teal-400', `background-color: ${colors.teal[400]};`, colors.teal[400]], + ['bg-teal-500', undefined, colors.teal[500]], + ['bg-teal-600', undefined, colors.teal[600]], + ['bg-teal-700', undefined, colors.teal[700]], + ['bg-teal-800', undefined, colors.teal[800]], + ['bg-teal-900', undefined, colors.teal[900]], + ['bg-top'], +] + +function CompletionDemo() { + const { ref, inView } = useInView({ threshold: 0.5, triggerOnce: true }) + + return ( + + {lines.map((tokens, lineIndex) => ( + + {tokens.map((token, tokenIndex) => { + if (token.content === '__CONFLICT__') { + return ( + + w-full{' '} + flex{' '} + items-center justify-between{' '} + block{' '} + p-6 space-x-6 + + ) + } + + if (token.content === '__COMPLETION__') { + return + } + + if ( + token.types[token.types.length - 1] === 'attr-value' && + tokens[tokenIndex - 3].content === 'class' + ) { + return ( + + {token.content.split(' ').map((c, i) => { + const space = i === 0 ? '' : ' ' + if (/^(bg|text|border)-/.test(c)) { + const color = dlv(colors, c.replace(/^(bg|text|border)-/, '').split('-')) + if (color) { + return ( + + {space} + + {c} + + ) + } + } + return space + c + })} + + ) + } + + return ( + + {token.content} + + ) + })} + {'\n'} + + ))} + + ) +} + +function Completion({ inView }) { + const [typed, setTyped] = useState('') + const [selectedCompletionIndex, setSelectedCompletionIndex] = useState(0) + const [stage, setStage] = useState(-1) + + useEffect(() => { + if (inView) { + setStage(0) + } + }, [inView]) + + useEffect(() => { + if (typed === ' bg-t') { + let i = 0 + let id = window.setInterval(() => { + if (i === 5) { + setStage(1) + setTyped('') + setSelectedCompletionIndex(0) + return window.clearInterval(id) + } + i++ + setSelectedCompletionIndex(i) + }, 300) + return () => window.clearInterval(id) + } + }, [typed]) + + useEffect(() => { + let id + if (stage === 1) { + id = window.setTimeout(() => { + setStage(2) + }, 2000) + } else if (stage === 2 || stage === 3 || stage === 4 || stage === 5) { + id = window.setTimeout(() => { + setStage(stage + 1) + }, 300) + } else if (stage === 6) { + id = window.setTimeout(() => { + setStage(-1) + setStage(0) + }, 2000) + } + return () => { + window.clearTimeout(id) + } + }, [stage]) + + return ( + + + + text-teal-600 + {stage >= 1 && stage < 2 && ' '} + + {stage >= 1 && stage < 2 && } + {stage >= 0 && + stage < 2 && + ' bg-t' + .split('') + .filter((char) => (stage >= 1 && stage < 6 ? char !== ' ' : true)) + .map((char, i) => ( + + setTyped(typed + char)} + > + {char} + + + ))} + {stage === 1 && 'eal-400'} + {(stage < 2 || stage === 6) && } + {stage >= 2 && stage <= 5 && ( + + {stage < 5 && ' '} + {stage < 5 && } + {stage >= 4 && } + {stage === 5 && ( + <> + +   + ​ + + + + )} + = 4 ? 'rgba(81, 92, 126, 0.4)' : undefined }} + > + bg- + + {stage === 3 && } + = 3 ? 'rgba(81, 92, 126, 0.4)' : undefined }} + > + teal- + + {stage === 2 && } + = 2 ? 'rgba(81, 92, 126, 0.4)' : undefined }} + > + 400 + + + )} + {typed && ( + +
    +
    +
      + {completions + .filter((completion) => completion[0].startsWith(typed.trim())) + .slice(0, 12) + .map((completion, i) => ( +
    • + + {completion[2] ? ( + + ) : ( + + + + )} + + + {completion[0].split(new RegExp(`(^${typed.trim()})`)).map((part, j) => + part ? ( + j % 2 === 0 ? ( + part + ) : ( + + {part} + + ) + ) : null + )} + + {i === selectedCompletionIndex && completion[1] ? ( + + {completion[1]} + + ) : null} +
    • + ))} +
    +
    +
    +
    + )} +
    + ) +} + +function ColorDecorator({ color }) { + return ( + + ) +} + +export function EditorTools() { + return ( +
    +
    + + + + {/* Editor tools */} + 編輯器插件 + {/* World-class IDE integration. */} + 世界級的 IDE 整合 + +

    + {/* Worried about remembering all of these class names? The Tailwind CSS IntelliSense + extension for VS Code has you covered. */} + 擔心記不住所有的 class 名稱嗎? + Tailwind CSS IntelliSense 這個 VS Code 擴充套件可以掩護你! +

    +

    + {/* Get intelligent autocomplete suggestions, linting, class definitions and more, all + within your editor and with no configuration required. */} + 智慧的語法建議、整理以及 class 定義等,全部在你的編輯器裡,而且還不用設定。 +

    +
    + + {/* Learn more, editor setup */} + 閱讀更多關於編輯器的設定 + +
    + + + +
    +
    + + + + + + + + + + +
    +
    + +
    +

    Problems

    +
      + {problems.map((problem, i) => ( +
    • + + + + +

      {problem[0]}

      +

      +  {problem[1]} +

      +
    • + ))} +
    +
    +
    +
    +
    + + } + /> +
    + ) +} \ No newline at end of file diff --git a/src/components/.ZH/home/Footer.js b/src/components/.ZH/home/Footer.js new file mode 100644 index 000000000..ae4c544a6 --- /dev/null +++ b/src/components/.ZH/home/Footer.js @@ -0,0 +1,62 @@ +import Link from 'next/link' +import { documentationNav } from '@/navs/.ZH/documentation' +import { Logo } from '@/components/Logo' + +const chineseNames = { + 'Getting Started': "起手式", + 'Core Concepts': "核心概念", + 'Customization': "客製化", + 'Community': "社群", +} + +const footerNav = [ + { + 'Getting Started': documentationNav['Getting Started'], + 'Core Concepts': documentationNav['Core Concepts'], + }, + { + Customization: documentationNav['Customization'], + Community: [ + { title: 'GitHub', href: 'https://github.com/tailwindlabs/tailwindcss' }, + { title: 'Discord', href: '/discord' }, + { title: 'Twitter', href: 'https://twitter.com/tailwindcss' }, + { title: 'YouTube', href: 'https://www.youtube.com/tailwindlabs' }, + ], + }, +] + +export function Footer() { + return ( +
    +
    +
    + {footerNav.map((sections) => ( +
    + {Object.entries(sections).map(([title, items]) => ( +
    + {/*

    {title}

    */} +

    {chineseNames[title]? chineseNames[title]: title}

    + +
    + ))} +
    + ))} +
    +
    + +
    +
    +
    + ) +} \ No newline at end of file diff --git a/src/components/.ZH/home/Hero.js b/src/components/.ZH/home/Hero.js new file mode 100644 index 000000000..22da9d324 --- /dev/null +++ b/src/components/.ZH/home/Hero.js @@ -0,0 +1,493 @@ +import { CodeWindow } from '@/components/CodeWindow' +import { Token } from '@/components/Code' +import { AnimateSharedLayout, motion, useAnimation } from 'framer-motion' +import { useEffect, useRef, useState } from 'react' +import clsx from 'clsx' +import { fit } from '@/utils/fit' +import { debounce } from 'debounce' +import { useMedia } from '@/hooks/useMedia' +import { wait } from '@/utils/wait' +import { createInViewPromise } from '@/utils/createInViewPromise' +// import { tokens, code } from '../../samples/hero.html?highlight' +import { tokens, code } from '../../../samples/.ZH/hero.html?highlight' +import colors from 'tailwindcss/colors' + +const CHAR_DELAY = 75 +const GROUP_DELAY = 1000 +const TRANSITION = { duration: 0.5 } + +function getRange(text, options = {}) { + return { start: code.indexOf(text), end: code.indexOf(text) + text.length, ...options } +} + +const ranges = [ + getRange(' p-8'), + getRange(' rounded-full'), + getRange(' mx-auto'), + getRange(' font-medium'), + getRange(' class="font-medium"'), + getRange(' class="text-sky-500"'), + getRange(' class="text-gray-700"'), + getRange(' text-center'), + getRange('md:flex '), + getRange(' md:p-0'), + getRange(' md:p-8', { immediate: true }), + getRange(' md:rounded-none'), + getRange(' md:w-48'), + getRange(' md:h-auto'), + getRange(' md:text-left'), +] + +function getRangeIndex(index, ranges) { + for (let i = 0; i < ranges.length; i++) { + const rangeArr = Array.isArray(ranges[i]) ? ranges[i] : [ranges[i]] + for (let j = 0; j < rangeArr.length; j++) { + if (index >= rangeArr[j].start && index < rangeArr[j].end) { + return [i, index - rangeArr[j].start, index === rangeArr[j].end - 1] + } + } + } + return [-1] +} + +function Words({ children, bolder = false, layout, transition }) { + return children.split(' ').map((word, i) => ( + + {bolder ? ( + <> + + {word}{' '} + + {word} + + ) : ( + word + ' ' + )} + + )) +} + +function augment(tokens, index = 0) { + for (let i = 0; i < tokens.length; i++) { + if (Array.isArray(tokens[i])) { + const _type = tokens[i][0] + const children = tokens[i][1] + if (Array.isArray(children)) { + index = augment(children, index) + } else { + const str = children + const result = [] + for (let j = 0; j < str.length; j++) { + const [rangeIndex, indexInRange, isLast] = getRangeIndex(index, ranges) + if (rangeIndex > -1) { + result.push([`char:${rangeIndex}:${indexInRange}${isLast ? ':last' : ''}`, str[j]]) + } else { + if (typeof result[result.length - 1] === 'string') { + result[result.length - 1] += str[j] + } else { + result.push(str[j]) + } + } + index++ + } + if (!(result.length === 1 && typeof result[0] === 'string')) { + tokens[i].splice(1, 1, result) + } + } + } else { + const str = tokens[i] + const result = [] + for (let j = 0; j < str.length; j++) { + const [rangeIndex, indexInRange, isLast] = getRangeIndex(index, ranges) + if (rangeIndex > -1) { + result.push([`char:${rangeIndex}:${indexInRange}${isLast ? ':last' : ''}`, str[j]]) + } else { + if (typeof result[result.length - 1] === 'string') { + result[result.length - 1] += str[j] + } else { + result.push(str[j]) + } + } + index++ + } + tokens.splice(i, 1, ...result) + i += result.length - 1 + } + } + return index +} + +augment(tokens) + +export function Hero() { + const containerRef = useRef() + const [step, setStep] = useState(-1) + const [state, setState] = useState({ group: -1, char: -1 }) + const cursorControls = useAnimation() + const [wide, setWide] = useState(false) + const [finished, setFinished] = useState(false) + const supportsMd = useMedia('(min-width: 640px)') + const [isMd, setIsMd] = useState(false) + const [containerRect, setContainerRect] = useState() + const md = supportsMd && isMd + const mounted = useRef(true) + const inViewRef = useRef() + const imageRef = useRef() + + const layout = !finished + + useEffect(() => { + return () => (mounted.current = false) + }, []) + + useEffect(() => { + let current = true + + const { promise: inViewPromise, disconnect } = createInViewPromise(inViewRef.current, { + threshold: 0.5, + }) + + const promises = [ + wait(1000), + inViewPromise, + new Promise((resolve) => { + if ('requestIdleCallback' in window) { + window.requestIdleCallback(resolve) + } else { + window.setTimeout(resolve, 0) + } + }), + new Promise((resolve) => { + if (imageRef.current.complete) { + resolve() + } else { + imageRef.current.addEventListener('load', resolve) + } + }), + ] + + Promise.all(promises).then(() => { + if (current) { + setState({ group: 0, char: 0 }) + } + }) + + return () => { + current = false + disconnect() + } + }, []) + + useEffect(() => { + if (step === 14) { + let id = window.setTimeout(() => { + setFinished(true) + }, 1000) + return () => { + window.clearTimeout(id) + } + } + }, [step]) + + useEffect(() => { + if (!finished) return + let count = 0 + cursorControls.start({ opacity: 0.5 }) + const id = window.setInterval(() => { + if (count === 2) { + return window.clearInterval(id) + } + count++ + cursorControls.start({ opacity: 1, scale: 0.9, transition: { duration: 0.25 } }).then(() => { + setWide((wide) => !wide) + cursorControls.start({ + opacity: count === 2 ? 0 : 0.5, + scale: 1, + transition: { duration: 0.25, delay: 0.6 }, + }) + }) + }, 2000) + return () => { + window.clearInterval(id) + } + }, [finished]) + + useEffect(() => { + if (finished) { + const id = window.setTimeout(() => { + setIsMd(wide) + }, 250) + return () => window.clearTimeout(id) + } + }, [wide, finished]) + + useEffect(() => { + const observer = new window.ResizeObserver( + debounce(() => { + if (containerRef.current) { + setContainerRect(containerRef.current.getBoundingClientRect()) + } + }, 500) + ) + observer.observe(containerRef.current) + return () => { + observer.disconnect() + } + }, []) + + return ( + + + + = 8 && md, + 'p-8': step >= 0, + 'text-center': (step >= 7 && !md) || (step < 14 && md), + })} + > + + + + + + = 1 && step < 11) || (step >= 11 && !md && !finished) + ? { borderRadius: 96 / 2 } + : { borderRadius: 0 }), + }} + transition={TRANSITION} + className={clsx( + 'relative z-10 overflow-hidden flex-none', + step >= 10 && md ? '-m-8 mr-8' : step >= 2 ? 'mx-auto' : undefined, + step >= 12 && md ? 'w-48' : 'w-24', + step >= 13 && md ? 'h-auto' : 'h-24' + )} + > + = 13 && md + ? fit(192, containerRect.height, 384, 512) + : step >= 12 && md + ? fit(192, 96, 384, 512) + : fit(96, 96, 384, 512) + } + /> + + = 10 && md ? '' : 'pt-6'} + transition={TRANSITION} + > + + = 3} layout={layout} transition={TRANSITION}> + {/* “Tailwind CSS is the only framework that I've seen scale on large teams. It’s + easy to customize, adapts to any design, and the build size is tiny.” */} + 「Tailwind CSS 是我見過唯一一個可以在大型團隊中 + 擴展開來的框架。它可以輕鬆的客製化、 + 適應任何設計,而且建置後又很輕巧。」 + + + = 7 && !md) || (step < 14 && md) ? 'items-center' : 'items-start' + }`} + style={{ + ...(step >= 4 ? { fontWeight: 500 } : { fontWeight: 400 }), + }} + transition={TRANSITION} + > + = 5 ? { color: colors.sky[500] } : { color: '#000' }), + }} + transition={TRANSITION} + > + Sarah Dayan + + = 6 ? { color: colors.gray[700] } : { color: '#000' }), + }} + transition={TRANSITION} + > + {/* Staff Engineer, Algolia */} + Algolia 的主管工程師 + + + + + + + + } + right={ + + ({ ...state, char: charIndex + 1 })) + }, + async onGroupComplete(groupIndex) { + if (!mounted.current) return + setStep(groupIndex) + + if (groupIndex === 7) { + if (!supportsMd) return + await cursorControls.start({ opacity: 0.5, transition: { delay: 1 } }) + if (!mounted.current) return + setWide(true) + setIsMd(true) + await cursorControls.start({ opacity: 0, transition: { delay: 0.5 } }) + } + + if (!mounted.current) return + + if (ranges[groupIndex + 1] && ranges[groupIndex + 1].immediate) { + setState({ char: 0, group: groupIndex + 1 }) + } else { + window.setTimeout(() => { + if (!mounted.current) return + setState({ char: 0, group: groupIndex + 1 }) + }, GROUP_DELAY) + } + }, + }} + /> + + } + /> + ) +} + +function AnimatedToken({ isActiveToken, onComplete, children }) { + const [visible, setVisible] = useState(false) + + useEffect(() => { + if (visible) { + onComplete() + } + }, [visible]) + + useEffect(() => { + if (isActiveToken) { + let id = window.setTimeout(() => { + setVisible(true) + }, CHAR_DELAY) + return () => { + window.clearTimeout(id) + } + } + }, [isActiveToken]) + + return ( + <> + {children} + {isActiveToken && } + + ) +} + +function HeroToken({ currentChar, onCharComplete, currentGroup, onGroupComplete, ...props }) { + const { token } = props + + if (token[0].startsWith('char:')) { + const [, groupIndex, indexInGroup] = token[0].split(':').map((x) => parseInt(x, 10)) + + return ( + { + if (token[0].endsWith(':last')) { + onGroupComplete(groupIndex) + } else { + onCharComplete(indexInGroup) + } + }} + {...props} + /> + ) + } + + return +} + +function Layout({ left, right, pin = 'left' }) { + return ( +
    +
    +
    {right}
    +
    +
    +
    +
    {left}
    +
    +
    +
    + ) +} \ No newline at end of file diff --git a/src/components/.ZH/home/MobileFirst.js b/src/components/.ZH/home/MobileFirst.js new file mode 100644 index 000000000..7cda5eb72 --- /dev/null +++ b/src/components/.ZH/home/MobileFirst.js @@ -0,0 +1,347 @@ +import { IconContainer, Caption, BigText, Paragraph, Link } from '@/components/home/common' +import { CodeWindow, getClassNameForToken } from '@/components/CodeWindow' +import { motion, useTransform, useMotionValue } from 'framer-motion' +import { useEffect, useRef, useState } from 'react' +import iconUrl from '@/img/icons/home/mobile-first.png' +import { addClassTokens2 } from '@/utils/addClassTokens' +import clsx from 'clsx' +// import { GridLockup } from '../GridLockup' +import { GridLockup } from '@/components/GridLockup' +import { useIsomorphicLayoutEffect } from '@/hooks/useIsomorphicLayoutEffect' +// import { lines } from '../../pages/examples/mobile-first-demo' +import { lines } from '../../../pages/examples/mobile-first-demo' + +addClassTokens2(lines) + +const MIN_WIDTH = 400 + +function BrowserWindow({ size, onChange }) { + let x = useMotionValue(0) + let constraintsRef = useRef() + let handleRef = useRef() + let iframeRef = useRef() + let iframePointerEvents = useMotionValue('auto') + + useEffect(() => { + function onMessage(e) { + if (e.source === iframeRef.current.contentWindow) { + onChange(e.data) + } + } + + window.addEventListener('message', onMessage) + + return () => { + window.removeEventListener('message', onMessage) + } + }, []) + + useIsomorphicLayoutEffect(() => { + let observer = new window.ResizeObserver(() => { + let width = constraintsRef.current.offsetWidth - handleRef.current.offsetWidth + if (x.get() > width) { + x.set(width) + } + }) + observer.observe(constraintsRef.current) + return () => { + observer.disconnect() + } + }, []) + + useEffect(() => { + handleRef.current.onselectstart = () => false + }, []) + + return ( +
    + x + MIN_WIDTH) }} + > +
    +
    +
    +
    +
    +
    +
    + {size !== undefined && ( + <> + + + + + + + + )} +
    +
    +
    + + + + workcation.com +
    +
    + {size !== undefined && ( +
    + + + +
    + )} +
    +
    +
    + + + + {/*
    Tailwind UI - Official Tailwind CSS Components
    */} +
    兔兔教 X Tailwind CSS Taiwan
    +
    +
    + + + + + {/*
    Workcation - Find a trip that suits you
    */} +
    Workcation - 找到適合你的旅行
    +
    +
    + + + + +
    + {/* Headless UI – Unstyled, fully accessible UI components */} + Headless UI – Unstyled, fully accessible UI components +
    +
    +
    +
    +
    + +
    +
    + +
    + { + document.documentElement.classList.add('dragging-ew') + iframePointerEvents.set('none') + }} + onDragEnd={() => { + document.documentElement.classList.remove('dragging-ew') + iframePointerEvents.set('auto') + }} + > +
    + +
    +
    + ) +} + +function Marker({ label, active, className }) { + return ( +
    +
    +
    +
    +
    +
    + {label} +
    +
    + ) +} + +export function MobileFirst() { + let [size, setSize] = useState() + + return ( +
    +
    + + + + {/* Mobile-first */} + 以行動裝置為主 + {/* Responsive everything. */} + 一切,都能是響應式的 + +

    + {/* Wrestling with a bunch of complex media queries in your CSS sucks, so Tailwind lets you + build responsive designs right in your HTML instead. */} + CSS 裡,有一堆令人頭痛又複雜的 media query 語法, + 所以 Tailwind 讓你直接在 HTML 建構出響應式設計。 +

    +

    + {/* Throw a screen size in front of literally any utility class and watch it magically apply + at a specific breakpoint. */} + 將 {'"螢幕尺寸"'} 擺在任何功能性 class 前面,看看它在特定斷點時如何神奇的變化。 +

    +
    + + {/* Learn more, responsive design */} + 快去看看,關於響應式設計的一切 + +
    +
    +
    + + + + + +
    +
    + +
    + +
    + + + {lines.map((tokens, lineIndex) => ( +
    + {tokens.map((token, tokenIndex) => { + if (token.types[token.types.length - 1] === 'class') { + let isSm = token.content.startsWith('sm:') + let isMd = token.content.startsWith('md:') + let isLg = token.content.startsWith('lg:') + + if (isSm || isMd || isLg) { + let faded = + size === undefined || + (size === 'sm' && (isMd || isLg)) || + (size === 'md' && isLg) + let highlighted = + (size === 'sm' && isSm) || + (size === 'md' && isMd) || + (size === 'lg' && isLg) + + return ( + + {token.content} + + ) + } + } + return ( + + {token.content} + + ) + })} +
    + ))} +
    +
    + + } + /> +
    + ) +} \ No newline at end of file diff --git a/src/components/.ZH/home/ModernFeatures.js b/src/components/.ZH/home/ModernFeatures.js new file mode 100644 index 000000000..2eaa772f5 --- /dev/null +++ b/src/components/.ZH/home/ModernFeatures.js @@ -0,0 +1,261 @@ +import { + IconContainer, + Caption, + BigText, + Paragraph, + Link, + Widont, + InlineCode, +} from '@/components/home/common' +import { Tabs } from '@/components/.ZH/Tabs' +import { CodeWindow, getClassNameForToken } from '@/components/CodeWindow' +import iconUrl from '@/img/icons/home/modern-features.png' +import { Fragment, useState } from 'react' +import { AnimatePresence, motion } from 'framer-motion' +import clsx from 'clsx' +// import { GridLockup } from '../GridLockup' +// import { lines as gridSample } from '../../samples/grid.html?highlight' +// import { lines as transformsSample } from '../../samples/transforms.html?highlight' +// import { lines as filtersSample } from '../../samples/filters.html?highlight' +import { GridLockup } from '@/components/GridLockup' +import { lines as gridSample } from '../../../samples/grid.html?highlight' +import { lines as transformsSample } from '../../../samples/transforms.html?highlight' +import { lines as filtersSample } from '../../../samples/filters.html?highlight' + +const lines = { + 'CSS Grid': gridSample, + Transforms: transformsSample, + Filters: filtersSample, +} + +const tabs = { + 'CSS Grid': (selected) => ( + <> + + + ), + Transforms: (selected) => ( + <> + + + ), + Filters: (selected) => ( + <> + + + + + ), +} + +function Block({ src, filter, ...props }) { + return ( + +
    + +
    +
    + ) +} + +export function ModernFeatures() { + const [feature, setFeature] = useState('CSS Grid') + + const animate = (transforms, grid) => { + if (feature === 'Transforms') { + return { + animate: transforms, + } + } + return { + animate: grid, + } + } + + return ( +
    +
    + + + + {/* Modern features */} + 現代趨勢 + + {/* Cutting-edge is our comfort zone. */} + 我們,只活在尖端。 + + +

    + {/* Tailwind is unapologetically modern, and takes advantage of all the latest and greatest + CSS features to make the developer experience as enjoyable as possible. */} + Tailwind 很潮。因為它具備最新最棒的 CSS 特色,只為了讓你的開發過程輕鬆愉快。 +

    +

    + {/* We've got first-class CSS grid support, composable transforms and gradients powered by + CSS variables, support for modern state selectors like{' '} + :focus-visible, and tons more. */} + 我們用 CSS 變數提供一流的 CSS 網格、組合式變形,和漸層,也支援像是{' '} + :focus-visible 這種的現代狀態選擇器,以及更多其他功能。 +

    +
    + + {/* Learn more, grid template columns */} + 去了解網格排版的相關內容 + +
    + +
    +
    + +
    + + + + + + + +
    +
    + } + right={ + + + + + {lines[feature].map((tokens, lineIndex) => ( + + {tokens.map((token, tokenIndex) => { + if (token.types[token.types.length - 1] === 'attr-value') { + return ( + + {token.content.split(/\[([^\]]+)\]/).map((part, i) => + i % 2 === 0 ? ( + part + ) : ( + + {part} + + ) + )} + + ) + } + return ( + + {token.content} + + ) + })} + {'\n'} + + ))} + + + + + } + /> + + ) +} \ No newline at end of file diff --git a/src/components/.ZH/home/Performance.js b/src/components/.ZH/home/Performance.js new file mode 100644 index 000000000..b4a999631 --- /dev/null +++ b/src/components/.ZH/home/Performance.js @@ -0,0 +1,359 @@ +import { IconContainer, Caption, BigText, Paragraph, Link } from '@/components/home/common' +import { CodeWindow, getClassNameForToken } from '@/components/CodeWindow' +import { TabBar } from '@/components/TabBar' +import iconUrl from '@/img/icons/home/performance.png' +import { Fragment, useCallback, useEffect, useRef, useState } from 'react' +import clsx from 'clsx' +// import { GridLockup } from '../GridLockup' +// import { lines as html } from '../../samples/performance.html?highlight' +// import { lines as css } from '../../samples/performance.txt?highlight=css' +import { GridLockup } from '@/components/GridLockup' +import { lines as html } from '../../../samples/.ZH/performance.html?highlight' +import { lines as css } from '../../../samples/performance.txt?highlight=css' +import { useInView } from 'react-intersection-observer' +import { animate } from 'framer-motion' + +const START_DELAY = 500 +const CLASS_DELAY = 1000 +const CHAR_DELAY = 75 +const SAVE_DELAY = 50 +const BUILD_DELAY = 100 + +function Typing({ classes, rules, onStartedClass, onFinishedClass }) { + let [text, setText] = useState('') + + useEffect(() => { + let newText = classes.substr(0, text.length + 1) + let isSpace = newText.endsWith(' ') + let isEnd = text.length + 1 > classes.length + let isEndOfClass = isSpace || isEnd + if (isEndOfClass) { + onFinishedClass(newText.split(' ').filter(Boolean).length - 1) + } + let handle = window.setTimeout( + () => { + if (newText.endsWith(' ') || newText.length === 1) { + onStartedClass() + } + setText(newText) + }, + isSpace ? CLASS_DELAY : CHAR_DELAY + ) + return () => { + window.clearTimeout(handle) + } + }, [classes, text, onStartedClass, onFinishedClass]) + + return text.split(' ').map((cls, index) => ( + + {index !== 0 && ' '} + + {cls} + + + )) +} + +export function Performance() { + let [visibleRules, setVisibleRules] = useState([]) + let [saved, setSaved] = useState(true) + let [lastFinishedClass, setLastFinishedClass] = useState(-1) + let [active, setActive] = useState(false) + let scrollRef = useRef() + let { ref: typingRef, inView: typingInView } = useInView({ threshold: 1 }) + let { ref: containerRef, inView: containerInView } = useInView({ threshold: 0 }) + + useEffect(() => { + if (typingInView && !active) { + let handle = window.setTimeout(() => setActive(true), START_DELAY) + return () => { + window.clearTimeout(handle) + } + } + }, [active, typingInView]) + + useEffect(() => { + if (!containerInView && active) { + setActive(false) + setVisibleRules([]) + setSaved(true) + setLastFinishedClass(-1) + } + }, [active, containerInView]) + + let rules = [] + let chunk = [] + for (let line of css) { + chunk.push(line) + let empty = line.every(({ content }) => content.trim() === '') + if (empty) { + rules.push(chunk) + chunk = [] + } + } + + rules = rules.filter((_, i) => visibleRules.includes(i)) + + let onStartedClass = useCallback(() => { + setSaved(false) + }, []) + + useEffect(() => { + if (lastFinishedClass < 0) return + let handle1 = window.setTimeout(() => setSaved(true), SAVE_DELAY) + let handle2 = window.setTimeout( + () => + setVisibleRules( + [ + [0], + [0, 1], + [0, 1, 3], + [0, 1, 3, 4], + [0, 1, 3, 4, 5], + [0, 1, 2, 3, 4, 5], + [0, 1, 2, 3, 4, 5, 6], + ][lastFinishedClass] + ), + SAVE_DELAY + BUILD_DELAY + ) + return () => { + window.clearTimeout(handle1) + window.clearTimeout(handle2) + } + }, [lastFinishedClass]) + + return ( +
    +
    + + + + {/* Performance */} + 效能 + {/* It's tiny — never ship unused CSS again. */} + 輕巧微小 - 不再傳送任何用不到的 CSS + + {/* Tailwind automatically removes all unused CSS when building for production, which means + your final CSS bundle is the smallest it could possibly be. In fact, most Tailwind + projects ship less than 10kB of CSS to the client. */} + Tailwind 會在生產環境建置時自動移除未使用的 CSS,這代表著你最後的 CSS 內容量會盡其所能的最小化。 + 事實上,大部分的 Tailwind 專案只會對客戶端傳送小於 10kB 的 CSS。 + + + {/* Learn more, optimizing for production */} + 繼續閱讀關於生產環境優化的部分 + +
    + +
    +
    + + + + + + + + + + + + + {html.map((tokens, lineIndex) => ( +
    +
    + {lineIndex + 1} +
    +
    + {tokens.map((token, tokenIndex) => { + if (token.content === '__CLASSES__') { + return ( + + {active && ( + + )} + + + ) + } + return ( + + {token.content} + + ) + })} +
    +
    + ))} +
    +
    +
    + + + + + + + + + {rules.map((rule) => ( + content.trim()).content} + rule={rule} + scrollRef={scrollRef} + /> + ))} + +
    +
    +
    + + + + + + + + +
    +
    +
    + } + /> +
    + ) +} + +function Terminal({ rules }) { + let scrollRef = useRef() + + useEffect(() => { + let top = scrollRef.current.scrollHeight - scrollRef.current.offsetHeight + + if (CSS.supports('scroll-behavior', 'smooth')) { + scrollRef.current.scrollTo({ top }) + } else { + animate(scrollRef.current.scrollTop, top, { + onUpdate: (top) => scrollRef.current.scrollTo({ top }), + }) + } + }, [rules.length]) + + return ( +
    + + + +
    +        
    +          
    + npx tailwindcss -o + --output build.css --content index.html{' '} + -w --watch +
    + {rules.map((_rule, index) => ( +
    + + Rebuilding... Done in {[5, 6, 5, 7, 4, 5][index % 6]}ms. + +
    + ))} +
    +
    +
    + ) +} + +function Rule({ rule, scrollRef }) { + let ref = useRef() + + useEffect(() => { + let top = + ref.current.offsetTop - scrollRef.current.offsetHeight / 2 + ref.current.offsetHeight / 2 + + if (CSS.supports('scroll-behavior', 'smooth')) { + scrollRef.current.scrollTo({ top }) + } else { + animate(scrollRef.current.scrollTop, top, { + onUpdate: (top) => scrollRef.current.scrollTo({ top }), + }) + } + }, [scrollRef]) + + return ( +
    + {rule.map((tokens, lineIndex) => { + let contentIndex = tokens.findIndex(({ content }) => content.trim()) + if (contentIndex === -1) { + return '\n' + } + return ( + + {tokens.slice(0, contentIndex).map((token) => token.content)} + + {tokens.slice(contentIndex).map((token, tokenIndex) => { + return ( + + {token.content} + + ) + })} + + {'\n'} + + ) + })} +
    + ) +} \ No newline at end of file diff --git a/src/components/.ZH/home/ReadyMadeComponents.js b/src/components/.ZH/home/ReadyMadeComponents.js new file mode 100644 index 000000000..135ec78cd --- /dev/null +++ b/src/components/.ZH/home/ReadyMadeComponents.js @@ -0,0 +1,97 @@ +import { IconContainer, Caption, BigText, Paragraph, Link, Widont } from '@/components/home/common' +import iconUrl from '@/img/icons/home/ready-made-components.png' +import { useInView } from 'react-intersection-observer' +import { motion } from 'framer-motion' +// import { GridLockup } from '../GridLockup' +import { GridLockup } from '@/components/GridLockup' + +function AnimatedImage({ animate = false, delay = 0, ...props }) { + return ( + + ) +} + +const w = 1213 +const h = 675 + +const getStyle = (x, y, width) => ({ + top: `${(y / h) * 100}%`, + left: `${(x / w) * 100}%`, + width: `${(width / w) * 100}%`, +}) + +const images = [ + { src: require('@/img/tailwindui/0.png').default, x: 27, y: 24, width: 236 }, + { src: require('@/img/tailwindui/1.png').default, x: 287, y: 0, width: 567 }, + { src: require('@/img/tailwindui/2.png').default, x: 878, y: 47, width: 308 }, + { src: require('@/img/tailwindui/3.jpg').default, x: 0, y: 289, width: 472 }, + { src: require('@/img/tailwindui/4.jpg').default, x: 496, y: 289, width: 441 }, + { src: require('@/img/tailwindui/5.png').default, x: 961, y: 289, width: 252 }, +] + +export function ReadyMadeComponents() { + const { ref: inViewRef, inView } = useInView({ threshold: 0.5, triggerOnce: true }) + + return ( +
    +
    + + + + {/* Ready-made components */} + 現成的元件 + + {/* Move even faster with Tailwind UI. */} + 用 Tailwind UI 快速前進 + + + {/* Tailwind UI is a collection of beautiful, fully responsive UI components, designed and + developed by us, the creators of Tailwind CSS. It's got hundreds of ready-to-use examples + to choose from, and is guaranteed to help you find the perfect starting point for what you + want to build. */} + Tailwind UI 集結了我們以及所有 Tailwind CSS 的創作者所設計研發的漂亮且完全響應式的 UI 元件。 + 那裏有數百個現成的範例讓你選擇,而且保證你能找到心目中想建構的完美起點。 + + + {/* Learn more */} + 了解詳情 + +
    + +
    +
    + {images.map(({ src, x, y, width }, index) => ( + + ))} +
    +
    +
    + } + /> + + ) +} \ No newline at end of file diff --git a/src/components/.ZH/home/StateVariants.js b/src/components/.ZH/home/StateVariants.js new file mode 100644 index 000000000..a2b4eb676 --- /dev/null +++ b/src/components/.ZH/home/StateVariants.js @@ -0,0 +1,312 @@ +import { + IconContainer, + Caption, + BigText, + Paragraph, + Link, + Widont, + InlineCode, +} from '@/components/home/common' +import { CodeWindow, getClassNameForToken } from '@/components/CodeWindow' +import iconUrl from '@/img/icons/home/state-variants.png' +import { addClassTokens2 } from '@/utils/addClassTokens' +import { useEffect, useRef, useState } from 'react' +import { usePrevious } from '@/hooks/usePrevious' +import clsx from 'clsx' +// import { GridLockup } from '../GridLockup' +// import { lines } from '../../samples/state-variants-zh.html?highlight' +import { GridLockup } from '@/components/GridLockup' +import { lines } from '../../../samples/.ZH/state-variants.html?highlight' +import { animate } from 'framer-motion' + +const projects = [ + // { title: 'API Integration', category: 'Engineering' }, + // { title: 'New Benefits Plan', category: 'Human Resources' }, + // { title: 'Onboarding Emails', category: 'Customer Success' }, + { title: 'API 整合', category: '工程' }, + { title: '薪資調整方案', category: '人資' }, + { title: '求職信', category: '顧客成功案例' }, +] + +const faces = [ + 'photo-1531123897727-8f129e1688ce', + 'photo-1494790108377-be9c29b29330', + 'photo-1552374196-c4e7ffc6e126', + 'photo-1546525848-3ce03ca516f6', + 'photo-1544005313-94ddf0286df2', + 'photo-1517841905240-472988babdf9', + 'photo-1506794778202-cad84cf45f1d', + 'photo-1500648767791-00dcc994a43e', + 'photo-1534528741775-53994a69daeb', + 'photo-1502685104226-ee32379fefbe', + 'photo-1546525848-3ce03ca516f6', + 'photo-1502685104226-ee32379fefbe', + 'photo-1494790108377-be9c29b29330', + 'photo-1506794778202-cad84cf45f1d', + 'photo-1534528741775-53994a69daeb', +] + +addClassTokens2(lines) + +const lineRanges = { + 'new-btn-hover': [4, 9], + 'input-focus': [15, 15], + 'item-hover': [20, 39], + 'new-hover': [42, 47], +} + +export function StateVariants() { + const [states, setStates] = useState([]) + const prevStates = usePrevious(states) + const codeContainerRef = useRef() + const linesContainerRef = useRef() + + function scrollTo(rangeOrRanges) { + let ranges = Array.isArray(rangeOrRanges) ? rangeOrRanges : [rangeOrRanges] + if (ranges.length === 0) return + let linesSorted = ranges.flat().sort((a, b) => a - b) + let minLine = linesSorted[0] + let maxLine = linesSorted[linesSorted.length - 1] + let $lines = linesContainerRef.current.children + let containerHeight = codeContainerRef.current.offsetHeight + let top = $lines[minLine].offsetTop + let height = $lines[maxLine].offsetTop + $lines[maxLine].offsetHeight - top + + top = top - containerHeight / 2 + height / 2 + + if (CSS.supports('scroll-behavior', 'smooth')) { + codeContainerRef.current.scrollTo({ top }) + } else { + animate(codeContainerRef.current.scrollTop, top, { + onUpdate: (top) => codeContainerRef.current.scrollTo({ top }), + }) + } + } + + useEffect(() => { + if (prevStates && prevStates.length > states.length) { + scrollTo(states.map((state) => lineRanges[state])) + } else if (states.length) { + scrollTo(lineRanges[states[states.length - 1]]) + } + }, [states, prevStates]) + + return ( +
    +
    + + + + {/* State variants */} + 狀態變化 + + {/* Hover and focus states? We got ’em. */} + 想要 hover 和 focus 的狀態? 我們準備給你。 + + + {/* Want to style something on hover? Stick hover: at the beginning + of the class you want to add. Works for focus,{' '} + active, disabled,{' '} + focus-within, focus-visible, and even + fancy states we invented ourselves like group-hover. */} + 想要在滑鼠停留時有不同的樣式嗎?那就在你想用的 class 前面加上 hover:! + 同樣,你也能加上 focusactive、 + disabledfocus-within、 + focus-visible ,甚至是我們自己發明的超酷炫狀態 group-hover。 + + + {/* Learn more, handling hover, focus, and other states */} + 了解關於 hover、focus 以及其他狀態的詳細說明 + +
    + +
    +
    +
    + {/*

    Projects

    */} +

    專案

    +
    { + setStates((states) => [...states, 'new-btn-hover']) + }} + onMouseLeave={() => { + setStates((states) => states.filter((x) => x !== 'new-btn-hover')) + }} + > + + + + {/* New */} + 建立 +
    +
    +
    + + + + { + setStates((states) => [...states, 'input-focus']) + }} + onBlur={() => { + setStates((states) => states.filter((x) => x !== 'input-focus')) + // resetScroll() + }} + type="text" + aria-label="Filter projects" + // placeholder="Filter projects..." + placeholder="搜尋專案..." + className="w-full text-sm leading-6 text-gray-900 placeholder-gray-400 rounded-md py-2 pl-10 ring-1 ring-gray-200 shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500" + /> +
    +
    +
      + {projects.map((project, i, a) => ( +
    • { + setStates((states) => [...states, 'item-hover']) + }} + onMouseLeave={() => { + setStates((states) => states.filter((x) => x !== 'item-hover')) + }} + > +
      +
      + {/*
      Title
      */} +
      標題
      +
      + {project.title} +
      +
      +
      + {/*
      Category
      */} +
      分類
      +
      {project.category}
      +
      +
      + {/*
      Users
      */} +
      用戶
      +
      + {Array.from({ length: 5 }).map((_, j) => ( + + ))} +
      +
      +
      +
    • + ))} +
    • +
      { + setStates((states) => [...states, 'new-hover']) + }} + onMouseLeave={() => { + setStates((states) => states.filter((x) => x !== 'new-hover')) + }} + > + + + + {/* New project */} + 建立專案 +
      +
    • +
    +
    +
    + } + right={ + + +
    0 })} + > + {lines.map((tokens, lineIndex) => ( +
    = lineRanges['new-btn-hover'][0] && + lineIndex <= lineRanges['new-btn-hover'][1]) || + (states.includes('input-focus') && + lineIndex >= lineRanges['input-focus'][0] && + lineIndex <= lineRanges['input-focus'][1]) || + (states.includes('item-hover') && + lineIndex >= lineRanges['item-hover'][0] && + lineIndex <= lineRanges['item-hover'][1]) || + (states.includes('new-hover') && + lineIndex >= lineRanges['new-hover'][0] && + lineIndex <= lineRanges['new-hover'][1]) + ? 'not-mono' + : '' + } + > + {tokens.map((token, tokenIndex) => { + if ( + token.types[token.types.length - 1] === 'class' && + token.content.startsWith('(') + ) { + const [, state] = token.content.match(/^\(([^)]+)\)/) + return ( + + {token.content.substr(token.content.indexOf(')') + 1)} + + ) + } + return ( + + {token.content} + + ) + })} +
    + ))} +
    +
    +
    + } + /> + + ) +} \ No newline at end of file diff --git a/src/components/.ZH/home/common.js b/src/components/.ZH/home/common.js new file mode 100644 index 000000000..931f835c7 --- /dev/null +++ b/src/components/.ZH/home/common.js @@ -0,0 +1,135 @@ +import clsx from 'clsx' +import { Button } from '../Button' + +export function IconContainer({ as: Component = 'div', color, className = '', ...props }) { + return ( + + ) +} + +export function Caption({ className = '', ...props }) { + return

    +} + +export function BigText({ className = '', ...props }) { + return ( +

    + ) +} + +export function Paragraph({ as: Component = 'p', className = '', ...props }) { + return +} + +export function Link({ className, ...props }) { + return

    + } + {...props} + > + + {children} + + + ) +} + +export function ContentsLayout({ children, meta, classes, tableOfContents, section }) { + const router = useRouter() + const toc = [ + ...(classes ? [{ title: 'Quick reference', slug: 'class-reference', children: [] }] : []), + ...tableOfContents, + ] + + const { currentSection, registerHeading, unregisterHeading } = useTableOfContents(toc) + let { prev, next } = usePrevNext() + + return ( +
    + + + {classes ? ( + <> + +
    + {children} +
    + + ) : ( +
    + {children} +
    + )} +
    + + + +
    + {toc.length > 0 && ( + + )} +
    +
    + ) +} diff --git a/src/layouts/.ZH/DocumentationLayout.js b/src/layouts/.ZH/DocumentationLayout.js new file mode 100644 index 000000000..73022b005 --- /dev/null +++ b/src/layouts/.ZH/DocumentationLayout.js @@ -0,0 +1,29 @@ +import { SidebarLayout } from '@/layouts/.ZH/SidebarLayout' +import Head from 'next/head' +import { useRouter } from 'next/router' +import socialSquare from '@/img/social-square.jpg' +import { Title } from '@/components/Title' +import { documentationNav } from '@/navs/.ZH/documentation' + +export function DocumentationLayout(props) { + let router = useRouter() + + return ( + <> + + {props.layoutProps.meta.metaTitle || props.layoutProps.meta.title} + + + + + + + + ) +} + +DocumentationLayout.nav = documentationNav diff --git a/src/layouts/.ZH/FrameworkGuideLayout.js b/src/layouts/.ZH/FrameworkGuideLayout.js new file mode 100644 index 000000000..a32bddf71 --- /dev/null +++ b/src/layouts/.ZH/FrameworkGuideLayout.js @@ -0,0 +1,22 @@ +import { BasicLayout } from '@/layouts/.ZH/BasicLayout' + +export function FrameworkGuideLayout({ title, description, children }) { + return ( + +
    +
    +

    Installation

    +

    + {title} +

    +

    + {description} +

    +
    +
    +
    + {children} +
    +
    + ) +} diff --git a/src/layouts/.ZH/InstallationLayout.js b/src/layouts/.ZH/InstallationLayout.js new file mode 100644 index 000000000..47c9564b4 --- /dev/null +++ b/src/layouts/.ZH/InstallationLayout.js @@ -0,0 +1,156 @@ +import { BasicLayout } from '@/layouts/.ZH/BasicLayout' +import clsx from 'clsx' +import { useRouter } from 'next/router' +import Link from 'next/link' +import { IconContainer } from '@/components/home/common' + +let tabs = { + 'Tailwind CLI': '/zh/docs/installation', + 'Using PostCSS': '/zh/docs/installation/using-postcss', + 'Framework Guides': '/zh/docs/installation/framework-guides', + 'Play CDN': '/zh/docs/installation/play-cdn', +} + +let readNext = [ + { + title: 'Utility-First Fundamentals', + href: '/zh/docs/utility-first', + body: () => ( +

    + Using a utility-first workflow to build complex components from a constrained set of + primitive utilities. +

    + ), + image: require('@/img/icons/home/utility-first.png').default, + }, + { + title: 'Responsive Design', + href: '/zh/docs/responsive-design', + body: () => ( +

    + Build fully responsive user interfaces that adapt to any screen size using responsive + modifiers. +

    + ), + image: require('@/img/icons/home/mobile-first.png').default, + }, + { + title: 'Hover, Focus & Other States', + href: '/zh/docs/hover-focus-and-other-states', + body: () => ( +

    + Style elements in interactive states like hover, focus, and more using conditional + modifiers. +

    + ), + image: require('@/img/icons/home/state-variants.png').default, + }, + { + title: 'Dark Mode', + href: '/zh/docs/dark-mode', + body: () => ( +

    Optimize your site for dark mode directly in your HTML using the dark mode modifier.

    + ), + image: require('@/img/icons/home/dark-mode.png').default, + }, + { + title: 'Reusing Styles', + href: '/zh/docs/reusing-styles', + body: () => ( +

    + Manage duplication and keep your projects maintainable by creating reusable abstractions. +

    + ), + image: require('@/img/icons/home/component-driven.png').default, + }, + { + title: 'Customizing the Framework', + href: '/zh/docs/adding-custom-styles', + body: () => ( +

    Customize the framework to match your brand and extend it with your own custom styles.

    + ), + image: require('@/img/icons/home/customization.png').default, + }, +] + +export function InstallationLayout({ children }) { + let router = useRouter() + + return ( + +
    +
    +

    安裝

    +

    + Get started with Tailwind CSS +

    +

    + Tailwind CSS works by scanning all of your HTML files, JavaScript components, and any + other templates for class names, generating the corresponding styles and then writing + them to a static CSS file. +

    +

    + It's fast, flexible, and reliable — with zero-runtime. +

    +
    +
    +
    +
    +

    Installation

    +
    +
    +
      + {Object.entries(tabs).map(([name, href]) => ( +
    • + + + {name} + + +
    • + ))} +
    +
    +
    +
    + {children} +
    + +
    +

    What to read next

    +
    +

    + Get familiar with some of the core concepts that make Tailwind CSS different from + writing traditional CSS. +

    +
    +
      + {readNext.map((item) => ( +
    • + + + +
      +

      + + {item.title} + +

      +
      + +
      +
      +
    • + ))} +
    +
    +
    + ) +} diff --git a/src/layouts/.ZH/SidebarLayout.js b/src/layouts/.ZH/SidebarLayout.js new file mode 100644 index 000000000..bf308d4e7 --- /dev/null +++ b/src/layouts/.ZH/SidebarLayout.js @@ -0,0 +1,430 @@ +import Link from 'next/link' +import { useRouter } from 'next/router' +import { createContext, forwardRef, useRef } from 'react' +import { useIsomorphicLayoutEffect } from '@/hooks/useIsomorphicLayoutEffect' +import clsx from 'clsx' +import { SearchButton } from '@/components/Search' +// import { SearchButton } from '@/components/.ZH/Search' +import { Dialog } from '@headlessui/react' +import chineseCategory from '@/components/.ZH/ChineseCategory' + +export const SidebarContext = createContext() + +const NavItem = forwardRef(({ href, children, isActive, isPublished, fallbackHref }, ref) => { + return ( +
  • + + + {children} + + +
  • + ) +}) + +/** + * Find the nearst scrollable ancestor (or self if scrollable) + * + * Code adapted and simplified from the smoothscroll polyfill + * + * + * @param {Element} el + */ +function nearestScrollableContainer(el) { + /** + * indicates if an element can be scrolled + * + * @param {Node} el + */ + function isScrollable(el) { + const style = window.getComputedStyle(el) + const overflowX = style['overflowX'] + const overflowY = style['overflowY'] + const canScrollY = el.clientHeight < el.scrollHeight + const canScrollX = el.clientWidth < el.scrollWidth + + const isScrollableY = canScrollY && (overflowY === 'auto' || overflowY === 'scroll') + const isScrollableX = canScrollX && (overflowX === 'auto' || overflowX === 'scroll') + + return isScrollableY || isScrollableX + } + + while (el !== document.body && isScrollable(el) === false) { + el = el.parentNode || el.host + } + + return el +} + +function Nav({ nav, children, fallbackHref }) { + const router = useRouter() + const activeItemRef = useRef() + const previousActiveItemRef = useRef() + const scrollRef = useRef() + + useIsomorphicLayoutEffect(() => { + function updatePreviousRef() { + previousActiveItemRef.current = activeItemRef.current + } + + if (activeItemRef.current) { + if (activeItemRef.current === previousActiveItemRef.current) { + updatePreviousRef() + return + } + + updatePreviousRef() + + const scrollable = nearestScrollableContainer(scrollRef.current) + + const scrollRect = scrollable.getBoundingClientRect() + const activeItemRect = activeItemRef.current.getBoundingClientRect() + + const top = activeItemRef.current.offsetTop + const bottom = top - scrollRect.height + activeItemRect.height + + if (scrollable.scrollTop > top || scrollable.scrollTop < bottom) { + scrollable.scrollTop = top - scrollRect.height / 2 + activeItemRect.height / 2 + } + } + }, [router.pathname]) + + return ( + + ) +} + +const TopLevelAnchor = forwardRef( + ({ children, href, className, icon, isActive, onClick, shadow }, ref) => { + return ( +
  • + +
    + + {icon} + +
    + {children} +
    +
  • + ) + } +) + +function TopLevelLink({ href, as, ...props }) { + if (/^https?:\/\//.test(href)) { + return + } + + return ( + + + + ) +} + +function TopLevelNav() { + let { pathname } = useRouter() + + return ( + <> + + + + + } + > + {/* Documentation */} + 技術文件 + + + + + + + } + > + {/* Components */} + 元件庫 + + + + + + } + > + {/* Screencasts */} + 教學影片 + + + + + + + } + > + {/* Playground */} + 遊樂場 + + + + + + + } + > + {/* Resources */} + 資源 + + + + + + + + } + > + {/* Community */} + 社群 + + + ) +} + +function Wrapper({ allowOverflow, children }) { + return
    {children}
    +} + +export function SidebarLayout({ + children, + navIsOpen, + setNavIsOpen, + nav, + sidebar, + fallbackHref, + layoutProps: { allowOverflow = true } = {}, +}) { + return ( + + +
    +
    + +
    +
    {children}
    +
    +
    + setNavIsOpen(false)} + className="fixed z-50 inset-0 overflow-y-auto lg:hidden" + > + +
    + + +
    +
    +
    + ) +} diff --git a/src/navs/.ZH/documentation.js b/src/navs/.ZH/documentation.js new file mode 100644 index 000000000..3f3adc986 --- /dev/null +++ b/src/navs/.ZH/documentation.js @@ -0,0 +1,242 @@ +import { createPageList } from '@/utils/createPageList' + +const pages = createPageList( + require.context(`../../pages/zh/docs/?meta=title,shortTitle,published`, false, /\.mdx$/), + 'zh/docs' +) + +export const documentationNav = { + 'Getting Started': [ + { + title: 'Installation', + // href: '/docs/installation', + // match: /^\/docs\/installation/, + href: '/zh/docs/installation', + match: /^\/zh\/docs\/installation/, + }, + // TODO: Add these pages + // pages['tailwind-cli'], + // { title: 'Play CDN', href: '#' }, + pages['editor-setup'], + pages['using-with-preprocessors'], + pages['optimizing-for-production'], + pages['browser-support'], + pages['upgrade-guide'], + ], + 'Core Concepts': [ + pages['utility-first'], + // TODO: Maybe write this page + // pages['writing-your-html'], + pages['hover-focus-and-other-states'], + pages['responsive-design'], + pages['dark-mode'], + pages['reusing-styles'], + pages['adding-custom-styles'], + pages['functions-and-directives'], + ], + Customization: [ + pages['configuration'], + pages['content-configuration'], + // TODO: Remove + redirect to v2 + // pages['just-in-time-mode'], + pages['theme'], + pages['screens'], + pages['customizing-colors'], + pages['customizing-spacing'], + // TODO: Redirect to v2 + // pages['configuring-variants'], + pages['plugins'], + pages['presets'], + ], + 'Base Styles': [pages['preflight']], + Layout: [ + pages['aspect-ratio'], + pages['container'], + pages['columns'], + pages['break-after'], + pages['break-before'], + pages['break-inside'], + pages['box-decoration-break'], + pages['box-sizing'], + pages['display'], + pages['float'], + pages['clear'], + pages['isolation'], + pages['object-fit'], + pages['object-position'], + pages['overflow'], + pages['overscroll-behavior'], + pages['position'], + pages['top-right-bottom-left'], + pages['visibility'], + pages['z-index'], + ], + 'Flexbox & Grid': [ + pages['flex-basis'], + pages['flex-direction'], + pages['flex-wrap'], + pages['flex'], + pages['flex-grow'], + pages['flex-shrink'], + pages['order'], + pages['grid-template-columns'], + pages['grid-column'], + pages['grid-template-rows'], + pages['grid-row'], + pages['grid-auto-flow'], + pages['grid-auto-columns'], + pages['grid-auto-rows'], + pages['gap'], + pages['justify-content'], + pages['justify-items'], + pages['justify-self'], + pages['align-content'], + pages['align-items'], + pages['align-self'], + pages['place-content'], + pages['place-items'], + pages['place-self'], + ], + Spacing: [pages['padding'], pages['margin'], pages['space']], + Sizing: [ + pages['width'], + pages['min-width'], + pages['max-width'], + pages['height'], + pages['min-height'], + pages['max-height'], + ], + Typography: [ + pages['font-family'], + pages['font-size'], + pages['font-smoothing'], + pages['font-style'], + pages['font-weight'], + pages['font-variant-numeric'], + pages['letter-spacing'], + pages['line-height'], + pages['list-style-type'], + pages['list-style-position'], + pages['text-align'], + pages['text-color'], + pages['text-decoration'], + pages['text-decoration-color'], + pages['text-decoration-style'], + pages['text-decoration-thickness'], + pages['text-underline-offset'], + pages['text-transform'], + pages['text-overflow'], + pages['text-indent'], + pages['vertical-align'], + pages['whitespace'], + pages['word-break'], + pages['content'], + ], + Backgrounds: [ + pages['background-attachment'], + pages['background-clip'], + pages['background-color'], + pages['background-origin'], + pages['background-position'], + pages['background-repeat'], + pages['background-size'], + pages['background-image'], + pages['gradient-color-stops'], + ], + Borders: [ + pages['border-radius'], + pages['border-width'], + pages['border-color'], + pages['border-style'], + pages['divide-width'], + pages['divide-color'], + pages['divide-style'], + pages['outline-width'], + pages['outline-color'], + pages['outline-style'], + pages['outline-offset'], + pages['ring-width'], + pages['ring-color'], + pages['ring-offset-width'], + pages['ring-offset-color'], + ], + Effects: [ + pages['box-shadow'], + pages['box-shadow-color'], + pages['opacity'], + pages['mix-blend-mode'], + pages['background-blend-mode'], + ], + Filters: [ + pages['blur'], + pages['brightness'], + pages['contrast'], + pages['drop-shadow'], + pages['grayscale'], + pages['hue-rotate'], + pages['invert'], + pages['saturate'], + pages['sepia'], + pages['backdrop-blur'], + pages['backdrop-brightness'], + pages['backdrop-contrast'], + pages['backdrop-grayscale'], + pages['backdrop-hue-rotate'], + pages['backdrop-invert'], + pages['backdrop-opacity'], + pages['backdrop-saturate'], + pages['backdrop-sepia'], + ], + Tables: [pages['border-collapse'], pages['table-layout']], + 'Transitions & Animation': [ + pages['transition-property'], + pages['transition-duration'], + pages['transition-timing-function'], + pages['transition-delay'], + pages['animation'], + ], + Transforms: [ + pages['scale'], + pages['rotate'], + pages['translate'], + pages['skew'], + pages['transform-origin'], + ], + Interactivity: [ + pages['accent-color'], + pages['appearance'], + pages['cursor'], + pages['caret-color'], + pages['pointer-events'], + pages['resize'], + pages['scroll-behavior'], + pages['scroll-margin'], + pages['scroll-padding'], + pages['scroll-snap-align'], + pages['scroll-snap-stop'], + pages['scroll-snap-type'], + pages['touch-action'], + pages['user-select'], + pages['will-change'], + ], + SVG: [pages['fill'], pages['stroke'], pages['stroke-width']], + Accessibility: [pages['screen-readers']], + 'Official Plugins': [ + { + title: 'Typography', + href: 'https://github.com/tailwindlabs/tailwindcss-typography', + }, + { + title: 'Forms', + href: 'https://github.com/tailwindlabs/tailwindcss-forms', + }, + { + title: 'Aspect Ratio', + href: 'https://github.com/tailwindlabs/tailwindcss-aspect-ratio', + }, + { + title: 'Line Clamp', + href: 'https://github.com/tailwindlabs/tailwindcss-line-clamp', + }, + ], +} diff --git a/src/pages/_app.js b/src/pages/_app.js index 891ff6890..9571c87c4 100644 --- a/src/pages/_app.js +++ b/src/pages/_app.js @@ -2,7 +2,8 @@ import '../css/fonts.css' import '../css/main.css' import 'focus-visible' import { useState, useEffect, Fragment } from 'react' -import { Header } from '@/components/Header' +// import { Header } from '@/components/Header' +import { Header } from '@/components/.ZH/Header' import { Title } from '@/components/Title' import Router from 'next/router' import ProgressBar from '@badrap/bar-of-progress' @@ -11,6 +12,7 @@ import socialCardLarge from '@/img/social-card-large.jpg' import { ResizeObserver } from '@juggle/resize-observer' import 'intersection-observer' import { SearchProvider } from '@/components/Search' +// import { SearchProvider } from '@/components/.ZH/Search' if (typeof window !== 'undefined' && !('ResizeObserver' in window)) { window.ResizeObserver = ResizeObserver @@ -55,7 +57,8 @@ export default function App({ Component, pageProps, router }) { const showHeader = router.pathname !== '/' const meta = Component.layoutProps?.meta || {} const description = - meta.metaDescription || meta.description || 'Documentation for the Tailwind CSS framework.' + // meta.metaDescription || meta.description || 'Documentation for the Tailwind CSS framework.' + meta.metaDescription || meta.description || 'Tailwind CSS 框架技術文件' if (router.pathname.startsWith('/examples/')) { return @@ -77,20 +80,23 @@ export default function App({ Component, pageProps, router }) { diff --git a/src/pages/_document.js b/src/pages/_document.js index 543d0e565..15d09ea54 100644 --- a/src/pages/_document.js +++ b/src/pages/_document.js @@ -15,7 +15,8 @@ export default class Document extends NextDocument { render() { return (
    - Search + {/* Search */} + 搜尋

    - Rapidly build modern websites without ever leaving your HTML. + {/* Rapidly build modern websites without ever leaving your HTML. */} + 不用離開HTML,你還是可以極速建立最潮的網站。

    - A utility-first CSS framework packed with classes like{' '} - flex,{' '} + {/* A utility-first CSS framework packed with classes like{' '} */} + {/* flex,{' '} pt-4,{' '} text-center and{' '} rotate-90 that can be - composed to build any design, directly in your markup. + composed to build any design, directly in your markup. */} + 一個功能優先的 CSS 框架,集結了像是 {' '} + flex、{' '} + pt-4、{' '} + text-center 以及 {' '} + rotate-90 等 class, + 讓你可以直接將其組合起來並且建構出任意的設計。

    -
    - + {/*
    */} +
    + {/* Get started - + */} +
    + 心動。不如... + + + 馬上行動 + + +
    {({ actionKey }) => ( <> @@ -101,7 +119,8 @@ function Header() { - Quick search... + {/* Quick search... */} + 快速搜尋... {actionKey && ( @@ -128,25 +147,29 @@ export default function Home() { - Tailwind CSS - Rapidly build modern websites without ever leaving your HTML. + {/* Tailwind CSS - Rapidly build modern websites without ever leaving your HTML. */} + Tailwind CSS - 不用離開HTML,你還是可以極速建立最潮的網站

    - “Best practices” don’t actually work. + {/* “Best practices” don’t actually work. */} + 實際上,「最佳做法」是沒用的。

    - I’ve written{' '} + {/* I’ve written{' '} + 幾千個字 + + 就只為了說明讓 CSS 變得難以維護的原因,其實就是傳統的「語意化 class 名稱」。 + 但在你實際嘗試之前,你絕對不會認同我說的話。 + 如果你能不嫌棄地給它一個機會, + 我相信你絕對會想知道怎麼透過其他的方式使用 CSS。

    @@ -168,7 +202,8 @@ export default function Home() { />
    Adam Wathan
    -
    Creator of Tailwind CSS
    + {/*
    Creator of Tailwind CSS
    */} +
    Tailwind CSS 作者
    @@ -191,4 +226,4 @@ export default function Home() {