From 978776b5dc25890bac60d66ff03176326b51b1fb Mon Sep 17 00:00:00 2001 From: Eric Feng Date: Thu, 29 Jan 2026 14:18:39 -0800 Subject: [PATCH 01/11] adding pricing calculator --- info/pricing.mdx | 6 +++++ snippets/calculator.jsx | 55 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 snippets/calculator.jsx diff --git a/info/pricing.mdx b/info/pricing.mdx index 68817603..a3c551ff 100644 --- a/info/pricing.mdx +++ b/info/pricing.mdx @@ -53,3 +53,9 @@ With Browser Pools, you pay the standard usage-based price per GB-second while b | Managed auth profiles | N/A | 3 | 20 | Custom | > Note: Limits are org-wide by default unless stated otherwise. `Managed auth profiles` refer to the number of active auth profiles that Kernel maintains using your stored [Credentials](/auth/credentials) or [1Password connection](/integrations/1password). + +## Pricing calculator + +import { PricingCalculator } from '/snippets/calculator.jsx'; + + diff --git a/snippets/calculator.jsx b/snippets/calculator.jsx new file mode 100644 index 00000000..fb5e2bbc --- /dev/null +++ b/snippets/calculator.jsx @@ -0,0 +1,55 @@ +export const PricingCalculator = () => { + const [plan, setPlan] = useState('free'); + const [headless, setHeadless] = useState(true); + const [avgSessionLength, setAvgSessionLength] = useState(30); + const [numSessions, setNumSessions] = useState(100); + + const calculatePrice = () => { + let price = 0; + if (plan === 'free') { + price = 0; + } else if (plan === 'hobbyist') { + price = 30; + } + if (plan === 'startup') { + price = 200; + } + let usageCost = 0 + if (headless) { + usageCost += 0.0000166667 * 1 * numSessions * avgSessionLength; + } else { + usageCost += 0.0000166667 * 8 * numSessions * avgSessionLength; + } + if (plan === 'free') { + if (usageCost > 5) { + price += usageCost - 5; + } + } else if (plan === 'hobbyist' && price < 30) { + if (usageCost > 10) { + price += usageCost - 10; + } + } else if (plan === 'startup') { + if (usageCost > 50) { + price += usageCost - 50; + } + } + return price; + }; + return ( +
+ + + setAvgSessionLength(e.target.value)} /> + setNumSessions(e.target.value)} /> + +

Price: {price}

+
+ ); +}; \ No newline at end of file From 4b79ff4af8b4a5b5faab36f791b6b168bc91747e Mon Sep 17 00:00:00 2001 From: Eric Feng Date: Thu, 29 Jan 2026 14:21:49 -0800 Subject: [PATCH 02/11] fix --- snippets/calculator.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/snippets/calculator.jsx b/snippets/calculator.jsx index fb5e2bbc..08699c0a 100644 --- a/snippets/calculator.jsx +++ b/snippets/calculator.jsx @@ -1,4 +1,5 @@ export const PricingCalculator = () => { + const [price, setPrice] = useState(0); const [plan, setPlan] = useState('free'); const [headless, setHeadless] = useState(true); const [avgSessionLength, setAvgSessionLength] = useState(30); @@ -33,7 +34,7 @@ export const PricingCalculator = () => { price += usageCost - 50; } } - return price; + setPrice(price); }; return (
From d7d1ec8a53c398a803108f84380644ed3f42ffa4 Mon Sep 17 00:00:00 2001 From: Eric Feng Date: Thu, 29 Jan 2026 14:34:35 -0800 Subject: [PATCH 03/11] working calc --- snippets/calculator.jsx | 106 +++++++++++++++++++++------------------- 1 file changed, 57 insertions(+), 49 deletions(-) diff --git a/snippets/calculator.jsx b/snippets/calculator.jsx index 08699c0a..f37ff953 100644 --- a/snippets/calculator.jsx +++ b/snippets/calculator.jsx @@ -1,56 +1,64 @@ export const PricingCalculator = () => { - const [price, setPrice] = useState(0); const [plan, setPlan] = useState('free'); const [headless, setHeadless] = useState(true); const [avgSessionLength, setAvgSessionLength] = useState(30); const [numSessions, setNumSessions] = useState(100); - const calculatePrice = () => { - let price = 0; - if (plan === 'free') { - price = 0; - } else if (plan === 'hobbyist') { - price = 30; - } - if (plan === 'startup') { - price = 200; - } - let usageCost = 0 - if (headless) { - usageCost += 0.0000166667 * 1 * numSessions * avgSessionLength; - } else { - usageCost += 0.0000166667 * 8 * numSessions * avgSessionLength; - } - if (plan === 'free') { - if (usageCost > 5) { - price += usageCost - 5; - } - } else if (plan === 'hobbyist' && price < 30) { - if (usageCost > 10) { - price += usageCost - 10; - } - } else if (plan === 'startup') { - if (usageCost > 50) { - price += usageCost - 50; - } - } - setPrice(price); - }; - return ( -
- - - setAvgSessionLength(e.target.value)} /> - setNumSessions(e.target.value)} /> - -

Price: {price}

-
- ); + console.log('re-render'); + + let price = 0; + if (plan === 'free') { + price = 0; + } else if (plan === 'hobbyist') { + price = 30; + } + if (plan === 'startup') { + price = 200; + } + let usageCost = 0 + if (headless) { + usageCost += 0.0000166667 * 1 * numSessions * avgSessionLength; + } else { + usageCost += 0.0000166667 * 8 * numSessions * avgSessionLength; + } + + let includedUsageCredits = 5; + if (plan === 'hobbyist') { + includedUsageCredits = 10; + } else if (plan === 'startup') { + includedUsageCredits = 50; + } + if (usageCost > includedUsageCredits) { + price += usageCost - includedUsageCredits; + } + return ( +
+
+ + +
+
+ + +
+
+ + setAvgSessionLength(parseInt(e.target.value))} /> +
+
+ + setNumSessions(parseInt(e.target.value))} /> +
+
Usage cost: ${usageCost.toFixed(6)}
+
Included usage credits: ${includedUsageCredits.toFixed(6)}
+
Price: ${price.toFixed(2)}
+
+ ); }; \ No newline at end of file From 2226aa74036e9830904a4996198d16ed0fa712c4 Mon Sep 17 00:00:00 2001 From: Eric Feng Date: Thu, 29 Jan 2026 15:00:37 -0800 Subject: [PATCH 04/11] prettier --- snippets/calculator.jsx | 74 +++++++++++++++++++++++++---------------- 1 file changed, 46 insertions(+), 28 deletions(-) diff --git a/snippets/calculator.jsx b/snippets/calculator.jsx index f37ff953..812b4202 100644 --- a/snippets/calculator.jsx +++ b/snippets/calculator.jsx @@ -1,8 +1,13 @@ +import { useState, useEffect, useRef } from 'react'; +import { Card, Columns } from '@mintlify/components'; + export const PricingCalculator = () => { const [plan, setPlan] = useState('free'); const [headless, setHeadless] = useState(true); const [avgSessionLength, setAvgSessionLength] = useState(30); const [numSessions, setNumSessions] = useState(100); + const [flash, setFlash] = useState(false); + const prevPriceRef = useRef(null); console.log('re-render'); @@ -31,34 +36,47 @@ export const PricingCalculator = () => { if (usageCost > includedUsageCredits) { price += usageCost - includedUsageCredits; } + useEffect(() => { + const prev = prevPriceRef.current; + if (prev !== null && (prev.usageCost !== usageCost || prev.includedUsageCredits !== includedUsageCredits || prev.price !== price)) { + setFlash(true); + const t = setTimeout(() => setFlash(false), 150); + return () => clearTimeout(t); + } + prevPriceRef.current = { usageCost, includedUsageCredits, price }; + }, [usageCost, includedUsageCredits, price]); + const labelStyle = { fontWeight: 600, fontSize: '0.875rem', color: '#374151', minWidth: '10rem', flexShrink: 0 }; + const rowStyle = { display: 'flex', alignItems: 'center', gap: '0.5rem', minHeight: '2.25rem' }; + const inputStyle = { minWidth: 0, flex: 1, maxWidth: '100%', boxSizing: 'border-box' }; return ( -
-
- - -
-
- - -
-
- - setAvgSessionLength(parseInt(e.target.value))} /> -
-
- - setNumSessions(parseInt(e.target.value))} /> -
-
Usage cost: ${usageCost.toFixed(6)}
-
Included usage credits: ${includedUsageCredits.toFixed(6)}
-
Price: ${price.toFixed(2)}
-
+ + +
+ + +
+
+ + setAvgSessionLength(parseInt(e.target.value))} /> +
+
+ + setNumSessions(parseInt(e.target.value))} /> +
+
+ + +
+
+ +
Usage cost: ${usageCost.toFixed(6)}
+
Included usage credits: ${includedUsageCredits.toFixed(6)}
+
Price: ${price.toFixed(2)}
+
+
); }; \ No newline at end of file From 6154ce5f8567d7a46319810bf347c5b948036636 Mon Sep 17 00:00:00 2001 From: Eric Feng Date: Thu, 29 Jan 2026 15:59:49 -0800 Subject: [PATCH 05/11] feedback --- snippets/calculator.jsx | 42 +++++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/snippets/calculator.jsx b/snippets/calculator.jsx index 812b4202..c57c389b 100644 --- a/snippets/calculator.jsx +++ b/snippets/calculator.jsx @@ -2,6 +2,12 @@ import { useState, useEffect, useRef } from 'react'; import { Card, Columns } from '@mintlify/components'; export const PricingCalculator = () => { + const planPrices = { + free: 0, + hobbyist: 30, + startup: 200, + } + const [plan, setPlan] = useState('free'); const [headless, setHeadless] = useState(true); const [avgSessionLength, setAvgSessionLength] = useState(30); @@ -9,17 +15,7 @@ export const PricingCalculator = () => { const [flash, setFlash] = useState(false); const prevPriceRef = useRef(null); - console.log('re-render'); - - let price = 0; - if (plan === 'free') { - price = 0; - } else if (plan === 'hobbyist') { - price = 30; - } - if (plan === 'startup') { - price = 200; - } + let price = planPrices[plan]; let usageCost = 0 if (headless) { usageCost += 0.0000166667 * 1 * numSessions * avgSessionLength; @@ -34,7 +30,7 @@ export const PricingCalculator = () => { includedUsageCredits = 50; } if (usageCost > includedUsageCredits) { - price += usageCost - includedUsageCredits; + price += Math.max(0, usageCost - includedUsageCredits); } useEffect(() => { const prev = prevPriceRef.current; @@ -45,22 +41,31 @@ export const PricingCalculator = () => { } prevPriceRef.current = { usageCost, includedUsageCredits, price }; }, [usageCost, includedUsageCredits, price]); - const labelStyle = { fontWeight: 600, fontSize: '0.875rem', color: '#374151', minWidth: '10rem', flexShrink: 0 }; + const labelStyle = { fontWeight: 600, fontSize: '0.875rem', color: '#374151', minWidth: '10rem', flexShrink: 0, maxWidth: '10rem' }; const rowStyle = { display: 'flex', alignItems: 'center', gap: '0.5rem', minHeight: '2.25rem' }; const inputStyle = { minWidth: 0, flex: 1, maxWidth: '100%', boxSizing: 'border-box' }; + const selectStyle = { + ...inputStyle, + appearance: 'none', + backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%23374151'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'/%3E%3C/svg%3E")`, + backgroundRepeat: 'no-repeat', + backgroundPosition: 'right 0.5rem center', + backgroundSize: '0.75rem', + paddingRight: '1.5rem', + }; return (
- setPlan(e.target.value)}>
- + setAvgSessionLength(parseInt(e.target.value))} />
@@ -73,9 +78,10 @@ export const PricingCalculator = () => {
-
Usage cost: ${usageCost.toFixed(6)}
-
Included usage credits: ${includedUsageCredits.toFixed(6)}
-
Price: ${price.toFixed(2)}
+
Base plan: ${planPrices[plan].toFixed(2)}
+
Usage: ${usageCost.toFixed(2)}
+
Total: ${price.toFixed(2)}
+
(${includedUsageCredits.toFixed(0)} credits included)
); From 04285da914bac542a86fc25467ef256f9407cdd1 Mon Sep 17 00:00:00 2001 From: Eric Feng Date: Thu, 29 Jan 2026 16:05:42 -0800 Subject: [PATCH 06/11] more feedback --- snippets/calculator.jsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/snippets/calculator.jsx b/snippets/calculator.jsx index c57c389b..a2843e84 100644 --- a/snippets/calculator.jsx +++ b/snippets/calculator.jsx @@ -7,6 +7,7 @@ export const PricingCalculator = () => { hobbyist: 30, startup: 200, } + const usagePrices = 0.0000166667 const [plan, setPlan] = useState('free'); const [headless, setHeadless] = useState(true); @@ -18,9 +19,9 @@ export const PricingCalculator = () => { let price = planPrices[plan]; let usageCost = 0 if (headless) { - usageCost += 0.0000166667 * 1 * numSessions * avgSessionLength; + usageCost += usagePrices * 1 * numSessions * avgSessionLength; } else { - usageCost += 0.0000166667 * 8 * numSessions * avgSessionLength; + usageCost += usagePrices * 8 * numSessions * avgSessionLength; } let includedUsageCredits = 5; @@ -76,6 +77,7 @@ export const PricingCalculator = () => {
+
${headless ? usagePrices.toFixed(6) : (usagePrices * 8).toFixed(6)}/second
Base plan: ${planPrices[plan].toFixed(2)}
From 83d6439618f36ebed7a070651af7dd5b587bd00b Mon Sep 17 00:00:00 2001 From: Eric Feng Date: Thu, 29 Jan 2026 16:17:17 -0800 Subject: [PATCH 07/11] feedback --- snippets/calculator.jsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/snippets/calculator.jsx b/snippets/calculator.jsx index a2843e84..d7a147d3 100644 --- a/snippets/calculator.jsx +++ b/snippets/calculator.jsx @@ -45,6 +45,9 @@ export const PricingCalculator = () => { const labelStyle = { fontWeight: 600, fontSize: '0.875rem', color: '#374151', minWidth: '10rem', flexShrink: 0, maxWidth: '10rem' }; const rowStyle = { display: 'flex', alignItems: 'center', gap: '0.5rem', minHeight: '2.25rem' }; const inputStyle = { minWidth: 0, flex: 1, maxWidth: '100%', boxSizing: 'border-box' }; + const numberInputStyle = { + borderBottom: '1px solid #7c3aed', textAlign: 'right' + }; const selectStyle = { ...inputStyle, appearance: 'none', @@ -67,11 +70,11 @@ export const PricingCalculator = () => {
- setAvgSessionLength(parseInt(e.target.value))} /> + setAvgSessionLength(parseInt(e.target.value))} />
- setNumSessions(parseInt(e.target.value))} /> + setNumSessions(parseInt(e.target.value))} />
From 47b20f8d796113263f178c24d9ea6d661281133b Mon Sep 17 00:00:00 2001 From: Eric Feng Date: Thu, 29 Jan 2026 19:22:02 -0800 Subject: [PATCH 08/11] dark mode fixes --- snippets/calculator.jsx | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/snippets/calculator.jsx b/snippets/calculator.jsx index d7a147d3..dd635ca5 100644 --- a/snippets/calculator.jsx +++ b/snippets/calculator.jsx @@ -14,7 +14,21 @@ export const PricingCalculator = () => { const [avgSessionLength, setAvgSessionLength] = useState(30); const [numSessions, setNumSessions] = useState(100); const [flash, setFlash] = useState(false); + const getIsDarkMode = () => typeof document !== 'undefined' && document.documentElement.classList.contains('dark'); + const [isDarkMode, setIsDarkMode] = useState(false); const prevPriceRef = useRef(null); + useEffect(() => { + setIsDarkMode(getIsDarkMode()); + const observer = new MutationObserver(() => setIsDarkMode(getIsDarkMode())); + observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] }); + const m = window.matchMedia('(prefers-color-scheme: dark)'); + const onSchemeChange = () => setIsDarkMode(getIsDarkMode()); + m.addEventListener('change', onSchemeChange); + return () => { + observer.disconnect(); + m.removeEventListener('change', onSchemeChange); + }; + }, []); let price = planPrices[plan]; let usageCost = 0 @@ -42,7 +56,7 @@ export const PricingCalculator = () => { } prevPriceRef.current = { usageCost, includedUsageCredits, price }; }, [usageCost, includedUsageCredits, price]); - const labelStyle = { fontWeight: 600, fontSize: '0.875rem', color: '#374151', minWidth: '10rem', flexShrink: 0, maxWidth: '10rem' }; + const labelStyle = { fontWeight: 600, fontSize: '0.875rem', minWidth: '10rem', flexShrink: 0, maxWidth: '10rem' }; const rowStyle = { display: 'flex', alignItems: 'center', gap: '0.5rem', minHeight: '2.25rem' }; const inputStyle = { minWidth: 0, flex: 1, maxWidth: '100%', boxSizing: 'border-box' }; const numberInputStyle = { @@ -77,16 +91,16 @@ export const PricingCalculator = () => { setNumSessions(parseInt(e.target.value))} />
- - + +
-
${headless ? usagePrices.toFixed(6) : (usagePrices * 8).toFixed(6)}/second
+
${headless ? usagePrices.toFixed(8) : (usagePrices * 8).toFixed(8)}/second
Base plan: ${planPrices[plan].toFixed(2)}
-
Usage: ${usageCost.toFixed(2)}
-
Total: ${price.toFixed(2)}
-
(${includedUsageCredits.toFixed(0)} credits included)
+
Usage: +${usageCost.toFixed(2)}
+
Free credits: -${includedUsageCredits.toFixed(2)}
+
Total cost: ${price.toFixed(2)}
); From 5a2daa5cec2f38e62a45e5d758a7226da1a7c886 Mon Sep 17 00:00:00 2001 From: Eric Feng Date: Thu, 29 Jan 2026 19:25:33 -0800 Subject: [PATCH 09/11] using css to handle dark mode buttons --- snippets/calculator.jsx | 18 ++---------------- style.css | 8 ++++++++ 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/snippets/calculator.jsx b/snippets/calculator.jsx index dd635ca5..deb58250 100644 --- a/snippets/calculator.jsx +++ b/snippets/calculator.jsx @@ -14,21 +14,7 @@ export const PricingCalculator = () => { const [avgSessionLength, setAvgSessionLength] = useState(30); const [numSessions, setNumSessions] = useState(100); const [flash, setFlash] = useState(false); - const getIsDarkMode = () => typeof document !== 'undefined' && document.documentElement.classList.contains('dark'); - const [isDarkMode, setIsDarkMode] = useState(false); const prevPriceRef = useRef(null); - useEffect(() => { - setIsDarkMode(getIsDarkMode()); - const observer = new MutationObserver(() => setIsDarkMode(getIsDarkMode())); - observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] }); - const m = window.matchMedia('(prefers-color-scheme: dark)'); - const onSchemeChange = () => setIsDarkMode(getIsDarkMode()); - m.addEventListener('change', onSchemeChange); - return () => { - observer.disconnect(); - m.removeEventListener('change', onSchemeChange); - }; - }, []); let price = planPrices[plan]; let usageCost = 0 @@ -91,8 +77,8 @@ export const PricingCalculator = () => { setNumSessions(parseInt(e.target.value))} />
- - + +
${headless ? usagePrices.toFixed(8) : (usagePrices * 8).toFixed(8)}/second
diff --git a/style.css b/style.css index c14456ed..830535d6 100644 --- a/style.css +++ b/style.css @@ -1,3 +1,11 @@ +/* pricing calculator button selected state */ +:root { + --btn-selected-bg: rgb(172, 134, 249); +} +:is(.dark) { + --btn-selected-bg: rgb(124, 58, 237); +} + /* style sidebar titles */ #navigation-items .sidebar-group-header #sidebar-title{ font-weight: bold; From 97a70d018486374e0ac080af4b4419b97e8b9d1f Mon Sep 17 00:00:00 2001 From: Eric Feng Date: Thu, 29 Jan 2026 19:31:27 -0800 Subject: [PATCH 10/11] lightening the button color --- style.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/style.css b/style.css index 830535d6..c9cc1ffb 100644 --- a/style.css +++ b/style.css @@ -1,6 +1,6 @@ /* pricing calculator button selected state */ :root { - --btn-selected-bg: rgb(172, 134, 249); + --btn-selected-bg: rgb(224 209 254); } :is(.dark) { --btn-selected-bg: rgb(124, 58, 237); From 41932563eb5fd3ef1e86916501080572b97ce61d Mon Sep 17 00:00:00 2001 From: Eric Feng Date: Thu, 29 Jan 2026 19:36:56 -0800 Subject: [PATCH 11/11] feeback --- snippets/calculator.jsx | 8 ++++---- style.css | 4 +++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/snippets/calculator.jsx b/snippets/calculator.jsx index deb58250..14e15a38 100644 --- a/snippets/calculator.jsx +++ b/snippets/calculator.jsx @@ -37,14 +37,14 @@ export const PricingCalculator = () => { const prev = prevPriceRef.current; if (prev !== null && (prev.usageCost !== usageCost || prev.includedUsageCredits !== includedUsageCredits || prev.price !== price)) { setFlash(true); - const t = setTimeout(() => setFlash(false), 150); + const t = setTimeout(() => setFlash(false), 300); return () => clearTimeout(t); } prevPriceRef.current = { usageCost, includedUsageCredits, price }; }, [usageCost, includedUsageCredits, price]); const labelStyle = { fontWeight: 600, fontSize: '0.875rem', minWidth: '10rem', flexShrink: 0, maxWidth: '10rem' }; const rowStyle = { display: 'flex', alignItems: 'center', gap: '0.5rem', minHeight: '2.25rem' }; - const inputStyle = { minWidth: 0, flex: 1, maxWidth: '100%', boxSizing: 'border-box' }; + const inputStyle = { minWidth: 0, flex: 1, maxWidth: '100%', boxSizing: 'border-box', background: 'transparent' }; const numberInputStyle = { borderBottom: '1px solid #7c3aed', textAlign: 'right' }; @@ -77,8 +77,8 @@ export const PricingCalculator = () => { setNumSessions(parseInt(e.target.value))} />
- - + +
${headless ? usagePrices.toFixed(8) : (usagePrices * 8).toFixed(8)}/second
diff --git a/style.css b/style.css index c9cc1ffb..ad733458 100644 --- a/style.css +++ b/style.css @@ -1,9 +1,11 @@ -/* pricing calculator button selected state */ +/* pricing calculator button styling */ :root { --btn-selected-bg: rgb(224 209 254); + --btn-border: #000; } :is(.dark) { --btn-selected-bg: rgb(124, 58, 237); + --btn-border: #fff; } /* style sidebar titles */