diff --git a/app/lib/HorizontalScrollText.tsx b/app/lib/HorizontalScrollText.tsx new file mode 100644 index 0000000..6ded13d --- /dev/null +++ b/app/lib/HorizontalScrollText.tsx @@ -0,0 +1,92 @@ +import { wrap } from '@motionone/utils'; +import clsx from 'clsx'; +import type { MotionValue } from 'framer-motion'; +import { + motion, + useAnimationFrame, + useMotionValue, + useScroll, + useSpring, + useTransform, + useVelocity, +} from 'framer-motion'; +import React, { useRef } from 'react'; + +import type { FramerCursorAttributes } from '~/types'; + +interface HorizontalScrollTextProps extends FramerCursorAttributes { + children: string; + baseVelocity: number; + className?: string; + href?: string; +} + +export function HorizontalScrollText({ + children, + baseVelocity = 100, + className, + href, + onMouseEnter, + onMouseLeave, +}: HorizontalScrollTextProps): React.ReactNode { + const baseX = useMotionValue(0); + + const { scrollY } = useScroll(); + + const scrollVelocity = useVelocity(scrollY); + + const smoothVelocity = useSpring(scrollVelocity, { + damping: 50, + stiffness: 400, + }) as MotionValue; + + const velocityFactor = useTransform(smoothVelocity, [0, 1000], [0, 5], { + clamp: false, + }); + + const motionValuePayload = useTransform( + baseX, + + (baseY) => `${String(wrap(-20, -45, baseY))}%`, + ); + + const directionFactor = useRef(1); + + useAnimationFrame((_, delta) => { + let moveBy = directionFactor.current * baseVelocity * (delta / 1000); + directionFactor.current = velocityFactor.get() < 0 ? -1 : 1; + moveBy += directionFactor.current * moveBy * velocityFactor.get(); + + baseX.set(baseX.get() + moveBy); + }); + + const HtmlTag = typeof href === 'string' ? motion.a : motion.h1; + + return ( +
+ + {Array.from({ length: 12 }, (_, idx) => ( + + {children} + + ))} + +
+ ); +} diff --git a/app/root.tsx b/app/root.tsx index 45979e0..3f0dc0c 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -47,7 +47,7 @@ export function Layout({ href='https://cdn.jsdelivr.net/gh/devicons/devicon@latest/devicon.min.css' /> - Welcome to Remix + Kurocado Studio { return [ - { title: 'New Remix App' }, - { name: 'description', content: 'Welcome to Remix!' }, + { title: 'Kurocado Studio' }, + { + name: 'description', + content: + 'Kurocado Studio specializes in SaaS development, open-source projects, and personalized web solutions.', + }, + { + name: 'keywords', + content: + 'Kurocado Studio, SaaS, open-source, web development, TypeScript', + }, + { name: 'author', content: 'Carlos Santiago' }, + { property: 'og:title', content: 'Kurocado Studio' }, ]; }; export default function Index(): React.ReactNode { - return ( -
-

Welcome to Remix

- -
- ); + return ; } diff --git a/app/tailwind.css b/app/tailwind.css index 04afbd1..b31dd73 100644 --- a/app/tailwind.css +++ b/app/tailwind.css @@ -6,7 +6,7 @@ * * Explore our open-source projects: {@link https://github.com/kurocado-studio} */ - +@import './typography.css'; @tailwind base; @tailwind components; @tailwind utilities; diff --git a/app/types/index.ts b/app/types/index.ts new file mode 100644 index 0000000..ccb922b --- /dev/null +++ b/app/types/index.ts @@ -0,0 +1,6 @@ +import type React from 'react'; + +export type FramerCursorAttributes = Pick< + Partial>, + 'onMouseEnter' | 'onMouseLeave' +>; diff --git a/app/typography.css b/app/typography.css new file mode 100644 index 0000000..9facaa6 --- /dev/null +++ b/app/typography.css @@ -0,0 +1,205 @@ +@layer components { + .typography { + color: theme(colors.neutral.950); + font-size: theme(fontSize.xl); + line-height: theme(fontSize.xl[1].lineHeight); + --shiki-color-background: theme(colors.neutral.950); + --shiki-color-text: theme(colors.white); + --shiki-token-comment: theme(colors.neutral.500); + --shiki-token-constant: theme(colors.neutral.300); + --shiki-token-function: theme(colors.neutral.300); + --shiki-token-keyword: theme(colors.neutral.400); + --shiki-token-parameter: theme(colors.neutral.400); + --shiki-token-punctuation: theme(colors.neutral.400); + --shiki-token-string-expression: theme(colors.neutral.300); + --shiki-token-string: theme(colors.neutral.400); + + :where(.typography > *) { + margin-top: theme(spacing.6); + margin-bottom: theme(spacing.6); + } + + /* Headings */ + + :where(h1) { + font-family: theme(fontFamily.display); + font-size: theme(fontSize.4xl); + font-variation-settings: theme( + fontFamily.display[1].fontVariationSettings + ); + font-weight: theme(fontWeight.semibold); + line-height: theme(fontSize.4xl[1].lineHeight); + margin-top: theme(spacing.16); + } + + :where(h2) { + font-family: theme(fontFamily.display); + font-size: theme(fontSize.2xl); + font-variation-settings: theme( + fontFamily.display[1].fontVariationSettings + ); + font-weight: theme(fontWeight.semibold); + line-height: theme(fontSize.2xl[1].lineHeight); + margin-top: theme(spacing.16); + } + + :where(h3) { + font-family: theme(fontFamily.display); + font-size: theme(fontSize.xl); + font-variation-settings: theme( + fontFamily.display[1].fontVariationSettings + ); + font-weight: theme(fontWeight.semibold); + line-height: theme(fontSize.xl[1].lineHeight); + margin-top: theme(spacing.10); + } + + :where(h2 + h3) { + margin-top: 0; + } + + /* Lists */ + + :where(ul, ol) { + padding-left: 1.5rem; + } + + :where(ul) { + list-style-type: disc; + } + + :where(ol) { + list-style-type: decimal; + } + + :where(li) { + padding-left: theme(spacing.3); + margin-top: theme(spacing.6); + } + + :where(li)::marker { + color: theme(colors.neutral.500); + } + + :where(li > *), + :where(li li) { + margin-top: theme(spacing.4); + } + + :where(ol > li)::marker { + font-size: theme(fontSize.base); + font-weight: theme(fontWeight.semibold); + } + + /* Tables */ + + :where(table) { + font-size: theme(fontSize.base); + line-height: theme(fontSize.base[1].lineHeight); + text-align: left; + width: 100%; + } + + :where(th) { + font-weight: theme(fontWeight.semibold); + } + + :where(thead th) { + border-bottom: 1px solid theme(colors.neutral.950); + padding-bottom: theme(spacing.6); + } + + :where(td) { + border-bottom: 1px solid theme(colors.neutral.950 / 0.1); + padding-bottom: theme(spacing.6); + padding-top: theme(spacing.6); + vertical-align: top; + } + + :where(:is(th, td):not(:last-child)) { + padding-right: theme(spacing.6); + } + + /* Code blocks */ + + :where(pre) { + background-color: theme(colors.neutral.950); + border-radius: theme(borderRadius.4xl); + display: flex; + margin: theme(spacing.10) calc(-1 * theme(spacing.6)); + overflow-x: auto; + + @screen sm { + margin-left: auto; + margin-right: auto; + } + } + + :where(pre code) { + color: theme(colors.white); + flex: none; + font-size: theme(fontSize.base); + line-height: theme(lineHeight.8); + padding: theme(padding.8) theme(padding.6); + + @screen sm { + padding: theme(spacing.10); + } + } + + /*
*/ + + :where(hr) { + border-color: theme(colors.neutral.950 / 0.1); + margin-bottom: theme(spacing.24); + margin-top: theme(spacing.24); + } + + /* Inline text */ + + :where(a) { + font-weight: theme(fontWeight.semibold); + text-decoration-skip-ink: none; + text-decoration-thickness: 1px; + text-decoration: underline; + text-underline-offset: 0.15em; + } + + :where(strong) { + font-weight: theme(fontWeight.semibold); + } + + :where(code:not(pre code)) { + font-size: calc(18 / 20 * 1em); + font-weight: theme(fontWeight.semibold); + + &::before, + &::after { + content: '`'; + } + } + + :where(h2 code, h3 code) { + font-weight: theme(fontWeight.bold); + } + + /* Figures */ + + :where(figure) { + margin-top: theme(spacing.32); + margin-bottom: theme(spacing.32); + } + + /* Spacing overrides */ + + :where(.typography:first-child > :first-child), + :where(li > :first-child) { + margin-top: 0 !important; + } + + :where(.typography:last-child > :last-child), + :where(li > :last-child) { + margin-bottom: 0 !important; + } + } +} diff --git a/app/views/Intro.tsx b/app/views/Intro.tsx new file mode 100644 index 0000000..ed14e02 --- /dev/null +++ b/app/views/Intro.tsx @@ -0,0 +1,15 @@ +import React from 'react'; + +import { HorizontalScrollText } from '~/lib/HorizontalScrollText'; + +export function Intro(): React.ReactNode { + return ( + <> +
+
+ Kurocado + Studio +
+ + ); +} diff --git a/package.json b/package.json index d52006c..194da27 100644 --- a/package.json +++ b/package.json @@ -20,9 +20,13 @@ "typecheck": "tsc" }, "dependencies": { + "@motionone/utils": "^10.18.0", "@remix-run/node": "^2.9.2", "@remix-run/react": "^2.9.2", + "clsx": "^2.1.1", + "framer-motion": "^11.13.5", "isbot": "^5.1.1", + "lodash-es": "^4.17.21", "react": "^18.2.0", "react-dom": "^18.2.0", "tailwind-merge": "^2.4.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 04190b2..9532103 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,15 +8,27 @@ importers: .: dependencies: + '@motionone/utils': + specifier: ^10.18.0 + version: 10.18.0 '@remix-run/node': specifier: ^2.9.2 version: 2.15.1(typescript@5.7.2) '@remix-run/react': specifier: ^2.9.2 version: 2.15.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.2) + clsx: + specifier: ^2.1.1 + version: 2.1.1 + framer-motion: + specifier: ^11.13.5 + version: 11.13.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) isbot: specifier: ^5.1.1 version: 5.1.17 + lodash-es: + specifier: ^4.17.21 + version: 4.17.21 react: specifier: ^18.2.0 version: 18.3.1 @@ -774,6 +786,12 @@ packages: '@microsoft/tsdoc@0.15.1': resolution: {integrity: sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==} + '@motionone/types@10.17.1': + resolution: {integrity: sha512-KaC4kgiODDz8hswCrS0btrVrzyU2CSQKO7Ps90ibBVSQmjkrt2teqta6/sOG59v7+dPnKMAg13jyqtMKV2yJ7A==} + + '@motionone/utils@10.18.0': + resolution: {integrity: sha512-3XVF7sgyTSI2KWvTf6uLlBJ5iAgRgmvp3bpuOiQJvInd4nZ19ET8lX5unn30SlmRH7hXbBbH+Gxd0m0klJ3Xtw==} + '@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1': resolution: {integrity: sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==} @@ -2099,6 +2117,10 @@ packages: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} @@ -2959,6 +2981,20 @@ packages: fraction.js@4.3.7: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + framer-motion@11.13.5: + resolution: {integrity: sha512-rArI0zPU9VkpS3Wt0J7dmRxAFUWtzPWoSofNQAP0UO276CmJ+Xlf5xN19GMw3w2QsdrS2sU+0+Q2vtuz4IEZaw==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + fresh@0.5.2: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} @@ -3178,6 +3214,9 @@ packages: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} hasBin: true + hey-listen@1.0.8: + resolution: {integrity: sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==} + highlight.js@10.7.3: resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} @@ -4100,6 +4139,12 @@ packages: resolution: {integrity: sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==} engines: {node: '>= 0.8.0'} + motion-dom@11.13.0: + resolution: {integrity: sha512-Oc1MLGJQ6nrvXccXA89lXtOqFyBmvHtaDcTRGT66o8Czl7nuA8BeHAd9MQV1pQKX0d2RHFBFaw5g3k23hQJt0w==} + + motion-utils@11.13.0: + resolution: {integrity: sha512-lq6TzXkH5c/ysJQBxgLXgM01qwBH1b4goTPh57VvZWJbVJZF/0SB31UWEn4EIqbVPf3au88n2rvK17SpDTja1A==} + mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} @@ -6815,6 +6860,14 @@ snapshots: '@microsoft/tsdoc@0.15.1': {} + '@motionone/types@10.17.1': {} + + '@motionone/utils@10.18.0': + dependencies: + '@motionone/types': 10.17.1 + hey-listen: 1.0.8 + tslib: 2.8.1 + '@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1': dependencies: eslint-scope: 5.1.1 @@ -8487,6 +8540,8 @@ snapshots: clone@1.0.4: {} + clsx@2.1.1: {} + color-convert@1.9.3: dependencies: color-name: 1.1.3 @@ -9511,6 +9566,15 @@ snapshots: fraction.js@4.3.7: {} + framer-motion@11.13.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + motion-dom: 11.13.0 + motion-utils: 11.13.0 + tslib: 2.8.1 + optionalDependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + fresh@0.5.2: {} from2@2.3.0: @@ -9755,6 +9819,8 @@ snapshots: he@1.2.0: {} + hey-listen@1.0.8: {} + highlight.js@10.7.3: {} hook-std@3.0.0: {} @@ -10821,6 +10887,10 @@ snapshots: transitivePeerDependencies: - supports-color + motion-dom@11.13.0: {} + + motion-utils@11.13.0: {} + mri@1.2.0: {} mrmime@1.0.1: {} diff --git a/tailwind.config.ts b/tailwind.config.ts index c0a86aa..3b38dd2 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -8,12 +8,173 @@ */ /* eslint import/no-default-export: 0 */ +import { get } from 'lodash-es'; import { type Config } from 'tailwindcss'; +import defaultTheme from 'tailwindcss/defaultTheme'; export default { - content: ['./app/**/*.{js,jsx,mjs,mdx,ts,tsx}'], + content: ['./app/**/*.{js,jsx,mjs,mdx,ts,tsx}', './app/**/*.css'], theme: { - extend: {}, + fontSize: { + xs: ['0.75rem', { lineHeight: '1rem' }], + sm: ['0.875rem', { lineHeight: '1.5rem' }], + base: ['1rem', { lineHeight: '1.75rem' }], + lg: ['1.125rem', { lineHeight: '1.75rem' }], + xl: ['1.25rem', { lineHeight: '2rem' }], + '2xl': ['1.5rem', { lineHeight: '2.25rem' }], + '3xl': ['1.75rem', { lineHeight: '2.25rem' }], + '4xl': ['2rem', { lineHeight: '2.5rem' }], + '5xl': ['2.5rem', { lineHeight: '3rem' }], + '6xl': ['3rem', { lineHeight: '3.5rem' }], + '7xl': ['4rem', { lineHeight: '4.5rem' }], + '8xl': ['8rem', { lineHeight: '4.5rem' }], + }, + extend: { + colors: { + brand: { + 50: 'rgb(240, 253, 244)', + 100: 'rgb(220, 252, 231)', + 200: 'rgb(187, 247, 208)', + 300: 'rgb(134, 239, 172)', + 400: 'rgb(74, 222, 128)', + 500: 'rgb(34, 197, 94)', + 600: 'rgb(22, 163, 74)', + 700: 'rgb(21, 128, 61)', + 800: 'rgb(22, 101, 52)', + 900: 'rgb(20, 83, 45)', + }, + neutral: { + 0: 'rgb(255, 255, 255)', + 50: 'rgb(250, 250, 250)', + 100: 'rgb(245, 245, 245)', + 200: 'rgb(229, 229, 229)', + 300: 'rgb(212, 212, 212)', + 400: 'rgb(163, 163, 163)', + 500: 'rgb(115, 115, 115)', + 600: 'rgb(82, 82, 82)', + 700: 'rgb(64, 64, 64)', + 800: 'rgb(38, 38, 38)', + 900: 'rgb(23, 23, 23)', + 950: 'rgb(10, 10, 10)', + }, + 'brand-primary': 'rgb(22, 163, 74)', + 'default-background': 'rgb(255, 255, 255)', + 'default-font': 'rgb(23, 23, 23)', + 'neutral-border': 'rgb(229, 229, 229)', + 'subtext-color': 'rgb(115, 115, 115)', + white: 'rgb(255, 255, 255)', + }, + fontSize: { + caption: [ + '12px', + { + lineHeight: '16px', + fontWeight: '400', + }, + ], + 'caption-bold': [ + '12px', + { + lineHeight: '16px', + fontWeight: '700', + }, + ], + body: [ + '14px', + { + lineHeight: '20px', + fontWeight: '400', + }, + ], + 'body-bold': [ + '14px', + { + lineHeight: '20px', + fontWeight: '700', + }, + ], + 'heading-3': [ + '16px', + { + lineHeight: '20px', + fontWeight: '700', + }, + ], + 'heading-2': [ + '20px', + { + lineHeight: '24px', + fontWeight: '700', + }, + ], + 'heading-1': [ + '30px', + { + lineHeight: '36px', + fontWeight: '700', + }, + ], + 'monospace-body': [ + '14px', + { + lineHeight: '20px', + fontWeight: '400', + }, + ], + }, + fontFamily: { + caption: 'Mona Sans', + sans: ['Mona Sans', ...get(defaultTheme, ['fontFamily', 'sans'], [])], + body: 'Mona Sans', + 'body-bold': 'Mona Sans', + 'heading-3': 'Mona Sans', + 'heading-2': 'Mona Sans', + 'heading-1': 'Mona Sans', + 'monospace-body': 'monospace', + display: [ + ['Mona Sans', ...get(defaultTheme, ['fontFamily', 'sans'], [])], + { fontVariationSettings: '"wdth" 125' }, + ], + }, + boxShadow: { + sm: '0px 1px 2px 0px rgba(0, 0, 0, 0.05)', + default: '0px 1px 2px 0px rgba(0, 0, 0, 0.05)', + md: '0px 4px 16px -2px rgba(0, 0, 0, 0.08), 0px 2px 4px -1px rgba(0, 0, 0, 0.08)', + lg: '0px 12px 32px -4px rgba(0, 0, 0, 0.08), 0px 4px 8px -2px rgba(0, 0, 0, 0.08)', + overlay: + '0px 12px 32px -4px rgba(0, 0, 0, 0.08), 0px 4px 8px -2px rgba(0, 0, 0, 0.08)', + }, + borderRadius: { + sm: '8px', + md: '16px', + DEFAULT: '16px', + lg: '24px', + full: '9999px', + '4xl': '2.5rem', + }, + container: { + padding: { + DEFAULT: '16px', + sm: 'calc((100vw + 16px - 640px) / 2)', + md: 'calc((100vw + 16px - 768px) / 2)', + lg: 'calc((100vw + 16px - 1024px) / 2)', + xl: 'calc((100vw + 16px - 1280px) / 2)', + '2xl': 'calc((100vw + 16px - 1536px) / 2)', + }, + }, + spacing: { + 112: '28rem', + 144: '36rem', + 192: '48rem', + 256: '64rem', + 320: '80rem', + }, + screens: { + mobile: { + max: '767px', + }, + }, + }, }, plugins: [], } satisfies Config;