diff --git a/package.json b/package.json index 852a31b..9828b60 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "@hono/node-server": "^1.13.5", "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-tabs": "^1.1.2", "@radix-ui/react-visually-hidden": "^1.1.0", "@remix-run/node": "^2.13.1", "@remix-run/react": "^2.13.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9b9bfc7..9036bf9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ dependencies: '@radix-ui/react-slot': specifier: ^1.1.0 version: 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-tabs': + specifier: ^1.1.2 + version: 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1)(react@18.3.1) '@radix-ui/react-visually-hidden': specifier: ^1.1.0 version: 1.1.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1)(react@18.3.1) @@ -1541,6 +1544,33 @@ packages: resolution: {integrity: sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==} dev: false + /@radix-ui/primitive@1.1.1: + resolution: {integrity: sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==} + dev: false + + /@radix-ui/react-collection@1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-LwT3pSho9Dljg+wY2KN2mrrh6y3qELfftINERIzBUO9e0N+t0oMTyn3k9iv+ZqgrwGkRnLpNJrsMv9BZlt2yuA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-slot': 1.1.1(@types/react@18.3.12)(react@18.3.1) + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + /@radix-ui/react-compose-refs@1.1.0(@types/react@18.3.12)(react@18.3.1): resolution: {integrity: sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==} peerDependencies: @@ -1554,6 +1584,19 @@ packages: react: 18.3.1 dev: false + /@radix-ui/react-compose-refs@1.1.1(@types/react@18.3.12)(react@18.3.1): + resolution: {integrity: sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.3.12 + react: 18.3.1 + dev: false + /@radix-ui/react-context@1.1.1(@types/react@18.3.12)(react@18.3.1): resolution: {integrity: sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==} peerDependencies: @@ -1600,6 +1643,19 @@ packages: react-remove-scroll: 2.6.0(@types/react@18.3.12)(react@18.3.1) dev: false + /@radix-ui/react-direction@1.1.0(@types/react@18.3.12)(react@18.3.1): + resolution: {integrity: sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.3.12 + react: 18.3.1 + dev: false + /@radix-ui/react-dismissable-layer@1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-QSxg29lfr/xcev6kSz7MAlmDnzbP1eI/Dwn3Tp1ip0KT5CUELsxkekFEMVBEoykI3oV39hKT4TKZzBNMbcTZYQ==} peerDependencies: @@ -1715,6 +1771,27 @@ packages: react-dom: 18.3.1(react@18.3.1) dev: false + /@radix-ui/react-presence@1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + /@radix-ui/react-primitive@2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==} peerDependencies: @@ -1735,6 +1812,54 @@ packages: react-dom: 18.3.1(react@18.3.1) dev: false + /@radix-ui/react-primitive@2.0.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/react-slot': 1.1.1(@types/react@18.3.12)(react@18.3.1) + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + + /@radix-ui/react-roving-focus@1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-QE1RoxPGJ/Nm8Qmk0PxP8ojmoaS67i0s7hVssS7KuI2FQoc/uzVlZsqKfQvxPE6D8hICCPHJ4D88zNhT3OOmkw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-collection': 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-direction': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + /@radix-ui/react-slot@1.1.0(@types/react@18.3.12)(react@18.3.1): resolution: {integrity: sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==} peerDependencies: @@ -1749,6 +1874,47 @@ packages: react: 18.3.1 dev: false + /@radix-ui/react-slot@1.1.1(@types/react@18.3.12)(react@18.3.1): + resolution: {integrity: sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@18.3.12)(react@18.3.1) + '@types/react': 18.3.12 + react: 18.3.1 + dev: false + + /@radix-ui/react-tabs@1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-9u/tQJMcC2aGq7KXpGivMm1mgq7oRJKXphDwdypPd/j21j/2znamPU8WkXgnhUaTrSFNIt8XhOyCAupg8/GbwQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-context': 1.1.1(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-direction': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@radix-ui/react-presence': 1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.12)(react@18.3.1) + '@types/react': 18.3.12 + '@types/react-dom': 18.3.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + dev: false + /@radix-ui/react-use-callback-ref@1.1.0(@types/react@18.3.12)(react@18.3.1): resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==} peerDependencies: diff --git a/src/remix-app/components/experience/DraftKingsExperience.tsx b/src/remix-app/components/experience/DraftKingsExperience.tsx new file mode 100644 index 0000000..1c1066c --- /dev/null +++ b/src/remix-app/components/experience/DraftKingsExperience.tsx @@ -0,0 +1,87 @@ +import * as stylex from '@stylexjs/stylex'; + +import Text from '../typography/text/Text'; + +import Experience from './Experience'; +import { experienceStyles } from './experience.stylex'; + +const DraftKingsExperience = () => ( + + + +); + +export default DraftKingsExperience; diff --git a/src/remix-app/components/experience/Experience.tsx b/src/remix-app/components/experience/Experience.tsx new file mode 100644 index 0000000..1a90ee1 --- /dev/null +++ b/src/remix-app/components/experience/Experience.tsx @@ -0,0 +1,53 @@ +import { Link } from '@remix-run/react'; +import * as stylex from '@stylexjs/stylex'; + +import { tokens } from '../../themes/tokens.stylex'; +import Heading from '../typography/heading/Heading'; +import Text from '../typography/text/Text'; + +import type { PropsWithChildren } from 'react'; + +interface Company { + name: string; + url: string; +} + +interface ExperienceProps extends PropsWithChildren { + position: string; + company: Company; + range: string; +} + +const styles = stylex.create({ + companyLink: { + color: tokens.color_text_brand, + background: `none, linear-gradient(to right, ${tokens.color_text_brand}, ${tokens.color_text_brand})`, + backgroundRepeat: 'no-repeat', + backgroundPosition: '100% 100%, 0 100%', + backgroundSize: { + default: '100% 0.2rem, 0 0.2rem', + ':hover': '0 0.2rem, 100% 0.2rem', + ':focus': '0 0.2rem, 100% 0.2rem', + }, + transition: 'background-size 400ms', + }, +}); + +const Experience = ({ position, company, range, children }: ExperienceProps) => ( +
+ + {position}{' '} + + @ {company.name} + + + + + {range} + + + {children} +
+); + +export default Experience; diff --git a/src/remix-app/components/experience/FactSetExperience.tsx b/src/remix-app/components/experience/FactSetExperience.tsx new file mode 100644 index 0000000..313c58b --- /dev/null +++ b/src/remix-app/components/experience/FactSetExperience.tsx @@ -0,0 +1,73 @@ +import * as stylex from '@stylexjs/stylex'; + +import Text from '../typography/text/Text'; + +import Experience from './Experience'; +import { experienceStyles } from './experience.stylex'; + +const FactSetExperience = () => ( + + + +); + +export default FactSetExperience; diff --git a/src/remix-app/components/experience/experience.stylex.ts b/src/remix-app/components/experience/experience.stylex.ts new file mode 100644 index 0000000..743ef7a --- /dev/null +++ b/src/remix-app/components/experience/experience.stylex.ts @@ -0,0 +1,27 @@ +import * as stylex from '@stylexjs/stylex'; + +import { spacing } from '../../themes/spacing.stylex'; +import { tokens } from '../../themes/tokens.stylex'; +import { size } from '../../themes/typography.stylex'; + +const experienceStyles = stylex.create({ + list: { + display: 'flex', + flexDirection: 'column', + gap: spacing.s2, + marginTop: spacing.s4, + marginLeft: spacing.s5, + color: tokens.color_text_base, + listStylePosition: 'inside', + }, + listElement: { + color: { '::marker': tokens.color_text_brand }, + content: { '::marker': '\\21A0' }, + fontSize: { '::marker': size.s4 }, + }, + listElementText: { + paddingLeft: spacing.s2, + }, +}); + +export { experienceStyles }; diff --git a/src/remix-app/components/tabs/Tabs.tsx b/src/remix-app/components/tabs/Tabs.tsx new file mode 100644 index 0000000..7dc26d4 --- /dev/null +++ b/src/remix-app/components/tabs/Tabs.tsx @@ -0,0 +1,126 @@ +import * as TabsPrimitive from '@radix-ui/react-tabs'; +import * as stylex from '@stylexjs/stylex'; +import { forwardRef } from 'react'; + +import { rounded } from '../../themes/rounded.stylex'; +import { spacing } from '../../themes/spacing.stylex'; +import { tokens } from '../../themes/tokens.stylex'; +import { size, weights } from '../../themes/typography.stylex'; + +import type { ComponentPropsWithoutRef, ElementRef } from 'react'; + +interface TabRootProps extends Omit, 'classname' | 'style'> { + style?: stylex.StyleXStyles; +} + +interface TabTriggerProps extends Omit, 'classname' | 'style'> { + style?: stylex.StyleXStyles; +} + +interface TabListProps extends Omit, 'classname' | 'style'> { + style?: stylex.StyleXStyles; +} + +interface TabContentProps extends Omit, 'classname' | 'style'> { + style?: stylex.StyleXStyles; +} + +const sm = '@media (min-width: 640px)'; + +const styles = stylex.create({ + tabsList: { + display: 'inline-flex', + height: size.s9, + alignItems: 'center', + justifyContent: 'center', + padding: spacing.s2, + }, + tabsTrigger: { + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', + position: { + default: 'relative', + ':is([data-state="inactive"])::after': 'absolute', + }, + bottom: { + ':is([data-state="inactive"])::after': 0, + }, + left: { + ':is([data-state="inactive"])::after': 0, + }, + width: { + ':is([data-state="inactive"])::after': '100%', + }, + height: { + ':is([data-state="inactive"])::after': '0.175rem', + }, + content: { + ':is([data-state="inactive"])::after': '', + }, + background: { + ':is([data-state="inactive"])::after': tokens.color_text_brand, + }, + whiteSpace: 'nowrap', + borderRadius: rounded.sm, + padding: `${spacing.s2} ${spacing.s3}`, + fontSize: size.s2, + fontWeight: weights.medium, + transition: { default: 'all 0.2s ease-in-out', ':is([data-state="inactive"])::after': 'transform 0.3s ease' }, + transform: { + ':is([data-state="inactive"])::after': 'scale(0, 1.25)', + ':is([data-state="inactive"]):hover::after': 'scale(1, 1.25)', + }, + outline: '3px solid transparent', + boxShadow: { + ':focus-visible': `0 0 0 3px ${tokens.color_action_outline}`, + }, + pointerEvents: { + ':disabled': 'none', + }, + opacity: { + ':disabled': 0.5, + }, + color: { + default: tokens.color_text_base, + ':is([data-state="active"])': tokens.color_text_brand, + }, + }, + tabsContent: { + borderTop: { default: 'none', [sm]: `solid 1px ${tokens.color_text_base}` }, + borderLeft: { default: `solid 1px ${tokens.color_text_base}`, [sm]: 'none' }, + paddingTop: { default: 'none', [sm]: spacing.s2 }, + paddingLeft: { default: spacing.s2, [sm]: 'none' }, + marginTop: spacing.s1, + outline: '3px solid transparent', + boxShadow: { + ':focus-visible': `0 0 0 3px ${tokens.color_action_outline}`, + }, + }, +}); + +const Tabs = forwardRef, TabRootProps>(({ style, ...props }, ref) => ( + +)); +Tabs.displayName = TabsPrimitive.Root.displayName; + +const TabsList = forwardRef, TabListProps>(({ style, ...props }, ref) => ( + +)); +TabsList.displayName = TabsPrimitive.List.displayName; + +const TabsTrigger = forwardRef, TabTriggerProps>( + ({ style, ...props }, ref) => ( + + ) +); +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; + +const TabsContent = forwardRef, TabContentProps>( + ({ style, ...props }, ref) => ( + + ) +); +TabsContent.displayName = TabsPrimitive.Content.displayName; + +export { Tabs, TabsContent, TabsList, TabsTrigger }; diff --git a/src/remix-app/routes/experience.tsx b/src/remix-app/routes/experience.tsx new file mode 100644 index 0000000..655c2d7 --- /dev/null +++ b/src/remix-app/routes/experience.tsx @@ -0,0 +1,94 @@ +import * as stylex from '@stylexjs/stylex'; +import { useCallback, useEffect, useState } from 'react'; + +import DraftKingsExperience from '../components/experience/DraftKingsExperience'; +import FactSetExperience from '../components/experience/FactSetExperience'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '../components/tabs/Tabs'; +import { spacing } from '../themes/spacing.stylex'; +import { size, weights } from '../themes/typography.stylex'; + +import type { MetaFunction } from '@remix-run/node'; + +const FACTSET_TAB_KEY = 'factset' as const; +const DRAFTKINGS_TAB_KEY = 'draftkings' as const; + +const smSize = 640 as const; +const sm = `@media (min-width: ${smSize}px)`; + +const meta: MetaFunction = () => [ + { title: 'Experience Shayne Preston - Software Engineer & Web Developer' }, + { + name: 'description', + content: "Learn more about Shayne Preston's software experience. Discover passed and present projects.", + }, +]; + +const styles = stylex.create({ + page: { + display: 'flex', + flexDirection: 'column', + flexGrow: 1, + paddingTop: spacing.s4, + alignItems: 'center', + }, + tabsContainer: { + display: 'flex', + flexDirection: { default: 'row', [sm]: 'column' }, + width: `min(100%, 800px)`, + flexGrow: 1, + padding: { default: `0 ${spacing.s2}`, [sm]: `0 ${spacing.s6}` }, + }, + tabListContainer: { + display: 'grid', + gridTemplateColumns: { default: 'none', [sm]: 'repeat(2, 1fr)' }, + gridTemplateRows: { default: 'repeat(2, 1fr)', [sm]: 'none' }, + width: 'min-content', + margin: '0 auto', + }, + tabListItem: { + fontWeight: weights.bold, + fontSize: size.s4, + }, +}); + +const ExperienceRoute = () => { + const [orientation, setOrientation] = useState<'horizontal' | 'vertical'>('horizontal'); + + const onResize = useCallback(() => { + setOrientation(window.innerWidth > smSize ? 'horizontal' : 'vertical'); + }, []); + + useEffect(() => { + window.addEventListener('resize', onResize); + + return () => { + window.removeEventListener('resize', onResize); + }; + }, [onResize]); + + return ( +
+ + + + DraftKings + + + FactSet + + + + + + + + + + + +
+ ); +}; + +export default ExperienceRoute; +export { meta };