Selaa lähdekoodia

feat: small improvements

Aaron Iker 7 kuukautta sitten
vanhempi
sitoutus
95266fcb6b

+ 191 - 118
packages/console/app/src/routes/black.css

@@ -14,7 +14,7 @@
     left: 0;
     width: 100%;
     height: 288px;
-    background: linear-gradient(180deg, rgba(255, 255, 255, 0.16) 0%, rgba(0, 0, 0, 0) 100%);
+    background: linear-gradient(180deg, rgba(255, 255, 255, 0.1) 0%, rgba(0, 0, 0, 0) 100%);
   }
 
   [data-component="header"] {
@@ -24,9 +24,6 @@
     justify-content: center;
     padding-top: 40px;
     flex-shrink: 0;
-
-    /* [data-component="header-logo"] { */
-    /* } */
   }
 
   [data-component="content"] {
@@ -85,24 +82,39 @@
       }
 
       svg {
+        --hero-black-fill-from: hsl(0 0% 100%);
+        --hero-black-fill-to: hsl(0 0% 100% / 0%);
+        --hero-black-stroke-from: hsl(0 0% 100% / 60%);
+        --hero-black-stroke-to: hsl(0 0% 100% / 0%);
+
         width: 100%;
-        max-width: 540px;
+        max-width: 590px;
         height: auto;
         filter: drop-shadow(0 0 20px rgba(255, 255, 255, 0.1));
+        mask-image: linear-gradient(to bottom, black, transparent);
+        stroke-width: 1.5;
+
+        [data-slot="black-fill"] {
+          fill: url(#hero-black-fill-gradient);
+        }
+
+        [data-slot="black-stroke"] {
+          fill: url(#hero-black-stroke-gradient);
+        }
       }
     }
 
     [data-slot="cta"] {
       display: flex;
       flex-direction: column;
-      gap: 16px;
+      gap: 32px;
       align-items: center;
       text-align: center;
-      margin-top: -40px;
+      margin-top: -32px;
       width: 100%;
 
       @media (min-width: 768px) {
-        margin-top: -20px;
+        margin-top: -16px;
       }
 
       [data-slot="heading"] {
@@ -111,12 +123,13 @@
         font-size: 18px;
         font-style: normal;
         font-weight: 400;
-        line-height: 160%; /* 28.8px */
+        line-height: 160%;
 
         span {
           display: inline-block;
         }
       }
+
       [data-slot="subheading"] {
         color: rgba(255, 255, 255, 0.59);
         font-size: 15px;
@@ -129,6 +142,7 @@
           line-height: 160%;
         }
       }
+
       [data-slot="button"] {
         display: inline-flex;
         height: 40px;
@@ -140,7 +154,7 @@
         background: rgba(255, 255, 255, 0.92);
         text-decoration: none;
         color: #000;
-        font-family: "JetBrains Mono Nerd Font";
+        font-family: var(--font-mono);
         font-size: 16px;
         font-style: normal;
         font-weight: 500;
@@ -154,14 +168,16 @@
           transform: scale(0.98);
         }
       }
+
       [data-slot="back-soon"] {
         color: rgba(255, 255, 255, 0.59);
         text-align: center;
         font-size: 13px;
         font-style: normal;
         font-weight: 400;
-        line-height: 160%; /* 20.8px */
+        line-height: 160%;
       }
+
       [data-slot="follow-us"] {
         display: inline-flex;
         height: 40px;
@@ -172,7 +188,7 @@
         border-radius: 4px;
         border: 1px solid rgba(255, 255, 255, 0.17);
         color: rgba(255, 255, 255, 0.59);
-        font-family: "JetBrains Mono Nerd Font";
+        font-family: var(--font-mono);
         font-size: 14px;
         font-style: normal;
         font-weight: 400;
@@ -185,84 +201,107 @@
         flex-direction: column;
         gap: 16px;
         width: 100%;
-        max-width: 540px;
+        max-width: 680px;
         padding: 0 20px;
-
-        @media (min-width: 768px) {
-          padding: 0;
-        }
+        box-sizing: border-box;
       }
 
       [data-slot="pricing-card"] {
         display: flex;
         flex-direction: column;
-        gap: 12px;
-        padding: 20px;
+        align-items: flex-start;
         border: 1px solid rgba(255, 255, 255, 0.17);
-        border-radius: 4px;
+        border-radius: 5px;
         text-decoration: none;
-        transition: border-color 0.15s ease;
-        background: transparent;
-        cursor: pointer;
+        background: #000;
         text-align: left;
+        overflow: hidden;
+        width: 100%;
+        transition: border-color 200ms ease;
 
-        &:hover {
+        &:hover:not([data-selected="true"]) {
           border-color: rgba(255, 255, 255, 0.35);
         }
 
-        [data-slot="icon"] {
-          color: rgba(255, 255, 255, 0.59);
-        }
-
-        [data-slot="price"] {
+        [data-slot="card-trigger"] {
           display: flex;
-          flex-wrap: wrap;
-          align-items: baseline;
-          gap: 8px;
-        }
+          flex-direction: column;
+          align-items: flex-start;
+          width: 100%;
+          padding: 24px;
+          background: transparent;
+          border: none;
+          cursor: pointer;
+          font-family: inherit;
+          text-align: left;
+          transition: padding 200ms ease;
 
-        [data-slot="amount"] {
-          color: rgba(255, 255, 255, 0.92);
-          font-size: 24px;
-          font-weight: 500;
+          &:disabled {
+            cursor: default;
+          }
         }
 
-        [data-slot="period"] {
-          color: rgba(255, 255, 255, 0.59);
-          font-size: 14px;
+        &[data-selected="true"] {
+          [data-slot="amount"] {
+            font-size: 22px;
+          }
+
+          [data-slot="terms"] {
+            animation: reveal 500ms cubic-bezier(0.25, 0, 0.5, 1) forwards;
+          }
+
+          [data-slot="actions"] {
+            [data-slot="continue"] {
+              animation-delay: 200ms;
+            }
+          }
         }
 
-        [data-slot="multiplier"] {
-          color: rgba(255, 255, 255, 0.39);
-          font-size: 14px;
+        &[data-collapsed="true"] {
+          [data-slot="card-trigger"] {
+            padding: 16px 24px;
+          }
 
-          &::before {
-            content: "·";
-            margin-right: 8px;
+          [data-slot="plan-header"] {
+            flex-direction: row;
+          }
+
+          [data-slot="amount"] {
+            font-size: 18px;
+          }
+
+          [data-slot="period"],
+          [data-slot="multiplier"] {
+            font-size: 13px;
           }
         }
-      }
 
-      [data-slot="selected-plan"] {
-        display: flex;
-        flex-direction: column;
-        gap: 32px;
-        width: fit-content;
-        max-width: calc(100% - 40px);
-        margin: 0 auto;
-      }
+        &[data-selected="false"][data-collapsed="false"] {
+          [data-slot="plan-header"] {
+            gap: 12px;
+          }
 
-      [data-slot="selected-card"] {
-        display: flex;
-        flex-direction: column;
-        gap: 16px;
-        padding: 20px;
-        border: 1px solid rgba(255, 255, 255, 0.17);
-        border-radius: 4px;
-        width: fit-content;
+          [data-slot="amount"] {
+            font-size: 22px;
+          }
+
+          [data-slot="period"],
+          [data-slot="multiplier"] {
+            font-size: 14px;
+          }
+        }
 
-        [data-slot="icon"] {
+        [data-slot="plan-header"] {
+          display: flex;
+          flex-direction: column;
+          width: 100%;
+          gap: 12px;
+          transition: gap 200ms ease;
+        }
+
+        [data-slot="plan-icon"] {
           color: rgba(255, 255, 255, 0.59);
+          flex-shrink: 0;
         }
 
         [data-slot="price"] {
@@ -270,22 +309,30 @@
           flex-wrap: wrap;
           align-items: baseline;
           gap: 8px;
+          margin: 0;
         }
 
         [data-slot="amount"] {
           color: rgba(255, 255, 255, 0.92);
-          font-size: 24px;
           font-weight: 500;
         }
 
-        [data-slot="period"] {
+        [data-slot="content"] {
+          width: 100%;
+        }
+
+        [data-slot="period"],
+        [data-slot="multiplier"] {
+          color: rgba(255, 255, 255, 0.59);
+        }
+
+        [data-slot="billing"] {
           color: rgba(255, 255, 255, 0.59);
           font-size: 14px;
         }
 
         [data-slot="multiplier"] {
           color: rgba(255, 255, 255, 0.39);
-          font-size: 14px;
 
           &::before {
             content: "·";
@@ -295,27 +342,33 @@
 
         [data-slot="terms"] {
           list-style: none;
-          padding: 0;
+          padding: 0 24px 24px 24px;
           margin: 0;
           display: flex;
           flex-direction: column;
           gap: 12px;
           text-align: left;
+          width: 100%;
+          opacity: 0;
+          mask-image: linear-gradient(to bottom, black 0%, black 50%, transparent 100%);
+          mask-repeat: no-repeat;
+          mask-size: 100% 200%;
+          mask-position: 0% 320%;
+        }
 
-          li {
-            color: rgba(255, 255, 255, 0.59);
-            font-size: 13px;
-            line-height: 1.5;
-            padding-left: 16px;
-            position: relative;
-            white-space: nowrap;
-
-            &::before {
-              content: "▪";
-              position: absolute;
-              left: 0;
-              color: rgba(255, 255, 255, 0.39);
-            }
+        [data-slot="terms"] li {
+          color: rgba(255, 255, 255, 0.59);
+          font-size: 13px;
+          line-height: 1.3;
+          padding-left: 16px;
+          position: relative;
+          white-space: nowrap;
+
+          &::before {
+            content: "▪";
+            position: absolute;
+            left: 0;
+            color: rgba(255, 255, 255, 0.39);
           }
         }
 
@@ -323,41 +376,44 @@
           display: flex;
           gap: 16px;
           margin-top: 8px;
+          padding: 0 24px 24px 24px;
+          box-sizing: border-box;
+          width: 100%;
+        }
 
-          button,
-          a {
-            flex: 1;
-            display: inline-flex;
-            height: 48px;
-            padding: 0 16px;
-            justify-content: center;
-            align-items: center;
-            border-radius: 4px;
-            font-family: var(--font-mono);
-            font-size: 16px;
-            font-weight: 400;
-            text-decoration: none;
-            cursor: pointer;
-          }
+        [data-slot="actions"] button,
+        [data-slot="actions"] a {
+          flex: 1;
+          display: inline-flex;
+          height: 48px;
+          padding: 0 16px;
+          justify-content: center;
+          align-items: center;
+          border-radius: 4px;
+          font-family: var(--font-mono);
+          font-size: 16px;
+          font-weight: 400;
+          text-decoration: none;
+          cursor: pointer;
+        }
 
-          [data-slot="cancel"] {
-            background: transparent;
-            border: 1px solid rgba(255, 255, 255, 0.17);
-            color: rgba(255, 255, 255, 0.92);
+        [data-slot="cancel"] {
+          background: transparent;
+          border: 1px solid rgba(255, 255, 255, 0.17);
+          color: rgba(255, 255, 255, 0.92);
 
-            &:hover {
-              border-color: rgba(255, 255, 255, 0.35);
-            }
+          &:hover {
+            border-color: rgba(255, 255, 255, 0.35);
           }
+        }
 
-          [data-slot="continue"] {
-            background: rgba(255, 255, 255, 0.17);
-            border: 1px solid rgba(255, 255, 255, 0.17);
-            color: rgba(255, 255, 255, 0.59);
+        [data-slot="continue"] {
+          background: rgba(255, 255, 255, 0.17);
+          border: 1px solid rgba(255, 255, 255, 0.17);
+          color: rgba(255, 255, 255, 0.59);
 
-            &:hover {
-              background: rgba(255, 255, 255, 0.25);
-            }
+          &:hover {
+            background: rgba(255, 255, 255, 0.25);
           }
         }
       }
@@ -368,8 +424,7 @@
         font-size: 13px;
         font-style: normal;
         font-weight: 400;
-        line-height: 160%; /* 20.8px */
-        font-style: italic;
+        line-height: 160%;
 
         a {
           color: rgba(255, 255, 255, 0.39);
@@ -441,7 +496,7 @@
 
       [data-slot="multiplier"] {
         color: rgba(255, 255, 255, 0.39);
-        font-size: 14px;
+        font-size: 13px;
 
         &::before {
           content: "·";
@@ -610,7 +665,7 @@
       span,
       a {
         color: rgba(255, 255, 255, 0.39);
-        font-family: "JetBrains Mono Nerd Font";
+        font-family: var(--font-mono);
         font-size: 16px;
         font-style: normal;
         font-weight: 400;
@@ -620,7 +675,7 @@
 
       [data-slot="github-stars"] {
         color: rgba(255, 255, 255, 0.25);
-        font-family: "JetBrains Mono Nerd Font";
+        font-family: var(--font-mono);
         font-size: 16px;
         font-style: normal;
         font-weight: 400;
@@ -635,9 +690,10 @@
         }
       }
     }
+
     [data-slot="anomaly-alt"] {
       color: rgba(255, 255, 255, 0.39);
-      font-family: "JetBrains Mono Nerd Font";
+      font-family: var(--font-mono);
       font-size: 16px;
       font-style: normal;
       font-weight: 400;
@@ -647,7 +703,7 @@
 
       a {
         color: rgba(255, 255, 255, 0.39);
-        font-family: "JetBrains Mono Nerd Font";
+        font-family: "JetBrains Mono Nerd Font", monospace;
         font-size: 16px;
         font-style: normal;
         font-weight: 400;
@@ -661,3 +717,20 @@
     }
   }
 }
+
+::view-transition-group(plan-header),
+::view-transition-group(price),
+::view-transition-group(amount),
+::view-transition-group(period),
+::view-transition-group(multiplier),
+::view-transition-group(billing) {
+  animation-duration: 200ms;
+  animation-timing-function: cubic-bezier(0.25, 0, 0.5, 1);
+}
+
+@keyframes reveal {
+  100% {
+    mask-position: 0% 0%;
+    opacity: 1;
+  }
+}

+ 3 - 0
packages/console/app/src/routes/black/common.tsx

@@ -13,11 +13,13 @@ export function PlanIcon(props: { plan: string }) {
     <Switch>
       <Match when={props.plan === "20"}>
         <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+          <title>Black 20 plan</title>
           <rect x="4" y="4" width="16" height="16" rx="2" stroke="currentColor" stroke-width="1.5" />
         </svg>
       </Match>
       <Match when={props.plan === "100"}>
         <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+          <title>Black 100 plan</title>
           <rect x="3" y="3" width="7" height="7" rx="1" stroke="currentColor" stroke-width="1.5" />
           <rect x="14" y="3" width="7" height="7" rx="1" stroke="currentColor" stroke-width="1.5" />
           <rect x="3" y="14" width="7" height="7" rx="1" stroke="currentColor" stroke-width="1.5" />
@@ -26,6 +28,7 @@ export function PlanIcon(props: { plan: string }) {
       </Match>
       <Match when={props.plan === "200"}>
         <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+          <title>Black 200 plan</title>
           <rect x="2" y="2" width="4" height="4" rx="0.5" stroke="currentColor" stroke-width="1" />
           <rect x="10" y="2" width="4" height="4" rx="0.5" stroke="currentColor" stroke-width="1" />
           <rect x="18" y="2" width="4" height="4" rx="0.5" stroke="currentColor" stroke-width="1" />

+ 3 - 3
packages/console/app/src/routes/black/subscribe.tsx

@@ -4,7 +4,7 @@ import { createEffect, createSignal, For, onMount, Show } from "solid-js"
 import { loadStripe } from "@stripe/stripe-js"
 import { Elements, PaymentElement, useStripe, useElements } from "solid-stripe"
 import { config } from "~/config"
-import { PlanIcon, plans } from "./common"
+import { Plan, PlanIcon, plans } from "./common"
 import { getActor } from "~/context/auth"
 import { withActor } from "~/context/auth.withActor"
 import { Actor } from "@opencode-ai/console-core/actor.js"
@@ -41,7 +41,7 @@ const getWorkspaces = query(async () => {
   })
 }, "black.subscribe.workspaces")
 
-function CheckoutForm(props: { plan: string; amount: number }) {
+function CheckoutForm(props: { plan: Plan["id"]; amount: number }) {
   const stripe = useStripe()
   const elements = useElements()
   const [error, setError] = createSignal<string | null>(null)
@@ -93,7 +93,7 @@ export default function BlackSubscribe() {
   const [selectedWorkspace, setSelectedWorkspace] = createSignal<string | null>(null)
 
   const [params] = useSearchParams()
-  const plan = (params.plan as string) || "200"
+  const plan = (params.plan as Plan["id"]) || "200"
   const planData = plansMap[plan] || plansMap["200"]
 
   const [clientSecret, setClientSecret] = createSignal<string | null>(null)