@@ -0,0 +1 @@ | |||
.idea/ |
@@ -0,0 +1,3 @@ | |||
{ | |||
"extends": "next/core-web-vitals" | |||
} |
@@ -0,0 +1,35 @@ | |||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. | |||
# dependencies | |||
/node_modules | |||
/.pnp | |||
.pnp.js | |||
# testing | |||
/coverage | |||
# next.js | |||
/.next/ | |||
/out/ | |||
# production | |||
/build | |||
# misc | |||
.DS_Store | |||
*.pem | |||
# debug | |||
npm-debug.log* | |||
yarn-debug.log* | |||
yarn-error.log* | |||
# local env files | |||
.env*.local | |||
# vercel | |||
.vercel | |||
# typescript | |||
*.tsbuildinfo | |||
next-env.d.ts |
@@ -0,0 +1,38 @@ | |||
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). | |||
## Getting Started | |||
First, run the development server: | |||
```bash | |||
npm run dev | |||
# or | |||
yarn dev | |||
# or | |||
pnpm dev | |||
``` | |||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. | |||
You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. | |||
[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. | |||
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. | |||
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. | |||
## Learn More | |||
To learn more about Next.js, take a look at the following resources: | |||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. | |||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. | |||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! | |||
## Deploy on Vercel | |||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. | |||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. |
@@ -0,0 +1,3 @@ | |||
[install.scopes] | |||
"@tesseract-design" = "http://localhost:4873/" | |||
"@modal-sh" = "http://localhost:4873/" |
@@ -0,0 +1,6 @@ | |||
/** @type {import('next').NextConfig} */ | |||
const nextConfig = { | |||
reactStrictMode: true, | |||
} | |||
module.exports = nextConfig |
@@ -0,0 +1,33 @@ | |||
{ | |||
"name": "web", | |||
"version": "0.1.0", | |||
"private": true, | |||
"scripts": { | |||
"dev": "next dev", | |||
"build": "next build", | |||
"start": "next start", | |||
"lint": "next lint" | |||
}, | |||
"dependencies": { | |||
"@tesseract-design/viewfinder-base": "0.0.1", | |||
"@tesseract-design/viewfinder-react": "0.0.1", | |||
"@tesseract-design/web-action-react": "^0.2.0", | |||
"@tesseract-design/web-formatted-react": "^0.2.0", | |||
"@tesseract-design/web-freeform-react": "^0.2.0", | |||
"@tesseract-design/web-navigation-react": "^0.2.0", | |||
"@theoryofnekomata/formxtra": "^1.0.3", | |||
"@types/node": "20.6.0", | |||
"@types/react": "18.2.21", | |||
"@types/react-dom": "18.2.7", | |||
"autoprefixer": "10.4.15", | |||
"clsx": "^2.0.0", | |||
"eslint": "8.49.0", | |||
"eslint-config-next": "13.4.19", | |||
"next": "13.4.19", | |||
"postcss": "8.4.29", | |||
"react": "18.2.0", | |||
"react-dom": "18.2.0", | |||
"tailwindcss": "3.3.3", | |||
"typescript": "5.2.2" | |||
} | |||
} |
@@ -0,0 +1,6 @@ | |||
module.exports = { | |||
plugins: { | |||
tailwindcss: {}, | |||
autoprefixer: {}, | |||
}, | |||
} |
@@ -0,0 +1 @@ | |||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg> |
@@ -0,0 +1 @@ | |||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg> |
@@ -0,0 +1,75 @@ | |||
import * as React from 'react'; | |||
export interface BackgroundGridProps { | |||
level?: number; | |||
images: string[]; | |||
parentKey?: React.Key; | |||
} | |||
export const BackgroundGrid: React.FC<BackgroundGridProps> = ({ | |||
level = 0, | |||
images, | |||
parentKey = level, | |||
}) => { | |||
const [values, setValues] = React.useState<(string | null)[]>(); | |||
React.useEffect(() => { | |||
const newValues = []; | |||
for (let i = 0; i < 4; i += 1) { | |||
if (level < 1) { | |||
newValues.push(null); | |||
continue; | |||
} | |||
if (level < 3) { | |||
const hasInnerGrid = Math.floor(Math.random() * 2) === 1; | |||
if (hasInnerGrid) { | |||
newValues.push(null); | |||
continue; | |||
} | |||
} | |||
const randomImage = images[Math.floor(Math.random() * images.length)]; | |||
newValues.push(randomImage); | |||
} | |||
setValues(newValues); | |||
}, []); | |||
return ( | |||
<div className="grid grid-cols-2 grid-rows-2 w-full h-full"> | |||
{values?.map((value, index) => { | |||
if (value === null) { | |||
return ( | |||
<BackgroundGrid | |||
images={images} | |||
level={level + 1} | |||
key={`${parentKey}:${index}`} | |||
/> | |||
); | |||
// return ( | |||
// level < 3 | |||
// ? ( | |||
// <BackgroundGrid | |||
// images={images} | |||
// level={level + 1} | |||
// key={index} | |||
// /> | |||
// ) | |||
// : <div /> | |||
// ); | |||
} | |||
return ( | |||
<div | |||
key={`${parentKey}:${index}`} | |||
className="bg-cover bg-center border border-shade" | |||
style={{backgroundImage: `url("${value}")`}} | |||
/> | |||
); | |||
})} | |||
</div> | |||
); | |||
} |
@@ -0,0 +1,7 @@ | |||
import * as React from 'react'; | |||
export const Brand = () => ( | |||
<span className="rounded-[0.25em] lowercase align-middle font-headings bg-positive text-negative px-1.5 font-semibold -mx-2 before:content-['@']"> | |||
TheoryOfNekomata | |||
</span> | |||
); |
@@ -0,0 +1,124 @@ | |||
import * as React from 'react'; | |||
import {Layouts} from '@tesseract-design/viewfinder-react'; | |||
import * as WebNavigationReact from '@tesseract-design/web-navigation-react'; | |||
import * as WebFreeformReact from '@tesseract-design/web-freeform-react'; | |||
import * as WebFormattedReact from '@tesseract-design/web-formatted-react'; | |||
import * as WebActionReact from '@tesseract-design/web-action-react'; | |||
export interface ContactCtaBannerProps { | |||
onSubmit?: React.FormEventHandler<HTMLFormElement>; | |||
} | |||
export const ContactCtaBanner: React.FC<ContactCtaBannerProps> = ({ | |||
onSubmit, | |||
}) => { | |||
const [visible, setVisible] = React.useState<boolean>(); | |||
const autofocusRef = React.useRef<HTMLInputElement>(null); | |||
const messageRef = React.useRef<HTMLTextAreaElement>(null); | |||
const openContactForm: React.MouseEventHandler<HTMLAnchorElement> = (e) => { | |||
e.preventDefault(); | |||
setVisible(true); | |||
window.setTimeout(() => { | |||
autofocusRef.current?.focus(); | |||
if (messageRef.current && messageRef.current.style.height === '0px') { | |||
messageRef.current.style.height = '12rem'; | |||
} | |||
}); | |||
}; | |||
React.useEffect(() => { | |||
setVisible(false); | |||
}, []); | |||
return ( | |||
<div className="pt-8 sm:pt-16 pb-8 sm:pb-16 flex items-center relative"> | |||
<div | |||
className="absolute top-0 left-0 w-full h-full bg-shade opacity-75" | |||
/> | |||
<Layouts.Basic.ContentContainer | |||
span="wide" | |||
className="relative" | |||
> | |||
<div className="flex flex-wrap flex-row items-center sm:justify-between gap-6 sm:gap-12"> | |||
<p className="m-0"> | |||
<strong | |||
className="font-extralight font-headings text-5xl lowercase" | |||
> | |||
Get in touch. | |||
</strong> | |||
</p> | |||
<p className="m-0"> | |||
Your message will be received via email. | |||
</p> | |||
<div | |||
className={`grow sm:grow-0 text-right ${visible ? 'hidden' : ''}`} | |||
> | |||
<WebNavigationReact.LinkButton | |||
component="a" | |||
href="/contact" | |||
onClick={openContactForm} | |||
> | |||
Open Form | |||
</WebNavigationReact.LinkButton> | |||
</div> | |||
</div> | |||
{visible !== false && ( | |||
<form | |||
className="mt-8" | |||
aria-label="Contact Form" | |||
onSubmit={onSubmit} | |||
method="post" | |||
action="/a/contact" | |||
> | |||
<fieldset className="contents"> | |||
<legend className="sr-only"> | |||
Contact Form | |||
</legend> | |||
<div | |||
className="flex flex-col xs:grid xs:grid-cols-2 gap-4" | |||
> | |||
<div> | |||
<WebFreeformReact.TextInput | |||
label="Name" | |||
name="name" | |||
block | |||
border | |||
ref={autofocusRef} | |||
/> | |||
</div> | |||
<div> | |||
<WebFormattedReact.EmailInput | |||
label="Email" | |||
name="email" | |||
block | |||
border | |||
required | |||
/> | |||
</div> | |||
<div className="col-span-2"> | |||
<WebFreeformReact.MultilineTextInput | |||
ref={messageRef} | |||
label="Message" | |||
name="message" | |||
block | |||
border | |||
required | |||
/> | |||
</div> | |||
<div className="col-span-2 text-right"> | |||
<WebActionReact.ActionButton | |||
type="submit" | |||
variant="filled" | |||
> | |||
Send Message | |||
</WebActionReact.ActionButton> | |||
</div> | |||
</div> | |||
</fieldset> | |||
</form> | |||
)} | |||
</Layouts.Basic.ContentContainer> | |||
</div> | |||
); | |||
} |
@@ -0,0 +1,72 @@ | |||
import * as React from 'react'; | |||
import Link from 'next/link'; | |||
import {Layouts} from '@tesseract-design/viewfinder-react'; | |||
import * as WebNavigationReact from '@tesseract-design/web-navigation-react'; | |||
import {ShowcaseItem, ShowcaseItemProps} from '@/components/molecules/ShowcaseItem'; | |||
interface EnvisionSectionDatum extends ShowcaseItemProps { | |||
id: string; | |||
url: string; | |||
} | |||
export interface EnvisionSectionProps { | |||
data: EnvisionSectionDatum[]; | |||
} | |||
export const EnvisionSection: React.FC<EnvisionSectionProps> = ({ | |||
data, | |||
}) => ( | |||
<div className="py-12 sm:py-24 flex items-center min-h-full bg-negative relative"> | |||
<Layouts.Basic.ContentContainer | |||
span="wide" | |||
> | |||
<div className="pb-8"> | |||
<h2 className="text-base leading-none m-0"> | |||
<span className="text-7xl sm:text-8xl"> | |||
I | |||
{' '} | |||
<Link | |||
className="no-underline px-1 -mx-1" | |||
href={{ | |||
query: { | |||
featured: 'all', | |||
}, | |||
}} | |||
> | |||
envision. | |||
</Link> | |||
</span> | |||
</h2> | |||
<div className="flex gap-2 font-headings lowercase text-3xl sm:text-4xl leading-none font-normal"> | |||
<div><Link href={{ query: { featured: 'ideas' }}} className="p-1 -m-1 after:content-['.']">Ideas</Link></div> | |||
<div><Link href={{ query: { featured: 'methods' }}} className="p-1 -m-1 after:content-['.']">Methods</Link></div> | |||
<div><Link href={{ query: { featured: 'solutions' }}} className="p-1 -m-1 after:content-['.']">Solutions</Link></div> | |||
</div> | |||
</div> | |||
<div className="flex flex-col xs:grid xs:grid-cols-2 gap-8"> | |||
{data.map(({ id, url, ...datum }) => ( | |||
<div key={id}> | |||
<a href={url} className="block"> | |||
<ShowcaseItem {...datum} /> | |||
</a> | |||
</div> | |||
))} | |||
</div> | |||
<div className="flex justify-end mt-8"> | |||
<div className="w-40"> | |||
<WebNavigationReact.LinkButton | |||
component={Link as unknown as 'a'} | |||
href={{ | |||
pathname: '/featured', | |||
} as unknown as string} | |||
menuItem | |||
block | |||
subtext="Featured" | |||
> | |||
Browse | |||
</WebNavigationReact.LinkButton> | |||
</div> | |||
</div> | |||
</Layouts.Basic.ContentContainer> | |||
</div> | |||
); |
@@ -0,0 +1,93 @@ | |||
import * as React from 'react'; | |||
import {Layouts} from '@tesseract-design/viewfinder-react'; | |||
import * as WebNavigationReact from '@tesseract-design/web-navigation-react'; | |||
interface IframeShowcaseProps extends Omit<React.HTMLProps<HTMLElementTagNameMap['iframe']>, 'children'> { | |||
type: 'iframe'; | |||
} | |||
interface ImageShowcaseProps extends Omit<React.HTMLProps<HTMLElementTagNameMap['img']>, 'alt'> { | |||
type: 'img'; | |||
} | |||
type ShowcaseProps = IframeShowcaseProps | ImageShowcaseProps; | |||
interface FeaturedProjectLink { | |||
href: string; | |||
children: React.ReactNode; | |||
primary?: boolean; | |||
subtext?: React.ReactNode; | |||
} | |||
export interface FeaturedProjectSectionProps { | |||
title: string; | |||
description: string; | |||
links?: FeaturedProjectLink[]; | |||
showcase: ShowcaseProps; | |||
} | |||
export const FeaturedProjectSection: React.FC<FeaturedProjectSectionProps> = ({ | |||
title, | |||
description, | |||
links = [], | |||
showcase, | |||
}) => { | |||
const { type: showcaseType, ...showcaseProps } = showcase; | |||
return ( | |||
<div className="pt-24 sm:py-24 flex flex-col gap-8 sm:flex-row items-center sm:min-h-full bg-negative relative group"> | |||
<div | |||
className="absolute top-0 left-0 w-full h-full bg-shade opacity-25 group-even:opacity-0" | |||
/> | |||
<div className="sm:absolute top-0 left-0 w-full h-full"> | |||
<Layouts.Basic.ContentContainer | |||
span="wide" | |||
className="relative sm:h-full" | |||
> | |||
<div className="sm:grid sm:grid-cols-5 sm:gap-32 sm:h-full"> | |||
<div className="col-start-1 group-even:col-start-3 col-span-3 flex flex-col justify-center h-full gap-8"> | |||
<h3 className="m-0 text-5xl font-light">{title}</h3> | |||
<p className="m-0">{description}</p> | |||
<div className="flex flex-wrap flex-row gap-4"> | |||
{links.map(({ href, primary, subtext, children }) => ( | |||
<div | |||
key={href} | |||
> | |||
<WebNavigationReact.LinkButton | |||
component="a" | |||
href={href} | |||
variant={primary ? 'filled' : 'outline'} | |||
subtext={subtext} | |||
target="_blank" | |||
> | |||
{children} | |||
</WebNavigationReact.LinkButton> | |||
</div> | |||
))} | |||
</div> | |||
</div> | |||
</div> | |||
</Layouts.Basic.ContentContainer> | |||
</div> | |||
<div | |||
className="border-0 m-0 sm:absolute sm:top-0 sm:right-0 group-even:sm:left-0 group-even:sm:right-0 w-full sm:w-2/5 h-96 sm:h-full" | |||
> | |||
{showcaseType === 'iframe' && ( | |||
<iframe | |||
{...showcaseProps as Omit<IframeShowcaseProps, 'type'>} | |||
className={`border-0 m-0 w-full h-full ${showcaseProps.className ?? ''}`.trim()} | |||
> | |||
Cannot display embedded content. | |||
</iframe> | |||
)} | |||
{showcaseType === 'img' && ( | |||
<img | |||
{...showcaseProps as Omit<ImageShowcaseProps, 'type'>} | |||
className={`object-cover object-center border-0 m-0 w-full h-full block ${showcaseProps.className ?? ''}`.trim()} | |||
alt={title} | |||
/> | |||
)} | |||
</div> | |||
</div> | |||
); | |||
}; |
@@ -0,0 +1,57 @@ | |||
import * as React from 'react'; | |||
import {Icon, IconProps} from '@/components/molecules/Icon'; | |||
export interface FeedItemProps { | |||
date: Date; | |||
type: string; | |||
description: string; | |||
} | |||
const formatFeedItemDate = (date: Date) => { | |||
const mm = (date.getMonth() + 1).toString().padStart(2, '0'); | |||
const dd = date.getDate().toString().padStart(2, '0'); | |||
return `${mm}.${dd}`; | |||
}; | |||
const FEED_ITEM_TYPE_ICONS: Record<FeedItemProps['type'], IconProps['name']> = { | |||
code: 'git-commit', | |||
}; | |||
export const FeedItem: React.FC<FeedItemProps> = ({ | |||
date, | |||
type, | |||
description, | |||
}) => ( | |||
<div className="pb-[35%] sm:pb-[50%] relative"> | |||
<div className="absolute top-0 left-0 w-full h-full"> | |||
<dl className="h-full w-full relative p-2 rounded before:pointer-events-none before:rounded-inherit before:opacity-10 before:bg-current before:absolute before:top-0 before:left-0 before:w-full before:h-full grid"> | |||
<div className="col-start-2 row-start-1 text-right text-xs font-semibold"> | |||
<dt className="sr-only">Date</dt> | |||
<dd> | |||
<time dateTime={date.toISOString()}> | |||
{formatFeedItemDate(date)} | |||
</time> | |||
</dd> | |||
</div> | |||
<div className="col-start-1 row-start-1"> | |||
<dt className="sr-only">Type</dt> | |||
<dd> | |||
<Icon | |||
name={FEED_ITEM_TYPE_ICONS[type]} | |||
aria-label={type} | |||
/> | |||
</dd> | |||
</div> | |||
<div className="col-start-1 col-span-2 row-start-2 flex items-end text-xs relative"> | |||
<dt className="sr-only">Description</dt> | |||
<dd | |||
className="absolute bottom-0 left-0 w-full line-clamp-3" | |||
> | |||
{description} | |||
</dd> | |||
</div> | |||
</dl> | |||
</div> | |||
</div> | |||
); |
@@ -0,0 +1,19 @@ | |||
import * as React from 'react'; | |||
export interface FeedItemHeadingProps { | |||
children?: React.ReactNode; | |||
} | |||
export const FeedItemHeading: React.FC<FeedItemHeadingProps> = ({ | |||
children, | |||
}) => ( | |||
<span className="block pb-[35%] sm:pb-[50%] relative"> | |||
<span className="absolute top-0 left-0 w-full h-full block"> | |||
<span | |||
className="block text-2xl leading-none h-full w-full relative px-2 py-2.5 rounded before:pointer-events-none before:rounded-inherit before:border-2 before:absolute before:top-0 before:left-0 before:w-full before:h-full" | |||
> | |||
{children} | |||
</span> | |||
</span> | |||
</span> | |||
); |
@@ -0,0 +1,151 @@ | |||
import * as React from 'react'; | |||
import {Layouts} from '@tesseract-design/viewfinder-react'; | |||
import {Brand} from '@/components/molecules/Brand'; | |||
export const Footer = () => ( | |||
<footer className="py-12 sm:py-24 flex items-center relative"> | |||
<div | |||
className="absolute top-0 left-0 w-full h-full bg-shade opacity-75" | |||
/> | |||
<Layouts.Basic.ContentContainer | |||
span="wide" | |||
className="flex flex-col gap-12 relative" | |||
> | |||
<div | |||
className="flex flex-col sm:flex-row gap-8 sm:gap-12" | |||
> | |||
<h2 className="m-0 flex-auto shrink-0"> | |||
See you around. | |||
</h2> | |||
<dl | |||
className="flex flex-col xs:grid gap-8 xs:gap-12 xs:grid-cols-3" | |||
> | |||
<div className="flex flex-col gap-4"> | |||
<dt className="text-xs uppercase font-bold"> | |||
</dt> | |||
<dd className="text-xs font-mono leading-normal"> | |||
<a | |||
href="https://linkedin.com/in/allancrisostomo" | |||
rel="noreferrer noopener" | |||
target="_blank" | |||
className="no-underline before:content-['/in/'] p-1 -m-1" | |||
> | |||
allancrisostomo | |||
</a> | |||
</dd> | |||
</div> | |||
<div className="flex flex-col gap-4"> | |||
<dt className="text-xs uppercase font-bold"> | |||
Modal Code | |||
</dt> | |||
<dd className="text-xs font-mono leading-normal"> | |||
<a | |||
href="https://code.modal.sh/TheoryOfNekomata" | |||
rel="noreferrer noopener" | |||
target="_blank" | |||
className="no-underline before:content-['@'] p-1 -m-1" | |||
> | |||
TheoryOfNekomata | |||
</a> | |||
</dd> | |||
</div> | |||
<div className="flex flex-col gap-4"> | |||
<dt className="text-xs uppercase font-bold"> | |||
GitHub | |||
</dt> | |||
<dd className="text-xs font-mono leading-normal"> | |||
<a | |||
href="https://github.com/TheoryOfNekomata" | |||
rel="noreferrer noopener" | |||
target="_blank" | |||
className="no-underline before:content-['@'] p-1 -m-1" | |||
> | |||
TheoryOfNekomata | |||
</a> | |||
</dd> | |||
</div> | |||
<div className="flex flex-col gap-4"> | |||
<dt className="text-xs uppercase font-bold"> | |||
Soundcloud | |||
</dt> | |||
<dd className="text-xs font-mono leading-normal"> | |||
<a | |||
href="https://soundcloud.com/TheoryOfNekomata" | |||
rel="noreferrer noopener" | |||
target="_blank" | |||
className="no-underline before:content-['@'] p-1 -m-1" | |||
> | |||
TheoryOfNekomata | |||
</a> | |||
</dd> | |||
</div> | |||
<div className="flex flex-col gap-4"> | |||
<dt className="text-xs uppercase font-bold"> | |||
YouTube | |||
</dt> | |||
<dd className="text-xs font-mono leading-normal"> | |||
<a | |||
href="https://youtube.com/@TheoryOfNekomata" | |||
rel="noreferrer noopener" | |||
target="_blank" | |||
className="no-underline before:content-['@'] p-1 -m-1" | |||
> | |||
TheoryOfNekomata | |||
</a> | |||
</dd> | |||
</div> | |||
<div className="flex flex-col gap-4"> | |||
<dt className="text-xs uppercase font-bold"> | |||
Pixiv | |||
</dt> | |||
<dd className="text-xs font-mono leading-normal"> | |||
<a | |||
href="https://pixiv.me/theoryofnekomata" | |||
rel="noreferrer noopener" | |||
target="_blank" | |||
className="no-underline before:content-['@'] p-1 -m-1" | |||
> | |||
theoryofnekomata | |||
</a> | |||
</dd> | |||
</div> | |||
<div className="flex flex-col gap-4"> | |||
<dt className="text-xs uppercase font-bold"> | |||
X (formerly Twitter) | |||
</dt> | |||
<dd className="text-xs font-mono leading-normal"> | |||
<a | |||
href="https://twitter.com/theoryofnekomata" | |||
rel="noreferrer noopener" | |||
target="_blank" | |||
className="no-underline before:content-['@'] p-1 -m-1" | |||
> | |||
theoryofnekomata | |||
</a> | |||
</dd> | |||
<dd className="text-xs font-mono leading-normal"> | |||
<a | |||
href="https://twitter.com/clearfix.5essnce" | |||
rel="noreferrer noopener" | |||
target="_blank" | |||
className="no-underline before:content-['@'] p-1 -m-1" | |||
> | |||
clearfix.5essnce | |||
</a> | |||
</dd> | |||
</div> | |||
</dl> | |||
</div> | |||
<div className="text-center"> | |||
© | |||
{' '} | |||
<span className="text-2xl mx-3"> | |||
<Brand/> | |||
</span> | |||
{' '} | |||
{new Date().getFullYear()}. | |||
</div> | |||
</Layouts.Basic.ContentContainer> | |||
</footer> | |||
) |
@@ -0,0 +1,42 @@ | |||
import * as React from 'react'; | |||
const ICONS = { | |||
'git-commit': ( | |||
<> | |||
<circle cx="12" cy="12" r="4" /> | |||
<line x1="1.05" y1="12" x2="7" y2="12" /> | |||
<line x1="17.01" y1="12" x2="22.96" y2="12" /> | |||
</> | |||
), | |||
'scroll-down': ( | |||
<> | |||
<polyline points="7 13 12 18 17 13" /> | |||
<polyline points="7 6 12 11 17 6" /> | |||
</> | |||
) | |||
}; | |||
export const IconDerivedElementComponent = 'svg' as const; | |||
export type IconDerivedElement = SVGElementTagNameMap[typeof IconDerivedElementComponent]; | |||
type IconName = keyof typeof ICONS; | |||
export interface IconProps extends React.SVGProps<IconDerivedElement> { | |||
name: IconName; | |||
} | |||
export const Icon = React.forwardRef<IconDerivedElement, IconProps>(({ | |||
name, | |||
className, | |||
...etcProps | |||
}, forwardedRef) => ( | |||
<IconDerivedElementComponent | |||
{...etcProps} | |||
ref={forwardedRef} | |||
viewBox="0 0 24 24" | |||
className={`w-6 h-6 linejoin-round linecap-round stroke-2 stroke-current fill-none ${className ?? ''}`.trim()} | |||
> | |||
{ICONS[name]} | |||
</IconDerivedElementComponent> | |||
)); |
@@ -0,0 +1,117 @@ | |||
import * as React from 'react'; | |||
import Link from 'next/link'; | |||
import {Layouts} from '@tesseract-design/viewfinder-react'; | |||
import {Brand} from '@/components/molecules/Brand'; | |||
import {Pillar} from '@/components/molecules/Pillar'; | |||
import {BackgroundGrid} from '@/components/molecules/BackgroundGrid'; | |||
import {Icon} from '@/components/molecules/Icon'; | |||
export interface MainLandingSectionProps { | |||
backgroundImages: string[]; | |||
} | |||
export const MainLandingSection: React.FC<MainLandingSectionProps> = ({ | |||
backgroundImages | |||
}) => { | |||
const scrollDown: React.MouseEventHandler<HTMLAnchorElement> = React.useCallback((event) => { | |||
event.preventDefault(); | |||
const target = window.document.querySelector(event.currentTarget.getAttribute('href')!); | |||
if (!target) { | |||
return; | |||
} | |||
target.scrollIntoView({ | |||
behavior: 'smooth', | |||
}); | |||
}, []); | |||
return ( | |||
<div | |||
className="sm:py-24 flex items-center min-h-full relative overflow-hidden" | |||
> | |||
<div | |||
className="fixed top-0 left-0 w-full h-full" | |||
> | |||
<div className="w-[125%] h-[125%] absolute -top-[12.5%] -left-[12.5%] opacity-25"> | |||
<BackgroundGrid images={backgroundImages} /> | |||
</div> | |||
</div> | |||
<div | |||
className="absolute top-0 left-0 w-full h-full bg-shade opacity-25" | |||
/> | |||
<Layouts.Basic.ContentContainer | |||
span="wide" | |||
className="relative py-24" | |||
> | |||
<div className="flex flex-col sm:flex-row sm:items-center gap-8 sm:gap-16"> | |||
<h1 className="text-center m-0 before:text-7xl sm:before:text-8xl before:block before:content-['I_am.'] text-base leading-none"> | |||
<span className="text-4xl"> | |||
<Brand/> | |||
</span> | |||
</h1> | |||
<div className="w-full sm:w-auto flex-auto flex flex-row sm:flex-col gap-2 h-64 sm:h-64"> | |||
<div className="w-0 sm:w-auto flex-auto"> | |||
<Link | |||
href="/credentials" | |||
className="block w-full h-full" | |||
> | |||
<Pillar> | |||
Credentials | |||
</Pillar> | |||
</Link> | |||
</div> | |||
<div className="w-0 sm:w-auto flex-auto"> | |||
<Link | |||
href="/portfolio" | |||
className="block w-full h-full" | |||
> | |||
<Pillar> | |||
Portfolio | |||
</Pillar> | |||
</Link> | |||
</div> | |||
<div className="w-0 sm:w-auto flex-auto"> | |||
<Link | |||
href="/blog" | |||
className="block w-full h-full" | |||
> | |||
<Pillar> | |||
Blog | |||
</Pillar> | |||
</Link> | |||
</div> | |||
<div className="w-0 sm:w-auto flex-auto"> | |||
<Link | |||
href="/services-values" | |||
className="block w-full h-full" | |||
> | |||
<Pillar> | |||
Services & Values | |||
</Pillar> | |||
</Link> | |||
</div> | |||
</div> | |||
</div> | |||
</Layouts.Basic.ContentContainer> | |||
<div className="absolute bottom-0 left-0 w-full h-24 flex justify-center items-center"> | |||
<div className="w-12 h-12"> | |||
<a | |||
href="#start" | |||
className="relative rounded-full overflow-hidden w-12 h-12 block" | |||
onClick={scrollDown} | |||
> | |||
<span | |||
className="absolute top-0 left-0 w-full h-full bg-current opacity-75" | |||
/> | |||
<span className="text-negative rounded-inherit absolute top-0 left-0 w-full h-full flex items-center justify-center"> | |||
<Icon | |||
name="scroll-down" | |||
/> | |||
</span> | |||
</a> | |||
</div> | |||
</div> | |||
</div> | |||
); | |||
} |
@@ -0,0 +1,115 @@ | |||
import * as React from 'react'; | |||
import Link from 'next/link'; | |||
import {Layouts} from '@tesseract-design/viewfinder-react'; | |||
import * as WebNavigationReact from '@tesseract-design/web-navigation-react'; | |||
import {FeedItem, FeedItemProps} from '@/components/molecules/FeedItem'; | |||
import {FeedItemHeading} from '@/components/molecules/FeedItemHeading'; | |||
interface MakeSectionDatum extends FeedItemProps { | |||
id: string; | |||
url: string; | |||
} | |||
export interface MakeSectionProps { | |||
data: MakeSectionDatum[]; | |||
} | |||
export const MakeSection: React.FC<MakeSectionProps> = ({ | |||
data, | |||
}) => { | |||
const groupedData = data.reduce( | |||
(grouped, datum) => ({ | |||
...grouped, | |||
[datum.date.getFullYear()]: [ | |||
...(grouped[datum.date.getFullYear()] ?? []), | |||
datum, | |||
], | |||
}), | |||
{} as Record<string, MakeSectionDatum[]>, | |||
); | |||
return ( | |||
<div className="py-12 sm:py-24 flex items-center min-h-full relative bg-negative"> | |||
<div | |||
className="absolute top-0 left-0 w-full h-full bg-shade opacity-25" | |||
/> | |||
<Layouts.Basic.ContentContainer | |||
span="wide" | |||
className="relative" | |||
> | |||
<div className="pb-8"> | |||
<h2 className="text-base leading-none m-0"> | |||
<span className="text-7xl sm:text-8xl"> | |||
I | |||
{' '} | |||
<Link | |||
className="no-underline px-1 -mx-1" | |||
href={{ | |||
query: { | |||
feed: 'all', | |||
}, | |||
}} | |||
> | |||
make. | |||
</Link> | |||
</span> | |||
</h2> | |||
<div className="flex gap-2 font-headings lowercase text-3xl sm:text-4xl leading-none font-normal"> | |||
<div><Link href={{ query: { feed: 'software' }}} className="p-1 -m-1 after:content-['.']">Software</Link></div> | |||
<div><Link href={{ query: { feed: 'music' }}} className="p-1 -m-1 after:content-['.']">Music</Link></div> | |||
<div><Link href={{ query: { feed: 'art' }}} className="p-1 -m-1 after:content-['.']">Art</Link></div> | |||
</div> | |||
</div> | |||
<div | |||
className="grid grid-cols-1 xs:grid-cols-2 sm:grid-cols-3 gap-2" | |||
> | |||
{Object.entries(groupedData).map(([year, data]) => ( | |||
<section | |||
key={year} | |||
className="contents" | |||
> | |||
<h3 | |||
className="text-base m-0" | |||
> | |||
<a | |||
href="#" | |||
className="block" | |||
> | |||
<FeedItemHeading> | |||
{year} | |||
</FeedItemHeading> | |||
</a> | |||
</h3> | |||
<div className="contents"> | |||
{data.map(({ id, ...datum }) => ( | |||
<div | |||
key={id} | |||
> | |||
<a href="#" className="block"> | |||
<FeedItem {...datum} /> | |||
</a> | |||
</div> | |||
))} | |||
</div> | |||
</section> | |||
))} | |||
</div> | |||
<div className="flex justify-end mt-8"> | |||
<div className="w-40"> | |||
<WebNavigationReact.LinkButton | |||
component={Link as unknown as 'a'} | |||
href={{ | |||
pathname: '/feed', | |||
} as unknown as string} | |||
menuItem | |||
block | |||
subtext="Feed" | |||
> | |||
Browse | |||
</WebNavigationReact.LinkButton> | |||
</div> | |||
</div> | |||
</Layouts.Basic.ContentContainer> | |||
</div> | |||
); | |||
} |
@@ -0,0 +1,16 @@ | |||
import * as React from 'react'; | |||
export interface PillarProps { | |||
children?: React.ReactNode; | |||
} | |||
export const Pillar: React.FC<PillarProps> = ({ | |||
children, | |||
}) => ( | |||
<span className="block h-full w-full relative rounded overflow-hidden"> | |||
<span className="absolute top-0 left-0 w-full h-full bg-current opacity-75" /> | |||
<span className="text-negative absolute bottom-0 right-0 sm:right-auto sm:left-0 text-sm uppercase font-bold whitespace-nowrap p-2 origin-bottom-left -rotate-90 translate-x-full sm:translate-x-0 sm:rotate-0"> | |||
{children} | |||
</span> | |||
</span> | |||
); |
@@ -0,0 +1,34 @@ | |||
import * as React from 'react'; | |||
export interface ShowcaseItemProps { | |||
imageUrl?: string; | |||
title: string; | |||
description: string; | |||
} | |||
export const ShowcaseItem: React.FC<ShowcaseItemProps> = ({ | |||
imageUrl, | |||
title, | |||
description, | |||
}) => ( | |||
<div className="relative rounded overflow-hidden"> | |||
<div className="bg-current absolute top-0 left-0 w-full h-full opacity-10" /> | |||
<div className="flex flex-col sm:flex-row sm:items-center sm:gap-4 relative"> | |||
<div className="shrink-0 w-full sm:max-w-[8rem]"> | |||
<span className="w-full pb-[75%] relative block"> | |||
<span className="absolute block w-full h-full"> | |||
<img | |||
src={imageUrl ?? 'http://placehold.it/1'} | |||
className="w-full h-full" | |||
alt={title} | |||
/> | |||
</span> | |||
</span> | |||
</div> | |||
<div className="flex-auto min-w-0 flex flex-col gap-2 p-4 sm:py-2"> | |||
<h3 className="m-0 whitespace-nowrap text-ellipsis overflow-hidden w-full text-2xl">{title}</h3> | |||
<p className="m-0 line-clamp-2 text-xs">{description}</p> | |||
</div> | |||
</div> | |||
</div> | |||
); |
@@ -0,0 +1,47 @@ | |||
import * as React from 'react'; | |||
import {Layouts} from '@tesseract-design/viewfinder-react'; | |||
import {MainLandingSection, MainLandingSectionProps} from '@/components/molecules/MainLandingSection'; | |||
import {MakeSection, MakeSectionProps} from '@/components/molecules/MakeSection'; | |||
import {EnvisionSection, EnvisionSectionProps} from '@/components/molecules/EnvisionSection'; | |||
import {Footer} from '@/components/molecules/Footer'; | |||
import {FeaturedProjectSection, FeaturedProjectSectionProps} from '@/components/molecules/FeaturedProjectSection'; | |||
import {ContactCtaBanner} from '@/components/molecules/ContactCtaBanner'; | |||
export interface IndexLayoutProps { | |||
backgroundImages: MainLandingSectionProps['backgroundImages']; | |||
makeSectionData: MakeSectionProps['data']; | |||
envisionSectionData: EnvisionSectionProps['data']; | |||
featuredProjectData?: FeaturedProjectSectionProps[]; | |||
onSubmit?: React.FormEventHandler<HTMLFormElement>; | |||
} | |||
export const IndexLayout: React.FC<IndexLayoutProps> = ({ | |||
backgroundImages, | |||
makeSectionData, | |||
envisionSectionData, | |||
featuredProjectData = [], | |||
onSubmit, | |||
}) => ( | |||
<Layouts.Basic.Root className="contents"> | |||
<MainLandingSection | |||
backgroundImages={backgroundImages} | |||
/> | |||
<div id="start" /> | |||
<MakeSection | |||
data={makeSectionData} | |||
/> | |||
<EnvisionSection | |||
data={envisionSectionData} | |||
/> | |||
{featuredProjectData.map((featuredProjectProps) => ( | |||
<FeaturedProjectSection | |||
key={featuredProjectProps.title} | |||
{...featuredProjectProps} | |||
/> | |||
))} | |||
<ContactCtaBanner | |||
onSubmit={onSubmit} | |||
/> | |||
<Footer/> | |||
</Layouts.Basic.Root> | |||
); |
@@ -0,0 +1,51 @@ | |||
import * as React from 'react'; | |||
const DEFAULT_COLOR_HUE_DEGREES = 320 as const; | |||
const DEFAULT_ANIMATION_DURATION = 60000 as const; | |||
const DEFAULT_UPDATE_INTERVAL_MS = 50 as const; | |||
const DEFAULT_COLOR_SATURATION_PERCENTAGE = 35 as const; | |||
const DEFAULT_COLOR_LIGHTNESS_PERCENTAGE = 66 as const; | |||
const MAX_HUE = 360 as const; | |||
export interface UseHuePulsateOptions { | |||
animationDuration?: number; | |||
initialColorHueDegrees?: number; | |||
colorSaturationPercentage?: number; | |||
colorLightnessPercentage?: number; | |||
updateIntervalMs?: number; | |||
propertyName: string; | |||
} | |||
export const useHuePulsate = (options : UseHuePulsateOptions) => { | |||
React.useEffect(() => { | |||
const { | |||
animationDuration = DEFAULT_ANIMATION_DURATION, | |||
initialColorHueDegrees = DEFAULT_COLOR_HUE_DEGREES, | |||
updateIntervalMs = DEFAULT_UPDATE_INTERVAL_MS, | |||
colorSaturationPercentage = DEFAULT_COLOR_SATURATION_PERCENTAGE, | |||
colorLightnessPercentage = DEFAULT_COLOR_LIGHTNESS_PERCENTAGE, | |||
propertyName, | |||
} = options; | |||
const start = Date.now(); | |||
const intervalHandle = window.setInterval(() => { | |||
//window.document.documentElement.style.setProperty('--scroll-y', `${window.scrollY}px`); | |||
const elapsed = Date.now() - start; | |||
const progress = elapsed / animationDuration; | |||
const colorPrimaryHue = (initialColorHueDegrees + (progress * MAX_HUE)) % MAX_HUE; | |||
window.document.documentElement.style.setProperty( | |||
propertyName, | |||
`${colorPrimaryHue} ${colorSaturationPercentage}% ${colorLightnessPercentage}%` | |||
); | |||
}, updateIntervalMs); | |||
return () => { | |||
window.clearInterval(intervalHandle); | |||
}; | |||
}, []); | |||
}; |
@@ -0,0 +1,6 @@ | |||
import '@/styles/globals.css' | |||
import type { AppProps } from 'next/app' | |||
export default function App({ Component, pageProps }: AppProps) { | |||
return <Component {...pageProps} /> | |||
} |
@@ -0,0 +1,24 @@ | |||
import { Html, Head, Main, NextScript } from 'next/document' | |||
import theme from '@/styles/theme' | |||
export default function Document() { | |||
return ( | |||
<Html lang="en-PH" className="w-full h-full bg-negative text-positive tracking-normal font-semi-expanded"> | |||
<Head> | |||
<style | |||
dangerouslySetInnerHTML={{ | |||
__html: ` | |||
:root { | |||
${Object.entries(theme).map(([name, value]) => `--${name}: ${value};`).join('\n')} | |||
} | |||
`, | |||
}} | |||
/> | |||
</Head> | |||
<body className="w-full h-full"> | |||
<Main /> | |||
<NextScript /> | |||
</body> | |||
</Html> | |||
) | |||
} |
@@ -0,0 +1,13 @@ | |||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction | |||
import type { NextApiRequest, NextApiResponse } from 'next' | |||
type Data = { | |||
name: string | |||
} | |||
export default function handler( | |||
req: NextApiRequest, | |||
res: NextApiResponse<Data> | |||
) { | |||
res.status(200).json({ name: 'John Doe' }) | |||
} |
@@ -0,0 +1,134 @@ | |||
import type { NextPage } from 'next'; | |||
import * as React from 'react'; | |||
import {useHuePulsate} from '@/hooks/effects'; | |||
import {IndexLayout} from '@/components/organisms/IndexLayout'; | |||
import {getFormValues} from '@theoryofnekomata/formxtra'; | |||
const IMAGE_CHOICES = [ | |||
'/images/3cd237361eada7fd30eb96d42d55ec00.jpg', | |||
'/images/5ace16248237a96f6dbbcc16a3c385fe.jpg', | |||
'/images/6af0fdc5eafb86f8b540e3d322c3a007.jpg', | |||
'/images/8f5e3d92d2da0a760b312a9387219447.jpg', | |||
'/images/162cab44da8420c50900d8f11c6da6fd.jpg', | |||
'/images/2626c3485668a19730c64a7ee672a214.jpg', | |||
'/images/9802f260887e9044312fb7d88547fb09.jpg', | |||
'/images/a3a56f54a11c76f46f4392b748f3026a.png', | |||
'/images/a96d02d8cc43cc3b9f1e77c43dfa5644.png', | |||
'/images/d69ab7f4e5747b5b8f297e4ebe9770f5.png', | |||
'/images/d1454662da2a4a8e77cd4c98eda6d662.png', | |||
'/images/e4753cf6a55db6b7ab5037ed0fd4a3fe.jpg', | |||
'/images/fe3050a31d3fb86accf26cc4bebec102.png', | |||
]; | |||
const makeSectionData = new Array(20).fill(null).map((_, index) => ({ | |||
id: (index + 1).toString(), | |||
url: 'http://www.example.com', | |||
date: new Date('2023-07-03'), | |||
type: 'code', | |||
description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed vitae diam eget nunc aliquam vestibulum. Sed vitae diam eget nunc aliquam vestibulum.', | |||
})); | |||
const envisionSectionData = new Array(6).fill(null).map((_, index) => ({ | |||
id: (index + 1).toString(), | |||
url: 'http://www.example.com', | |||
imageUrl: 'http://placehold.it/1', | |||
title: 'Lorem ipsum dolor sit amet', | |||
description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed vitae diam eget nunc aliquam vestibulum. Sed vitae diam eget nunc aliquam vestibulum.', | |||
})); | |||
const featuredProjectData = [ | |||
{ | |||
title: 'formxtra', | |||
description: "Extract and set form values through the DOM—no frameworks required!", | |||
links:[ | |||
{ | |||
href: 'https://codepen.io/theoryofnekomata/pen/xxajmvJ', | |||
children: 'View Demo', | |||
}, | |||
{ | |||
href: 'https://code.modal.sh/TheoryOfNekomata/formxtra', | |||
children: 'Explore Code', | |||
primary: true, | |||
}, | |||
], | |||
showcase: { | |||
type: 'iframe' as const, | |||
allowFullScreen: true, | |||
src: 'https://codepen.io/theoryofnekomata/embed/xxajmvJ?default-tab=result&theme-id=dark', | |||
sandbox: 'allow-forms allow-modals allow-pointer-lock allow-popups allow-same-origin allow-scripts allow-top-navigation-by-user-activation allow-downloads allow-presentation', | |||
allow: 'accelerometer; camera; encrypted-media; display-capture; geolocation; gyroscope; microphone; midi; clipboard-read; clipboard-write; web-share', | |||
}, | |||
}, | |||
{ | |||
title: 'tesseract', | |||
description: 'Functional, accessible, aesthetic design system.', | |||
links:[ | |||
{ | |||
href: 'https://code.modal.sh/tesseract-design/tesseract', | |||
children: 'Explore Code', | |||
primary: true, | |||
}, | |||
], | |||
showcase: { | |||
type: 'img' as const, | |||
src: '/images/162cab44da8420c50900d8f11c6da6fd.jpg', | |||
}, | |||
}, | |||
{ | |||
title: 'numerica', | |||
description: 'Gets the name of a number, even if it\'s stupidly big.', | |||
links:[ | |||
{ | |||
href: 'https://code.modal.sh/modal-soft/numerica', | |||
children: 'Explore Code', | |||
primary: true, | |||
}, | |||
], | |||
showcase: { | |||
type: 'img' as const, | |||
src: '/images/d69ab7f4e5747b5b8f297e4ebe9770f5.png', | |||
}, | |||
}, | |||
{ | |||
title: 'webvideo-transcript-summarizer', | |||
description: 'Get transcript summaries of Web videos. Powered by OpenAI.', | |||
links:[ | |||
{ | |||
href: 'https://code.modal.sh/modal-soft/webvideo-transcript-summary', | |||
children: 'Explore Code', | |||
primary: true, | |||
}, | |||
], | |||
showcase: { | |||
type: 'img' as const, | |||
src: '/images/3cd237361eada7fd30eb96d42d55ec00.jpg', | |||
}, | |||
}, | |||
]; | |||
const IndexPage: NextPage = () => { | |||
useHuePulsate({ | |||
propertyName: '--color-primary', | |||
initialColorHueDegrees: 320, | |||
colorSaturationPercentage: 35, | |||
colorLightnessPercentage: 66, | |||
}); | |||
const processContactForm: React.FormEventHandler<HTMLFormElement> = (e) => { | |||
e.preventDefault(); | |||
const values = getFormValues(e.currentTarget); | |||
console.log(values); | |||
}; | |||
return ( | |||
<IndexLayout | |||
backgroundImages={IMAGE_CHOICES} | |||
makeSectionData={makeSectionData} | |||
envisionSectionData={envisionSectionData} | |||
featuredProjectData={featuredProjectData} | |||
onSubmit={processContactForm} | |||
/> | |||
); | |||
}; | |||
export default IndexPage; |
@@ -0,0 +1,197 @@ | |||
@tailwind base; | |||
@tailwind components; | |||
@tailwind utilities; | |||
input, | |||
textarea, | |||
button, | |||
select, | |||
a { | |||
-webkit-tap-highlight-color: transparent; | |||
} | |||
#__next { | |||
display: contents; | |||
} | |||
/* | |||
@property --color-primary-hue { | |||
syntax: '<number>'; | |||
initial-value: 0; | |||
inherits: false; | |||
} | |||
@keyframes rgb-pulsate { | |||
0% { | |||
--color-primary-hue: 320; | |||
} | |||
100% { | |||
--color-primary-hue: 680; | |||
} | |||
} | |||
:root { | |||
animation: rgb-pulsate 60000ms infinite; | |||
--color-primary: var(--color-primary-hue) 35% 66%; | |||
} | |||
*/ | |||
/* TODO add to tesseract plugin */ | |||
@layer base { | |||
:root { | |||
--color-sidebar: 29 29 29; | |||
--color-topbar: 19 19 19; | |||
--color-sidebar-menu: 24 24 24; | |||
} | |||
h1 { | |||
@apply font-headings lowercase text-5xl font-thin leading-none my-8; | |||
} | |||
h2 { | |||
@apply font-headings lowercase text-4xl font-light leading-none my-8; | |||
} | |||
h3 { | |||
@apply font-headings lowercase text-3xl leading-none my-8; | |||
} | |||
h4 { | |||
@apply font-headings lowercase text-2xl leading-none my-8; | |||
} | |||
h5 { | |||
@apply font-headings lowercase text-xl leading-none my-8; | |||
} | |||
h6 { | |||
@apply font-headings lowercase text-lg leading-none my-8; | |||
} | |||
p { | |||
@apply my-8; | |||
} | |||
small { | |||
font-size: 0.75em; | |||
} | |||
a[href]:not([class]) { | |||
@apply p-1 -m-1 underline; | |||
} | |||
a[href] { | |||
@apply text-primary ring-secondary/50; | |||
} | |||
a[href]:not([class~="rounded-full"]) { | |||
@apply rounded | |||
} | |||
a[href]:focus { | |||
@apply ring-4 text-secondary; | |||
outline: 0; | |||
} | |||
button:hover { | |||
@apply ring-4 text-secondary; | |||
} | |||
a[href]:hover { | |||
@apply ring-4 text-secondary; | |||
} | |||
a[href]:active { | |||
@apply ring-tertiary/50 text-tertiary; | |||
} | |||
a[href].focus\:text-negative:focus { | |||
color: rgb(var(--color-negative)); | |||
} | |||
a[href].active\:text-negative:active { | |||
color: rgb(var(--color-negative)); | |||
} | |||
/*pre {*/ | |||
/* overflow: auto;*/ | |||
/* margin: 0 -1rem;*/ | |||
/* padding: 0 1rem;*/ | |||
/* line-height: 1.2;*/ | |||
/* box-sizing: border-box;*/ | |||
/*}*/ | |||
@media only print { | |||
pre, pre * { | |||
color: inherit !important; | |||
} | |||
code, code * { | |||
color: inherit !important; | |||
} | |||
img { | |||
filter: grayscale(100%); | |||
} | |||
:root { | |||
--color-accent: black !important; | |||
--color-active: black !important; | |||
} | |||
} | |||
} | |||
:root .rti--container { | |||
--rti-bg: transparent; | |||
--rti-border: transparent; | |||
--rti-main: transparent; | |||
--rti-radius: 0; | |||
--rti-s: 0.5rem; | |||
--rti-tag: transparent; | |||
--rti-tag-remove: transparent; | |||
--rti-tag-padding: 0 0; | |||
} | |||
.highlight .token.number { color: rgb(var(--color-code-number)); } | |||
.highlight .token.keyword { color: rgb(var(--color-code-keyword)); } | |||
.highlight .token.tag { color: rgb(var(--color-code-keyword)); } | |||
.highlight .token.type { color: rgb(var(--color-code-type)); } | |||
.highlight .token.instance-attribute { color: rgb(var(--color-code-instance-attribute)); } | |||
.highlight .token.maybe-class-name { color: rgb(var(--color-code-function)); font-style: italic; } | |||
.highlight .token.function { color: rgb(var(--color-code-function)); font-style: italic; } | |||
.highlight .token.parameter { color: rgb(var(--color-code-parameter)); } | |||
.highlight .token.property { color: rgb(var(--color-code-property)); } | |||
.highlight .token.attr-name { color: rgb(var(--color-code-property)); font-style: italic; } | |||
.highlight .token.string { color: rgb(var(--color-code-string)); } | |||
.highlight .token.attr-value { color: rgb(var(--color-code-string)); } | |||
.highlight .token.attr-value .attr-equals { color: rgb(var(--color-positive)); } | |||
.highlight .token.variable { color: rgb(var(--color-code-variable)); } | |||
.highlight .token.regexp { color: rgb(var(--color-code-regexp)); } | |||
.highlight .token.url { color: rgb(var(--color-code-url)); } | |||
.highlight .token.global { color: rgb(var(--color-code-global)); } | |||
.highlight .token.comment { opacity: 0.5; } | |||
.highlight .x00 { color: rgb(var(--color-code-keyword)); } | |||
.highlight .x10 { color: rgb(var(--color-code-global)); } | |||
.highlight .x20 { color: rgb(var(--color-code-string)); } | |||
.highlight .x30 { color: rgb(var(--color-code-number)); } | |||
.highlight .x40 { color: rgb(var(--color-code-url)); } | |||
.highlight .x50 { color: rgb(var(--color-code-type)); } | |||
.highlight .x60 { color: rgb(var(--color-code-parameter)); } | |||
.highlight .x70 { color: rgb(var(--color-code-property)); } | |||
.highlight .x80 { color: rgb(var(--color-negative)); background-color: rgb(var(--color-code-keyword)); } | |||
.highlight .x90 { color: rgb(var(--color-negative)); background-color: rgb(var(--color-code-global)); } | |||
.highlight .xa0 { color: rgb(var(--color-negative)); background-color: rgb(var(--color-code-string)); } | |||
.highlight .xb0 { color: rgb(var(--color-negative)); background-color: rgb(var(--color-code-number)); } | |||
.highlight .xc0 { color: rgb(var(--color-negative)); background-color: rgb(var(--color-code-url)); } | |||
.highlight .xd0 { color: rgb(var(--color-negative)); background-color: rgb(var(--color-code-type)); } | |||
.highlight .xe0 { color: rgb(var(--color-negative)); background-color: rgb(var(--color-code-parameter)); } | |||
.highlight .xf0 { color: rgb(var(--color-negative)); background-color: rgb(var(--color-code-property)); } | |||
.focus\:outline-0:-moz-focusring { | |||
outline: 0; | |||
} |
@@ -0,0 +1,25 @@ | |||
const theme = { | |||
"color-shade": "0 0 0", | |||
"color-negative": "34 34 34", | |||
"color-positive": "238 238 238", | |||
"color-primary": "320 35% 66%", | |||
"color-secondary": "215 95 75", | |||
"color-tertiary": "255 153 0", | |||
"color-code-number": "116 249 94", | |||
"color-code-keyword": "255 67 137", | |||
"color-code-type": "80 151 210", | |||
"color-code-instance-attribute": "118 167 210", | |||
"color-code-function": "103 194 82", | |||
"color-code-parameter": "145 94 194", | |||
"color-code-property": "255 161 201", | |||
"color-code-string": "238 211 113", | |||
"color-code-variable": "139 194 117", | |||
"color-code-regexp": "116 167 43", | |||
"color-code-url": "0 153 204", | |||
"color-code-global": "194 128 80", | |||
"font-sans": '"Encode Sans"', | |||
"font-headings": 'Glory, var(--font-sans)', | |||
"font-mono": 'MonoLisa, mononoki', | |||
} as const; | |||
export default theme; |
@@ -0,0 +1,132 @@ | |||
import defaultTheme from 'tailwindcss/defaultTheme'; | |||
import plugin from 'tailwindcss/plugin'; | |||
import type { Config } from 'tailwindcss'; | |||
import { tailwind } from '@tesseract-design/viewfinder-base'; | |||
const tesseractPlugin = plugin( | |||
({ addUtilities }) => { | |||
addUtilities({ | |||
'.font-condensed': { | |||
'font-stretch': 'condensed', | |||
}, | |||
'.font-semi-condensed': { | |||
'font-stretch': 'semi-condensed', | |||
}, | |||
'.font-expanded': { | |||
'font-stretch': 'expanded', | |||
}, | |||
'.font-semi-expanded': { | |||
'font-stretch': 'semi-expanded', | |||
}, | |||
'.font-inherit': { | |||
'font-stretch': 'inherit', | |||
}, | |||
'.linejoin-round': { | |||
'stroke-linejoin': 'round', | |||
}, | |||
'.linecap-round': { | |||
'stroke-linecap': 'round', | |||
}, | |||
}); | |||
}, | |||
{ | |||
theme: { | |||
fontFamily: { | |||
sans: ['var(--font-sans)', ...defaultTheme.fontFamily.sans], | |||
headings: ['var(--font-headings)', ...defaultTheme.fontFamily.sans], | |||
mono: ['var(--font-mono)', ...defaultTheme.fontFamily.mono], | |||
inherit: ['inherit'], | |||
}, | |||
colors: { | |||
'sidebar': 'rgb(var(--color-sidebar)', | |||
'topbar': 'rgb(var(--color-topbar)', | |||
'shade': 'rgb(var(--color-shade))', | |||
'negative': 'rgb(var(--color-negative))', | |||
'positive': 'rgb(var(--color-positive))', | |||
'primary': 'hsl(var(--color-primary))', | |||
'secondary': 'rgb(var(--color-secondary))', | |||
'tertiary': 'rgb(var(--color-tertiary))', | |||
'code-number': 'rgb(var(--color-code-number))', | |||
'code-keyword': 'rgb(var(--color-code-keyword))', | |||
'code-type': 'rgb(var(--color-code-type))', | |||
'code-instance-attribute': 'rgb(var(--color-code-instance-attribute))', | |||
'code-function': 'rgb(var(--color-code-function))', | |||
'code-parameter': 'rgb(var(--color-code-parameter))', | |||
'code-property': 'rgb(var(--color-code-property))', | |||
'code-string': 'rgb(var(--color-code-string))', | |||
'code-variable': 'rgb(var(--color-code-variable))', | |||
'code-regexp': 'rgb(var(--color-code-regexp))', | |||
'code-url': 'rgb(var(--color-code-url))', | |||
'code-global': 'rgb(var(--color-code-global))', | |||
'current': 'currentcolor', | |||
'inherit': 'inherit', | |||
'transparent': 'transparent', | |||
}, | |||
extend: { | |||
fontSize: { | |||
'lg': '1.125em', | |||
'xl': '1.25em', | |||
'2xl': '1.5em', | |||
'3xl': '1.75em', | |||
'4xl': '2em', | |||
'5xl': '3em', | |||
'6xl': '4em', | |||
'xxs': '0.625rem', | |||
}, | |||
borderRadius: { | |||
inherit: 'inherit', | |||
}, | |||
maxWidth: { | |||
'1/3': '33.333333%', | |||
}, | |||
minWidth: { | |||
6: '1.5rem', | |||
10: '2.5rem', | |||
12: '3rem', | |||
16: '4rem', | |||
48: '12rem', | |||
64: '16rem', | |||
32: '8rem', | |||
}, | |||
minHeight: { | |||
6: '1.5rem', | |||
10: '2.5rem', | |||
12: '3rem', | |||
16: '4rem', | |||
64: '16rem', | |||
}, | |||
strokeWidth: { | |||
3: '3', | |||
}, | |||
}, | |||
}, | |||
}, | |||
); | |||
const config: Config = { | |||
content: [ | |||
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}', | |||
'./src/components/**/*.{js,ts,jsx,tsx,mdx}', | |||
'./src/app/**/*.{js,ts,jsx,tsx,mdx}', | |||
'./node_modules/@tesseract-design/viewfinder-react/dist/**/*.{js,ts,jsx,tsx,mdx}', | |||
'./node_modules/@tesseract-design/web-*-react/**/*.{js.ts,jsx,tsx,mdx}', | |||
], | |||
theme: { | |||
extend: { | |||
backgroundImage: { | |||
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', | |||
'gradient-conic': | |||
'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', | |||
}, | |||
}, | |||
}, | |||
plugins: [ | |||
tailwind.plugin({ | |||
}), | |||
tesseractPlugin, | |||
], | |||
} | |||
export default config |
@@ -0,0 +1,22 @@ | |||
{ | |||
"compilerOptions": { | |||
"target": "es5", | |||
"lib": ["dom", "dom.iterable", "esnext"], | |||
"allowJs": true, | |||
"skipLibCheck": true, | |||
"strict": true, | |||
"noEmit": true, | |||
"esModuleInterop": true, | |||
"module": "esnext", | |||
"moduleResolution": "bundler", | |||
"resolveJsonModule": true, | |||
"isolatedModules": true, | |||
"jsx": "preserve", | |||
"incremental": true, | |||
"paths": { | |||
"@/*": ["./src/*"] | |||
} | |||
}, | |||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], | |||
"exclude": ["node_modules"] | |||
} |