1- import { RGBA , TextAttributes } from "@opentui/core"
1+ import { BoxRenderable , RGBA , TextAttributes } from "@opentui/core"
22import { useKeyboard } from "@opentui/solid"
33import open from "open"
4- import { createSignal } from "solid-js"
4+ import { createSignal , onCleanup , onMount } from "solid-js"
55import { selectedForeground , useTheme } from "@tui/context/theme"
66import { useDialog , type DialogContext } from "@tui/ui/dialog"
77import { Link } from "@tui/ui/link"
8+ import { GoLogo } from "./logo"
9+ import { BgPulse , type BgPulseMask } from "./bg-pulse"
810
911const GO_URL = "https://opencode.ai/go"
12+ const PAD_X = 3
13+ const PAD_TOP_OUTER = 1
1014
1115export type DialogGoUpsellProps = {
1216 onClose ?: ( dontShowAgain ?: boolean ) => void
@@ -27,62 +31,116 @@ export function DialogGoUpsell(props: DialogGoUpsellProps) {
2731 const dialog = useDialog ( )
2832 const { theme } = useTheme ( )
2933 const fg = selectedForeground ( theme )
30- const [ selected , setSelected ] = createSignal ( 0 )
34+ const [ selected , setSelected ] = createSignal < "dismiss" | "subscribe" > ( "subscribe" )
35+ const [ center , setCenter ] = createSignal < { x : number ; y : number } | undefined > ( )
36+ const [ masks , setMasks ] = createSignal < BgPulseMask [ ] > ( [ ] )
37+ let content : BoxRenderable | undefined
38+ let logoBox : BoxRenderable | undefined
39+ let headingBox : BoxRenderable | undefined
40+ let descBox : BoxRenderable | undefined
41+ let buttonsBox : BoxRenderable | undefined
42+
43+ const sync = ( ) => {
44+ if ( ! content || ! logoBox ) return
45+ setCenter ( {
46+ x : logoBox . x - content . x + logoBox . width / 2 ,
47+ y : logoBox . y - content . y + logoBox . height / 2 + PAD_TOP_OUTER ,
48+ } )
49+ const next : BgPulseMask [ ] = [ ]
50+ const baseY = PAD_TOP_OUTER
51+ for ( const b of [ headingBox , descBox , buttonsBox ] ) {
52+ if ( ! b ) continue
53+ next . push ( {
54+ x : b . x - content . x ,
55+ y : b . y - content . y + baseY ,
56+ width : b . width ,
57+ height : b . height ,
58+ pad : 2 ,
59+ strength : 0.78 ,
60+ } )
61+ }
62+ setMasks ( next )
63+ }
64+
65+ onMount ( ( ) => {
66+ sync ( )
67+ for ( const b of [ content , logoBox , headingBox , descBox , buttonsBox ] ) b ?. on ( "resize" , sync )
68+ } )
69+
70+ onCleanup ( ( ) => {
71+ for ( const b of [ content , logoBox , headingBox , descBox , buttonsBox ] ) b ?. off ( "resize" , sync )
72+ } )
3173
3274 useKeyboard ( ( evt ) => {
3375 if ( evt . name === "left" || evt . name === "right" || evt . name === "tab" ) {
34- setSelected ( ( s ) => ( s === 0 ? 1 : 0 ) )
76+ setSelected ( ( s ) => ( s === "subscribe" ? "dismiss" : "subscribe" ) )
3577 return
3678 }
37- if ( evt . name !== "return" ) return
38- if ( selected ( ) === 0 ) subscribe ( props , dialog )
39- else dismiss ( props , dialog )
79+ if ( evt . name === "return" ) {
80+ if ( selected ( ) === "subscribe" ) subscribe ( props , dialog )
81+ else dismiss ( props , dialog )
82+ }
4083 } )
4184
4285 return (
43- < box paddingLeft = { 2 } paddingRight = { 2 } gap = { 1 } >
44- < box flexDirection = "row" justifyContent = "space-between" >
45- < text attributes = { TextAttributes . BOLD } fg = { theme . text } >
46- Free limit reached
47- </ text >
48- < text fg = { theme . textMuted } onMouseUp = { ( ) => dialog . clear ( ) } >
49- esc
50- </ text >
51- </ box >
52- < box gap = { 1 } paddingBottom = { 1 } >
53- < text fg = { theme . textMuted } >
54- Subscribe to OpenCode Go to keep going with reliable access to the best open-source models, starting at
55- $5/month.
56- </ text >
57- < box flexDirection = "row" gap = { 1 } >
58- < Link href = { GO_URL } fg = { theme . primary } />
59- </ box >
86+ < box ref = { ( item : BoxRenderable ) => ( content = item ) } >
87+ < box position = "absolute" top = { - PAD_TOP_OUTER } left = { 0 } right = { 0 } bottom = { 0 } zIndex = { 0 } >
88+ < BgPulse centerX = { center ( ) ?. x } centerY = { center ( ) ?. y } masks = { masks ( ) } />
6089 </ box >
61- < box flexDirection = "row" justifyContent = "flex-end" gap = { 1 } paddingBottom = { 1 } >
62- < box
63- paddingLeft = { 3 }
64- paddingRight = { 3 }
65- backgroundColor = { selected ( ) === 0 ? theme . primary : RGBA . fromInts ( 0 , 0 , 0 , 0 ) }
66- onMouseOver = { ( ) => setSelected ( 0 ) }
67- onMouseUp = { ( ) => subscribe ( props , dialog ) }
68- >
69- < text fg = { selected ( ) === 0 ? fg : theme . text } attributes = { selected ( ) === 0 ? TextAttributes . BOLD : undefined } >
70- subscribe
90+ < box paddingLeft = { PAD_X } paddingRight = { PAD_X } paddingBottom = { 1 } gap = { 1 } >
91+ < box ref = { ( item : BoxRenderable ) => ( headingBox = item ) } flexDirection = "row" justifyContent = "space-between" >
92+ < text attributes = { TextAttributes . BOLD } fg = { theme . text } >
93+ Free limit reached
7194 </ text >
95+ < text fg = { theme . textMuted } onMouseUp = { ( ) => dialog . clear ( ) } >
96+ esc
97+ </ text >
98+ </ box >
99+ < box ref = { ( item : BoxRenderable ) => ( descBox = item ) } gap = { 0 } >
100+ < box flexDirection = "row" >
101+ < text fg = { theme . textMuted } > Subscribe to </ text >
102+ < text attributes = { TextAttributes . BOLD } fg = { theme . textMuted } >
103+ OpenCode Go
104+ </ text >
105+ < text fg = { theme . textMuted } > for reliable access to the</ text >
106+ </ box >
107+ < text fg = { theme . textMuted } > best open-source models, starting at $5/month.</ text >
72108 </ box >
73- < box
74- paddingLeft = { 3 }
75- paddingRight = { 3 }
76- backgroundColor = { selected ( ) === 1 ? theme . primary : RGBA . fromInts ( 0 , 0 , 0 , 0 ) }
77- onMouseOver = { ( ) => setSelected ( 1 ) }
78- onMouseUp = { ( ) => dismiss ( props , dialog ) }
79- >
80- < text
81- fg = { selected ( ) === 1 ? fg : theme . textMuted }
82- attributes = { selected ( ) === 1 ? TextAttributes . BOLD : undefined }
109+ < box alignItems = "center" gap = { 1 } paddingBottom = { 1 } >
110+ < box ref = { ( item : BoxRenderable ) => ( logoBox = item ) } >
111+ < GoLogo />
112+ </ box >
113+ < Link href = { GO_URL } fg = { theme . primary } />
114+ </ box >
115+ < box ref = { ( item : BoxRenderable ) => ( buttonsBox = item ) } flexDirection = "row" justifyContent = "space-between" >
116+ < box
117+ paddingLeft = { 2 }
118+ paddingRight = { 2 }
119+ backgroundColor = { selected ( ) === "dismiss" ? theme . primary : RGBA . fromInts ( 0 , 0 , 0 , 0 ) }
120+ onMouseOver = { ( ) => setSelected ( "dismiss" ) }
121+ onMouseUp = { ( ) => dismiss ( props , dialog ) }
83122 >
84- don't show again
85- </ text >
123+ < text
124+ fg = { selected ( ) === "dismiss" ? fg : theme . textMuted }
125+ attributes = { selected ( ) === "dismiss" ? TextAttributes . BOLD : undefined }
126+ >
127+ don't show again
128+ </ text >
129+ </ box >
130+ < box
131+ paddingLeft = { 2 }
132+ paddingRight = { 2 }
133+ backgroundColor = { selected ( ) === "subscribe" ? theme . primary : RGBA . fromInts ( 0 , 0 , 0 , 0 ) }
134+ onMouseOver = { ( ) => setSelected ( "subscribe" ) }
135+ onMouseUp = { ( ) => subscribe ( props , dialog ) }
136+ >
137+ < text
138+ fg = { selected ( ) === "subscribe" ? fg : theme . text }
139+ attributes = { selected ( ) === "subscribe" ? TextAttributes . BOLD : undefined }
140+ >
141+ subscribe
142+ </ text >
143+ </ box >
86144 </ box >
87145 </ box >
88146 </ box >
0 commit comments