diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 24aa44bb5..1fb02a9d0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,8 +4,7 @@ This file is automatically synced from docs/pages/contribute/contributing.mdx du ⚠️ DO NOT EDIT THIS FILE DIRECTLY. Instead, make changes to docs/pages/contribute/contributing.mdx and make this be synced just by running the build command. -Both files contain the same content: the docs file is there to render the content on the website, -while this copy is needed to make it highlighted in the repository dashboard. +Both files contain the same content: the docs file is there to render the content on the website, while this copy is needed to make it highlighted in the repository dashboard. --> # **Contributing guidelines** @@ -279,8 +278,8 @@ the site renders each one as its own section on the page: "Authored by", "Review the bar is the same one: the page has to be more accurate afterwards than it was before. The three roles carry the same weight. A page is only as good as the writing, the checking, and the sign-off behind -it, and the Security Frameworks need all three to be worth trusting. What follows is about describing different kinds of work -accurately, not about ranking contributors. +it, and the Security Frameworks need all three to be worth trusting. What follows is about describing different kinds +of work accurately, not about ranking contributors. The short version: a reviewer answers "is this page good?", a fact-checker answers "is this statement true?". Both roles are earned by the work of checking, not by the size of the diff. Text that adds words without settling either @@ -482,6 +481,9 @@ Pages with minimal content which need more work to cover the topic need to inclu This page is also open for contributions. Suggest improvements to our style and guidelines in the GitHub repo. +Funding the work is another way to contribute. [Support the Frameworks](https://securityalliance.org/donate) to help +keep them free, open, and independent. + ## About this page Originally inspired by the [Ethereum Protocol Fellows](https://github.com/eth-protocol-fellows/protocol-studies). diff --git a/README.md b/README.md index 796fe88e2..1384f71c7 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,9 @@ Every contribution helps strengthen the resource and improve security practices See our [Contributing Guide](https://github.com/security-alliance/frameworks/blob/develop/CONTRIBUTING.md) for details on how to get started. +Funding is another way to contribute. [Support the Frameworks](https://securityalliance.org/donate) to help keep +this resource free, open, and independent. + ## License Security Frameworks © 2026 by Security Alliance, licensed under [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/). diff --git a/components/donate/DonateNavButton.css b/components/donate/DonateNavButton.css new file mode 100644 index 000000000..7c4351fee --- /dev/null +++ b/components/donate/DonateNavButton.css @@ -0,0 +1,46 @@ +.donate-nav-button { + display: inline-flex; + align-items: center; + align-self: stretch; /* matches the search box height */ + gap: 0.5rem; + flex-shrink: 0; + padding: 0 0.75rem; + font-size: 0.875rem; + line-height: 1.25rem; + font-weight: 450; + white-space: nowrap; + text-decoration: none; + color: var(--vocs-text-color-secondary); + background-color: var(--vocs-background-color-surface); + border: 1px solid var(--vocs-border-color-primary); + border-radius: 0.75rem; + transition: color 0.1s ease, background-color 0.1s ease, border-color 0.1s ease; +} + +/* Light mode borders are set alongside the search trigger in docs/pages/_root.css, + which overrides the Vocs default for both controls together. */ + +.donate-nav-button:hover { + color: var(--vocs-text-color-primary); + background-color: var(--vocs-background-color-surfaceTint); +} + +/* The one accent: SEAL blue, matching the sidebar heart and the footer buttons. + Deliberately not `--vocs-color-accent`, which defaults to + light-dark(black, white) and is not overridden by this site. */ +.donate-nav-button svg { + color: #4339db; + flex-shrink: 0; +} + +:root[data-vocs-theme='dark'] .donate-nav-button svg { + color: #6b63e8; /* Brighter SEAL Blue for dark themes */ +} + +/* Follows the search input, which Vocs hides below its `md` breakpoint. + Vocs overrides that breakpoint to 748px, so this is not Tailwind's default. */ +@media (width < 748px) { + .donate-nav-button { + display: none; + } +} diff --git a/components/donate/DonateNavButton.tsx b/components/donate/DonateNavButton.tsx new file mode 100644 index 000000000..aca92a612 --- /dev/null +++ b/components/donate/DonateNavButton.tsx @@ -0,0 +1,47 @@ +"use client"; + +import { createPortal } from 'react-dom' +import { DONATE_URL } from '../shared/donate' +import { usePortalTargets } from './usePortalTargets' +import './DonateNavButton.css' + +const LABEL = 'Support us' +const TITLE = 'Help sustain the Frameworks' + +function DonateButton() { + return ( + + + {LABEL} + + ) +} + +/** + * Renders a donation button beside the top nav search box. + * + * Vocs exposes no slot in the top nav, so the button is portalled into + * `[data-v-gutter-top-left]`, the flex row that holds the logo and search. + * It is hidden on small screens along with the search input itself, where the + * mobile nav drawer carries the donation link instead. + */ +export function DonateNavButton() { + const targets = usePortalTargets('[data-v-gutter-top-left]') + + return targets.map(target => createPortal(, target, 'donate-nav-button')) +} diff --git a/components/donate/DonateSidebarLink.css b/components/donate/DonateSidebarLink.css new file mode 100644 index 000000000..e2ffa7ae9 --- /dev/null +++ b/components/donate/DonateSidebarLink.css @@ -0,0 +1,29 @@ +.donate-sidebar-divider { + width: 1px; + height: 1rem; + margin: 0 0.25rem; + background-color: var(--vocs-background-color-primary); +} + +.donate-sidebar-link { + display: flex; + align-items: center; + justify-content: center; + width: 1.75rem; + height: 1.75rem; + color: #4339db; /* SEAL Blue */ + transition: color 0.15s ease, transform 0.15s ease; +} + +.donate-sidebar-link:hover { + color: #352eb0; /* Darker SEAL Blue */ + transform: scale(1.1); +} + +:root[data-vocs-theme='dark'] .donate-sidebar-link { + color: #6b63e8; /* Brighter SEAL Blue for dark themes */ +} + +:root[data-vocs-theme='dark'] .donate-sidebar-link:hover { + color: #8c85f0; +} diff --git a/components/donate/DonateSidebarLink.tsx b/components/donate/DonateSidebarLink.tsx new file mode 100644 index 000000000..630454c46 --- /dev/null +++ b/components/donate/DonateSidebarLink.tsx @@ -0,0 +1,48 @@ +"use client"; + +import { createPortal } from 'react-dom' +import { DONATE_URL } from '../shared/donate' +import { usePortalTargets } from './usePortalTargets' +import './DonateSidebarLink.css' + +const LABEL = 'Help sustain the Frameworks' + +function DonateAnchor() { + return ( + <> +
+ + + + + ) +} + +/** + * Renders a donation link alongside the social icons. + * + * Vocs only accepts a fixed set of `socials` icons, so the link is portalled + * into every `[data-v-socials]` row instead. That covers both the desktop + * sidebar footer and the mobile nav drawer, which mounts on demand. + */ +export function DonateSidebarLink() { + const targets = usePortalTargets('[data-v-socials]') + + return targets.map(target => createPortal(, target, 'donate-sidebar-link')) +} diff --git a/components/donate/usePortalTargets.ts b/components/donate/usePortalTargets.ts new file mode 100644 index 000000000..c383b57f6 --- /dev/null +++ b/components/donate/usePortalTargets.ts @@ -0,0 +1,33 @@ +"use client"; + +import { useEffect, useState } from 'react' + +/** + * Tracks every element matching `selector`, so content can be portalled into + * parts of the Vocs chrome that expose no slot of their own. + * + * Targets are re-read on DOM mutations, which is what makes this work for + * regions that mount on demand, such as the mobile nav drawer. + */ +export function usePortalTargets(selector: string): Element[] { + const [targets, setTargets] = useState([]) + + useEffect(() => { + const sync = () => { + const found = Array.from(document.querySelectorAll(selector)) + // Only commit a new array when the matched set actually changed, so the + // observer does not loop on the nodes the caller itself portals in. + setTargets(prev => + prev.length === found.length && prev.every((el, i) => el === found[i]) ? prev : found, + ) + } + + sync() + + const observer = new MutationObserver(sync) + observer.observe(document.body, { childList: true, subtree: true }) + return () => observer.disconnect() + }, [selector]) + + return targets +} diff --git a/components/footer/ContributeFooter.css b/components/footer/ContributeFooter.css index 90c3d3a6d..48043dc61 100644 --- a/components/footer/ContributeFooter.css +++ b/components/footer/ContributeFooter.css @@ -44,6 +44,21 @@ color: #352eb0; /* Darker SEAL Blue */ } +.contribute-footer-actions { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + justify-content: center; + align-items: center; +} + +.contribute-footer-note { + font-size: 0.9rem; + color: #6C757D; /* Gray from SEAL palette */ + margin-top: 0.6rem; + line-height: 1.3; +} + .contribute-footer-button { display: inline-block; font-weight: 500; @@ -70,31 +85,10 @@ border: none; } -:root[data-vocs-theme='dark'] .contribute-footer-title { - color: #6b63e8; /* Brighter SEAL Blue for dark themes */ -} - :root[data-vocs-theme='dark'] .contribute-footer-subtitle { color: #a8adb2; /* Lighter gray for dark themes */ } -:root[data-vocs-theme='dark'] .contribute-footer-link { - color: #6b63e8; /* Brighter SEAL Blue for dark themes */ -} - -:root[data-vocs-theme='dark'] .contribute-footer-link:hover { - color: #8c85f0; /* Even brighter SEAL Blue for dark themes */ -} - -:root[data-vocs-theme='dark'] .contribute-footer-button { - color: #FFFFFF; - border-color: #6b63e8; /* Brighter SEAL Blue for dark themes */ - background-color: #6b63e8; -} - -:root[data-vocs-theme='dark'] .contribute-footer-button:hover { - background-color: #8c85f0; /* Even brighter SEAL Blue for dark themes */ - border-color: #8c85f0; - color: #1a1a1a; /* Darker background for contrast */ - box-shadow: 0 4px 8px rgba(107, 99, 232, 0.3); +:root[data-vocs-theme='dark'] .contribute-footer-note { + color: #a8adb2; /* Lighter gray for dark themes */ } diff --git a/components/footer/ContributeFooter.tsx b/components/footer/ContributeFooter.tsx index 28d61a8d0..4c403c205 100644 --- a/components/footer/ContributeFooter.tsx +++ b/components/footer/ContributeFooter.tsx @@ -1,16 +1,19 @@ "use client"; import { useEffect, useState } from 'react'; +import { DONATE_URL } from '../shared/donate' import './ContributeFooter.css' interface ContributeFooterProps { learnMoreUrl?: string; contributeUrl?: string; + donateUrl?: string; } -export function ContributeFooter({ - learnMoreUrl = "/contribute/contributing", - contributeUrl = "https://github.com/security-alliance/frameworks/blob/develop/docs/pages/" +export function ContributeFooter({ + learnMoreUrl = "/contribute/contributing", + contributeUrl = "https://github.com/security-alliance/frameworks/blob/develop/docs/pages/", + donateUrl = DONATE_URL }: ContributeFooterProps) { const [currentPath, setCurrentPath] = useState(""); @@ -34,14 +37,27 @@ export function ContributeFooter({ Learn more

- - ✏️ Contribute today! - + +

+ Short on time? Supporting SEAL keeps the Frameworks free and independent. +

); diff --git a/components/index.ts b/components/index.ts index c69a3ee8d..47df7880d 100644 --- a/components/index.ts +++ b/components/index.ts @@ -12,12 +12,15 @@ export { TagFilter } from './tags/TagFilter' export { TagList } from './tags/TagList' export { AttributionList } from './attribution/AttributionList' export { ContributeFooter } from './footer/ContributeFooter' +export { DonateSidebarLink } from './donate/DonateSidebarLink' +export { DonateNavButton } from './donate/DonateNavButton' export { Contributors } from './contributors/Contributors' export { BenchmarkList } from './benchmark/Benchmark' export { CertList } from './cert/CertList' export { ExportAllCerts } from './cert/ExportAllCerts' export type { Control, Section, CertListProps, ControlState, ControlData } from './cert/types' export * from './shared/tagColors' +export * from './shared/donate' export { CertifiedProtocols } from './cert/certified-protocols/CertifiedProtocols' export { CertifiedProtocolsWrapper } from './cert/certified-protocols/CertifiedProtocolsWrapper' export { BadgeLegend } from './contributors/BadgeLegend' diff --git a/components/shared/donate.ts b/components/shared/donate.ts new file mode 100644 index 000000000..b3abb6db6 --- /dev/null +++ b/components/shared/donate.ts @@ -0,0 +1,5 @@ +/** + * Donation link + */ + +export const DONATE_URL = 'https://securityalliance.org/donate' diff --git a/docs/pages/_root.css b/docs/pages/_root.css index a85c540aa..e1b0b2ffe 100644 --- a/docs/pages/_root.css +++ b/docs/pages/_root.css @@ -34,11 +34,15 @@ html:not([data-vocs-theme='dark']) [data-v-content] blockquote>p { font-weight: bolder; } -html:not([data-vocs-theme='dark']) [data-v-gutter-top-left] button[aria-haspopup='dialog'] { +/* Light-mode border treatment for the top-left controls: the search trigger + and the donate button, which is styled to match it. */ +html:not([data-vocs-theme='dark']) [data-v-gutter-top-left] button[aria-haspopup='dialog'], +html:not([data-vocs-theme='dark']) [data-v-gutter-top-left] .donate-nav-button { border-color: #908f8f; } -html:not([data-vocs-theme='dark']) [data-v-gutter-top-left] button[aria-haspopup='dialog']:hover { +html:not([data-vocs-theme='dark']) [data-v-gutter-top-left] button[aria-haspopup='dialog']:hover, +html:not([data-vocs-theme='dark']) [data-v-gutter-top-left] .donate-nav-button:hover { border-color: #303030; color: #1e1e1e; } diff --git a/docs/pages/_slots.tsx b/docs/pages/_slots.tsx index ac371dbd5..b0d04e8f8 100644 --- a/docs/pages/_slots.tsx +++ b/docs/pages/_slots.tsx @@ -1,4 +1,4 @@ -import { TagFilter } from '../../components' +import { DonateNavButton, DonateSidebarLink, TagFilter } from '../../components' export function Footer() { return ( @@ -9,5 +9,11 @@ export function Footer() { } export function SidebarHeader() { - return + return ( + <> + + + + + ) } diff --git a/docs/pages/contribute/contributing.mdx b/docs/pages/contribute/contributing.mdx index 2370c21c2..09adf27f8 100644 --- a/docs/pages/contribute/contributing.mdx +++ b/docs/pages/contribute/contributing.mdx @@ -491,6 +491,9 @@ Pages with minimal content which need more work to cover the topic need to inclu This page is also open for contributions. Suggest improvements to our style and guidelines in the GitHub repo. +Funding the work is another way to contribute. [Support the Frameworks](https://securityalliance.org/donate) to help +keep them free, open, and independent. + ## About this page Originally inspired by the [Ethereum Protocol Fellows](https://github.com/eth-protocol-fellows/protocol-studies). diff --git a/vocs.config.ts b/vocs.config.ts index 784e19efa..2412fb14f 100644 --- a/vocs.config.ts +++ b/vocs.config.ts @@ -19,7 +19,7 @@ const config = { return { canonical: `https://frameworks.securityalliance.org${cleanPath || '/'}` } }, banner: { - content: 'This is a work in progress and releases are experimental. We are looking for volunteers. See [Issues](https://github.com/security-alliance/frameworks/issues) and [Contribution](https://github.com/security-alliance/frameworks/blob/develop/docs/pages/contribute/contributing.mdx) to know how to collaborate.', + content: 'This is a work in progress and releases are experimental. We are looking for volunteers. See [Issues](https://github.com/security-alliance/frameworks/issues) and [Contribution](https://github.com/security-alliance/frameworks/blob/develop/docs/pages/contribute/contributing.mdx) to know how to collaborate, or [chip in to keep this work going](https://securityalliance.org/donate).', height: '30px', backgroundColor: '#8b5cf6', textColor: 'white', @@ -42,6 +42,7 @@ const config = { items: [ { text: 'Changelog', link: '/changelog' }, { text: 'Contributing', link: '/contribute/contributing' }, + { text: 'Support the project', link: 'https://securityalliance.org/donate' }, ], }, ],