docs: Add consent banner (#50302)

Adds a consent banner, similar to the one on zed.dev

Before you mark this PR as ready for review, make sure that you have:
- [x] Added a solid test coverage and/or screenshots from doing manual
testing
- [x] Done a self-review taking into account security and performance
aspects
- [x] Aligned any UI changes with the [UI
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)

Release Notes:

- N/A
This commit is contained in:
Gaauwe Rombouts 2026-03-04 15:38:31 +01:00 committed by GitHub
parent d5137d76c1
commit 5641ccf250
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 497 additions and 19 deletions

View file

@ -26,6 +26,7 @@ jobs:
CC: clang
CXX: clang++
DOCS_AMPLITUDE_API_KEY: ${{ secrets.DOCS_AMPLITUDE_API_KEY }}
DOCS_CONSENT_IO_INSTANCE: ${{ secrets.DOCS_CONSENT_IO_INSTANCE }}
- name: Deploy Docs
uses: cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65 # v3

View file

@ -578,6 +578,7 @@ fn handle_postprocessing() -> Result<()> {
.expect("Default title not a string")
.to_string();
let amplitude_key = std::env::var("DOCS_AMPLITUDE_API_KEY").unwrap_or_default();
let consent_io_instance = std::env::var("DOCS_CONSENT_IO_INSTANCE").unwrap_or_default();
output.insert("html".to_string(), zed_html);
mdbook::Renderer::render(&mdbook::renderer::HtmlHandlebars::new(), &ctx)?;
@ -647,6 +648,7 @@ fn handle_postprocessing() -> Result<()> {
zlog::trace!(logger => "Updating {:?}", pretty_path(&file, &root_dir));
let contents = contents.replace("#description#", meta_description);
let contents = contents.replace("#amplitude_key#", &amplitude_key);
let contents = contents.replace("#consent_io_instance#", &consent_io_instance);
let contents = title_regex()
.replace(&contents, |_: &regex::Captures| {
format!("<title>{}</title>", meta_title)

View file

@ -1,2 +1,5 @@
# Handlebars partials are not supported by Prettier.
*.hbs
# Automatically generated
theme/c15t@*.js

View file

@ -64,6 +64,22 @@ This will render a human-readable version of the action name, e.g., "zed: open s
Templates are functions that modify the source of the docs pages (usually with a regex match and replace).
You can see how the actions and keybindings are templated in `crates/docs_preprocessor/src/main.rs` for reference on how to create new templates.
## Consent Banner
We pre-bundle the `c15t` package because the docs pipeline does not include a JS bundler. If you need to update `c15t` and rebuild the bundle, use:
```
mkdir c15t-bundle && cd c15t-bundle
npm init -y
npm install c15t@<version> esbuild
echo "import { getOrCreateConsentRuntime } from 'c15t'; window.c15t = { getOrCreateConsentRuntime };" > entry.js
npx esbuild entry.js --bundle --format=iife --minify --outfile=c15t@<version>.js
cp c15t@<version>.js ../theme/c15t@<version>.js
cd .. && rm -rf c15t-bundle
```
Replace `<version>` with the new version of `c15t` you are installing. Then update `book.toml` to reference the new bundle filename.
### References
- Template Trait: `crates/docs_preprocessor/src/templates.rs`

View file

@ -23,8 +23,8 @@ default-description = "Learn how to use and customize Zed, the fast, collaborati
default-title = "Zed Code Editor Documentation"
no-section-label = true
preferred-dark-theme = "dark"
additional-css = ["theme/page-toc.css", "theme/plugins.css", "theme/highlight.css"]
additional-js = ["theme/page-toc.js", "theme/plugins.js"]
additional-css = ["theme/page-toc.css", "theme/plugins.css", "theme/highlight.css", "theme/consent-banner.css"]
additional-js = ["theme/page-toc.js", "theme/plugins.js", "theme/c15t@2.0.0-rc.3.js", "theme/analytics.js"]
[output.zed-html.print]
enable = false

93
docs/theme/analytics.js vendored Normal file
View file

@ -0,0 +1,93 @@
const amplitudeKey = document.querySelector(
'meta[name="amplitude-key"]',
)?.content;
const consentInstance = document.querySelector(
'meta[name="consent-io-instance"]',
)?.content;
document.addEventListener("DOMContentLoaded", () => {
if (!consentInstance || consentInstance.length === 0) return;
const { getOrCreateConsentRuntime } = window.c15t;
const { consentStore } = getOrCreateConsentRuntime({
mode: "c15t",
backendURL: consentInstance,
consentCategories: ["necessary", "measurement", "marketing"],
storageConfig: {
crossSubdomain: true,
},
scripts: [
{
id: "amplitude",
src: `https://cdn.amplitude.com/script/${amplitudeKey}.js`,
category: "measurement",
onLoad: () => {
window.amplitude.init(amplitudeKey, {
fetchRemoteConfig: true,
autocapture: true,
});
},
},
],
});
let previousActiveUI = consentStore.getState().activeUI;
const banner = document.getElementById("c15t-banner");
const configureSection = document.getElementById("c15t-configure-section");
const configureBtn = document.getElementById("c15t-configure-btn");
const measurementToggle = document.getElementById("c15t-toggle-measurement");
const marketingToggle = document.getElementById("c15t-toggle-marketing");
const toggleConfigureMode = () => {
const currentConsents = consentStore.getState().consents;
measurementToggle.checked = currentConsents
? (currentConsents.measurement ?? false)
: false;
marketingToggle.checked = currentConsents
? (currentConsents.marketing ?? false)
: false;
configureSection.style.display = "flex";
configureBtn.innerHTML = "Save";
configureBtn.className = "c15t-button secondary";
configureBtn.title = "";
};
consentStore.subscribe((state) => {
const hideBanner =
state.activeUI === "none" ||
(state.activeUI === "banner" && state.mode === "opt-out");
banner.style.display = hideBanner ? "none" : "block";
if (state.activeUI === "dialog" && previousActiveUI !== "dialog") {
toggleConfigureMode();
}
previousActiveUI = state.activeUI;
});
configureBtn.addEventListener("click", () => {
if (consentStore.getState().activeUI === "dialog") {
consentStore
.getState()
.setConsent("measurement", measurementToggle.checked);
consentStore.getState().setConsent("marketing", marketingToggle.checked);
consentStore.getState().saveConsents("custom");
} else {
consentStore.getState().setActiveUI("dialog");
}
});
document.getElementById("c15t-accept").addEventListener("click", () => {
consentStore.getState().saveConsents("all");
});
document.getElementById("c15t-decline").addEventListener("click", () => {
consentStore.getState().saveConsents("necessary");
});
document
.getElementById("c15t-manage-consent-btn")
.addEventListener("click", () => {
consentStore.getState().setActiveUI("dialog");
});
});

1
docs/theme/c15t@2.0.0-rc.3.js vendored Normal file

File diff suppressed because one or more lines are too long

292
docs/theme/consent-banner.css vendored Normal file
View file

@ -0,0 +1,292 @@
#c15t-banner {
--color-offgray-50: hsl(218, 12%, 95%);
--color-offgray-100: hsl(218, 12%, 88%);
--color-offgray-200: hsl(218, 12%, 80%);
--color-offgray-300: hsl(218, 12%, 75%);
--color-offgray-400: hsl(218, 12%, 64%);
--color-offgray-500: hsl(218, 12%, 56%);
--color-offgray-600: hsl(218, 12%, 48%);
--color-offgray-700: hsl(218, 12%, 40%);
--color-offgray-800: hsl(218, 12%, 34%);
--color-offgray-900: hsl(218, 12%, 24%);
--color-offgray-950: hsl(218, 12%, 15%);
--color-offgray-1000: hsl(218, 12%, 5%);
--color-blue-50: oklch(97% 0.014 254.604);
--color-blue-100: oklch(93.2% 0.032 255.585);
--color-blue-200: oklch(88.2% 0.059 254.128);
--color-blue-300: oklch(80.9% 0.105 251.813);
--color-blue-400: oklch(70.7% 0.165 254.624);
--color-blue-500: oklch(62.3% 0.214 259.815);
--color-blue-600: oklch(54.6% 0.245 262.881);
--color-blue-700: oklch(48.8% 0.243 264.376);
--color-blue-800: oklch(42.4% 0.199 265.638);
--color-blue-900: oklch(37.9% 0.146 265.522);
--color-blue-950: oklch(28.2% 0.091 267.935);
--color-accent-blue: hsla(218, 93%, 42%, 1);
position: fixed;
z-index: 9999;
bottom: 16px;
right: 16px;
border-radius: 4px;
max-width: 300px;
background: white;
border: 1px solid
color-mix(in oklab, var(--color-offgray-200) 50%, transparent);
box-shadow: 6px 6px 0
color-mix(in oklab, var(--color-accent-blue) 6%, transparent);
}
.dark #c15t-banner {
border-color: color-mix(in oklab, var(--color-offgray-600) 14%, transparent);
background: var(--color-offgray-1000);
box-shadow: 5px 5px 0
color-mix(in oklab, var(--color-accent-blue) 8%, transparent);
}
#c15t-banner > div:first-child {
padding: 12px;
display: flex;
flex-direction: column;
}
#c15t-banner a {
color: var(--links);
text-decoration: underline;
text-decoration-color: var(--link-line-decoration);
}
#c15t-banner a:hover {
text-decoration-color: var(--link-line-decoration-hover);
}
#c15t-description {
font-size: 12px;
margin: 0;
margin-top: 4px;
}
#c15t-configure-section {
display: flex;
flex-direction: column;
gap: 8px;
border-top: 1px solid var(--divider);
padding: 12px;
}
#c15t-configure-section > div {
display: flex;
align-items: center;
justify-content: space-between;
}
#c15t-configure-section label {
text-transform: uppercase;
font-size: 11px;
}
#c15t-footer {
padding: 12px;
display: flex;
justify-content: space-between;
border-top: 1px solid var(--divider);
background-color: color-mix(
in oklab,
var(--color-offgray-50) 50%,
transparent
);
}
.dark #c15t-footer {
background-color: color-mix(
in oklab,
var(--color-offgray-600) 4%,
transparent
);
}
.c15t-button {
display: inline-flex;
align-items: center;
justify-content: center;
max-height: 28px;
color: black;
padding: 4px 8px;
font-size: 14px;
border-radius: 4px;
background: transparent;
border: 1px solid transparent;
transition: 100ms;
transition-property: box-shadow, border-color, background-color;
}
.c15t-button:hover {
background: color-mix(in oklab, var(--color-offgray-100) 50%, transparent);
}
.dark .c15t-button {
color: var(--color-offgray-50);
}
.dark .c15t-button:hover {
background: color-mix(in oklab, var(--color-offgray-500) 10%, transparent);
}
.c15t-button.icon {
padding: 0;
width: 24px;
height: 24px;
}
.c15t-button.primary {
color: var(--color-blue-700);
background: color-mix(in oklab, var(--color-blue-50) 60%, transparent);
border-color: color-mix(in oklab, var(--color-blue-500) 20%, transparent);
box-shadow: color-mix(in oklab, var(--color-blue-400) 10%, transparent) 0 -2px
0 0 inset;
}
.c15t-button.primary:hover {
background: color-mix(in oklab, var(--color-blue-100) 50%, transparent);
box-shadow: none;
}
.dark .c15t-button.primary {
color: var(--color-blue-50);
background: color-mix(in oklab, var(--color-blue-500) 10%, transparent);
border-color: color-mix(in oklab, var(--color-blue-300) 10%, transparent);
box-shadow: color-mix(in oklab, var(--color-blue-300) 8%, transparent) 0 -2px
0 0 inset;
}
.dark .c15t-button.primary:hover {
background: color-mix(in oklab, var(--color-blue-500) 20%, transparent);
box-shadow: none;
}
.c15t-button.secondary {
background: color-mix(in oklab, var(--color-offgray-50) 60%, transparent);
border-color: color-mix(in oklab, var(--color-offgray-200) 50%, transparent);
box-shadow: color-mix(in oklab, var(--color-offgray-500) 10%, transparent)
0 -2px 0 0 inset;
}
.c15t-button.secondary:hover {
background: color-mix(in oklab, var(--color-offgray-100) 50%, transparent);
box-shadow: none;
}
.dark .c15t-button.secondary {
background: color-mix(in oklab, var(--color-offgray-300) 5%, transparent);
border-color: color-mix(in oklab, var(--color-offgray-400) 20%, transparent);
box-shadow: color-mix(in oklab, var(--color-offgray-300) 8%, transparent)
0 -2px 0 0 inset;
}
.dark .c15t-button.secondary:hover {
background: color-mix(in oklab, var(--color-offgray-200) 10%, transparent);
box-shadow: none;
}
.c15t-switch {
position: relative;
display: inline-block;
width: 32px;
height: 20px;
flex-shrink: 0;
}
.c15t-switch input {
opacity: 0;
width: 0;
height: 0;
position: absolute;
}
.c15t-slider {
position: absolute;
cursor: pointer;
inset: 0;
background-color: color-mix(
in oklab,
var(--color-offgray-100) 80%,
transparent
);
border-radius: 20px;
box-shadow: inset 0 0 0 1px color-mix(in oklab, #000 5%, transparent);
transition: background-color 0.2s;
}
.c15t-slider:hover {
background-color: var(--color-offgray-100);
}
.dark .c15t-slider {
background-color: color-mix(in oklab, #fff 5%, transparent);
box-shadow: inset 0 0 0 1px color-mix(in oklab, #fff 15%, transparent);
}
.dark .c15t-slider:hover {
background-color: color-mix(in oklab, #fff 10%, transparent);
}
.c15t-slider:before {
position: absolute;
content: "";
height: 14px;
width: 14px;
left: 3px;
bottom: 3px;
background-color: white;
border-radius: 50%;
box-shadow:
0 1px 3px 0 rgb(0 0 0 / 0.1),
0 1px 2px -1px rgb(0 0 0 / 0.1);
transition: transform 0.2s;
}
.c15t-switch input:checked + .c15t-slider {
background-color: var(--color-accent-blue);
box-shadow: inset 0 0 0 1px color-mix(in oklab, #000 5%, transparent);
}
.c15t-switch input:checked + .c15t-slider:hover {
background-color: var(--color-accent-blue);
}
.dark .c15t-switch input:checked + .c15t-slider {
background-color: var(--color-accent-blue);
box-shadow: inset 0 0 0 1px color-mix(in oklab, #fff 15%, transparent);
}
.c15t-switch input:checked + .c15t-slider:before {
transform: translateX(12px);
}
.c15t-switch input:disabled + .c15t-slider {
opacity: 0.5;
cursor: default;
pointer-events: none;
}
.c15t-switch input:disabled + .c15t-slider:hover {
background-color: color-mix(
in oklab,
var(--color-offgray-100) 80%,
transparent
);
}
#c15t-manage-consent-btn {
appearance: none;
background: none;
border: none;
padding: 0;
cursor: pointer;
}
#c15t-manage-consent-btn:hover {
text-decoration-color: var(--link-line-decoration-hover);
}

102
docs/theme/index.hbs vendored
View file

@ -70,6 +70,8 @@
<!-- MathJax -->
<script async src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
{{/if}}
<meta name="amplitude-key" content="#amplitude_key#" />
<meta name="consent-io-instance" content="#consent_io_instance#" />
</head>
<body class="no-js">
<div id="body-container">
@ -343,6 +345,13 @@
href="https://zed.dev/blog"
>Blog</a
>
<span class="footer-separator">•</span>
<button
id="c15t-manage-consent-btn"
class="footer-link"
>
Manage Site Cookies
</button>
</footer>
</main>
<div class="toc-container">
@ -444,23 +453,82 @@
{{/if}}
{{/if}}
<!-- Amplitude Analytics -->
<script>
(function() {
var amplitudeKey = '#amplitude_key#';
if (amplitudeKey && amplitudeKey.indexOf('#') === -1) {
var script = document.createElement('script');
script.src = 'https://cdn.amplitude.com/script/' + amplitudeKey + '.js';
script.onload = function() {
window.amplitude.init(amplitudeKey, {
fetchRemoteConfig: true,
autocapture: true
});
};
document.head.appendChild(script);
}
})();
</script>
<!-- c15t Consent Banner -->
<div id="c15t-banner" style="display: none;">
<div>
<p id="c15t-description">
Zed uses cookies to improve your experience and for marketing. Read <a href="https://zed.dev/cookie-policy">our cookie policy</a> for more details.
</p>
</div>
<div id="c15t-configure-section" style="display: none">
<div>
<label for="c15t-toggle-necessary"
>Strictly Necessary</label
>
<label class="c15t-switch">
<input
type="checkbox"
id="c15t-toggle-necessary"
checked
disabled
/>
<span class="c15t-slider"></span>
</label>
</div>
<div>
<label for="c15t-toggle-measurement">Analytics</label>
<label class="c15t-switch">
<input
type="checkbox"
id="c15t-toggle-measurement"
/>
<span class="c15t-slider"></span>
</label>
</div>
<div>
<label for="c15t-toggle-marketing">Marketing</label>
<label class="c15t-switch">
<input
type="checkbox"
id="c15t-toggle-marketing"
/>
<span class="c15t-slider"></span>
</label>
</div>
</div>
<div id="c15t-footer">
<button
id="c15t-configure-btn"
class="c15t-button icon"
title="Configure"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M20 7h-9" />
<path d="M14 17H5" />
<circle cx="17" cy="17" r="3" />
<circle cx="7" cy="7" r="3" />
</svg>
</button>
<div>
<button id="c15t-decline" class="c15t-button">
Reject all
</button>
<button id="c15t-accept" class="c15t-button primary">
Accept all
</button>
</div>
</div>
</div>
</div>
</body>
</html>

View file

@ -42,6 +42,8 @@ extend-exclude = [
"crates/gpui_windows/src/window.rs",
# Some typos in the base mdBook CSS.
"docs/theme/css/",
# Automatically generated JS.
"docs/theme/c15t@*.js",
# Spellcheck triggers on `|Fixe[sd]|` regex part.
"script/danger/dangerfile.ts",
# Eval examples for prompts and criteria