Cursorless enablement group page (#1660)

See [deploy
preview](https://deploy-preview-1660--cursorless.netlify.app/enablement-group)

- Also adds support for mdx in our top-level cursorless-org package

## Checklist

- [x] Add meta social tags
- [x] Use latest logo
- [x] Why is logo on social slow? And is it new logo?
- [x] Make prices clickable
- [x] document that sponsorship levels are yearly
- [-] I have added
[tests](https://www.cursorless.org/docs/contributing/test-case-recorder/)
- [-] I have updated the
[docs](https://github.com/cursorless-dev/cursorless/tree/main/docs) and
[cheatsheet](https://github.com/cursorless-dev/cursorless/tree/main/cursorless-talon/src/cheatsheet)
- [-] I have not broken the cheatsheet

---------

Co-authored-by: F-Kunkle <140621314+F-Kunkle@users.noreply.github.com>
This commit is contained in:
Pokey Rule 2023-11-02 19:53:05 +00:00 committed by GitHub
parent 1453284dbb
commit 962f64fe94
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 722 additions and 45 deletions

BIN
images/logo-white.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

View File

@ -0,0 +1,15 @@
import type { MDXComponents } from "mdx/types";
// This file allows you to provide custom React components
// to be used in MDX files. You can import and use any
// React component you want, including components from
// other libraries.
// This file is required to use MDX in `app` directory.
export function useMDXComponents(components: MDXComponents): MDXComponents {
return {
// Allows customizing built-in components, e.g. to add styling.
// h1: ({ children }) => <h1 style={{ fontSize: "100px" }}>{children}</h1>,
...components,
};
}

View File

@ -1,3 +1,9 @@
const withMDX = require("@next/mdx")({
options: {
providerImportSource: "@mdx-js/react",
},
});
/** @type {import('next').NextConfig} */
const nextConfig = {
webpack(config) {
@ -9,7 +15,10 @@ const nextConfig = {
return config;
},
experimental: {
mdxRs: true,
},
reactStrictMode: true,
};
module.exports = nextConfig;
module.exports = withMDX(nextConfig);

View File

@ -14,6 +14,9 @@
},
"dependencies": {
"@cursorless/cheatsheet": "workspace:*",
"@mdx-js/loader": "2.3.0",
"@mdx-js/react": "2.3.0",
"@next/mdx": "13.4.10",
"eslint": "^8.38.0",
"eslint-config-next": "13.5.4",
"next": "13.5.4",
@ -24,6 +27,8 @@
},
"devDependencies": {
"@svgr/webpack": "6.5.1",
"@types/mdx": "2.0.5",
"@types/mdx-js__react": "1.5.5",
"@types/node": "^16.11.3",
"@types/react": "18.0.28",
"@types/react-dom": "18.0.11",

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 166 KiB

View File

@ -1,17 +1,34 @@
import {
DESCRIPTION,
BASE_URL,
VIDEO_SHARE_THUMBNAIL_URL,
YOUTUBE_SLUG,
TITLE,
VIDEO_SHARE_THUMBNAIL_WIDTH,
VIDEO_SHARE_THUMBNAIL_HEIGHT,
VIDEO_SHARE_THUMBNAIL_URL,
VIDEO_SHARE_THUMBNAIL_WIDTH,
} from "./constants";
export default function Social() {
export interface Props {
title: string;
description: string;
relativeUrl: string;
youtubeSlug?: string;
thumbnailUrl?: string;
thumbnailWidth?: string;
thumbnailHeight?: string;
}
export default function BaseSocial({
title,
description,
relativeUrl,
youtubeSlug,
thumbnailUrl = VIDEO_SHARE_THUMBNAIL_URL,
thumbnailWidth = VIDEO_SHARE_THUMBNAIL_WIDTH,
thumbnailHeight = VIDEO_SHARE_THUMBNAIL_HEIGHT,
}: Props) {
const url = `${BASE_URL}/${relativeUrl}`;
return (
<>
<meta property="og:title" content={TITLE} key="title" />
<meta property="og:title" content={title} key="title" />
<link
rel="apple-touch-icon"
sizes="180x180"
@ -31,30 +48,63 @@ export default function Social() {
/>
<link rel="manifest" href="/site.webmanifest" />
<meta property="og:site_name" content="Cursorless" />
<meta property="og:url" content={BASE_URL} />
<meta property="og:image" content={VIDEO_SHARE_THUMBNAIL_URL} />
<meta property="og:image:width" content={VIDEO_SHARE_THUMBNAIL_WIDTH} />
<meta property="og:image:height" content={VIDEO_SHARE_THUMBNAIL_HEIGHT} />
<meta property="og:description" content={DESCRIPTION} />
<meta property="og:url" content={url} />
<meta property="og:image" content={thumbnailUrl} />
<meta property="og:image:width" content={thumbnailWidth} />
<meta property="og:image:height" content={thumbnailHeight} />
<meta property="og:description" content={description} />
<meta property="og:type" content="video" />
{youtubeSlug != null ? (
<VideoSocial youtubeSlug={youtubeSlug} />
) : (
<meta name="twitter:card" content="summary_large_image" />
)}
<meta name="twitter:site" content="@GoCursorless" />
<meta name="twitter:url" content={url} />
<meta name="twitter:title" content="Cursorless" />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={thumbnailUrl} />
<link itemProp="url" href={url} />
<meta itemProp="name" content="Cursorless" />
<meta itemProp="description" content={description} />
<meta name="description" content={description} />
<meta itemProp="paid" content="false" />
<meta itemProp="channelId" content="UCIh61mLlS6Do3R_8KnEScIQ" />
<link itemProp="thumbnailUrl" href={thumbnailUrl} />
<meta itemProp="isFamilyFriendly" content="true" />
<meta
itemProp="regionsAllowed"
content="HK,HT,TC,BW,PA,FK,AW,TN,AU,GE,SL,CC,TF,PN,TZ,CL,ET,BN,AR,PH,VI,EE,MY,LB,BM,UG,LU,NZ,KR,GG,BJ,SO,HM,GT,PR,IT,AZ,ZA,MH,MF,CW,UY,MN,MQ,MW,SE,PT,MK,SR,MA,RS,GQ,NP,BD,FM,GD,KI,IL,CK,YE,ML,BF,AO,SG,CG,GS,BI,CY,LR,UM,GH,BR,GY,KP,PM,IO,EH,SK,LV,AQ,IN,RE,LY,ZW,SZ,HR,LT,KE,TT,CR,KW,ER,NF,TK,BE,JM,SA,BH,RO,BO,MM,TH,IE,TD,QA,CN,FR,SC,VU,MG,SY,JP,PS,JO,MV,MD,TL,BY,GN,TJ,DE,TO,TR,BT,FJ,TW,AE,ES,DO,BS,NO,GU,DK,KH,SD,GB,ZM,VE,SS,IQ,AD,KG,NI,PK,PL,CZ,NA,LC,PY,SV,LA,AI,YT,US,VC,IR,NL,NU,AS,AL,GR,SH,GM,LS,ME,TV,EG,CF,DZ,SX,CA,PF,KM,AF,HN,NE,CD,MX,NC,CM,CX,SJ,GW,GF,VG,TG,BA,GP,CV,BQ,MO,SN,CH,BZ,MP,PE,FI,BB,GI,IS,PG,SM,BL,BG,AX,AM,AT,AG,ID,CI,GA,MC,NG,RW,SI,BV,UA,UZ,SB,LI,CU,VN,KN,WS,LK,IM,TM,OM,KY,VA,MT,MZ,DJ,EC,DM,HU,MU,FO,JE,NR,CO,WF,KZ,MR,GL,RU,MS,PW,ST"
/>
</>
);
}
interface VideoProps {
youtubeSlug: string;
}
function VideoSocial({ youtubeSlug }: VideoProps) {
return (
<>
<meta
property="og:video:url"
content={`https://www.youtube.com/embed/${YOUTUBE_SLUG}`}
content={`https://www.youtube.com/embed/${youtubeSlug}`}
/>
<meta
property="og:video:secure_url"
content={`https://www.youtube.com/embed/${YOUTUBE_SLUG}`}
content={`https://www.youtube.com/embed/${youtubeSlug}`}
/>
<meta property="og:video:type" content="text/html" />
<meta property="og:video:width" content="1280" />
<meta property="og:video:height" content="720" />
<meta
property="og:video:url"
content={`http://www.youtube.com/v/${YOUTUBE_SLUG}?version=3&amp;autohide=1`}
content={`http://www.youtube.com/v/${youtubeSlug}?version=3&amp;autohide=1`}
/>
<meta
property="og:video:secure_url"
content={`https://www.youtube.com/v/${YOUTUBE_SLUG}?version=3&amp;autohide=1`}
content={`https://www.youtube.com/v/${youtubeSlug}?version=3&amp;autohide=1`}
/>
<meta property="og:video:type" content="application/x-shockwave-flash" />
<meta property="og:video:width" content="1280" />
@ -67,28 +117,12 @@ export default function Social() {
<meta property="og:video:tag" content="Talon voice" />
<meta name="twitter:card" content="player" />
<meta name="twitter:site" content="@GoCursorless" />
<meta name="twitter:url" content={BASE_URL} />
<meta name="twitter:title" content="Cursorless" />
<meta name="twitter:description" content={DESCRIPTION} />
<meta name="twitter:image" content={VIDEO_SHARE_THUMBNAIL_URL} />
<meta
name="twitter:player"
content={`https://www.youtube.com/embed/${YOUTUBE_SLUG}`}
content={`https://www.youtube.com/embed/${youtubeSlug}`}
/>
<meta name="twitter:player:width" content="1280" />
<meta name="twitter:player:height" content="720" />
<link itemProp="url" href={BASE_URL} />
<meta itemProp="name" content="Cursorless" />
<meta itemProp="description" content={DESCRIPTION} />
<meta itemProp="paid" content="false" />
<meta itemProp="channelId" content="UCIh61mLlS6Do3R_8KnEScIQ" />
<link itemProp="thumbnailUrl" href={VIDEO_SHARE_THUMBNAIL_URL} />
<meta itemProp="isFamilyFriendly" content="true" />
<meta
itemProp="regionsAllowed"
content="HK,HT,TC,BW,PA,FK,AW,TN,AU,GE,SL,CC,TF,PN,TZ,CL,ET,BN,AR,PH,VI,EE,MY,LB,BM,UG,LU,NZ,KR,GG,BJ,SO,HM,GT,PR,IT,AZ,ZA,MH,MF,CW,UY,MN,MQ,MW,SE,PT,MK,SR,MA,RS,GQ,NP,BD,FM,GD,KI,IL,CK,YE,ML,BF,AO,SG,CG,GS,BI,CY,LR,UM,GH,BR,GY,KP,PM,IO,EH,SK,LV,AQ,IN,RE,LY,ZW,SZ,HR,LT,KE,TT,CR,KW,ER,NF,TK,BE,JM,SA,BH,RO,BO,MM,TH,IE,TD,QA,CN,FR,SC,VU,MG,SY,JP,PS,JO,MV,MD,TL,BY,GN,TJ,DE,TO,TR,BT,FJ,TW,AE,ES,DO,BS,NO,GU,DK,KH,SD,GB,ZM,VE,SS,IQ,AD,KG,NI,PK,PL,CZ,NA,LC,PY,SV,LA,AI,YT,US,VC,IR,NL,NU,AS,AL,GR,SH,GM,LS,ME,TV,EG,CF,DZ,SX,CA,PF,KM,AF,HN,NE,CD,MX,NC,CM,CX,SJ,GW,GF,VG,TG,BA,GP,CV,BQ,MO,SN,CH,BZ,MP,PE,FI,BB,GI,IS,PG,SM,BL,BG,AX,AM,AT,AG,ID,CI,GA,MC,NG,RW,SI,BV,UA,UZ,SB,LI,CU,VN,KN,WS,LK,IM,TM,OM,KY,VA,MT,MZ,DJ,EC,DM,HU,MU,FO,JE,NR,CO,WF,KZ,MR,GL,RU,MS,PW,ST"
/>
</>
);
}

View File

@ -0,0 +1,13 @@
import BaseSocial from "./BaseSocial";
import { DESCRIPTION, TITLE, VIDEO_SHARE_THUMBNAIL_URL } from "./constants";
export default function IndexSocial() {
return (
<BaseSocial
title={TITLE}
description={DESCRIPTION}
youtubeSlug={VIDEO_SHARE_THUMBNAIL_URL}
relativeUrl=""
/>
);
}

View File

@ -0,0 +1,143 @@
import { MDXProvider } from "@mdx-js/react";
import type { MDXComponents } from "mdx/types.js";
import Head from "next/head";
import Logo from "../pages/logo.svg";
import BaseSocial from "./BaseSocial";
import { SpamProofEmailLink } from "./SpamProofEmailLink";
import Link from "next/link";
const components: MDXComponents = {
h1: ({ children }) => (
<h1 className="text-center uppercase text-[1.88em] leading-tight mt-7 tracking-[0.14em] font-semibold">
{children}
</h1>
),
h2: ({ children }) => (
<h2 className="uppercase text-[1.4em] mt-8 mb-4 sm:mb-8 font-semibold tracking-[0.08em]">
{children}
</h2>
),
h3: ({ children }) => (
<h3 className="uppercase text-[1.5rem] mt-6 mb-3 sm:mb-6 font-medium tracking-[0.08em]">
{children}
</h3>
),
h4: ({ children }) => (
<h4 className="uppercase text-[1.5rem] mt-11 mb-3 sm:mb-6 font-medium tracking-[0.08em]">
{children}
</h4>
),
hr: () => <hr className="my-8 border-teal-400" />,
ul: ({ children }) => <ul className="list-disc ml-8">{children}</ul>,
ol: ({ children }) => <ol className="list-decimal ml-8">{children}</ol>,
li: ({ children }) => <li className="my-2">{children}</li>,
img: ({ src, alt }) => (
// FIXME: Figure out how to use next/image with MDX
// eslint-disable-next-line @next/next/no-img-element
<img
className="mx-auto my-12 rounded-[4px] border-teal-400 border-[1.5px]"
src={src}
alt={alt}
style={{ maxWidth: "100%" }}
/>
),
CursorlessScreenshot: ({ src, alt }) => (
// FIXME: Figure out how to use next/image with MDX
// eslint-disable-next-line @next/next/no-img-element
<img
className="mx-auto my-12 border-teal-400 border p-3 sm:p-6 rounded-sm"
src={src}
alt={alt}
style={{ maxWidth: "100%" }}
/>
),
CalloutParent: ({ children }) => (
<div className="border-teal-400 border rounded-sm px-7 my-12">
{children}
</div>
),
CalloutBox: ({ title, children }) => (
<div className="my-6">
<h4 className="uppercase text-[1.5rem] mb-3 sm:mb-6 leading-tight font-medium tracking-[0.08em]">
{title}
</h4>
{children}
</div>
),
Testimonials: ({ children }) => (
<div className="flex flex-col gap-5 mt-8">{children}</div>
),
Testimonial: ({ children, src, name, title, company }) => (
<div className="flex flex-col items-center bg-teal-100 dark:bg-teal-900 border border-teal-400 text-teal-700 dark:text-teal-300 p-3 sm:p-6 rounded-sm">
<blockquote className="mb-5 sm:mb-6 flex flex-col gap-4">
{children}
</blockquote>
<div className="flex items-center gap-4">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
className="rounded-full w-[4.5em] h-[4.5em] border-teal-400 border dark:border-[0.5px]"
src={src}
alt={name}
/>
<div className="flex flex-col gap-[0.375rem]">
<div className="text-teal-800 dark:text-teal-200 font-semibold text-[1.2em] leading-none">
{name}
</div>
<div className="text-[0.9em] leading-none">{title}</div>
<div className="text-[0.9em] leading-none">{company}</div>
</div>
</div>
</div>
),
Tiers: ({ children }) => (
<div className="my-8 font-medium tracking-[0.1em] text-[1.2em]">
{children}
</div>
),
Tier: ({ emoji, type, price, address, subject, body }) => (
<div className="flex gap-3 leading-8">
<span className="">{emoji}</span>
<span className="uppercase">{type}</span>
<span>{"-"}</span>
<SpamProofEmailLink address={address} subject={subject} body={body}>
{price} / year
</SpamProofEmailLink>
</div>
),
};
export const bodyClasses = "bg-salmon-100 dark:bg-salmon-900";
export interface Props extends React.PropsWithChildren {
title: string;
description: string;
relativeUrl: string;
}
export function Layout({ title, description, relativeUrl, children }: Props) {
return (
<>
<Head>
<title>{title}</title>
<BaseSocial
title={title}
description={description}
relativeUrl={relativeUrl}
/>
</Head>
<MDXProvider components={components}>
<main className="text-salmon-900 dark:text-salmon-100 font-mono font-normal sm:dark:font-light px-4 pt-8 sm:pt-16 lg:pt-20 pb-8 tracking-[0.08em] leading-6">
<div className="max-w-prose mx-auto">
<Link href="/">
<Logo
title="Logo"
className="mx-auto align-middle w-[6.284em] h-[6.284em]"
/>
</Link>
{children}
</div>
</main>
</MDXProvider>
</>
);
}

View File

@ -0,0 +1,69 @@
import { EmailAddress } from "../parseEmailAddress";
interface Props extends React.PropsWithChildren {
address: EmailAddress;
subject?: string;
body?: string;
}
/**
* Encodes a string for use in a URL, but unlike encodeURIComponent, it encodes
* every character, including regular ASCII characters [a-zA-Z] etc. For example:
*
* encodeURIComponent("user@example.com") === "%75%73%65%72%40%65%78%61%6D%70%6C%65%2E%63%6F%6D"
*
* @param str The string to encode
* @returns A URL-encoded version of the string, where every character is encoded
*/
function strictEncodeURIComponent(str: string) {
const components: string[] = [];
for (let i = 0; i < str.length; i++) {
components.push("%" + str.charCodeAt(i).toString(16).toUpperCase());
}
return components.join("");
}
/**
* A link to an email address, attempting to prevent spam bots from finding it.
* Encodes the URI for the href using very aggressive uri encoding, and for the
* displayed email text, injects dummy text in a hidden span so that bots will
* see it but humans won't.
*
* Tricks taken from https://spencermortensen.com/articles/email-obfuscation/
*
* @param param0 The email address to use
* @returns A link to the email address, attempting to prevent spam bots from
* finding it
*/
export function SpamProofEmailLink({
address: { username, domain },
subject,
body,
children,
}: Props) {
// URL encode every character of the email address, including the mailto: prefix
const rawEmailHref = `${username}@${domain}`;
let href = `mailto:${strictEncodeURIComponent(rawEmailHref)}`;
if (subject != null) {
const subjectEncoded = encodeURIComponent(subject).replace(/\+/g, "%20");
href += `?subject=${subjectEncoded}`;
}
if (body != null) {
const bodyEncoded = encodeURIComponent(body).replace(/\+/g, "%20");
href += (href.includes("?") ? "&" : "?") + `body=${bodyEncoded}`;
}
return (
<a href={href} className="text-teal-400 underline underline-offset-4">
{children ?? (
<>
{`${username}@`}
<span className="hidden">Die spam!</span>
{domain}
</>
)}
</a>
);
}

View File

@ -0,0 +1,210 @@
export const meta = {
title: "Cursorless Enablement Group",
description:
"Help enable the adoption of Cursorless by accelerating its development.",
};
import { SpamProofEmailLink } from "../components/SpamProofEmailLink";
export const emailSubject = "Cursorless Enablement Group";
export const emailBody = `Hi Pokey,
I'm interested in joining the Cursorless Enablement Group. Please send me more information.
Thanks,
`;
# {meta.title}
---
## {meta.description}
Cursorless, an open-source spoken language for editing code, enables users to write software entirely by voice faster than with a keyboard and mouse. Software engineers can code using high-level semantic manipulations (increased productivity), and all computer users can reduce strain on their wrists to prevent injury (preventative healthcare).
<CursorlessScreenshot
src="/big-hats.png"
alt="Screenshot of Cursorless in action containing javascript code with symbols over characters in the source code"
/>
## Cursorless development is user-led
Your support will help Cursorless founder, Pokey Rule, and his team develop the following feature requests as quickly as possible:
- Reduce the learning curve with interactive tutorials, videos, and documentation to increase the rate of adoption.
- Launch Cursorless in other IDEs, such as JetBrains, emacs, etc, as well as in a web browser, and even work globally using OCR / accessibility APIs to operate anywhere on the screen.
- Further improvements to the Cursorless execution engine to advance the state of the art in voice coding.
### 🫶 Developers love Cursorless
<Testimonials>
<Testimonial
src="/james-stout.jpeg"
name="James Stout"
title="Director of Accessibility Engineering"
company="Google"
>
"Phenomenal extension. This is the state of the art for coding by voice.
Nothing else comes close. Awesome to see this from the open source community!"
</Testimonial>
<Testimonial
src="/max-foxley-marrable.jpeg"
name="Max Foxley-Marrable"
title="Data Scientist"
company="Revolution Data Platforms"
>
"This extension is a genuine game-changer. As someone who suffers with chronic
RSI, I was seriously concerned I would be unfit to continue my career
long-term. Discovering Talon and Cursorless has given me hope again and has
additionally given me the means to write code and interface with my computer
in ways I never thought possible. It's also made coding way more fun than
before! I often feel like a code-slinging wizard!"
</Testimonial>
<Testimonial
src="/sohee-yang.jpeg"
name="Sohee Yang"
title="Ph.D. student"
company="University College London"
>
"For developers turning to Talon due to typing limitations, Cursorless isnt just beneficial—its indispensable. When I lost my ability to type due to RSI, I was consumed with the fear of significantly reduced productivity and potentially not being able to work anymore. However, when I discovered Cursorless, it gave me hope and confidence that I could regain my prior efficiency once I mastered the tool."
</Testimonial>
<Testimonial
src="/nathan-heffley.jpeg"
name="Nathan Heffley"
title="Senior Software Engineer"
company="Industrial Resolution"
>
"This is fantastic extension that is not only saving my career but is even speeding up my workflow within VS Code more than when I was typing anyway. The best part is you really feel like a hacker when you rattle off a long command and the code does exactly what you wanted!"
</Testimonial>
</Testimonials>
### 🎯 Goals
Cursorless needs a dedicated, full-time software engineer on staff. This will cost $5,000 USD per month. The Enablement Group guides development and the participation fees contribute to this crucial funding. Cursorless will always be open source.
## 🙌 <SpamProofEmailLink address={props.emailAddress} subject={emailSubject} body={emailBody}> Join the Cursorless Enablement Group </SpamProofEmailLink>
The Enablement Group consists of stakeholders whose talents, lived experiences, and career experiences bring vital perspectives to the development of Cursorless. Support and input help increase the speed at which new features can be delivered.
**To join, send an email to Cursorless founder Pokey Rule**
<SpamProofEmailLink
address={props.emailAddress}
subject={emailSubject}
body={emailBody}
/>
<CalloutParent>
<CalloutBox title="Become a Distinguished Accessibility Champion">
By joining the Enablement Group, you solidify your position as a dedicated
advocate for accessible technology and inclusion. Your active involvement
showcases your commitment to driving positive change and making technology
more accessible for everyone.
</CalloutBox>
<CalloutBox title="Gain Prominent Visibility Online">
As a valued member of the Enablement Group, you gain increased visibility
within both the open source and accessible technology communities. Your
participation will be recognized on all web properties including GitHub Repo,
cursorless.org, social media, via any other acknowledgments in project
updates, newsletters, and events to cement your role as a key player in
advancing Cursorless.
</CalloutBox>
<CalloutBox title="Empower Future Accessibility">
Your commitment to the Enablement Group supports the long-term vision of
creating a full-time staff engineer dedicated to developing Cursorless. As the
group grows, the possibility of achieving this goal becomes more tangible,
contributing to a sustainable and impactful initiative for accessible
technology.
</CalloutBox>
<CalloutBox title="Join a Collaboration with Industry Leaders">
Joining the Enablement Group provides an opportunity to collaborate with
like-minded individuals and industry leaders who share your passion for
accessible technology. This network allows you to exchange insights, share
best practices, and collectively drive the advancement of inclusive digital
solutions.
</CalloutBox>
<CalloutBox title="Obtain Collective Influence on Development">
While the development of Cursorless is guided by its community of users, your
annual fee directly contributes to the development of these feature requests!
Use your expertise and experience to help prioritize and comment on effective
solutions around feature requests. You're ensuring that development directly
aligns with the needs of users who value inclusive and accessible technology
and rely on it to succeed in their lives.
</CalloutBox>
</CalloutParent>
### All participants also enjoy the following benefits
Members of the Cursorless Enablement Group enjoy several privileges. The access and public visibility participants receive make for a dynamic combination of benefits. The value that individuals and companies gain from their involvement in the group is tangible.
#### 🤓 Technical
- Be the first to access Cursorless feature updates and new versions.
- Lead the adoption of Cursorless by understanding its implementation and having superior knowledge of its use.
- Have some influence over the direction of the technology through your expertise.
- Gain an advanced and deeper knowledge of any outputs of the group (recommended practices, engineering guidelines)
#### 🥽 Visibility
- Ability to promote your organization as a champion for accessible technology and in particular Cursorless.
- Ability to promote your organization as a leader in the area of accessible technology.
- Opportunity to connect with other influencers in software and accessibility.
- Networking and ability to cultivate deep relationships and potential partnerships with leaders, founders, and influencers.
#### 🪖 Strategic
- Support an initiative targeted at facilitating an increased and more rapid shift of adoption of Cursorless as a faster way to code and a healthier way to code.
- Support of open standards approach. Standards are critical to interoperability and openness is consistent with a positive image in the industry.
## 🚀 <SpamProofEmailLink address={props.emailAddress} subject={emailSubject} body={emailBody}> Join the Cursorless Enablement Group </SpamProofEmailLink>
Your participation will help Cursorless maintain its growth trajectory and uphold its cutting-edge quality and esteemed 5-star reputation in coding by voice. And you'll make a lasting impact on the direction of accessible technology.
<Tiers>
<Tier
emoji="😍️"
type="individuals"
price="$495"
address={props.emailAddress}
subject={emailSubject}
body={emailBody}
/>
<Tier
emoji="🕊️"
type="non-profits"
price="$995"
address={props.emailAddress}
subject={emailSubject}
body={emailBody}
/>
<Tier
emoji="🏢"
type="companies"
price="$2,495"
address={props.emailAddress}
subject={emailSubject}
body={emailBody}
/>
</Tiers>
To join, send an email to Cursorless founder Pokey Rule
<SpamProofEmailLink
address={props.emailAddress}
subject={emailSubject}
body={emailBody}
/>

View File

@ -0,0 +1,6 @@
export { default } from "*.mdx";
export const meta: {
title: string;
description: string;
};

View File

@ -0,0 +1,41 @@
import {
default as EnablementGroup,
meta,
} from "../content/enablement-group.mdx";
import { Layout, bodyClasses } from "../components/Layout";
import { env } from "process";
import { parseEmailAddress, EmailAddress } from "../parseEmailAddress";
const RELATIVE_URL = "cursorless-enablement";
export async function getStaticProps() {
return {
props: {
// See https://github.com/vercel/next.js/discussions/12325#discussioncomment-1116108
bodyClasses,
//! IMPORTANT: Don't return the email address unparsed, because it will
//! be serialized as JSON and exposed to the client, so spam bots might
//! find it. Instead, parse it and return the parsed object.
emailAddress: parseEmailAddress(
env["ENABLEMENT_GROUP_EMAIL"] ?? "user@example.com",
),
},
};
}
interface Props extends React.PropsWithChildren {
emailAddress: EmailAddress;
}
export default function Page({ emailAddress }: Props) {
return (
<Layout
title={meta.title}
description={meta.description}
relativeUrl={RELATIVE_URL}
>
<EnablementGroup emailAddress={emailAddress} />
</Layout>
);
}

View File

@ -2,7 +2,7 @@ import { EmbeddedVideo } from "../components/embedded-video";
import Head from "next/head";
import Button from "../components/Button";
import { TITLE, YOUTUBE_SLUG } from "../components/constants";
import Social from "../components/Social";
import IndexSocial from "../components/IndexSocial";
import Logo from "./logo.svg";
// See https://github.com/vercel/next.js/discussions/12325#discussioncomment-1116108
@ -19,9 +19,9 @@ export default function LandingPage() {
<>
<Head>
<title>{TITLE}</title>
<Social />
<IndexSocial />
</Head>
<main className="items-center justify-center text-salmon-900 dark:text-salmon-100 font-mono font-bold tracking-[0.18em] overflow-auto fixed top-0 bottom-0 left-0 right-0 p-2 sm:p-0 sm:flex ">
<main className="items-center justify-center text-salmon-900 dark:text-salmon-100 font-monoWide font-bold tracking-[0.18em] overflow-auto fixed top-0 bottom-0 left-0 right-0 p-2 sm:p-0 sm:flex ">
{/*
Note that the font scale gets applied to this element so that all nested elements can use
`em` units and will automatically be scaled.

View File

@ -1,6 +1,6 @@
<svg width="30" height="30" viewBox="0.4 0.85 29 28.5" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M24.8086 22.0457C26.2355 20.0628 27.0758 17.6296 27.0758 15.0002C27.0758 12.3709 26.2355 9.93778 24.8088 7.95489C27.3909 8.60499 29.3385 11.5122 29.3385 15.0003C29.3385 18.4885 27.3908 21.3957 24.8086 22.0457Z" fill="currentColor"/>
<path d="M7.95922 5.18788C9.94126 3.7632 12.3726 2.92419 14.9998 2.92419C17.6279 2.92419 20.0598 3.76368 22.0422 5.18911C21.3846 2.61442 18.4819 0.674316 15.0005 0.674316C11.5197 0.674316 8.61735 2.6138 7.95922 5.18788Z" fill="currentColor"/>
<path d="M5.22143 7.91264C3.77615 9.90318 2.92383 12.3522 2.92383 15.0002C2.92383 17.6483 3.77621 20.0974 5.22158 22.088C2.54852 21.5326 0.507812 18.5708 0.507812 15.0003C0.507812 11.4298 2.54844 8.46805 5.22143 7.91264Z" fill="currentColor"/>
<path d="M22.042 24.8114C20.0597 26.2368 17.6278 27.0762 14.9998 27.0762C12.3726 27.0762 9.94137 26.2373 7.95936 24.8126C8.61775 27.3864 11.5199 29.3257 15.0005 29.3257C18.4817 29.3257 21.3842 27.3858 22.042 24.8114Z" fill="currentColor"/>
<svg width="30" height="30" viewBox="0 0 1920 1920" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M960.001 237.896C1140.58 237.896 1306.36 310.143 1436.6 430.88C1388.26 184.524 1193.47 0 960.001 0C726.538 0 531.747 184.524 483.405 431.206C613.645 310.468 779.429 237.896 960.001 237.896Z" fill="currentColor"/>
<path d="M960.001 1681.92C779.429 1681.92 613.645 1609.62 483.405 1488.79C531.747 1735.34 726.538 1920 960.001 1920C1193.47 1920 1388.26 1735.34 1436.6 1488.79C1306.36 1609.62 1140.58 1681.92 960.001 1681.92Z" fill="currentColor"/>
<path d="M238.075 960C238.075 779.429 310.377 613.644 431.206 483.405C184.663 531.747 0 726.537 0 960C0 1193.47 184.663 1388.26 431.206 1436.6C310.703 1306.36 238.075 1140.58 238.075 960Z" fill="currentColor"/>
<path d="M1681.92 960C1681.92 1140.58 1609.62 1306.36 1488.79 1436.6C1735.34 1388.26 1920 1193.47 1920 960C1920 726.537 1735.34 531.747 1488.79 483.405C1609.62 613.644 1681.92 779.429 1681.92 960Z" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 992 B

View File

@ -0,0 +1,8 @@
export interface EmailAddress {
username: string;
domain: string;
}
export function parseEmailAddress(email: string): EmailAddress {
const [username, domain] = email.split("@");
return { username, domain };
}

View File

@ -74,3 +74,67 @@
/* Modern Browsers */ url("/fonts/Inconsolata_SemiExpanded-ExtraBold.ttf")
format("truetype");
}
@font-face {
font-family: "Inconsolata";
font-style: normal;
font-weight: 200;
font-stretch: 1% 500%; /* Required by Chrome */
src:
local(""),
/* Modern Browsers */ url("/fonts/Inconsolata-ExtraLight.ttf")
format("truetype");
}
@font-face {
font-family: "Inconsolata";
font-style: normal;
font-weight: 300;
font-stretch: 1% 500%; /* Required by Chrome */
src:
local(""),
/* Modern Browsers */ url("/fonts/Inconsolata-Light.ttf") format("truetype");
}
@font-face {
font-family: "Inconsolata";
font-style: normal;
font-weight: 400;
font-stretch: 1% 500%; /* Required by Chrome */
src:
local(""),
/* Modern Browsers */ url("/fonts/Inconsolata-Regular.ttf")
format("truetype");
}
@font-face {
font-family: "Inconsolata";
font-style: normal;
font-weight: 500;
font-stretch: 1% 500%; /* Required by Chrome */
src:
local(""),
/* Modern Browsers */ url("/fonts/Inconsolata-Medium.ttf")
format("truetype");
}
@font-face {
font-family: "Inconsolata";
font-style: normal;
font-weight: 600;
font-stretch: 1% 500%; /* Required by Chrome */
src:
local(""),
/* Modern Browsers */ url("/fonts/Inconsolata-SemiBold.ttf")
format("truetype");
}
@font-face {
font-family: "Inconsolata";
font-style: normal;
font-weight: 700;
font-stretch: 1% 500%; /* Required by Chrome */
src:
local(""),
/* Modern Browsers */ url("/fonts/Inconsolata-Bold.ttf") format("truetype");
}

View File

@ -58,7 +58,8 @@ module.exports = {
stretched: { raw: "(min-aspect-ratio: 2/1), (max-aspect-ratio: 1/1)" },
},
fontFamily: {
mono: ["Inconsolata-SemiExpanded", ...defaultTheme.fontFamily.mono],
mono: ["Inconsolata", ...defaultTheme.fontFamily.mono],
monoWide: ["Inconsolata-SemiExpanded", ...defaultTheme.fontFamily.mono],
},
width: {
smBase: smallWidth,
@ -74,15 +75,26 @@ module.exports = {
xs: "1.2em",
lg: "1.8em",
"2xl": "2.4em",
"3xl": "3.6em",
},
colors: {
salmon: {
100: "#FFFAF8",
300: "#F8C9BA",
400: "#FF9273",
700: "#372e2a",
800: "#161110",
900: "#0A0707",
},
teal: {
100: "#F9FFFE",
200: "#CDFFF9",
300: "#99FFF3",
400: "#00907F",
700: "#005349",
800: "#00443C",
900: "#00110F",
},
},
},
},

View File

@ -8,7 +8,7 @@
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "nodenext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",

View File

@ -275,6 +275,15 @@ importers:
'@cursorless/cheatsheet':
specifier: workspace:*
version: link:../cheatsheet
'@mdx-js/loader':
specifier: 2.3.0
version: 2.3.0(webpack@5.88.2)
'@mdx-js/react':
specifier: 2.3.0
version: 2.3.0(react@18.2.0)
'@next/mdx':
specifier: 13.4.10
version: 13.4.10(@mdx-js/loader@2.3.0)(@mdx-js/react@2.3.0)
eslint:
specifier: ^8.38.0
version: 8.38.0
@ -300,6 +309,12 @@ importers:
'@svgr/webpack':
specifier: 6.5.1
version: 6.5.1
'@types/mdx':
specifier: 2.0.5
version: 2.0.5
'@types/mdx-js__react':
specifier: 1.5.5
version: 1.5.5
'@types/node':
specifier: ^16.11.3
version: 16.18.13
@ -3309,6 +3324,18 @@ packages:
/@leichtgewicht/ip-codec@2.0.4:
resolution: {integrity: sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==}
/@mdx-js/loader@2.3.0(webpack@5.88.2):
resolution: {integrity: sha512-IqsscXh7Q3Rzb+f5DXYk0HU71PK+WuFsEhf+mSV3fOhpLcEpgsHvTQ2h0T6TlZ5gHOaBeFjkXwB52by7ypMyNg==}
peerDependencies:
webpack: '>=4'
dependencies:
'@mdx-js/mdx': 2.3.0
source-map: 0.7.4
webpack: 5.88.2(webpack-cli@5.1.4)
transitivePeerDependencies:
- supports-color
dev: false
/@mdx-js/mdx@2.3.0:
resolution: {integrity: sha512-jLuwRlz8DQfQNiUCJR50Y09CGPq3fLtmtUQfVrj79E0JWu3dvsVcxVIcfhR5h0iXu+/z++zDrYeiJqifRynJkA==}
dependencies:
@ -3358,6 +3385,22 @@ packages:
glob: 7.1.7
dev: false
/@next/mdx@13.4.10(@mdx-js/loader@2.3.0)(@mdx-js/react@2.3.0):
resolution: {integrity: sha512-0ZbUIr3yuFFfkaYth2kNFAT0fbyylJTMqZy5zTdb7YGqvYjKFD8n75L3UYAX0g5mibGp3iETJ0I7730sW13PKQ==}
peerDependencies:
'@mdx-js/loader': '>=0.15.0'
'@mdx-js/react': '>=0.15.0'
peerDependenciesMeta:
'@mdx-js/loader':
optional: true
'@mdx-js/react':
optional: true
dependencies:
'@mdx-js/loader': 2.3.0(webpack@5.88.2)
'@mdx-js/react': 2.3.0(react@18.2.0)
source-map: 0.7.4
dev: false
/@next/swc-darwin-arm64@13.5.4:
resolution: {integrity: sha512-Df8SHuXgF1p+aonBMcDPEsaahNo2TCwuie7VXED4FVyECvdXfRT9unapm54NssV9tF3OQFKBFOdlje4T43VO0w==}
engines: {node: '>= 10'}
@ -4758,9 +4801,14 @@ packages:
'@types/unist': 2.0.6
dev: false
/@types/mdx-js__react@1.5.5:
resolution: {integrity: sha512-k8pnaP6JXVlQh18HgL5X6sYFNC/qZnzO7R2+HsmwrwUd+JnnsU0d9lyyT0RQrHg1anxDU36S98TI/fsGtmYqqg==}
dependencies:
'@types/react': 18.0.28
dev: true
/@types/mdx@2.0.5:
resolution: {integrity: sha512-76CqzuD6Q7LC+AtbPqrvD9AqsN0k8bsYo2bM2J8pmNldP1aIPAbzUQ7QbobyXL4eLr1wK5x8FZFe8eF/ubRuBg==}
dev: false
/@types/mime@3.0.1:
resolution: {integrity: sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==}