summaryrefslogtreecommitdiffhomepage
path: root/packages/console/app/src
diff options
context:
space:
mode:
authorDavid Hill <[email protected]>2025-10-29 15:16:17 +0000
committerDavid Hill <[email protected]>2025-10-29 15:16:17 +0000
commit7baa75135159907d4e28f82e1e9a22fe6ef7e6e1 (patch)
treeac5f47615db1ef3b47931c5e9cfe4a92c592e940 /packages/console/app/src
parent5b86fa91099d66cdc876cd4209a97ae2c903d510 (diff)
downloadopencode-7baa75135159907d4e28f82e1e9a22fe6ef7e6e1.tar.gz
opencode-7baa75135159907d4e28f82e1e9a22fe6ef7e6e1.zip
First pass at adding an enterprise page
Diffstat (limited to 'packages/console/app/src')
-rw-r--r--packages/console/app/src/component/header.tsx6
-rw-r--r--packages/console/app/src/routes/api/enterprise.ts52
-rw-r--r--packages/console/app/src/routes/enterprise/index.css552
-rw-r--r--packages/console/app/src/routes/enterprise/index.tsx199
-rw-r--r--packages/console/app/src/routes/index.css6
5 files changed, 810 insertions, 5 deletions
diff --git a/packages/console/app/src/component/header.tsx b/packages/console/app/src/component/header.tsx
index 29b35bfa4..ea8921f30 100644
--- a/packages/console/app/src/component/header.tsx
+++ b/packages/console/app/src/component/header.tsx
@@ -37,6 +37,9 @@ export function Header(props: { zen?: boolean }) {
<a href="/docs">Docs</a>
</li>
<li>
+ <A href="/enterprise">Enterprise</A>
+ </li>
+ <li>
<Switch>
<Match when={props.zen}>
<a href="/auth">Login</a>
@@ -108,6 +111,9 @@ export function Header(props: { zen?: boolean }) {
<a href="/docs">Docs</a>
</li>
<li>
+ <A href="/enterprise">Enterprise</A>
+ </li>
+ <li>
<Switch>
<Match when={props.zen}>
<a href="/auth">Login</a>
diff --git a/packages/console/app/src/routes/api/enterprise.ts b/packages/console/app/src/routes/api/enterprise.ts
new file mode 100644
index 000000000..6298ae349
--- /dev/null
+++ b/packages/console/app/src/routes/api/enterprise.ts
@@ -0,0 +1,52 @@
+import type { APIEvent } from "@solidjs/start/server"
+import { AWS } from "@opencode-ai/console-core/aws.js"
+
+interface EnterpriseFormData {
+ name: string
+ company: string
+ role: string
+ email: string
+ message: string
+}
+
+export async function POST(event: APIEvent) {
+ try {
+ const body = (await event.request.json()) as EnterpriseFormData
+
+ // Validate required fields
+ if (!body.name || !body.company || !body.role || !body.email || !body.message) {
+ return Response.json({ error: "All fields are required" }, { status: 400 })
+ }
+
+ // Validate email format
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
+ if (!emailRegex.test(body.email)) {
+ return Response.json({ error: "Invalid email format" }, { status: 400 })
+ }
+
+ // Create email content
+ const emailContent = `
+New Enterprise Inquiry
+
+Name: ${body.name}
+Company: ${body.company}
+Role: ${body.role}
+Email: ${body.email}
+
+Message:
+${body.message}
+ `.trim()
+
+ // Send email using AWS SES
+ await AWS.sendEmail({
+ subject: `Enterprise Inquiry from ${body.company}`,
+ body: emailContent,
+ })
+
+ return Response.json({ success: true, message: "Form submitted successfully" }, { status: 200 })
+ } catch (error) {
+ console.error("Error processing enterprise form:", error)
+ return Response.json({ error: "Internal server error" }, { status: 500 })
+ }
+}
diff --git a/packages/console/app/src/routes/enterprise/index.css b/packages/console/app/src/routes/enterprise/index.css
new file mode 100644
index 000000000..c0cb478c5
--- /dev/null
+++ b/packages/console/app/src/routes/enterprise/index.css
@@ -0,0 +1,552 @@
+::selection {
+ background: var(--color-background-interactive);
+ color: var(--color-text-strong);
+
+ @media (prefers-color-scheme: dark) {
+ background: var(--color-background-interactive);
+ color: var(--color-text-inverted);
+ }
+}
+
+
+[data-page="enterprise"] {
+ --color-background: hsl(0, 20%, 99%);
+ --color-background-weak: hsl(0, 8%, 97%);
+ --color-background-weak-hover: hsl(0, 8%, 94%);
+ --color-background-strong: hsl(0, 5%, 12%);
+ --color-background-strong-hover: hsl(0, 5%, 18%);
+ --color-background-interactive: hsl(62, 84%, 88%);
+ --color-background-interactive-weaker: hsl(64, 74%, 95%);
+
+ --color-text: hsl(0, 1%, 39%);
+ --color-text-weak: hsl(0, 1%, 60%);
+ --color-text-weaker: hsl(30, 2%, 81%);
+ --color-text-strong: hsl(0, 5%, 12%);
+ --color-text-inverted: hsl(0, 20%, 99%);
+
+ --color-border: hsl(30, 2%, 81%);
+ --color-border-weak: hsl(0, 1%, 85%);
+
+ --color-icon: hsl(0, 1%, 55%);
+ --color-success: hsl(142, 76%, 36%);
+
+ background: var(--color-background);
+ font-family: var(--font-mono);
+ color: var(--color-text);
+ padding-bottom: 5rem;
+
+ @media (prefers-color-scheme: dark) {
+ --color-background: hsl(0, 9%, 7%);
+ --color-background-weak: hsl(0, 6%, 10%);
+ --color-background-weak-hover: hsl(0, 6%, 15%);
+ --color-background-strong: hsl(0, 15%, 94%);
+ --color-background-strong-hover: hsl(0, 15%, 97%);
+ --color-background-interactive: hsl(62, 100%, 90%);
+ --color-background-interactive-weaker: hsl(60, 20%, 8%);
+
+ --color-text: hsl(0, 4%, 71%);
+ --color-text-weak: hsl(0, 2%, 49%);
+ --color-text-weaker: hsl(0, 3%, 28%);
+ --color-text-strong: hsl(0, 15%, 94%);
+ --color-text-inverted: hsl(0, 9%, 7%);
+
+ --color-border: hsl(0, 3%, 28%);
+ --color-border-weak: hsl(0, 4%, 23%);
+
+ --color-icon: hsl(10, 3%, 43%);
+ --color-success: hsl(142, 76%, 46%);
+ }
+
+ /* Header and Footer styles - copied from index.css */
+ [data-component="top"] {
+ padding: 24px 5rem;
+ height: 80px;
+ position: sticky;
+ top: 0;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ background: var(--color-background);
+ border-bottom: 1px solid var(--color-border-weak);
+ z-index: 10;
+
+ @media (max-width: 60rem) {
+ padding: 24px 1.5rem;
+ }
+
+ img {
+ height: 34px;
+ width: auto;
+ }
+
+ [data-component="nav-desktop"] {
+ ul {
+ display: flex;
+ justify-content: space-between;
+ gap: 48px;
+ li {
+ display: inline-block;
+ a {
+ text-decoration: none;
+ span {
+ color: var(--color-text-weak);
+ }
+ }
+ a:hover {
+ text-decoration: underline;
+ text-underline-offset: 2px;
+ text-decoration-thickness: 1px;
+ }
+ }
+ }
+
+ @media (max-width: 40rem) {
+ display: none;
+ }
+ }
+
+ [data-component="nav-mobile"] {
+ button > svg {
+ color: var(--color-icon);
+ }
+ }
+
+ [data-component="nav-mobile-toggle"] {
+ border: none;
+ background: none;
+ outline: none;
+ height: 40px;
+ width: 40px;
+ cursor: pointer;
+ margin-right: -8px;
+ }
+
+ [data-component="nav-mobile-toggle"]:hover {
+ background: var(--color-background-weak);
+ }
+
+ [data-component="nav-mobile"] {
+ display: none;
+
+ @media (max-width: 40rem) {
+ display: block;
+
+ [data-component="nav-mobile-icon"] {
+ cursor: pointer;
+ height: 40px;
+ width: 40px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ [data-component="nav-mobile-menu-list"] {
+ position: fixed;
+ background: var(--color-background);
+ top: 80px;
+ left: 0;
+ right: 0;
+ height: 100vh;
+
+ ul {
+ list-style: none;
+ padding: 20px 0;
+
+ li {
+ a {
+ text-decoration: none;
+ padding: 20px;
+ display: block;
+
+ span {
+ color: var(--color-text-weak);
+ }
+ }
+
+ a:hover {
+ background: var(--color-background-weak);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ [data-slot="logo dark"] {
+ display: none;
+ }
+
+ @media (prefers-color-scheme: dark) {
+ [data-slot="logo light"] {
+ display: none;
+ }
+ [data-slot="logo dark"] {
+ display: block;
+ }
+ }
+ }
+
+ [data-component="footer"] {
+ border-top: 1px solid var(--color-border-weak);
+ display: flex;
+ flex-direction: row;
+
+ @media (max-width: 65rem) {
+ border-bottom: 1px solid var(--color-border-weak);
+ }
+
+ [data-slot="cell"] {
+ flex: 1;
+ text-align: center;
+
+ a {
+ text-decoration: none;
+ padding: 2rem 0;
+ width: 100%;
+ display: block;
+
+ span {
+ color: var(--color-text-weak);
+
+ @media (max-width: 40rem) {
+ display: none;
+ }
+ }
+ }
+
+ a:hover {
+ background: var(--color-background-weak);
+ text-decoration: underline;
+ text-underline-offset: 2px;
+ text-decoration-thickness: 1px;
+ }
+ }
+
+ [data-slot="cell"] + [data-slot="cell"] {
+ border-left: 1px solid var(--color-border-weak);
+
+ @media (max-width: 40rem) {
+ border-left: none;
+ }
+ }
+
+ /* Mobile: third column on its own row */
+ @media (max-width: 25rem) {
+ flex-wrap: wrap;
+
+ [data-slot="cell"] {
+ flex: 1 0 100%;
+ border-left: none;
+ border-top: 1px solid var(--color-border-weak);
+ }
+
+ [data-slot="cell"]:nth-child(1) {
+ border-top: none;
+ }
+ }
+ }
+
+ [data-component="container"] {
+ max-width: 67.5rem;
+ margin: 0 auto;
+ border: 1px solid var(--color-border-weak);
+ border-top: none;
+
+ @media (max-width: 65rem) {
+ border: none;
+ }
+ }
+
+ [data-component="content"] {
+ }
+
+ [data-component="enterprise-content"] {
+ padding: 4rem 0;
+
+ @media (max-width: 60rem) {
+ padding: 2rem 0;
+ }
+ }
+
+ [data-component="enterprise-columns"] {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 4rem;
+ padding: 4rem 5rem;
+
+ @media (max-width: 80rem) {
+ gap: 3rem;
+ }
+
+ @media (max-width: 60rem) {
+ grid-template-columns: 1fr;
+ gap: 3rem;
+ padding: 2rem 1.5rem;
+ }
+ }
+
+ [data-component="enterprise-column-1"] {
+ h2 {
+ font-size: 1.5rem;
+ font-weight: 500;
+ color: var(--color-text-strong);
+ margin-bottom: 1rem;
+ }
+
+ h3 {
+ font-size: 1.25rem;
+ font-weight: 500;
+ color: var(--color-text-strong);
+ margin: 2rem 0 1rem 0;
+ }
+
+ p {
+ line-height: 1.6;
+ margin-bottom: 1.5rem;
+ color: var(--color-text);
+ }
+
+ ul {
+ list-style: none;
+ padding: 0;
+ margin-bottom: 2rem;
+
+ li {
+ margin-bottom: 0.75rem;
+ padding-left: 1.5rem;
+ position: relative;
+ line-height: 1.5;
+ color: var(--color-text);
+
+ &::before {
+ content: "•";
+ position: absolute;
+ left: 0;
+ color: var(--color-background-interactive);
+ font-weight: bold;
+ }
+
+ strong {
+ color: var(--color-text-strong);
+ font-weight: 500;
+ }
+ }
+ }
+ }
+
+ [data-component="enterprise-column-2"] {
+ [data-component="enterprise-form"] {
+ padding: 0;
+
+ h2 {
+ font-size: 1.5rem;
+ font-weight: 500;
+ color: var(--color-text-strong);
+ margin-bottom: 1.5rem;
+ }
+
+ [data-component="form-group"] {
+ margin-bottom: 1.5rem;
+
+ label {
+ display: block;
+ font-weight: 500;
+ color: var(--color-text-weak);
+ margin-bottom: 0.5rem;
+ font-size: 0.875rem;
+ }
+
+ input:-webkit-autofill,
+ input:-webkit-autofill:hover,
+ input:-webkit-autofill:focus,
+ input:-webkit-autofill:active {
+ transition: background-color 5000000s ease-in-out 0s;
+ }
+
+ input:-webkit-autofill {
+ -webkit-text-fill-color: var(--color-text-strong) !important;
+ }
+
+ input:-moz-autofill {
+ -moz-text-fill-color: var(--color-text-strong) !important;
+ }
+
+ input,
+ textarea {
+ width: 100%;
+ padding: 0.75rem;
+ border: 1px solid var(--color-border-weak);
+ border-radius: 4px;
+ background: var(--color-background-weak);
+ color: var(--color-text-strong);
+ font-family: inherit;
+
+ &::placeholder {
+ color: var(--color-text-weak);
+ }
+
+ &:focus {
+ background: var(--color-background-interactive-weaker);
+ outline: none;
+ border: none;
+ color: var(--color-text-strong);
+ border: 1px solid var(--color-background-strong);
+ box-shadow: 0 0 0 3px var(--color-background-interactive);
+
+ @media (prefers-color-scheme: dark) {
+ box-shadow: none;
+ border: 1px solid var(--color-background-interactive)
+ }
+ }
+ }
+
+ textarea {
+ resize: vertical;
+ min-height: 120px;
+ }
+ }
+
+ [data-component="submit-button"] {
+ padding: 0.5rem 1.5rem;
+ background: var(--color-background-strong);
+ color: var(--color-text-inverted);
+ border: none;
+ border-radius: 4px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: background-color 0.2s ease;
+
+ &:hover:not(:disabled) {
+ background: var(--color-background-strong-hover);
+ }
+
+ &:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+ }
+ }
+
+ [data-component="success-message"] {
+ margin-top: 1rem;
+ padding: 1rem;
+ background: var(--color-success);
+ color: white;
+ border-radius: 4px;
+ font-size: 0.875rem;
+ text-align: center;
+ }
+ }
+ }
+
+ [data-component="faq"] {
+ border-top: 1px solid var(--color-border-weak);
+ padding: 4rem 5rem;
+
+ @media (max-width: 60rem) {
+ padding: 2rem 1.5rem;
+ }
+
+ [data-slot="section-title"] {
+ margin-bottom: 24px;
+
+ h3 {
+ font-size: 16px;
+ font-weight: 500;
+ color: var(--color-text-strong);
+ margin-bottom: 12px;
+ }
+
+ p {
+ margin-bottom: 12px;
+ color: var(--color-text);
+ }
+ }
+
+ ul {
+ padding: 0;
+
+ li {
+ list-style: none;
+ margin-bottom: 24px;
+ line-height: 200%;
+
+ @media (max-width: 60rem) {
+ line-height: 180%;
+ }
+ }
+ }
+
+ [data-slot="faq-question"] {
+ display: flex;
+ gap: 16px;
+ margin-bottom: 8px;
+ color: var(--color-text-strong);
+ font-weight: 500;
+ cursor: pointer;
+ background: none;
+ border: none;
+ padding: 0;
+
+ [data-slot="faq-icon-plus"] {
+ flex-shrink: 0;
+ color: var(--color-text-weak);
+ margin-top: 2px;
+
+ [data-closed] & {
+ display: block;
+ }
+ [data-expanded] & {
+ display: none;
+ }
+ }
+ [data-slot="faq-icon-minus"] {
+ flex-shrink: 0;
+ color: var(--color-text-weak);
+ margin-top: 2px;
+
+ [data-closed] & {
+ display: none;
+ }
+ [data-expanded] & {
+ display: block;
+ }
+ }
+ [data-slot="faq-question-text"] {
+ flex-grow: 1;
+ text-align: left;
+ }
+ }
+
+ [data-slot="faq-answer"] {
+ margin-left: 40px;
+ margin-bottom: 32px;
+ color: var(--color-text);
+ }
+ }
+
+ [data-component="legal"] {
+ color: var(--color-text-weak);
+ text-align: center;
+ padding: 2rem 5rem;
+
+ @media (max-width: 60rem) {
+ padding: 2rem 1.5rem;
+ }
+
+ a {
+ color: var(--color-text-weak);
+ text-decoration: none;
+ }
+ }
+
+ a {
+ color: var(--color-text-strong);
+ text-decoration: underline;
+ text-underline-offset: 2px;
+ text-decoration-thickness: 1px;
+
+ &:hover {
+ text-decoration-thickness: 2px;
+ }
+ }
+}
diff --git a/packages/console/app/src/routes/enterprise/index.tsx b/packages/console/app/src/routes/enterprise/index.tsx
new file mode 100644
index 000000000..21c3db74e
--- /dev/null
+++ b/packages/console/app/src/routes/enterprise/index.tsx
@@ -0,0 +1,199 @@
+import "./index.css"
+import { Title, Meta } from "@solidjs/meta"
+import { createSignal } from "solid-js"
+import { Header } from "~/component/header"
+import { Footer } from "~/component/footer"
+import { Legal } from "~/component/legal"
+import { Faq } from "~/component/faq"
+
+export default function Enterprise() {
+ const [formData, setFormData] = createSignal({
+ name: "",
+ role: "",
+ email: "",
+ message: "",
+ })
+ const [isSubmitting, setIsSubmitting] = createSignal(false)
+ const [showSuccess, setShowSuccess] = createSignal(false)
+
+ const handleInputChange = (field: string) => (e: Event) => {
+ const target = e.target as HTMLInputElement | HTMLTextAreaElement
+ setFormData((prev) => ({ ...prev, [field]: target.value }))
+ }
+
+ const handleSubmit = async (e: Event) => {
+ e.preventDefault()
+ setIsSubmitting(true)
+
+ try {
+ const response = await fetch("/api/enterprise", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(formData()),
+ })
+
+ if (response.ok) {
+ setShowSuccess(true)
+ setFormData({
+ name: "",
+ role: "",
+ email: "",
+ message: "",
+ })
+ setTimeout(() => setShowSuccess(false), 5000)
+ }
+ } catch (error) {
+ console.error("Failed to submit form:", error)
+ } finally {
+ setIsSubmitting(false)
+ }
+ }
+
+ return (
+ <main data-page="enterprise">
+ <Title>OpenCode Enterprise | How can we help your organisation?</Title>
+ <Meta name="description" content="Contact OpenCode for enterprise solutions" />
+ <div data-component="container">
+ <Header />
+
+ <div data-component="content">
+ <section data-component="enterprise-content">
+ <div data-component="enterprise-columns">
+ <div data-component="enterprise-column-1">
+ <h2>Your code is yours</h2>
+ <p>
+ Run OpenCode securely inside your organization with no data or context stored, and
+ no licensing restrictions or ownership claims. Start a trial with your team today,
+ then scale with enterprise features like SSO, private registries, and
+ self-hosting.
+ </p>
+ </div>
+
+ <div data-component="enterprise-column-2">
+ <div data-component="enterprise-form">
+ <form onSubmit={handleSubmit}>
+ <div data-component="form-group">
+ <label for="name">Full name</label>
+ <input
+ id="name"
+ type="text"
+ required
+ value={formData().name}
+ onInput={handleInputChange("name")}
+ placeholder="Jeff Bezos"
+ />
+ </div>
+
+ <div data-component="form-group">
+ <label for="role">Role</label>
+ <input
+ id="role"
+ type="text"
+ required
+ value={formData().role}
+ onInput={handleInputChange("role")}
+ placeholder="Executive Chairman"
+ />
+ </div>
+
+ <div data-component="form-group">
+ <label for="email">Company email</label>
+ <input
+ id="email"
+ type="email"
+ required
+ value={formData().email}
+ onInput={handleInputChange("email")}
+ placeholder="[email protected]"
+ />
+ </div>
+
+ <div data-component="form-group">
+ <label for="message">What problem are you trying to solve?</label>
+ <textarea
+ id="message"
+ required
+ rows={5}
+ value={formData().message}
+ onInput={handleInputChange("message")}
+ placeholder="We need help with"
+ />
+ </div>
+
+ <button type="submit" disabled={isSubmitting()} data-component="submit-button">
+ {isSubmitting() ? "Sending..." : "Send"}
+ </button>
+ </form>
+
+ {showSuccess() && (
+ <div data-component="success-message">
+ Message successfully sent, we'll be in touch soon.
+ </div>
+ )}
+ </div>
+ </div>
+ </div>
+ </section>
+
+ <section data-component="faq">
+ <div data-slot="section-title">
+ <h3>FAQ</h3>
+ </div>
+ <ul>
+ <li>
+ <Faq question="Does Opencode store our code or context data?">
+ No. OpenCode never stores your code or context data. All
+ processing happens locally or directly with your AI provider.
+ </Faq>
+ </li>
+ <li>
+ <Faq question="Who owns the code generated with OpenCode?">
+ You do. All code produced is yours, with no licensing
+ restrictions or ownership claims.
+ </Faq>
+ </li>
+ <li>
+ <Faq
+ question="How can we trial OpenCode inside our organization?">
+ Simply install and run an internal trial with your team. Since
+ OpenCode doesn’t store any data, your developers can get
+ started right away.
+ </Faq>
+ </li>
+ <li>
+ <Faq
+ question="What happens if someone uses the `/share` feature?">
+ By default, sharing is disabled. If enabled, conversations are
+ sent to our share service and cached through our CDN. For
+ enterprise use, we recommend disabling or self-hosting this
+ feature.
+ </Faq>
+ </li>
+ <li>
+ <Faq question="Can OpenCode integrate with our company’s SSO?">
+ Yes. Enterprise deployments can include SSO integration so all
+ sessions and shared conversations are protected by your
+ authentication system.
+ </Faq>
+ </li>
+ <li>
+ <Faq question="Can OpenCode be self-hosted?">
+ Absolutely. You can fully self-host OpenCode, including the share feature, ensuring that data and pages are accessible only after authentication.
+ </Faq>
+ </li>
+ <li>
+ <Faq question="How do we get started with enterprise deployment?">
+ Contact us to discuss pricing, implementation, and enterprise options like SSO, private registries, and self-hosting.
+ </Faq>
+ </li>
+ </ul>
+ </section>
+ </div>
+ <Footer />
+ </div>
+ <Legal />
+ </main>
+ )
+}
diff --git a/packages/console/app/src/routes/index.css b/packages/console/app/src/routes/index.css
index 7171bd39d..b46958b80 100644
--- a/packages/console/app/src/routes/index.css
+++ b/packages/console/app/src/routes/index.css
@@ -827,12 +827,8 @@ body {
outline: none;
border: none;
color: var(--color-text-strong);
-
- border: 1px solid var(--color-background-strong); /* Tailwind blue-600 as example */
-
- /* Tailwind-style ring */
+ border: 1px solid var(--color-background-strong);
box-shadow: 0 0 0 3px var(--color-background-interactive);
- /* mimics "ring-2 ring-blue-600/50" */
@media (prefers-color-scheme: dark) {
box-shadow: none;