mirror of
				https://github.com/Foundry376/Mailspring.git
				synced 2025-10-25 21:46:31 +08:00 
			
		
		
		
	feat(usage): add new feature usage modal
Summary: This adds the "You've reached max features" modal in N1. http://g.recordit.co/9O7R0mLlXE.gif Test Plan: 1. Pull latest nylas/cloud-core and start Billing site: ``` cd cloud-core vagrant up vagrant ssh cd /vagrant bin/setup-up-feature-usage bin/launch ``` 2. Blow away ~/.nylas-mail (err backup your old one first) 3. Restart N1 4. Before logging in, edit `~/.nylas-mail/config.json` - set env to "local" - remove `thread-snooze` from the list of `disabledPlugins` 5. `cd /nylas-mail/src/k2` and run `npm start` 6. Restart N1 and create accounts & log in Reviewers: khamidou, juan, halla Reviewed By: halla Differential Revision: https://phab.nylas.com/D3846
This commit is contained in:
		
							parent
							
								
									fbce62d97b
								
							
						
					
					
						commit
						7aefb73ef8
					
				
					 12 changed files with 304 additions and 66 deletions
				
			
		|  | @ -1,59 +1,8 @@ | ||||||
| import React from 'react'; | import React from 'react'; | ||||||
| import {Actions, IdentityStore} from 'nylas-exports'; | import {Actions, IdentityStore} from 'nylas-exports'; | ||||||
| import {RetinaImg} from 'nylas-component-kit'; | import {OpenIdentityPageButton, RetinaImg} from 'nylas-component-kit'; | ||||||
| import {shell} from 'electron'; | import {shell} from 'electron'; | ||||||
| 
 | 
 | ||||||
| class OpenIdentityPageButton extends React.Component { |  | ||||||
|   static propTypes = { |  | ||||||
|     path: React.PropTypes.string, |  | ||||||
|     label: React.PropTypes.string, |  | ||||||
|     source: React.PropTypes.string, |  | ||||||
|     campaign: React.PropTypes.string, |  | ||||||
|     img: React.PropTypes.string, |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   constructor(props) { |  | ||||||
|     super(props); |  | ||||||
|     this.state = { |  | ||||||
|       loading: false, |  | ||||||
|     }; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   _onClick = () => { |  | ||||||
|     this.setState({loading: true}); |  | ||||||
|     IdentityStore.fetchSingleSignOnURL(this.props.path, { |  | ||||||
|       source: this.props.source, |  | ||||||
|       campaign: this.props.campaign, |  | ||||||
|       content: this.props.label, |  | ||||||
|     }).then((url) => { |  | ||||||
|       this.setState({loading: false}); |  | ||||||
|       shell.openExternal(url); |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   render() { |  | ||||||
|     if (this.state.loading) { |  | ||||||
|       return ( |  | ||||||
|         <div className="btn btn-disabled"> |  | ||||||
|           <RetinaImg name="sending-spinner.gif" width={15} height={15} mode={RetinaImg.Mode.ContentPreserve} /> |  | ||||||
|            {this.props.label}… |  | ||||||
|         </div> |  | ||||||
|       ); |  | ||||||
|     } |  | ||||||
|     if (this.props.img) { |  | ||||||
|       return ( |  | ||||||
|         <div className="btn" onClick={this._onClick}> |  | ||||||
|           <RetinaImg name={this.props.img} mode={RetinaImg.Mode.ContentPreserve} /> |  | ||||||
|             {this.props.label} |  | ||||||
|         </div> |  | ||||||
|       ) |  | ||||||
|     } |  | ||||||
|     return ( |  | ||||||
|       <div className="btn" onClick={this._onClick}>{this.props.label}</div> |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| class PreferencesIdentity extends React.Component { | class PreferencesIdentity extends React.Component { | ||||||
|   static displayName = 'PreferencesIdentity'; |   static displayName = 'PreferencesIdentity'; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										
											BIN
										
									
								
								internal_packages/thread-snooze/assets/ic-snooze-modal@2x.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								internal_packages/thread-snooze/assets/ic-snooze-modal@2x.png
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 8.5 KiB | 
|  | @ -1,7 +1,7 @@ | ||||||
| import {remote} from 'electron' |  | ||||||
| import _ from 'underscore'; | import _ from 'underscore'; | ||||||
| import {FeatureUsageStore, Actions, NylasAPIHelpers, AccountStore, | import {React, FeatureUsageStore, Actions, NylasAPIHelpers, AccountStore, | ||||||
|   DatabaseStore, Message, CategoryStore} from 'nylas-exports'; |   DatabaseStore, Message, CategoryStore} from 'nylas-exports'; | ||||||
|  | import {FeatureUsedUpModal} from 'nylas-component-kit' | ||||||
| import SnoozeUtils from './snooze-utils' | import SnoozeUtils from './snooze-utils' | ||||||
| import {PLUGIN_ID, PLUGIN_NAME} from './snooze-constants'; | import {PLUGIN_ID, PLUGIN_NAME} from './snooze-constants'; | ||||||
| import SnoozeActions from './snooze-actions'; | import SnoozeActions from './snooze-actions'; | ||||||
|  | @ -70,12 +70,45 @@ class SnoozeStore { | ||||||
| 
 | 
 | ||||||
|   onSnoozeThreads = (threads, snoozeDate, label) => { |   onSnoozeThreads = (threads, snoozeDate, label) => { | ||||||
|     if (!FeatureUsageStore.isUsable("snooze")) { |     if (!FeatureUsageStore.isUsable("snooze")) { | ||||||
|       remote.dialog.showMessageBox({ |       const featureData = FeatureUsageStore.featureData("snooze"); | ||||||
|         title: 'Out of snoozes', | 
 | ||||||
|         detail: `You have used your monthly quota of Snoozes`, |       let headerText = ""; | ||||||
|         buttons: ['OK'], |       let rechargeText = "" | ||||||
|         type: 'info', |       if (!featureData.quota) { | ||||||
|       }); |         headerText = "Snooze not yet enabled"; | ||||||
|  |         rechargeText = "Upgrade to Pro to start Snoozing" | ||||||
|  |       } else { | ||||||
|  |         headerText = "All Snoozes used"; | ||||||
|  |         let time = "later"; | ||||||
|  |         if (featureData.period === "hourly") { | ||||||
|  |           time = "next hour" | ||||||
|  |         } else if (featureData.period === "daily") { | ||||||
|  |           time = "tomorrow" | ||||||
|  |         } else if (featureData.period === "weekly") { | ||||||
|  |           time = "next week" | ||||||
|  |         } else if (featureData.period === "monthly") { | ||||||
|  |           time = "next month" | ||||||
|  |         } else if (featureData.period === "yearly") { | ||||||
|  |           time = "next year" | ||||||
|  |         } else if (featureData.period === "unlimited") { | ||||||
|  |           time = "if you upgrade to Pro" | ||||||
|  |         } | ||||||
|  |         rechargeText = `You’ll have ${featureData.quota} more snoozes ${time}` | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       Actions.openModal({ | ||||||
|  |         component: ( | ||||||
|  |           <FeatureUsedUpModal | ||||||
|  |             modalClass="snooze" | ||||||
|  |             featureName="Snooze" | ||||||
|  |             headerText={headerText} | ||||||
|  |             iconUrl="nylas://thread-snooze/assets/ic-snooze-modal@2x.png" | ||||||
|  |             rechargeText={rechargeText} | ||||||
|  |           /> | ||||||
|  |         ), | ||||||
|  |         height: 575, | ||||||
|  |         width: 412, | ||||||
|  |       }) | ||||||
|       return Promise.resolve() |       return Promise.resolve() | ||||||
|     } |     } | ||||||
|     this.recordSnoozeEvent(threads, snoozeDate, label) |     this.recordSnoozeEvent(threads, snoozeDate, label) | ||||||
|  | @ -0,0 +1,20 @@ | ||||||
|  | @import "ui-variables"; | ||||||
|  | 
 | ||||||
|  | .feature-usage-modal.snooze { | ||||||
|  |   @snooze-color: #8e6ce3; | ||||||
|  |   .feature-header { | ||||||
|  |     @from: @snooze-color; | ||||||
|  |     @to: lighten(@snooze-color, 10%); | ||||||
|  |     background: linear-gradient(to top, @from, @to); | ||||||
|  |   } | ||||||
|  |   .feature-name { | ||||||
|  |     color: @snooze-color; | ||||||
|  |   } | ||||||
|  |   .pro-description { | ||||||
|  |     li { | ||||||
|  |       &:before { | ||||||
|  |         color: @snooze-color; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										2
									
								
								src/K2
									
										
									
									
									
								
							
							
						
						
									
										2
									
								
								src/K2
									
										
									
									
									
								
							|  | @ -1 +1 @@ | ||||||
| Subproject commit 13750c7ba3f5abb4f083ab4a8eaf0f5ab14fffce | Subproject commit 32c820d2e50607f9381392bf825b5076f5accd8f | ||||||
							
								
								
									
										50
									
								
								src/components/feature-used-up-modal.jsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								src/components/feature-used-up-modal.jsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,50 @@ | ||||||
|  | import React from 'react' | ||||||
|  | import {shell} from 'electron' | ||||||
|  | import RetinaImg from './retina-img' | ||||||
|  | import OpenIdentityPageButton from './open-identity-page-button' | ||||||
|  | 
 | ||||||
|  | export default function FeatureUsedUpModal(props = {}) { | ||||||
|  |   const gotoFeatures = () => shell.openExternal("https://nylas.com/nylas-pro"); | ||||||
|  |   return ( | ||||||
|  |     <div className={`feature-usage-modal ${props.modalClass}`}> | ||||||
|  |       <div className="feature-header"> | ||||||
|  |         <div className="icon"> | ||||||
|  |           <RetinaImg | ||||||
|  |             url={props.iconUrl} | ||||||
|  |             style={{position: "relative", top: "-2px"}} | ||||||
|  |             mode={RetinaImg.Mode.ContentPreserve} | ||||||
|  |           /> | ||||||
|  |         </div> | ||||||
|  |         <h2 className="header-text">{props.headerText}</h2> | ||||||
|  |         <p className="recharge-text">{props.rechargeText}</p> | ||||||
|  |       </div> | ||||||
|  |       <div className="feature-cta"> | ||||||
|  |         <h2>Want to <span className="feature-name">{props.featureName} more</span>?</h2> | ||||||
|  |         <div className="pro-description"> | ||||||
|  |           <h3>Nylas Pro includes:</h3> | ||||||
|  |           <ul> | ||||||
|  |             <li>Unlimited Snoozing</li> | ||||||
|  |             <li>Unlimited Reminders</li> | ||||||
|  |             <li>Unlimited Mail Merge</li> | ||||||
|  |           </ul> | ||||||
|  |           <p>… plus <a onClick={gotoFeatures}>dozens of other features</a></p> | ||||||
|  |         </div> | ||||||
|  | 
 | ||||||
|  |         <OpenIdentityPageButton | ||||||
|  |           label="Upgrade" | ||||||
|  |           path="/dashboard?upgrade_to_pro=true" | ||||||
|  |           source={`${props.featureName}-Limit-Modal`} | ||||||
|  |           campaign="Limit-Modals" | ||||||
|  |           isCTA | ||||||
|  |         /> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   ) | ||||||
|  | } | ||||||
|  | FeatureUsedUpModal.propTypes = { | ||||||
|  |   modalClass: React.PropTypes.string.isRequired, | ||||||
|  |   featureName: React.PropTypes.string.isRequired, | ||||||
|  |   headerText: React.PropTypes.string.isRequired, | ||||||
|  |   rechargeText: React.PropTypes.string.isRequired, | ||||||
|  |   iconUrl: React.PropTypes.string.isRequired, | ||||||
|  | } | ||||||
|  | @ -45,22 +45,25 @@ class Modal extends React.Component { | ||||||
| 
 | 
 | ||||||
|   _computeModalStyles = (height, width) => { |   _computeModalStyles = (height, width) => { | ||||||
|     const modalStyle = { |     const modalStyle = { | ||||||
|       top: "50%", |  | ||||||
|       left: "50%", |  | ||||||
|       margin: `-${height / 2}px 0 0 -${width / 2}px`, |  | ||||||
|       height: height, |       height: height, | ||||||
|  |       maxHeight: "95%", | ||||||
|       width: width, |       width: width, | ||||||
|  |       maxWidth: "95%", | ||||||
|  |       overflow: "auto", | ||||||
|       position: "absolute", |       position: "absolute", | ||||||
|       backgroundColor: "white", |       backgroundColor: "white", | ||||||
|       boxShadow: "0 10px 20px rgba(0,0,0,0.19), inset 0 0 1px rgba(0,0,0,0.5)", |       boxShadow: "0 10px 20px rgba(0,0,0,0.19), inset 0 0 1px rgba(0,0,0,0.5)", | ||||||
|       borderRadius: "5px", |       borderRadius: "5px", | ||||||
|     }; |     }; | ||||||
|     const containerStyle = { |     const containerStyle = { | ||||||
|  |       display: "flex", | ||||||
|  |       alignItems: "center", | ||||||
|  |       justifyContent: "center", | ||||||
|       height: "100%", |       height: "100%", | ||||||
|       width: "100%", |       width: "100%", | ||||||
|       zIndex: 1000, |       zIndex: 1000, | ||||||
|       position: "absolute", |       position: "absolute", | ||||||
|       backgroundColor: "transparent", |       backgroundColor: "rgba(255,255,255,0.58)", | ||||||
|     }; |     }; | ||||||
|     return {containerStyle, modalStyle}; |     return {containerStyle, modalStyle}; | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
							
								
								
									
										62
									
								
								src/components/open-identity-page-button.jsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								src/components/open-identity-page-button.jsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,62 @@ | ||||||
|  | import React from 'react'; | ||||||
|  | import {shell} from 'electron'; | ||||||
|  | import classnames from 'classnames' | ||||||
|  | import RetinaImg from './retina-img' | ||||||
|  | import IdentityStore from '../flux/stores/identity-store'; | ||||||
|  | 
 | ||||||
|  | export default class OpenIdentityPageButton extends React.Component { | ||||||
|  |   static propTypes = { | ||||||
|  |     path: React.PropTypes.string, | ||||||
|  |     label: React.PropTypes.string, | ||||||
|  |     source: React.PropTypes.string, | ||||||
|  |     campaign: React.PropTypes.string, | ||||||
|  |     img: React.PropTypes.string, | ||||||
|  |     isCTA: React.PropTypes.bool, | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   constructor(props) { | ||||||
|  |     super(props); | ||||||
|  |     this.state = { | ||||||
|  |       loading: false, | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   _onClick = () => { | ||||||
|  |     this.setState({loading: true}); | ||||||
|  |     IdentityStore.fetchSingleSignOnURL(this.props.path, { | ||||||
|  |       source: this.props.source, | ||||||
|  |       campaign: this.props.campaign, | ||||||
|  |       content: this.props.label, | ||||||
|  |     }).then((url) => { | ||||||
|  |       this.setState({loading: false}); | ||||||
|  |       shell.openExternal(url); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   render() { | ||||||
|  |     if (this.state.loading) { | ||||||
|  |       return ( | ||||||
|  |         <div className="btn btn-disabled"> | ||||||
|  |           <RetinaImg name="sending-spinner.gif" width={15} height={15} mode={RetinaImg.Mode.ContentPreserve} /> | ||||||
|  |            {this.props.label}… | ||||||
|  |         </div> | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |     if (this.props.img) { | ||||||
|  |       return ( | ||||||
|  |         <div className="btn" onClick={this._onClick}> | ||||||
|  |           <RetinaImg name={this.props.img} mode={RetinaImg.Mode.ContentPreserve} /> | ||||||
|  |             {this.props.label} | ||||||
|  |         </div> | ||||||
|  |       ) | ||||||
|  |     } | ||||||
|  |     const cls = classnames({ | ||||||
|  |       "btn": true, | ||||||
|  |       "btn-emphasis": this.props.isCTA, | ||||||
|  |     }) | ||||||
|  |     return ( | ||||||
|  |       <div className={cls} onClick={this._onClick}>{this.props.label}</div> | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | @ -11,6 +11,45 @@ import SendFeatureUsageEventTask from '../tasks/send-feature-usage-event-task' | ||||||
|  * The billing site is responsible for returning with the Identity object |  * The billing site is responsible for returning with the Identity object | ||||||
|  * a usage hash that includes all supported features, their quotas for the |  * a usage hash that includes all supported features, their quotas for the | ||||||
|  * user, and the current usage of that user. We keep a cache locally |  * user, and the current usage of that user. We keep a cache locally | ||||||
|  |  * | ||||||
|  |  * The Identity object (aka Nylas ID or N1User) has a field called | ||||||
|  |  * `feature_usage`. The schema for `feature_usage` is computed dynamically | ||||||
|  |  * in `compute_feature_usage` here: | ||||||
|  |  * https://github.com/nylas/cloud-core/blob/master/redwood/models/n1.py#L175-207 | ||||||
|  |  * | ||||||
|  |  * The schema of each feature is determined by the `FeatureUsage` model in | ||||||
|  |  * redwood here: | ||||||
|  |  * https://github.com/nylas/cloud-core/blob/master/redwood/models/feature_usage.py#L14-32 | ||||||
|  |  * | ||||||
|  |  * The final schema looks like (Feb 7, 2017): | ||||||
|  |  * | ||||||
|  |  * NylasID = { | ||||||
|  |  *   ... | ||||||
|  |  *   "feature_usage": { | ||||||
|  |  *     "snooze": { | ||||||
|  |  *       "quota": 15, | ||||||
|  |  *       "period": "monthly", | ||||||
|  |  *       "used_in_period": 10, | ||||||
|  |  *       "feature_limit_name": "snooze-experiment-A", | ||||||
|  |  *     }, | ||||||
|  |  *     "send-later": { | ||||||
|  |  *       "quota": 99999, | ||||||
|  |  *       "period": "unlimited", | ||||||
|  |  *       "used_in_period": 228, | ||||||
|  |  *       "feature_limit_name": "send-later-unlimited-A", | ||||||
|  |  *     }, | ||||||
|  |  *     "reminders": { | ||||||
|  |  *       "quota": 10, | ||||||
|  |  *       "period": "daily", | ||||||
|  |  *       "used_in_period": 10, | ||||||
|  |  *       "feature_limit_name": null, | ||||||
|  |  *     }, | ||||||
|  |  *   }, | ||||||
|  |  *   ... | ||||||
|  |  * } | ||||||
|  |  * | ||||||
|  |  * Valid periods are: | ||||||
|  |  * 'hourly', 'daily', 'weekly', 'monthly', 'yearly', 'unlimited' | ||||||
|  */ |  */ | ||||||
| class FeatureUsageStore extends NylasStore { | class FeatureUsageStore extends NylasStore { | ||||||
|   activate() { |   activate() { | ||||||
|  | @ -23,10 +62,19 @@ class FeatureUsageStore extends NylasStore { | ||||||
|     }) |     }) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   featureData(feature) { | ||||||
|  |     const usage = this._featureUsage() | ||||||
|  |     if (!usage[feature]) { | ||||||
|  |       NylasEnv.reportError(new Error(`${feature} isn't supported`)); | ||||||
|  |       return {} | ||||||
|  |     } | ||||||
|  |     return usage[feature] | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   isUsable(feature) { |   isUsable(feature) { | ||||||
|     const usage = this._featureUsage() |     const usage = this._featureUsage() | ||||||
|     if (!usage[feature]) { |     if (!usage[feature]) { | ||||||
|       NylasEnv.reportError(`${feature} isn't supported`); |       NylasEnv.reportError(new Error(`${feature} isn't supported`)); | ||||||
|       return false |       return false | ||||||
|     } |     } | ||||||
|     return usage[feature].used_in_period < usage[feature].quota |     return usage[feature].used_in_period < usage[feature].quota | ||||||
|  |  | ||||||
|  | @ -46,6 +46,8 @@ class NylasComponentKit | ||||||
|   @load "FixedPopover", 'fixed-popover' |   @load "FixedPopover", 'fixed-popover' | ||||||
|   @require "DatePickerPopover", 'date-picker-popover' |   @require "DatePickerPopover", 'date-picker-popover' | ||||||
|   @load "Modal", 'modal' |   @load "Modal", 'modal' | ||||||
|  |   @load "FeatureUsedUpModal", 'feature-used-up-modal' | ||||||
|  |   @load "OpenIdentityPageButton", 'open-identity-page-button' | ||||||
|   @load "Flexbox", 'flexbox' |   @load "Flexbox", 'flexbox' | ||||||
|   @load "RetinaImg", 'retina-img' |   @load "RetinaImg", 'retina-img' | ||||||
|   @load "SwipeContainer", 'swipe-container' |   @load "SwipeContainer", 'swipe-container' | ||||||
|  |  | ||||||
							
								
								
									
										70
									
								
								static/components/feature-used-up-modal.less
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								static/components/feature-used-up-modal.less
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,70 @@ | ||||||
|  | @import "ui-variables"; | ||||||
|  | 
 | ||||||
|  | .feature-usage-modal { | ||||||
|  |   position: absolute; | ||||||
|  |   top: 0; | ||||||
|  |   width: 100%; | ||||||
|  |   height: 100%; | ||||||
|  | 
 | ||||||
|  |   .feature-header { | ||||||
|  |     text-align: center; | ||||||
|  |     padding-top: 32px; | ||||||
|  |     padding-bottom: 30px; | ||||||
|  |     color: @white; | ||||||
|  |     border-radius: 5px 5px 0 0; | ||||||
|  |   } | ||||||
|  |   .header-text { | ||||||
|  |     color: @white; | ||||||
|  |     margin-top: 24px; | ||||||
|  |     margin-bottom: 11px; | ||||||
|  |   } | ||||||
|  |   .recharge-text { | ||||||
|  |     margin: 0; | ||||||
|  |     opacity: 0.67; | ||||||
|  |   } | ||||||
|  |   .feature-cta { | ||||||
|  |     text-align: center; | ||||||
|  |     padding-bottom: 26px; | ||||||
|  |     h2 { | ||||||
|  |       margin-top: 26px; | ||||||
|  |       margin-bottom: 26px; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   .pro-description { | ||||||
|  |     width: 275px; | ||||||
|  |     margin: 0 auto; | ||||||
|  |     padding-bottom: 28px; | ||||||
|  |     border-top: 1px solid rgba(0,0,0,0.1); | ||||||
|  |     border-bottom: 1px solid rgba(0,0,0,0.1); | ||||||
|  | 
 | ||||||
|  |     ul { | ||||||
|  |       color: rgba(51, 51, 51, 0.67); | ||||||
|  |       text-align: left; | ||||||
|  |       list-style: none; | ||||||
|  |       line-height: 24px; | ||||||
|  |       margin-bottom: 0; | ||||||
|  |     } | ||||||
|  |     li { | ||||||
|  |       &:before { | ||||||
|  |         content: '✓'; | ||||||
|  |         margin-right: 11px; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     h3 { | ||||||
|  |       margin-top: 23px; | ||||||
|  |       margin-bottom: 18px; | ||||||
|  |     } | ||||||
|  |     p { | ||||||
|  |       color: rgba(51, 51, 51, 0.67); | ||||||
|  |       margin: 0; | ||||||
|  |       text-align: left; | ||||||
|  |       padding-left: 62px; | ||||||
|  |       line-height: 24px; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   .btn { | ||||||
|  |     margin-top: 21px; | ||||||
|  |     padding: 0 33px 25px 33px; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | @ -47,3 +47,4 @@ | ||||||
| @import "components/attachment-items"; | @import "components/attachment-items"; | ||||||
| @import "components/search-bar"; | @import "components/search-bar"; | ||||||
| @import "components/code-snippet"; | @import "components/code-snippet"; | ||||||
|  | @import "components/feature-used-up-modal"; | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		
		Reference in a new issue