diff --git a/docs/config/_category_.json b/docs/config/_category_.json index 95b47f89..65f663b3 100644 --- a/docs/config/_category_.json +++ b/docs/config/_category_.json @@ -1,5 +1,5 @@ { - "label": "Config Reference", + "label": "Config reference", "position": 4, "link": { "type": "generated-index", diff --git a/docs/config/firewall.mdx b/docs/config/firewall.mdx index 2fb52783..500e182a 100644 --- a/docs/config/firewall.mdx +++ b/docs/config/firewall.mdx @@ -139,7 +139,7 @@ enabling the built-in Nebula SSH server, you may wish to grant access over the N ## firewall.default_local_cidr_any -Default: False Reloadable +Default: false Reloadable {/** children passed as prop to avoid MDX generating a paragraph inside Pill */} diff --git a/docs/config/lighthouse.mdx b/docs/config/lighthouse.mdx index 31350642..fa89a6db 100644 --- a/docs/config/lighthouse.mdx +++ b/docs/config/lighthouse.mdx @@ -42,14 +42,14 @@ lighthouse: ## lighthouse.am_lighthouse -Default: False +Default: false `am_lighthouse` is used to enable lighthouse functionality for a node. This should ONLY be `true` on nodes you have configured to be lighthouses in your network ## lighthouse.serve_dns -Default: False +Default: false `serve_dns` optionally starts a DNS listener that responds to `A` and `TXT` queries and can even be delegated to for name resolution by external DNS hosts. To enable listening on IPv6 in addition to IPv4, set @@ -64,7 +64,7 @@ regardless of whether it is communicating over the Nebula network. `TXT` records can only be queried over the Nebula network, and contain certificate information for the requested host IP address. -For example, if `192.168.100.1` was your Lighthouse node running a DNS server and you wanted to find the Nebula IP +For example, if `192.168.100.1` was your lighthouse node running a DNS server and you wanted to find the Nebula IP address of a host named `web01`: ```shell diff --git a/docs/config/logging.mdx b/docs/config/logging.mdx index 220536b0..44cb5649 100644 --- a/docs/config/logging.mdx +++ b/docs/config/logging.mdx @@ -29,7 +29,7 @@ Controls the logging format. The options are `json` or `text` ## logging.disable_timestamp -Default: False Reloadable +Default: false Reloadable Disables timestamp logging. Useful when output is redirected to logging system that already adds timestamps. diff --git a/docs/config/pki.mdx b/docs/config/pki.mdx index 2224a0ab..4dcaec51 100644 --- a/docs/config/pki.mdx +++ b/docs/config/pki.mdx @@ -60,7 +60,7 @@ The `key` is a private key unique to every host on a Nebula network. It is used to prove a host’s identity to other members of the Nebula network. The private key should never be shared with other hosts. -### PKCS11 Support +### PKCS11 support Added in v1.10 @@ -100,7 +100,7 @@ support! :::note -The blocklist is _not_ distributed via Lighthouses. To ensure access to your entire network is blocked you must +The blocklist is _not_ distributed via lighthouses. To ensure access to your entire network is blocked you must distribute the full blocklist to every host in your network. This is typically done via tooling such as Ansible, Chef, or Puppet. @@ -112,7 +112,7 @@ stolen or compromised. ## pki.disconnect_invalid -Default: False Reloadable +Default: false Reloadable `disconnect_invalid` is a toggle to force a client to be disconnected if the certificate is expired or invalid. diff --git a/docs/config/preferred-ranges.mdx b/docs/config/preferred-ranges.mdx index b5c8fb3c..6056bf8c 100644 --- a/docs/config/preferred-ranges.mdx +++ b/docs/config/preferred-ranges.mdx @@ -17,7 +17,7 @@ ranges admitted as a set of preferred ranges of IP addresses. > An underlay network is the network that a nebula [overlay network](https://en.wikipedia.org/wiki/Overlay_network) maps > onto. It is the LAN network that a host is connected to in addition to the wider internet. -## How nebula orders underlay IP addresses it learns about +## How Nebula orders underlay IP addresses it learns about
diff --git a/docs/config/punchy.mdx b/docs/config/punchy.mdx index 5da403eb..e588b15b 100644 --- a/docs/config/punchy.mdx +++ b/docs/config/punchy.mdx @@ -8,10 +8,10 @@ import { Pill } from '@components/Pill/Pill'; # punchy `punchy` configures the sending of inbound/outbound packets at a regular interval to avoid expiration of firewall NAT -mappings. See [NAT Traversal and Firewalls](/docs/guides/nat-traversal/) for when to reach for these settings and the +mappings. See [NAT traversal and firewalls](/docs/guides/nat-traversal/) for when to reach for these settings and the NAT behaviors that make them necessary. -Regardless of how `punchy` is configured, the Lighthouse will notify hosts when a peer is attempting to handshake with +Regardless of how `punchy` is configured, the lighthouse will notify hosts when a peer is attempting to handshake with it and Nebula will issue an "empty" packet to the initiating peer's IP addresses in an attempt to punch a hole through its own NAT. @@ -25,7 +25,7 @@ punchy: ## punchy.punch -Default: False +Default: false When enabled, Nebula will periodically send "empty" packets to the underlay IP addresses of hosts it has established tunnels to in order to maintain the "hole" punched in the NAT's firewall. @@ -34,14 +34,14 @@ tunnels to in order to maintain the "hole" punched in the NAT's firewall. Default: 1s Reloadable -`delay` is the period of time Nebula waits between receiving a Lighthouse handshake notification and sending an empty +`delay` is the period of time Nebula waits between receiving a lighthouse handshake notification and sending an empty packet in order to try to punch a hole in the NAT firewall. This is helpful in some NAT race condition situations. ## punchy.respond -Default: False Reloadable +Default: false Reloadable -When enabled, the node will attempt a handshake to the initiating peer in response to the Lighthouse's notification of +When enabled, the node will attempt a handshake to the initiating peer in response to the lighthouse's notification of the peer attempting to handshake with it. This can be useful when a node is behind a difficult NAT for which regular hole punching does not work. Some combinations of NAT still will not work and [relays](/docs/config/relay/) can be used for this scenario. @@ -50,5 +50,5 @@ for this scenario. Default: 5s Reloadable -`respond_delay` is the period of time Nebula waits between receiving a Lighthouse handshake notification and attempting +`respond_delay` is the period of time Nebula waits between receiving a lighthouse handshake notification and attempting its own "reverse" handshake with the initiating peer. diff --git a/docs/config/relay.mdx b/docs/config/relay.mdx index 8a55eb16..7929a8b3 100644 --- a/docs/config/relay.mdx +++ b/docs/config/relay.mdx @@ -15,7 +15,7 @@ Relay support was introduced in Nebula v1.6.0. Relay hosts forward traffic between two peers. This can be useful if two nodes struggle to communicate directly with each other (e.g. some NATs can make it difficult to establish direct connections between two nodes.) -[NAT Traversal and Firewalls](/docs/guides/nat-traversal/) explains which NAT behaviors cause this and when relays are +[NAT traversal and firewalls](/docs/guides/nat-traversal/) explains which NAT behaviors cause this and when relays are the right fix. ```yml @@ -51,7 +51,7 @@ relays: - ``` -This list of relays is reported to the Lighthouse. When other nodes attempt to handshake with this host, the Lighthouse +This list of relays is reported to the lighthouse. When other nodes attempt to handshake with this host, the lighthouse will indicate its supported relays in addition to its known IP addresses. ## relay.am_relay diff --git a/docs/config/sshd.mdx b/docs/config/sshd.mdx index d38d0f2e..509c147b 100644 --- a/docs/config/sshd.mdx +++ b/docs/config/sshd.mdx @@ -30,7 +30,7 @@ See also the [Debugging with Nebula SSH commands](/docs/guides/debug-ssh-command ## sshd.enabled -Default: False Reloadable +Default: false Reloadable `enabled` toggles this feature globally. diff --git a/docs/config/static-map.mdx b/docs/config/static-map.mdx index 1fda1314..24aeaaa8 100644 --- a/docs/config/static-map.mdx +++ b/docs/config/static-map.mdx @@ -31,7 +31,7 @@ In general, this should be left as the default value `ip4` to avoid issues commu Lighthouses learn each node's IP addresses by looking at the source address of incoming packets as well as the self-reported addresses sent by the node. Because most devices are behind a NAT of some sort (e.g. a network router) -they cannot self-report their public IPv4 address. By contacting the Lighthouse via its IPv4 address, the Lighthouse is +they cannot self-report their public IPv4 address. By contacting the lighthouse via its IPv4 address, the lighthouse is able to learn the node's public IPv4 address. It is necessary to set this setting to `ip6` (or `ip`) for IPv6-only hosts. diff --git a/docs/config/stats.mdx b/docs/config/stats.mdx index 9429e3f3..8d3cff02 100644 --- a/docs/config/stats.mdx +++ b/docs/config/stats.mdx @@ -47,14 +47,14 @@ A golang [Duration](https://pkg.go.dev/time#ParseDuration). Recommended to be se ## stats.message_metrics -Default: False +Default: false Enables counter metrics for meta packets, e.g.: `messages.tx.handshake`. NOTE: `message.{tx,rx}.recv_error` is always emitted. ## stats.lighthouse_metrics -Default: False +Default: false Enables detailed counter metrics for lighthouse packets, e.g.: `lighthouse.rx.HostQuery`. @@ -64,14 +64,14 @@ Config options if `stats.type` is chosen to be `graphite` ### stats.prefix -DEFAULT: nebula +Default: nebula The prefix for Graphite metrics that nebula will prepend: https://graphite.readthedocs.io/en/latest/feeding-carbon.html#step-1-plan-a-naming-hierarchy ### stats.protocol -DEFAULT: tcp +Default: tcp Choose which protocol is used for passing stats to Graphite. The options are `tcp` and `udp`. diff --git a/docs/config/tun.mdx b/docs/config/tun.mdx index 38d7f655..4daa1fae 100644 --- a/docs/config/tun.mdx +++ b/docs/config/tun.mdx @@ -27,7 +27,7 @@ tun: ## tun.disabled -Default: False +Default: false Allows the nebula interface (tun) to be disabled, which lets you run a lighthouse without a nebula interface (and therefore without root). You will not be able to communicate over IP with a nebula node that uses this setting. @@ -39,13 +39,13 @@ required. If set, must be in the form `utun[0-9]+`. For FreeBSD: Required to be ## tun.drop_local_broadcast -Default: False +Default: false Toggles forwarding of local broadcast packets, the address of which depends on the ip/mask encoded in pki.cert ## tun.drop_multicast -Default: False +Default: false Toggles forwarding of multicast packets @@ -135,7 +135,7 @@ remaining available gateways, though load balancing may become uneven until the ## tun.use_system_route_table -Default: False +Default: false Added in v1.7.0 This option is only supported on Linux. diff --git a/docs/guides/debug-ssh-commands/index.mdx b/docs/guides/debug-ssh-commands/index.mdx index 88e7dddb..b15c71a2 100644 --- a/docs/guides/debug-ssh-commands/index.mdx +++ b/docs/guides/debug-ssh-commands/index.mdx @@ -1,5 +1,6 @@ --- title: Debugging with Nebula SSH commands +sidebar_label: SSH debug commands description: Learn how to use Nebula's built-in SSH server commands to debug network connectivity issues on overlay hosts. summary: @@ -95,7 +96,7 @@ list-hostmap - List all known previously connected hosts ## Notes about some commands `query-lighthouse ` will return an empty result set initially if the host is not connected, but it will trigger -a background request to the Lighthouse. Meaning, you need to run it twice to actually get a result. +a background request to the lighthouse. Meaning, you need to run it twice to actually get a result. `change-remote` has only a temporary effect: after a period of time, Nebula will "revert" to its [preferred remote](https://nebula.defined.net/docs/config/preferred-ranges/#how-nebula-orders-underlay-ip-addresses-it-learns-about) diff --git a/docs/guides/host-discovery/index.mdx b/docs/guides/host-discovery/index.mdx index 980a2968..edbeba65 100644 --- a/docs/guides/host-discovery/index.mdx +++ b/docs/guides/host-discovery/index.mdx @@ -1,6 +1,6 @@ --- sidebar_position: 2 -title: How Hosts Find Each Other +title: How hosts find each other description: The two mechanisms Nebula hosts use to discover each other's routable addresses — the static host map and lighthouse discovery — and when to use each. @@ -10,12 +10,12 @@ summary: to choose between them for each host in your network. --- -# How Hosts Find Each Other +# How hosts find each other Every host in your network has a Nebula IP, but that address only exists inside the overlay. To establish a tunnel with a peer, a host needs the peer's _underlay_ address — a routable IP and port like `203.0.113.42:4242` — to send packets to. Host discovery is how a host learns that address. Whether packets can actually get through the NATs and firewalls in -between is covered in [NAT Traversal and Firewalls](/docs/guides/nat-traversal/). +between is covered in [NAT traversal and firewalls](/docs/guides/nat-traversal/). There are two ways a host can learn where to reach a peer: @@ -151,4 +151,4 @@ advanced config overrides. - [`static_map` reference](/docs/config/static-map/) - [`lighthouse` reference](/docs/config/lighthouse/) - [`listen` reference](/docs/config/listen/) -- [NAT Traversal and Firewalls](/docs/guides/nat-traversal/) +- [NAT traversal and firewalls](/docs/guides/nat-traversal/) diff --git a/docs/guides/nat-traversal/index.mdx b/docs/guides/nat-traversal/index.mdx index e9227beb..e68022bb 100644 --- a/docs/guides/nat-traversal/index.mdx +++ b/docs/guides/nat-traversal/index.mdx @@ -1,6 +1,6 @@ --- sidebar_position: 3 -title: NAT Traversal and Firewalls +title: NAT traversal and firewalls description: What Nebula needs from the firewalls and NATs between hosts — how UDP hole punching works, which NAT behaviors break it, and the fallbacks available when a direct tunnel cannot be established. @@ -10,7 +10,7 @@ summary: what to reach for when the network gets in the way. --- -# NAT Traversal and Firewalls +# NAT traversal and firewalls Nebula sends handshakes, tunneled traffic, and reports to [lighthouses](/docs/config/lighthouse/) over a single UDP socket, and connections always start from the inside out. That's why most hosts behind home routers and office firewalls @@ -139,7 +139,7 @@ In rough order of preference: 2. **Give the host a public address.** A port forward plus [`lighthouse.advertise_addrs`](/docs/config/lighthouse/#lighthouseadvertise_addrs) skips observation entirely. Peers dial a known-good address, so no punching is needed. Pin [`listen.port`](/docs/config/listen/) so the forward has a - stable target, and see [How Hosts Find Each Other](/docs/guides/host-discovery/) for how the advertised address + stable target, and see [How hosts find each other](/docs/guides/host-discovery/) for how the advertised address spreads. 3. **Use relays.** [Relays](/docs/config/relay/) are the designed fallback when no direct path exists. A host that both peers can reach forwards traffic between them. The tunnel stays end-to-end encrypted; the relay can see routing @@ -178,6 +178,6 @@ a relay from the admin panel. See [Using dedicated relays](https://docs.defined. - [`punchy` reference](/docs/config/punchy/) - [`relay` reference](/docs/config/relay/) - [`listen` reference](/docs/config/listen/) -- [How Hosts Find Each Other](/docs/guides/host-discovery/) +- [How hosts find each other](/docs/guides/host-discovery/) - [Connectivity and NAT traversal](https://docs.defined.net/architecture/#connectivity-and-nat-traversal) in the Managed Nebula architecture overview diff --git a/docs/guides/quick-start/index.mdx b/docs/guides/quick-start/index.mdx index 158f87aa..e45ec92f 100644 --- a/docs/guides/quick-start/index.mdx +++ b/docs/guides/quick-start/index.mdx @@ -1,6 +1,6 @@ --- title: Quick start -description: How to create your first overlay network using a Certificate Authority, Lighthouse, and Hosts +description: How to create your first overlay network using a certificate authority, lighthouse, and hosts summary: This section will walk you through setting up a simple nebula network for testing. The examples will need to be modified to suit your particular environment. @@ -16,15 +16,15 @@ sidebar_position: 1 In Nebula, a lighthouse is a Nebula host that is responsible for keeping track of all of the other Nebula hosts, and helping them find each other within a Nebula network. -### Certificate Authority +### Certificate authority -In its simplest form, a Nebula Certificate Authority (CA) consists of two files, a CA certificate, and an associated +In its simplest form, a Nebula certificate authority (CA) consists of two files, a CA certificate, and an associated private key. A CA certificate is distributed to, and trusted by, every host on the network. The CA private key should not be distributed, and can be kept offline when not being used to add hosts to a Nebula network. ### Hosts -A Nebula host is simply any single node in the network, e.g. a server, laptop, phone, tablet. The Certificate Authority +A Nebula host is simply any single node in the network, e.g. a server, laptop, phone, tablet. The certificate authority is used to sign keys for each host added to a Nebula network. A host certificate contains the name, IP address, group membership, and a number of other details about a host. Individual hosts cannot modify their own certificate, because doing so will invalidate it. This allows us to trust that a host cannot impersonate another host within a Nebula @@ -41,7 +41,7 @@ to suit your particular environment. To start, you'll need to download Nebula for your specific platform(s). Specifically you'll need `nebula-cert` and the specific `nebula` binary for each platform you use. -#### Desktop and Server +#### Desktop and server Check the [releases](https://github.com/slackhq/nebula/releases/latest) page for downloads @@ -117,7 +117,7 @@ would generate a CA valid for just under two years. ## Building a Nebula network -### Establishing a Lighthouse +### Establishing a lighthouse Nebula lighthouses allow hosts to find each other, anywhere in the world. Lighthouses are the only hosts in a Nebula network whose IP addresses should not change. Running a lighthouse requires very few compute resources, and you can @@ -127,7 +127,7 @@ of us have used $5/mo [DigitalOcean](https://digitalocean.com) droplets as light Once you have launched an instance, ensure that Nebula UDP traffic (default port udp/4242) can reach it over the internet and is not blocked by any inbound firewall. -### Creating Keys and Certificates +### Creating keys and certificates This assumes you have three hosts, which we will name `lighthouse1`, `laptop`, and `server`. You can name the hosts any way you'd like, including FQDN. You'll also need to choose the Nebula IP address for each host when generating its diff --git a/docs/guides/rotating-certificate-authority/index.mdx b/docs/guides/rotating-certificate-authority/index.mdx index 03aee241..aa290ba3 100644 --- a/docs/guides/rotating-certificate-authority/index.mdx +++ b/docs/guides/rotating-certificate-authority/index.mdx @@ -1,5 +1,6 @@ --- title: Rotating a certificate authority +sidebar_label: Rotating a CA description: How to rotate an expiring Nebula certificate authority without downtime. summary: This guide will teach you how to migrate from an expiring certificate authority by creating a new certificate @@ -34,7 +35,7 @@ bundle, giving you as much time as possible to rollback and fix any issues befor ## Let's get started! -### Step 1: Generate a new Certificate Authority +### Step 1: Generate a new certificate authority The first thing we need to do is create a new certificate authority with an expiration in the future. The new CA should use the exact same CIDR, group, and subnet restrictions as the original certificate. You can use `nebula-cert print` to diff --git a/docs/guides/running-nebula-as-non-root/index.mdx b/docs/guides/running-nebula-as-non-root/index.mdx index 910eaa09..ec4ac069 100644 --- a/docs/guides/running-nebula-as-non-root/index.mdx +++ b/docs/guides/running-nebula-as-non-root/index.mdx @@ -1,5 +1,6 @@ --- title: Running Nebula as a non-root user +sidebar_label: Running as non-root description: How to run Nebula on Linux as an unprivileged user by granting the CAP_NET_ADMIN capability via systemd or setcap. summary: diff --git a/docs/guides/sign-certificates-with-public-keys/index.mdx b/docs/guides/sign-certificates-with-public-keys/index.mdx index 9f86909b..cd85328c 100644 --- a/docs/guides/sign-certificates-with-public-keys/index.mdx +++ b/docs/guides/sign-certificates-with-public-keys/index.mdx @@ -1,5 +1,6 @@ --- title: Signing a certificate without a private key +sidebar_label: Signing without a key description: How to sign Nebula certificates without copying private keys across devices. summary: After reading this guide you will be able to create public/private keypairs on devices you wish to add to the Nebula diff --git a/docs/guides/unsafe_routes/index.mdx b/docs/guides/unsafe_routes/index.mdx index 1ca718e6..f971af91 100644 --- a/docs/guides/unsafe_routes/index.mdx +++ b/docs/guides/unsafe_routes/index.mdx @@ -1,5 +1,6 @@ --- title: Extending network access beyond overlay hosts +sidebar_label: Unsafe routes description: Configure Nebula unsafe_routes to route traffic through overlay hosts and reach devices that cannot run Nebula directly. @@ -65,7 +66,7 @@ nebula-cert print -json -path ca.crt | jq .details } ``` -## Example Network +## Example network The following IP addresses, hostnames, and subnets are used throughout this guide to illustate a valid configuration for our use case. @@ -89,12 +90,12 @@ This is the overlay network that will be used by hosts running Nebula. - `192.168.100.0/24` (192.168.100.1–192.168.100.254) - The macOS host in this example has Internet access but it not on the same, physical LAN as the Linux host. -| Overlay Host IP | Overlay Hostname | Description | +| Overlay host IP | Overlay hostname | Description | | ---------------- | ---------------- | ---------------------------------------------------------- | | `192.168.100.10` | `home-raspi` | Linux host that will route traffic between LAN and overlay | | `192.168.100.11` | `laptop-mac` | Mac that will access printer _via_ `home-raspi` | -## Configuration Steps +## Configuration steps Using the example network and hosts referenced above, the following steps explain how to configure the macOS host (`laptop-mac`, `192.168.100.11`) to route traffic through the Linux host (`home-raspi`, `192.168.100.10`) in order to diff --git a/docs/guides/upgrade-to-cert-v2-and-ipv6/index.mdx b/docs/guides/upgrade-to-cert-v2-and-ipv6/index.mdx index f07fe89f..4230a2ab 100644 --- a/docs/guides/upgrade-to-cert-v2-and-ipv6/index.mdx +++ b/docs/guides/upgrade-to-cert-v2-and-ipv6/index.mdx @@ -1,5 +1,6 @@ --- title: Upgrading a Nebula network to IPv6 overlay addresses +sidebar_label: Upgrading to IPv6 description: Step-by-step guide to upgrading an existing Nebula network to the v2 certificate format and enabling IPv6 overlay addresses. @@ -40,9 +41,9 @@ All hosts must be upgraded to v1.10+ before proceeding. Older versions cannot va ::: -## Create a v2 Certificate Authority +## Create a v2 certificate authority -Create a new v2 Certificate Authority that will coexist with your existing v1 CA during the migration. Creating a new CA +Create a new v2 certificate authority that will coexist with your existing v1 CA during the migration. Creating a new CA with Nebula v1.10 will create a v2 CA by default. ```bash diff --git a/docs/guides/using-lighthouse-dns/index.mdx b/docs/guides/using-lighthouse-dns/index.mdx index c16a18de..63ba1273 100644 --- a/docs/guides/using-lighthouse-dns/index.mdx +++ b/docs/guides/using-lighthouse-dns/index.mdx @@ -1,7 +1,8 @@ --- title: Using Lighthouse DNS with Nebula +sidebar_label: Lighthouse DNS description: - Configure Nebula's experimental built-in DNS server on Lighthouse hosts to resolve overlay network hostnames. + Configure Nebula's experimental built-in DNS server on lighthouse hosts to resolve overlay network hostnames. sidebar_position: 7 --- @@ -14,18 +15,18 @@ Lighthouse DNS in nebula is experimental and should not be considered to be a ro ::: -Nebula comes with built-in DNS server support via Lighthouse hosts. +Nebula comes with built-in DNS server support via lighthouse hosts. Lighthouse DNS can generate DNS records based on dynamic nebula hosts, useful if you are spinning up new nebula hosts on demand. ## Prerequisites -This guide assumes you already have a working Lighthouse and at least one other host communicating with it. If you +This guide assumes you already have a working lighthouse and at least one other host communicating with it. If you haven't setup a Nebula network yet, check out the [Quick Start guide](/docs/guides/quick-start/). You'll then want to set up the lighthouse as a DNS server for the other two hosts. This can be either the public static -lighthouse IP or the private nebula IP depending on the Lighthouse's configuration. +lighthouse IP or the private nebula IP depending on the lighthouse's configuration. ## Configuration @@ -45,7 +46,7 @@ ensure the DNS is only accessible to hosts that are allowed to contact the light :::note -Only Lighthouses should have `lighthouse.serve_dns` enabled, as DNS info is collected when hosts report to the +Only lighthouses should have `lighthouse.serve_dns` enabled, as DNS info is collected when hosts report to the lighthouse. Nebula will not honor the option if enabled on a non-lighthouse host. ::: @@ -104,7 +105,7 @@ curl --dns-servers "100.100.0.1" http://alice-laptop:3000 - If the name in the Nebula certificate is not a [valid hostname](https://www.rfc-editor.org/rfc/rfc1035#section-2.3.1), Lighthouse DNS will return an empty result. -## Hostname Validity +## Hostname validity import { ValidateHostnameInput } from './ValidateHostnameInput'; diff --git a/docs/intro.md b/docs/intro.md index 80e34d49..81d619fd 100644 --- a/docs/intro.md +++ b/docs/intro.md @@ -25,7 +25,7 @@ want Nebula without running the infrastructure themselves. ## Core features -- Peer-to-peer, layer 3, virtual network ([Technical Details](#technical-details)) +- Peer-to-peer, layer 3, virtual network ([Technical details](#technical-details)) - Supports TCP/UDP/ICMP traffic via TUN adapter with split-tunneling - Host firewall with groups-based rules engine for overlay traffic - Route discovery and NAT traversal assisted by simple "lookup" hosts diff --git a/docs/security/2025-10-07-source-ip-spoofing-defect.mdx b/docs/security/2025-10-07-source-ip-spoofing-defect.mdx index 0f41dd26..d3fac50a 100644 --- a/docs/security/2025-10-07-source-ip-spoofing-defect.mdx +++ b/docs/security/2025-10-07-source-ip-spoofing-defect.mdx @@ -5,13 +5,13 @@ description: source IP when the sender's certificate is configured with unsafe_routes. --- -# 2025-10-07 - Source IP Spoofing Defect +# 2025-10-07 - Source IP spoofing defect Due to a bug in Nebula’s packet validation logic, hosts configured with a certificate that includes unsafe_routes (cert v1 / cert v2) or multiple IP addresses can spoof the source IP of packets sent to other hosts running an affected version of Nebula. We do not believe that it is possible to receive return traffic for the spoofed packets. -## Affected Versions +## Affected versions - v1.9.4 – v1.9.6 (stable) - v1.9.4-nightly20240801 – v1.10.0-nightly20240730 (nightly) diff --git a/docs/security/_category_.json b/docs/security/_category_.json index 2cd94d8c..e2a41046 100644 --- a/docs/security/_category_.json +++ b/docs/security/_category_.json @@ -1,5 +1,5 @@ { - "label": "Security Bulletins", + "label": "Security bulletins", "position": 3, "link": { "type": "generated-index", diff --git a/docusaurus.config.ts b/docusaurus.config.ts index 8b66852b..a44c143e 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -45,6 +45,24 @@ const config: Config = { ], ], + headTags: [ + { + tagName: 'link', + attributes: { rel: 'preconnect', href: 'https://fonts.googleapis.com' }, + }, + { + tagName: 'link', + attributes: { rel: 'preconnect', href: 'https://fonts.gstatic.com', crossorigin: 'anonymous' }, + }, + ], + + stylesheets: [ + { + href: 'https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@500;600;700&family=Inter:wght@400;500;600;700&display=swap', + rel: 'stylesheet', + }, + ], + scripts: [ { src: 'https://plausible.io/js/pa--fa02jhZoajfPxSb4zpFj.js', @@ -73,6 +91,7 @@ const config: Config = { customCss: [ require.resolve('./src/css/base.css'), require.resolve('./src/css/theme.css'), + require.resolve('./src/css/components.css'), require.resolve('./src/css/utility.css'), ], }, @@ -82,10 +101,10 @@ const config: Config = { ], themeConfig: { - image: 'img/nebula-docs-og.png', + image: 'img/nebula-docs-og.jpg', metadata: [{ name: 'keywords', content: 'nebula, overlay network, VPN, mesh networking, defined networking' }], navbar: { - title: 'Nebula Documentation', + title: 'Nebula documentation', logo: { alt: 'Nebula logo', href: '/docs/', @@ -111,11 +130,11 @@ const config: Config = { to: '/docs/guides/', }, { - label: 'Config Reference', + label: 'Config reference', to: '/docs/config/', }, { - label: 'Docs Github', + label: 'Docs GitHub', href: 'https://github.com/DefinedNet/nebula-docs', }, ], diff --git a/package.json b/package.json index 687443a0..dd1bf2fe 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ }, "dependencies": { "@docusaurus/core": "3.9.2", + "@docusaurus/plugin-content-docs": "3.9.2", "@docusaurus/preset-classic": "3.9.2", "@docusaurus/theme-common": "3.9.2", "@mdx-js/react": "^3.1.1", @@ -31,6 +32,8 @@ "@docusaurus/types": "^3.9.2", "@ianvs/prettier-plugin-sort-imports": "^4.7.1", "@types/node": "^25.5.0", + "@types/react": "^19.2.2", + "@types/react-dom": "^19.2.7", "alias-hq": "^6.2.4", "autoprefixer": "^10.4.27", "docusaurus-plugin-module-alias": "^0.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 41d1fdb3..5df1dce0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@docusaurus/core': specifier: 3.9.2 version: 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.2)(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@5.9.3) + '@docusaurus/plugin-content-docs': + specifier: 3.9.2 + version: 3.9.2(@mdx-js/react@3.1.1(@types/react@19.2.2)(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@5.9.3) '@docusaurus/preset-classic': specifier: 3.9.2 version: 3.9.2(@algolia/client-search@5.41.0)(@mdx-js/react@3.1.1(@types/react@19.2.2)(react@19.2.8))(@types/react@19.2.2)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(search-insights@2.17.0)(typescript@5.9.3) @@ -54,6 +57,12 @@ importers: '@types/node': specifier: ^25.5.0 version: 25.5.0 + '@types/react': + specifier: ^19.2.2 + version: 19.2.2 + '@types/react-dom': + specifier: ^19.2.7 + version: 19.2.7(@types/react@19.2.2) alias-hq: specifier: ^6.2.4 version: 6.2.4(@babel/preset-env@7.28.5(@babel/core@7.28.5)) @@ -1807,6 +1816,11 @@ packages: '@types/range-parser@1.2.7': resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + '@types/react-dom@19.2.7': + resolution: {integrity: sha512-I8bPpDLcHBv1qiIiXDCy71Rt8eQDKJP0sMSWJphDdAcdqiJ1sGpZamavoEIRZmYzjia9LuEb2HlYdDpmoENpvQ==} + peerDependencies: + '@types/react': ^19.2.0 + '@types/react-router-config@5.0.11': resolution: {integrity: sha512-WmSAg7WgqW7m4x8Mt4N6ZyKz0BubSj/2tVUMsAHp+Yd2AMwcSbeFq9WympT19p5heCFmF97R9eD5uUR/t4HEqw==} @@ -8350,6 +8364,10 @@ snapshots: '@types/range-parser@1.2.7': {} + '@types/react-dom@19.2.7(@types/react@19.2.2)': + dependencies: + '@types/react': 19.2.2 + '@types/react-router-config@5.0.11': dependencies: '@types/history': 4.7.11 diff --git a/sidebars.js b/sidebars.js index 05abe851..02f82bfb 100644 --- a/sidebars.js +++ b/sidebars.js @@ -12,7 +12,19 @@ /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ const sidebars = { // By default, Docusaurus generates a sidebar from the docs folder structure - tutorialSidebar: [{ type: 'autogenerated', dirName: '.' }], + tutorialSidebar: [ + // A plain section label above the intro doc, matching the uppercase headings + // the Guides / Security bulletins / Config reference categories render as. + // `html` rather than a category so it stays a label: no link, no caret, no + // collapse. Styled by .dn-sidebar-label in src/css/components.css. + { + type: 'html', + value: 'Get started', + className: 'dn-sidebar-label', + defaultStyle: false, + }, + { type: 'autogenerated', dirName: '.' }, + ], // But you can create a sidebar manually /* diff --git a/src/components/Pill/Pill.module.css b/src/components/Pill/Pill.module.css index 12ec1efb..31e015d2 100644 --- a/src/components/Pill/Pill.module.css +++ b/src/components/Pill/Pill.module.css @@ -1,24 +1,26 @@ .Pill { display: inline-block; - text-transform: uppercase; + font-family: var(--font-display); + /* + * Deliberately not uppercased: these pills carry literal YAML values + * (`false`, `ip4`, `always`), and YAML is case sensitive. Size, weight and a + * hairline border give them the label role instead. + */ font-size: 12px; font-weight: 600; line-height: 1; - border-radius: 10rem; - padding: 4px 8px; - - &:global(.no-transform), - &:global(.no-transform) { - text-transform: none; - } + border: 1px solid var(--border-color); + border-radius: var(--radius-pill); + padding: 5px 10px; } .Pill___info { - color: var(--ifm-color-secondary-contrast-foreground); - background-color: var(--ifm-color-secondary-contrast-background); + color: var(--text-muted); + background-color: var(--surface-1); } .Pill___warning { background-color: var(--ifm-color-warning-contrast-background); color: var(--ifm-color-warning-contrast-foreground); + border-color: color-mix(in srgb, var(--ifm-color-warning) 35%, transparent); } diff --git a/src/components/RailSearch/RailSearchContext.tsx b/src/components/RailSearch/RailSearchContext.tsx new file mode 100644 index 00000000..8ba05a3c --- /dev/null +++ b/src/components/RailSearch/RailSearchContext.tsx @@ -0,0 +1,29 @@ +import React, { createContext, useContext, useMemo, useState, type ReactNode } from 'react'; + +type RailSearchContextValue = { + /** True while the desktop sidebar rail is rendering its own search. */ + railHasSearch: boolean; + setRailHasSearch: (value: boolean) => void; +}; + +const RailSearchContext = createContext(null); + +/** + * Lets the sidebar rail tell the navbar that it owns the search box. + * + * The two live in separate React subtrees — the navbar renders outside + * `DocsSidebarProvider`, so `useDocsSidebar` throws there — and the route alone + * cannot answer the question, because the docs plugin owns `/docs/**` + * including paths that 404. This carries the fact from the one place that + * knows it to the one place that needs it. + */ +export function RailSearchProvider({ children }: { children: ReactNode }): ReactNode { + const [railHasSearch, setRailHasSearch] = useState(false); + const value = useMemo(() => ({ railHasSearch, setRailHasSearch }), [railHasSearch]); + + return {children}; +} + +export function useRailSearch(): RailSearchContextValue { + return useContext(RailSearchContext) ?? { railHasSearch: false, setRailHasSearch: () => {} }; +} diff --git a/src/css/base.css b/src/css/base.css index 56548095..46bae75e 100644 --- a/src/css/base.css +++ b/src/css/base.css @@ -1,6 +1,21 @@ -/* base color palettes, don't use these directly */ +/** + * Base design tokens. + * + * Two layers live here: + * 1. Raw brand ramps (--dn-color-*). Don't use these directly in components. + * 2. Semantic tokens (--surface-*, --text-*, --border-*, --accent-*) that the + * rest of the stylesheets consume. Those are the ones to reach for. + * + * Brand colors come from https://www.defined.net/brand/: + * Purple #5d22dd · Slate #6c7d93 · White #ffffff + * Red #e2411d · Orange #fb7b04 · Yellow #f2c10d + * Blue #2583da · Turquoise #22ddba · Green #2ed157 + */ + +/* Raw brand ramps, don't use these directly */ :root { - --dn-color-purple-hs: 260, 75%; + /* Purple 50 === brand purple #5d22dd */ + --dn-color-purple-hs: 259, 73%; /* lightest */ --dn-color-purple-95: hsl(var(--dn-color-purple-hs), 95%); --dn-color-purple-90: hsl(var(--dn-color-purple-hs), 90%); @@ -14,8 +29,8 @@ --dn-color-purple-10: hsl(var(--dn-color-purple-hs), 10%); /* darkest */ - --dn-color-cyan-hs: 163, 79%; - /* lightest */ + /* Cyan 50 === brand turquoise #22ddba */ + --dn-color-cyan-hs: 169, 73%; --dn-color-cyan-95: hsl(var(--dn-color-cyan-hs), 95%); --dn-color-cyan-90: hsl(var(--dn-color-cyan-hs), 90%); --dn-color-cyan-80: hsl(var(--dn-color-cyan-hs), 80%); @@ -26,36 +41,123 @@ --dn-color-cyan-30: hsl(var(--dn-color-cyan-hs), 30%); --dn-color-cyan-20: hsl(var(--dn-color-cyan-hs), 20%); --dn-color-cyan-10: hsl(var(--dn-color-cyan-hs), 10%); - /* darkest */ - --dn-color-gray-hs: 214, 15%; + /* Gray 55 === brand slate #6c7d93 */ + --dn-color-gray-hs: 213, 15%; /* lightest */ + --dn-color-gray-98: hsl(var(--dn-color-gray-hs), 98%); + --dn-color-gray-96: hsl(var(--dn-color-gray-hs), 96%); --dn-color-gray-95: hsl(var(--dn-color-gray-hs), 95%); + --dn-color-gray-92: hsl(var(--dn-color-gray-hs), 92%); --dn-color-gray-90: hsl(var(--dn-color-gray-hs), 90%); + --dn-color-gray-86: hsl(var(--dn-color-gray-hs), 86%); --dn-color-gray-80: hsl(var(--dn-color-gray-hs), 80%); --dn-color-gray-70: hsl(var(--dn-color-gray-hs), 70%); --dn-color-gray-60: hsl(var(--dn-color-gray-hs), 60%); + --dn-color-gray-55: hsl(var(--dn-color-gray-hs), 55%); --dn-color-gray-50: hsl(var(--dn-color-gray-hs), 50%); + --dn-color-gray-42: hsl(var(--dn-color-gray-hs), 42%); --dn-color-gray-40: hsl(var(--dn-color-gray-hs), 40%); --dn-color-gray-30: hsl(var(--dn-color-gray-hs), 30%); + --dn-color-gray-25: hsl(var(--dn-color-gray-hs), 25%); --dn-color-gray-20: hsl(var(--dn-color-gray-hs), 20%); + --dn-color-gray-17: hsl(var(--dn-color-gray-hs), 17%); --dn-color-gray-15: hsl(var(--dn-color-gray-hs), 15%); + --dn-color-gray-12: hsl(var(--dn-color-gray-hs), 12%); --dn-color-gray-10: hsl(var(--dn-color-gray-hs), 10%); --dn-color-gray-05: hsl(var(--dn-color-gray-hs), 5%); /* darkest */ - --dn-color-green-hs: 135, 64%; - --dn-color-orange-hs: 29, 90%; + /* Brand secondaries, hue/sat anchored on the 50% stop */ + --dn-color-green-hs: 138, 64%; + --dn-color-orange-hs: 29, 97%; --dn-color-blue-hs: 209, 71%; - --dn-color-red-hs: 11, 77%; + --dn-color-red-hs: 13, 77%; --dn-color-yellow-hs: 47, 90%; } -/* Fonts */ +/* Typography */ :root { --font-fallback: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji; - --ifm-font-family-base: var(--font-fallback); - --ifm-heading-font-family: var(--font-fallback); - --ifm-font-family-monospace: Menlo, Consolas, monospace; + /* Both webfonts are loaded in docusaurus.config.ts */ + /* Inter is Defined Networking's secondary typeface, and carries body copy */ + --font-sans: 'Inter', var(--font-fallback); + /* Plus Jakarta Sans carries headings and labels */ + --font-display: 'Plus Jakarta Sans', var(--font-sans); + --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + + --ifm-font-family-base: var(--font-sans); + --ifm-heading-font-family: var(--font-display); + --ifm-font-family-monospace: var(--font-mono); +} + +/** + * Semantic tokens. + * + * Four surface tiers (background < surface < surface-2 < border) give the flat, + * layered, hairline-ruled look the design is built around. Nothing floats on a + * drop shadow: depth comes from these tiers plus 1px borders. + */ +:root { + --surface-bg: var(--dn-color-gray-98); + --surface-1: var(--dn-color-gray-96); + --surface-2: var(--dn-color-gray-92); + --surface-raised: #ffffff; + --border-color: var(--dn-color-gray-86); + --border-subtle: var(--dn-color-gray-90); + + --text-strong: var(--dn-color-gray-15); + --text-body: var(--dn-color-gray-25); + /* gray-42 is the lightest stop that clears WCAG AA (4.5:1) on every light + surface this sits on, including --surface-2 at 4.65:1 */ + --text-muted: var(--dn-color-gray-42); + + /* + * The nav rail reads white, with form fields one tier away from it so they + * still register as inputs. The two swap direction between themes: on light + * the field is darker than the rail, on dark it is lighter. + */ + --sidebar-bg: var(--surface-raised); + --field-bg: var(--surface-1); + + --accent: var(--dn-color-purple-50); + --accent-strong: var(--dn-color-purple-40); + --accent-subtle: var(--dn-color-purple-95); + /* Solid brand-purple fill for selected states, always paired with white text */ + --accent-solid: var(--dn-color-purple-50); + --accent-solid-text: #ffffff; + + /* Shape: controls sit at -sm, cards and panels at -md */ + --radius-sm: 8px; + --radius-md: 12px; + --radius-lg: 16px; + --radius-pill: 999px; + + /* The small, tracked, uppercase eyebrow used for section and group labels */ + --label-size: 11px; + --label-tracking: 0.08em; + --label-weight: 600; +} + +html[data-theme='dark'] { + --surface-bg: var(--dn-color-gray-10); + --surface-1: var(--dn-color-gray-12); + --surface-2: var(--dn-color-gray-17); + --surface-raised: var(--dn-color-gray-15); + --border-color: var(--dn-color-gray-20); + --border-subtle: var(--dn-color-gray-17); + + --text-strong: var(--dn-color-gray-95); + --text-body: var(--dn-color-gray-86); + --text-muted: var(--dn-color-gray-60); + + --sidebar-bg: var(--surface-1); + --field-bg: var(--surface-2); + + --accent: hsl(var(--dn-color-purple-hs), 75%); + --accent-strong: hsl(var(--dn-color-purple-hs), 85%); + --accent-subtle: hsl(var(--dn-color-purple-hs), 18%); + --accent-solid: hsl(var(--dn-color-purple-hs), 55%); + --accent-solid-text: #ffffff; } diff --git a/src/css/components.css b/src/css/components.css new file mode 100644 index 00000000..cd183be6 --- /dev/null +++ b/src/css/components.css @@ -0,0 +1,905 @@ +/** + * Component styling. + * + * The look: flat, layered surfaces separated by hairline borders instead of + * shadows; restrained radii; semibold (not heavy) headings; and small + * uppercase tracked labels for section and group headers. + */ + +/* + * Narrower doc sidebar. theme-classic declares `--doc-sidebar-width: 300px` on + * a bare `:root` in a stylesheet that loads after this one, so an equal + * specificity `:root` here would lose the cascade. `html:root` outranks it. + */ +html:root { + --doc-sidebar-width: 240px; +} + +html, +body { + background-color: var(--surface-bg); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +/* The uppercase tracked eyebrow, reused across the sidebar and footer */ +.dn-eyebrow, +.menu__list-item.theme-doc-sidebar-item-category-level-1 > .menu__list-item-collapsible > .menu__link, +.footer__title { + font-family: var(--font-display); + font-size: var(--label-size); + font-weight: var(--label-weight); + letter-spacing: var(--label-tracking); + text-transform: uppercase; + color: var(--text-muted); +} + +/* ---------------------------------------------------------------- navbar - */ + +.navbar { + border-bottom: 1px solid var(--border-color); + background-color: var(--ifm-navbar-background-color); +} + +/* + * Desktop has no top bar: the logo, search and color mode toggle live in the + * sidebar instead (see src/theme/DocSidebar/Desktop). Zeroing the navbar height + * collapses the offsets derived from it — the sidebar container's negative top + * margin and the TOC's sticky offset — so the layout closes up cleanly. + * + * Below this breakpoint the navbar stays: it carries the hamburger drawer that + * is the only navigation on mobile, plus its own search and toggle. + */ +@media (min-width: 997px) { + /* + * Scoped to pages that actually have the sidebar which replaces the top bar. + * Hiding it everywhere stranded the 404 and any src/pages route with no logo, + * search or colour-mode toggle. Where :has() is unsupported the rule simply + * does not apply, leaving the navbar visible — redundant chrome, not a + * broken page. + */ + #__docusaurus:has(.theme-doc-sidebar-container) { + --ifm-navbar-height: 0px; + } + + #__docusaurus:has(.theme-doc-sidebar-container) .navbar { + display: none; + } +} + +.navbar__link, +.navbar__brand { + font-family: var(--font-display); + font-size: 0.875rem; + font-weight: 500; + color: var(--text-muted); +} + +.navbar__link:hover { + color: var(--text-strong); +} + +.navbar__logo { + height: 1.75rem; +} + +/* Icon-style navbar controls sit in a bordered pill, like the reference's chips */ +.navbar__items--right .clean-btn, +.navbar__items--right .navbar__link { + border-radius: var(--radius-sm); +} + +/* ------------------------------------------------------------- search box - */ + +/* + * DocSearch ships its own palette and loads after this stylesheet, so plain + * class overrides lose the cascade (the button was still rendering DocSearch's + * default lavender at a 4px radius). It is designed to be themed through these + * custom properties instead — its own rules consume them, so no specificity + * fight — and they style the modal as well as the button. + */ +.DocSearch { + --docsearch-primary-color: var(--accent); + --docsearch-text-color: var(--text-body); + --docsearch-muted-color: var(--text-muted); + --docsearch-highlight-color: var(--accent); + + --docsearch-searchbox-background: var(--field-bg); + --docsearch-searchbox-focus-background: var(--surface-raised); + --docsearch-searchbox-shadow: none; + + --docsearch-container-background: color-mix(in srgb, var(--dn-color-gray-10) 55%, transparent); + --docsearch-modal-background: var(--surface-bg); + --docsearch-modal-shadow: 0 12px 40px hsla(var(--dn-color-gray-hs), 10%, 0.18); + --docsearch-modal-width: 36rem; + + --docsearch-hit-background: var(--surface-raised); + --docsearch-hit-color: var(--text-body); + --docsearch-hit-active-color: #ffffff; + --docsearch-hit-shadow: none; + + --docsearch-footer-background: var(--surface-1); + --docsearch-footer-shadow: inset 0 1px 0 0 var(--border-color); + + /* Flat key chips: the default is a faux-3D gradient with a stacked shadow */ + --docsearch-key-gradient: var(--surface-2); + --docsearch-key-shadow: none; + --docsearch-key-pressed-shadow: none; +} + +/* + * Shape and border rules need a specificity bump: DocSearch's own single-class + * rules load after this file and would otherwise win. The trigger carries both + * `DocSearch` and `DocSearch-Button`; the modal sits inside `.DocSearch + * .DocSearch-Container`, so a descendant selector does it there. + */ + +/* --- the trigger button --- */ + +.DocSearch.DocSearch-Button { + box-sizing: border-box; + height: 2.25rem; + margin: 0; + padding: 0 0.625rem; + gap: 0.5rem; + border: 1px solid var(--border-color); + border-radius: var(--radius-sm); + background: var(--field-bg); + box-shadow: none; + transition: + border-color 150ms ease, + background-color 150ms ease; +} + +.DocSearch.DocSearch-Button:hover, +.DocSearch.DocSearch-Button:focus-visible { + border-color: var(--accent); + background: var(--surface-raised); + box-shadow: none; +} + +/* Icon and placeholder share this span; it owns their spacing */ +.DocSearch.DocSearch-Button .DocSearch-Button-Container { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.DocSearch.DocSearch-Button .DocSearch-Search-Icon { + width: 1rem; + height: 1rem; + color: var(--text-muted); + stroke-width: 2; +} + +.DocSearch.DocSearch-Button .DocSearch-Button-Placeholder { + padding: 0; + font-family: var(--font-sans); + font-size: 0.875rem; + color: var(--text-muted); +} + +.DocSearch.DocSearch-Button .DocSearch-Button-Keys { + display: flex; + align-items: center; + min-width: auto; + gap: 3px; +} + +.DocSearch.DocSearch-Button .DocSearch-Button-Key { + display: flex; + align-items: center; + justify-content: center; + width: auto; + min-width: 1.0625rem; + height: 1.0625rem; + margin: 0; + padding: 0 0.1875rem; + top: 0; + background: var(--surface-2); + border: 1px solid var(--border-color); + border-radius: 4px; + box-shadow: none; + font-size: 0.6875rem; + color: var(--text-muted); +} + +/* + * DocSearch renders the shortcut glyphs as SVGs sized 20px, which overflowed + * the chip and threw the alignment off. Size them to the chip instead; the + * flex centering above then does the aligning. + */ +.DocSearch.DocSearch-Button .DocSearch-Button-Key svg { + display: block; + width: 0.6875rem; + height: 0.6875rem; +} + +/* --- the modal --- */ + +.DocSearch .DocSearch-Modal { + border: 1px solid var(--border-color); + border-radius: var(--radius-lg); + overflow: hidden; +} + +.DocSearch .DocSearch-SearchBar { + padding: 0.75rem 0.75rem 0; +} + +.DocSearch .DocSearch-Form { + border: 1px solid var(--border-color); + border-radius: var(--radius-sm); + box-shadow: none; + background: var(--field-bg); +} + +.DocSearch .DocSearch-Form:focus-within { + border-color: var(--accent); +} + +.DocSearch .DocSearch-Input { + font-family: var(--font-sans); + font-size: 1rem; +} + +.DocSearch .DocSearch-Reset { + border-radius: var(--radius-sm); +} + +.DocSearch .DocSearch-Cancel { + font-family: var(--font-display); + font-size: 0.875rem; + font-weight: 500; + color: var(--text-muted); +} + +.DocSearch .DocSearch-Dropdown { + padding: 0 0.75rem; +} + +.DocSearch .DocSearch-Hit-source { + font-family: var(--font-display); + font-size: var(--label-size); + font-weight: var(--label-weight); + letter-spacing: var(--label-tracking); + text-transform: uppercase; + color: var(--text-muted); + background: transparent; + padding-top: 1rem; +} + +.DocSearch .DocSearch-Hit a { + border: 1px solid var(--border-color); + border-radius: var(--radius-sm); + box-shadow: none; + background: var(--surface-raised); +} + +/* --accent-solid, not --accent: the latter is a light purple in dark mode and + would leave the white hit text unreadable. Matches the sidebar's active chip. */ +.DocSearch .DocSearch-Hit[aria-selected='true'] a { + background: var(--accent-solid); + border-color: var(--accent-solid); +} + +.DocSearch .DocSearch-Hit-title { + font-size: 0.9375rem; +} + +.DocSearch .DocSearch-Hit-path { + font-size: 0.8125rem; +} + +.DocSearch .DocSearch-Footer { + border-radius: 0; + box-shadow: var(--docsearch-footer-shadow); +} + +.DocSearch .DocSearch-Commands { + font-size: 0.75rem; +} + +.DocSearch .DocSearch-Commands-Key { + background: var(--surface-2); + border: 1px solid var(--border-color); + border-radius: 4px; + box-shadow: none; + color: var(--text-muted); + padding: 0; +} + +/* ------------------------------------------------------- touch targets - */ + +/* + * Below the desktop breakpoint every control is finger-driven. WCAG 2.2 SC + * 2.5.8 (AA) sets a 24x24 CSS px minimum — the drawer's close button was 21x21 + * and failed outright, while the hamburger (30), colour-mode toggle (32) and + * drawer rows (36/28) cleared it but sat well under a comfortable 44. + * + * Scoped to mobile: the desktop rail keeps its tighter 32px rhythm, where a + * mouse makes the smaller target a non-issue. + */ +@media (max-width: 996px) { + .navbar__toggle, + .navbar-sidebar__close, + .navbar button[class*='toggleButton'] { + display: flex; + align-items: center; + justify-content: center; + min-width: 44px; + min-height: 44px; + } + + .navbar .DocSearch-Button { + min-height: 44px; + } + + /* Drawer rows, including the collapsible category headers and their carets */ + .navbar-sidebar .menu__link, + .navbar-sidebar .menu__caret { + min-height: 44px; + align-items: center; + } + + .navbar-sidebar .menu__caret { + min-width: 44px; + justify-content: center; + } +} + +/* ------------------------------------------------------------- sidebar - */ + +.theme-doc-sidebar-container { + background-color: var(--sidebar-bg); + border-right: 1px solid var(--border-color) !important; +} + +/* + * theme-classic pads this scroll container `8px 0 8px 8px`, so nav rows sat 8px + * further from the left edge than the right. Zero the horizontal padding and + * let .theme-doc-sidebar-menu below provide one symmetric gutter, matching the + * logo, search and footer inset. + */ +.theme-doc-sidebar-container .menu.thin-scrollbar { + padding-left: 0; + padding-right: 0; +} + +.theme-doc-sidebar-menu { + padding: 0.5rem 0.75rem 2rem; + font-size: 0.875rem; +} + +/* Tighter rhythm for the desktop rail only; horizontal padding holds the 12px gutter */ +.theme-doc-sidebar-container .menu__link { + padding-top: 0.375rem; + padding-bottom: 0.375rem; +} + +.menu__link { + font-family: var(--font-display); + font-size: 0.875rem; + font-weight: 500; + line-height: 1.4; + color: var(--text-muted); + border-radius: var(--radius-sm); + padding: 0.5rem 0.75rem; + transition: + background-color 120ms ease, + color 120ms ease; +} + +/* + * theme-classic clamps sidebar labels to two lines (-webkit-line-clamp on the + * inner label span, in DocSidebarItem/{Link,Category}/styles.module.css), which + * silently ellipsised the longest guide titles. Let them wrap in full. The + * class names are CSS-module hashed, hence the prefix match; the two spellings + * differ in case, so both are listed. + */ +.theme-doc-sidebar-menu [class*='linkLabel'], +.theme-doc-sidebar-menu [class*='categoryLinkLabel'] { + display: block; + overflow: visible; + line-clamp: none; + -webkit-line-clamp: none; +} + +.menu__link:hover, +.menu__list-item-collapsible:hover { + background-color: var(--surface-2); + color: var(--text-strong); +} + +/* The selected page reads as a solid brand-purple chip */ +.menu__link--active, +.menu__link--active:hover { + color: var(--accent-solid-text); + font-weight: 600; + background-color: var(--accent-solid); +} + +.menu__link--active:not(.menu__link--sublist) { + background-color: var(--accent-solid); +} + +/* + * A standalone section label injected from sidebars.js as an `html` item. It is + * not a link, so it carries the category-label styling on its own rather than + * inheriting the .menu__link rules. + */ +.dn-sidebar-label { + padding: 0.25rem 0.75rem; + font-family: var(--font-display); + font-size: var(--label-size); + font-weight: 700; + letter-spacing: var(--label-tracking); + text-transform: uppercase; + color: var(--text-strong); +} + +/* Top-level categories read as section labels, not as links */ +.menu__list-item.theme-doc-sidebar-item-category-level-1 { + margin-top: 1.25rem; +} + +/* Weightier and higher contrast than the shared eyebrow, so sections anchor the rail */ +.menu__list-item.theme-doc-sidebar-item-category-level-1 > .menu__list-item-collapsible > .menu__link { + font-weight: 700; + color: var(--text-strong); +} + +.menu__list-item.theme-doc-sidebar-item-category-level-1 > .menu__list-item-collapsible > .menu__link { + padding-bottom: 0.25rem; +} + +.menu__list-item.theme-doc-sidebar-item-category-level-1 > .menu__list-item-collapsible:hover, +.menu__list-item.theme-doc-sidebar-item-category-level-1 > .menu__list-item-collapsible > .menu__link:hover { + background-color: transparent; + color: var(--text-strong); +} + +/* Section labels never take a highlight box, active or not */ +.menu__list-item.theme-doc-sidebar-item-category-level-1 > .menu__list-item-collapsible--active, +.menu__list-item.theme-doc-sidebar-item-category-level-1 > .menu__list-item-collapsible > .menu__link--active { + background-color: transparent; +} + +/* Nested items sit flush under their section label, with no indent or rule */ +.menu__list .menu__list { + margin-left: 0; + padding-left: 0; + border-left: none; +} + +.menu__caret::before, +.menu__link--sublist-caret::after { + width: 16px; + min-width: 16px; + height: 16px; + background-size: 16px 16px; + opacity: 0.5; +} + +/* ------------------------------------------------------------ content - */ + +.theme-doc-markdown { + font-size: 1rem; +} + +.markdown h1:first-child { + --ifm-h1-font-size: 2.125rem; + letter-spacing: -0.02em; + margin-bottom: 1.25rem; +} + +/* + * Infima re-declares the heading sizes scoped to `.markdown` (h2 at 2rem, h3 at + * 1.5rem), which overrides the :root scale in theme.css. Restate them here so + * the ramp stays 34 / 24 / 19 and h1 reads clearly larger than h2. + */ +.markdown > h2 { + --ifm-h2-font-size: 1.5rem; + + letter-spacing: -0.01em; + margin-top: 2.5rem; + padding-top: 1.75rem; + border-top: 1px solid var(--border-subtle); +} + +.markdown > h3 { + --ifm-h3-font-size: 1.1875rem; + + margin-top: 2rem; +} + +.markdown > h4 { + margin-top: 1.5rem; + color: var(--text-strong); +} + +/* Config reference pages open with a heading that is really an identifier */ +.markdown h1 code, +.markdown h2 code, +.markdown h3 code { + font-size: 0.9em; + background: var(--surface-2); + border: 1px solid var(--border-color); +} + +.markdown a { + text-decoration-color: color-mix(in srgb, var(--accent) 35%, transparent); + text-underline-offset: 0.2em; +} + +.markdown a:hover { + text-decoration-color: var(--accent); +} + +.markdown blockquote { + border-left: 2px solid var(--accent); + background: var(--surface-1); + border-radius: 0 var(--radius-sm) var(--radius-sm) 0; + padding: 0.75rem 1rem; + color: var(--text-muted); +} + +.markdown hr { + border: none; + border-top: 1px solid var(--border-subtle); +} + +/* ---------------------------------------------------------------- code - */ + +.markdown code { + border: 1px solid var(--border-subtle); +} + +div[class^='codeBlockContainer'] { + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + box-shadow: none; +} + +div[class^='codeBlockTitle'] { + border-bottom: 1px solid var(--border-color); + font-family: var(--font-display); + font-size: 0.75rem; + font-weight: 600; + letter-spacing: var(--label-tracking); + text-transform: uppercase; + color: var(--text-muted); + background: var(--surface-2); +} + +/* -------------------------------------------------------------- tables - */ + +/* + * Keep Infima's `display: block; overflow: auto`. That is what lets a wide + * table scroll inside the content column; `display: table` made it push the + * whole document sideways on narrow viewports. border-radius still clips the + * corners of a scrolling block, so the rounded border survives. + */ +.markdown table { + display: block; + overflow: auto; + border-collapse: collapse; + border: 1px solid var(--border-color); + border-radius: var(--radius-md); +} + +.markdown table thead tr { + border-bottom: 1px solid var(--border-color); +} + +.markdown table th { + font-family: var(--font-display); + font-size: var(--label-size); + font-weight: var(--label-weight); + letter-spacing: var(--label-tracking); + text-transform: uppercase; + color: var(--text-muted); +} + +.markdown table td, +.markdown table th { + border: none; + border-bottom: 1px solid var(--border-subtle); + padding: 0.625rem 0.875rem; +} + +.markdown table tr:last-child td { + border-bottom: none; +} + +/* --------------------------------------------------------- admonitions - */ + +/* + * Each admonition type takes a brand colour through --admonition-accent, which + * drives the left rule, the heading and icon, and a faint wash of the same hue + * over the surface. Body copy stays --text-body: Infima tints the whole block's + * text with the alert colour, which is hard to read at paragraph length. + */ +.theme-admonition { + --admonition-accent: var(--dn-color-gray-55); + --admonition-tint: 7%; + + border: 1px solid color-mix(in srgb, var(--admonition-accent) 25%, var(--border-color)); + border-left: 3px solid var(--admonition-accent); + border-radius: var(--radius-md); + box-shadow: none; + background: color-mix(in srgb, var(--admonition-accent) var(--admonition-tint), var(--surface-1)); + color: var(--text-body); +} + +html[data-theme='dark'] .theme-admonition { + --admonition-tint: 12%; +} + +/* Brand secondaries, one per type */ +.theme-admonition-tip { + /* Green and orange carry high luminance, so they need deep stops to clear + AA (4.5:1) for 13px heading text against their own tint */ + --admonition-accent: hsl(var(--dn-color-green-hs), 28%); +} + +.theme-admonition-info { + /* 45% only reached 3.99:1 against the tinted surface; 40% clears AA */ + --admonition-accent: hsl(var(--dn-color-blue-hs), 40%); +} + +.theme-admonition-note { + --admonition-accent: var(--dn-color-purple-50); +} + +.theme-admonition-warning, +.theme-admonition-caution { + --admonition-accent: hsl(var(--dn-color-orange-hs), 32%); +} + +.theme-admonition-danger { + --admonition-accent: hsl(var(--dn-color-red-hs), 41%); +} + +/* Dark mode lifts each hue so it reads against the dark ground */ +html[data-theme='dark'] .theme-admonition-tip { + --admonition-accent: hsl(var(--dn-color-green-hs), 62%); +} + +html[data-theme='dark'] .theme-admonition-info { + --admonition-accent: hsl(var(--dn-color-blue-hs), 66%); +} + +html[data-theme='dark'] .theme-admonition-note { + --admonition-accent: hsl(var(--dn-color-purple-hs), 75%); +} + +html[data-theme='dark'] .theme-admonition-warning, +html[data-theme='dark'] .theme-admonition-caution { + --admonition-accent: hsl(var(--dn-color-orange-hs), 62%); +} + +html[data-theme='dark'] .theme-admonition-danger { + --admonition-accent: hsl(var(--dn-color-red-hs), 66%); +} + +/* + * Admonition titles stay in sentence case rather than Docusaurus's default + * uppercase: several are full sentences or questions, which shout when + * uppercased. Weight and size carry the label role instead. + */ +.theme-admonition > div:first-child { + font-family: var(--font-display); + font-size: 0.8125rem; + font-weight: 700; + letter-spacing: 0; + text-transform: none; + color: var(--admonition-accent); +} + +.theme-admonition > div:first-child svg { + fill: currentColor; +} + +/* ----------------------------------------------------- cards & indexes - */ + +/* + * `display: flex` on the grid cell makes the card stretch to the cell's full + * height, so cards sitting side by side match regardless of how long their + * descriptions run — the ragged bottoms were the worst of the old layout. + * + * Scoped to the card-list item rather than a page wrapper: generated index pages + * render inside .generatedIndexPage, not .theme-doc-markdown, so a wrapper-based + * selector would miss them. This matches both, plus any inline . + */ +section.row > article[class*='docCardListItem'] { + display: flex; + margin-bottom: 1rem; +} + +.card { + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + background: var(--surface-1); + box-shadow: none !important; +} + +/* ------------------------------------------------------------ TOC & nav - */ + +/* + * No rule and no indicator bar: the current section is marked by the type + * itself, going accent-coloured and semibold. Keeps the column to just its + * words. + */ +.table-of-contents { + font-size: 0.8125rem; + padding: 0; +} + +.table-of-contents__left-border { + border-left: none; + padding-left: 0; +} + +.table-of-contents li { + margin: 0; +} + +.table-of-contents__link { + display: block; + padding: 0.3125rem 0.75rem; + color: var(--text-muted); + transition: color 120ms ease; +} + +.table-of-contents__link:hover { + color: var(--text-strong); +} + +.table-of-contents__link--active { + color: var(--accent); + font-weight: 600; +} + +.table-of-contents .table-of-contents .table-of-contents__link { + padding-left: 1.5rem; +} + +.pagination-nav__link { + position: relative; + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + background: var(--surface-1); + padding: 1rem 1.25rem; + transition: + border-color 150ms ease, + background-color 150ms ease; +} + +.pagination-nav__link:hover { + border-color: var(--accent); + background: var(--surface-raised); +} + +/* Room for the chevron on the side the link points toward */ +.pagination-nav__link--prev { + padding-left: 2.5rem; +} + +.pagination-nav__link--next { + padding-right: 2.5rem; +} + +.pagination-nav__sublabel { + font-family: var(--font-display); + font-size: var(--label-size); + font-weight: var(--label-weight); + letter-spacing: var(--label-tracking); + text-transform: uppercase; + color: var(--text-muted); +} + +.pagination-nav__label { + font-family: var(--font-display); + font-size: 0.9375rem; + font-weight: 600; + color: var(--text-strong); + transition: color 150ms ease; +} + +.pagination-nav__link:hover .pagination-nav__label { + color: var(--accent); +} + +/* + * Docusaurus renders the direction as guillemets inside the label text + * (« / »). Drop those for a drawn chevron that can sit apart from the label + * and animate on hover. + */ +.pagination-nav__link--prev .pagination-nav__label::before, +.pagination-nav__link--next .pagination-nav__label::after { + content: none; +} + +.pagination-nav__link::before { + content: ''; + position: absolute; + top: 50%; + width: 7px; + height: 7px; + border-top: 2px solid var(--text-muted); + border-right: 2px solid var(--text-muted); + transition: + border-color 150ms ease, + transform 150ms ease; +} + +.pagination-nav__link--prev::before { + left: 1.25rem; + transform: translateY(-50%) rotate(-135deg); +} + +.pagination-nav__link--next::before { + right: 1.25rem; + transform: translateY(-50%) rotate(45deg); +} + +.pagination-nav__link:hover::before { + border-color: var(--accent); +} + +.pagination-nav__link--prev:hover::before { + transform: translateY(-50%) translateX(-3px) rotate(-135deg); +} + +.pagination-nav__link--next:hover::before { + transform: translateY(-50%) translateX(3px) rotate(45deg); +} + +@media (prefers-reduced-motion: reduce) { + .pagination-nav__link::before, + .pagination-nav__link--prev:hover::before, + .pagination-nav__link--next:hover::before { + transition: border-color 150ms ease; + } + + .pagination-nav__link--prev:hover::before { + transform: translateY(-50%) rotate(-135deg); + } + + .pagination-nav__link--next:hover::before { + transform: translateY(-50%) rotate(45deg); + } +} + +.breadcrumbs__item .breadcrumbs__link { + font-size: 0.8125rem; + color: var(--text-muted); + background: transparent; + border-radius: var(--radius-sm); +} + +.breadcrumbs__item--active .breadcrumbs__link { + background: var(--surface-2); + color: var(--text-strong); +} + +/* -------------------------------------------------------------- footer - */ + +.footer { + border-top: 1px solid var(--border-color); + font-size: 0.875rem; +} + +.footer__link-item { + color: var(--text-muted); +} + +.footer__link-item:hover { + color: var(--accent); +} + +.footer__copyright { + font-size: 0.8125rem; + color: var(--text-muted); +} diff --git a/src/css/theme.css b/src/css/theme.css index d2535d1b..5304fabc 100644 --- a/src/css/theme.css +++ b/src/css/theme.css @@ -1,40 +1,109 @@ /** - * Any CSS included here will be global. The classic template - * bundles Infima by default. Infima is a CSS framework designed to - * work well for content-centric websites. + * Infima variable overrides. + * + * This file only wires Infima's own variables up to the semantic tokens in + * base.css. Anything that needs real selectors lives in components.css. */ -/* You can override the default Infima variables here. */ :root { - --ifm-color-primary: var(--dn-color-purple-30); - --ifm-color-primary-dark: var(--dn-color-purple-20); - --ifm-color-primary-darker: var(--don-color-purple-10); - --ifm-color-primary-darkest: var(--dn-color-purple-10); - --ifm-color-primary-light: var(--dn-color-purple-50); - --ifm-color-primary-lighter: var(--dn-color-purple-60); - --ifm-color-primary-lightest: var(--dn-color-purple-70); + /* Brand purple drives every accent */ + --ifm-color-primary: hsl(var(--dn-color-purple-hs), 45%); + --ifm-color-primary-dark: hsl(var(--dn-color-purple-hs), 40%); + --ifm-color-primary-darker: hsl(var(--dn-color-purple-hs), 35%); + --ifm-color-primary-darkest: hsl(var(--dn-color-purple-hs), 28%); + --ifm-color-primary-light: hsl(var(--dn-color-purple-hs), 55%); + --ifm-color-primary-lighter: hsl(var(--dn-color-purple-hs), 62%); + --ifm-color-primary-lightest: hsl(var(--dn-color-purple-hs), 70%); + + /* Surfaces */ + --ifm-background-color: var(--surface-bg); + --ifm-background-surface-color: var(--surface-1); + --ifm-footer-background-color: var(--surface-1); + /* The top bar reads as white, sitting above the off-white page */ + --ifm-navbar-background-color: var(--surface-raised); + --ifm-hover-overlay: var(--surface-2); + --ifm-toc-border-color: var(--border-color); + --ifm-hr-background-color: var(--border-color); + + /* Text */ + --ifm-font-color-base: var(--text-body); + --ifm-heading-color: var(--text-strong); + --ifm-link-color: var(--ifm-color-primary); + --ifm-link-hover-color: var(--ifm-color-primary-dark); + + /* Type scale: headings stay semibold rather than heavy, and sit tighter */ + --ifm-font-size-base: 16px; + --ifm-line-height-base: 1.7; + --ifm-heading-font-weight: 700; + --ifm-heading-line-height: 1.25; + --ifm-h1-font-size: 2.125rem; + --ifm-h2-font-size: 1.5rem; + --ifm-h3-font-size: 1.1875rem; + --ifm-h4-font-size: 1rem; + + /* Shape */ + --ifm-global-radius: var(--radius-sm); + --ifm-button-border-radius: var(--radius-sm); + --ifm-card-border-radius: var(--radius-md); + --ifm-alert-border-radius: var(--radius-md); + --ifm-code-border-radius: 5px; + --ifm-pre-border-radius: var(--radius-md); + + /* Chrome */ + --ifm-navbar-height: 3.75rem; + --ifm-navbar-shadow: none; + --ifm-navbar-item-padding-horizontal: 0.75rem; + --ifm-global-shadow-lw: none; + --ifm-global-shadow-md: none; + --ifm-global-shadow-tl: none; + + /* Code */ --ifm-code-font-size: 0.875em; - --docusaurus-highlighted-code-line-bg: hsla(0, 0%, 0%, 0.1); - --ifm-footer-background-color: var(--dn-color-gray-95); - --ifm-background-surface-color: var(--dn-color-gray-95); - --ifm-hover-overlay: var(--dn-color-gray-95); + --ifm-code-background: var(--surface-2); + --ifm-code-padding-horizontal: 0.35em; + --ifm-code-padding-vertical: 0.15em; + --ifm-pre-background: var(--surface-1); + --docusaurus-highlighted-code-line-bg: hsla(var(--dn-color-purple-hs), 50%, 0.1); + + /* Tables */ + --ifm-table-border-color: var(--border-color); + --ifm-table-head-background: var(--surface-1); + --ifm-table-stripe-background: transparent; + --ifm-table-head-font-weight: 600; + + /* Admonitions, mapped onto the brand secondaries */ + --ifm-color-info: hsl(var(--dn-color-blue-hs), 50%); + --ifm-color-success: hsl(var(--dn-color-green-hs), 42%); + --ifm-color-warning: hsl(var(--dn-color-yellow-hs), 45%); + --ifm-color-danger: hsl(var(--dn-color-red-hs), 50%); } -/* For readability concerns, you should choose a lighter palette in dark mode. */ html[data-theme='dark'] { - --ifm-color-primary: var(--dn-color-purple-70); - --ifm-color-primary-dark: var(--dn-color-purple-60); - --ifm-color-primary-darker: var(--dn-color-purple-50); - --ifm-color-primary-darkest: var(--dn-color-purple-40); - --ifm-color-primary-light: var(--dn-color-purple-80); - --ifm-color-primary-lighter: var(--dn-color-purple-90); - --ifm-color-primary-lightest: var(--dn-color-purple-95); - --ifm-footer-background-color: var(--dn-color-gray-10); - --ifm-background-surface-color: var(--dn-color-gray-15); - --ifm-hover-overlay: var(--dn-color-gray-20); + --ifm-color-primary: hsl(var(--dn-color-purple-hs), 75%); + --ifm-color-primary-dark: hsl(var(--dn-color-purple-hs), 68%); + --ifm-color-primary-darker: hsl(var(--dn-color-purple-hs), 62%); + --ifm-color-primary-darkest: hsl(var(--dn-color-purple-hs), 55%); + --ifm-color-primary-light: hsl(var(--dn-color-purple-hs), 80%); + --ifm-color-primary-lighter: hsl(var(--dn-color-purple-hs), 86%); + --ifm-color-primary-lightest: hsl(var(--dn-color-purple-hs), 92%); + + --ifm-background-color: var(--surface-bg); + --ifm-background-surface-color: var(--surface-1); + --ifm-footer-background-color: var(--surface-1); + --ifm-navbar-background-color: var(--surface-bg); + --ifm-hover-overlay: var(--surface-2); + + --ifm-code-background: var(--surface-2); + --ifm-pre-background: var(--surface-1); + --docusaurus-highlighted-code-line-bg: hsla(var(--dn-color-purple-hs), 75%, 0.14); + + --ifm-color-info: hsl(var(--dn-color-blue-hs), 66%); + --ifm-color-success: hsl(var(--dn-color-green-hs), 60%); + --ifm-color-warning: hsl(var(--dn-color-yellow-hs), 62%); + --ifm-color-danger: hsl(var(--dn-color-red-hs), 65%); & ::selection { - color: var(--dn-color-gray-15); + color: var(--dn-color-gray-10); } } @@ -42,7 +111,3 @@ html[data-theme='dark'] { color: white; background-color: var(--ifm-color-primary); } - -[data-theme='dark'] .DocSearch { - --docsearch-hit-active-color: var(--ifm-color-black); -} diff --git a/src/prism-dark.cjs b/src/prism-dark.cjs index ed9670bd..471e8d68 100644 --- a/src/prism-dark.cjs +++ b/src/prism-dark.cjs @@ -39,7 +39,8 @@ var theme = { { types: ['comment', 'prolog', 'punctuation'], style: { - color: 'hsl(var(--dn-color-gray-hs), 50%)', + // Meets WCAG AA (4.5:1) on the dark code surface; 50% fell short at 3.7:1 + color: 'hsl(var(--dn-color-gray-hs), 58%)', }, }, ], diff --git a/src/prism-light.cjs b/src/prism-light.cjs index f4b2c3c4..cce3cad5 100644 --- a/src/prism-light.cjs +++ b/src/prism-light.cjs @@ -39,7 +39,8 @@ var theme = { { types: ['comment', 'prolog', 'punctuation'], style: { - color: 'hsl(var(--dn-color-gray-hs), 50%)', + // Meets WCAG AA (4.5:1) on the light code surface; 50% fell short at 3.7:1 + color: 'hsl(var(--dn-color-gray-hs), 42%)', }, }, ], diff --git a/src/theme/DocCard/index.tsx b/src/theme/DocCard/index.tsx new file mode 100644 index 00000000..2b016ea6 --- /dev/null +++ b/src/theme/DocCard/index.tsx @@ -0,0 +1,83 @@ +import Link from '@docusaurus/Link'; +import { findFirstSidebarItemLink, useDocById } from '@docusaurus/plugin-content-docs/client'; +import { usePluralForm } from '@docusaurus/theme-common'; +import { translate } from '@docusaurus/Translate'; +import type { Props } from '@theme/DocCard'; +import Heading from '@theme/Heading'; +import React, { type ReactNode } from 'react'; +import styles from './styles.module.css'; + +/** + * Replaces theme-classic's card with a flatter, tighter one. + * + * Ejected rather than wrapped because the upstream card renders its icon as a + * text node inside the heading (`{icon} {title}`), which no wrapper or CSS can + * remove. + * + * Titles come from the doc itself, not the sidebar item's label: the guides + * shorten those via `sidebar_label` to keep the nav rail to one line, and the + * full title is the more useful thing to read when browsing. + */ + +function useCategoryItemsPlural() { + const { selectMessage } = usePluralForm(); + return (count: number) => + selectMessage( + count, + translate( + { + message: '1 item|{count} items', + id: 'theme.docs.DocCard.categoryDescription.plurals', + description: + 'The default description for a category card in the generated index about how many items this category includes', + }, + { count }, + ), + ); +} + +function CardRow({ href, title, description }: { href: string; title: string; description?: string }): ReactNode { + return ( + + + + {title} + + + + {description && {description}} + + ); +} + +export default function DocCard({ item }: Props): ReactNode { + const doc = useDocById(item.type === 'link' ? (item.docId ?? undefined) : undefined); + const categoryItemsPlural = useCategoryItemsPlural(); + + if (item.type === 'link') { + return ( + + ); + } + + if (item.type === 'category') { + const href = findFirstSidebarItemLink(item); + // Categories without a link are filtered out upstream before reaching here + if (!href) { + return null; + } + return ( + + ); + } + + throw new Error(`unknown item type ${JSON.stringify(item)}`); +} diff --git a/src/theme/DocCard/styles.module.css b/src/theme/DocCard/styles.module.css new file mode 100644 index 00000000..49b1a887 --- /dev/null +++ b/src/theme/DocCard/styles.module.css @@ -0,0 +1,88 @@ +/* + * A flat card: hairline border and a restrained radius, no drop shadow. Fills + * its grid cell so cards in a row share a height regardless of copy length. + */ + +.card { + display: flex; + flex-direction: column; + gap: 0.375rem; + /* + * The grid cell is a flex container (so cards in a row match heights), which + * makes this a flex item — without flex-grow it sizes to its own content and + * leaves the cell short. `flex: 1` fills the width, `height: 100%` the height. + */ + flex: 1; + height: 100%; + padding: 1.125rem 1.25rem; + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + background: var(--surface-1); + color: inherit; + text-decoration: none !important; + transition: + border-color 150ms ease, + background-color 150ms ease; +} + +.card:hover { + border-color: var(--accent); + background: var(--surface-raised); + color: inherit; +} + +.header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 0.75rem; +} + +.title { + margin: 0; + font-size: 1rem; + font-weight: 700; + line-height: 1.35; + color: var(--text-strong); + transition: color 150ms ease; +} + +.card:hover .title { + color: var(--accent); +} + +.description { + font-size: 0.875rem; + line-height: 1.55; + color: var(--text-muted); +} + +.chevron { + display: flex; + align-items: center; + flex-shrink: 0; + margin-top: 0.0625rem; + color: var(--text-muted); + opacity: 0; + transform: translateX(-4px); + transition: + opacity 150ms ease, + transform 150ms ease, + color 150ms ease; +} + +.card:hover .chevron, +.card:focus-visible .chevron { + color: var(--accent); + opacity: 1; + transform: translateX(0); +} + +/* The affordance is decorative; keep it visible where motion is unwelcome */ +@media (prefers-reduced-motion: reduce) { + .chevron { + opacity: 1; + transform: none; + transition: none; + } +} diff --git a/src/theme/DocSidebar/Desktop/index.tsx b/src/theme/DocSidebar/Desktop/index.tsx new file mode 100644 index 00000000..c1934568 --- /dev/null +++ b/src/theme/DocSidebar/Desktop/index.tsx @@ -0,0 +1,112 @@ +import Link from '@docusaurus/Link'; +import { useColorMode, useThemeConfig } from '@docusaurus/theme-common'; +import ColorModeToggle from '@theme/ColorModeToggle'; +import type { Props } from '@theme/DocSidebar/Desktop'; +import CollapseButton from '@theme/DocSidebar/Desktop/CollapseButton'; +import Content from '@theme/DocSidebar/Desktop/Content'; +import Logo from '@theme/Logo'; +import SearchBar from '@theme/SearchBar'; +import clsx from 'clsx'; +import React, { useEffect, type ReactNode } from 'react'; +import { useRailSearch } from '@components/RailSearch/RailSearchContext'; +import styles from './styles.module.css'; + +/** + * Swizzled to move the site's chrome — logo, search and the color mode toggle — + * out of the top bar and into the sidebar, so the docs read as a single + * left-hand rail with no header above the content. + * + * This component only renders at >=997px (see @theme/DocSidebar, which switches + * to DocSidebar/Mobile below that). The top bar is hidden at the same + * breakpoint in components.css, so on mobile the navbar still supplies the + * hamburger drawer, search and toggle exactly as before. + */ + +/** + * The configured navbar links, rendered in the sidebar footer so they survive + * the top bar's removal. Only plain link items are supported — a dropdown or + * any other item type would need its own rendering, and this site has none. + */ +function SidebarNavbarItems(): ReactNode { + const { navbar } = useThemeConfig(); + + const links = navbar.items.flatMap((item) => { + // Only the props that belong on an anchor. Spreading the rest of the item + // would put config keys like `position` into the DOM as invalid attributes. + const { label, href, to, rel, target } = item as { + label?: string; + href?: string; + to?: string; + rel?: string; + target?: string; + }; + if (!label || (!href && !to)) { + return []; + } + return [ + + {label} + , + ]; + }); + + return links.length > 0 ?
{links}
: null; +} + +function SidebarColorModeToggle(): ReactNode { + const { + colorMode: { disableSwitch, respectPrefersColorScheme }, + } = useThemeConfig(); + const { colorModeChoice, setColorMode } = useColorMode(); + + if (disableSwitch) { + return null; + } + + return ( + + ); +} + +function DocSidebarDesktop({ path, sidebar, onCollapse, isHidden }: Props): ReactNode { + const { + docs: { + sidebar: { hideable }, + }, + } = useThemeConfig(); + + // Tell the navbar to drop its own search while this rail is mounted, so only + // one DocSearch exists and Cmd+K opens a single modal. + const { setRailHasSearch } = useRailSearch(); + useEffect(() => { + setRailHasSearch(true); + return () => setRailHasSearch(false); + }, [setRailHasSearch]); + + return ( +
+
+ +
+ +
+
+ + + +
+ + +
+ + {hideable && } +
+ ); +} + +export default React.memo(DocSidebarDesktop); diff --git a/src/theme/DocSidebar/Desktop/styles.module.css b/src/theme/DocSidebar/Desktop/styles.module.css new file mode 100644 index 00000000..00f80f8f --- /dev/null +++ b/src/theme/DocSidebar/Desktop/styles.module.css @@ -0,0 +1,118 @@ +/* + * Replaces theme-classic's DocSidebar/Desktop styles. The upstream rules + * reserve `--ifm-navbar-height` of padding for the top bar; that bar is hidden + * on desktop, so the sidebar owns the full viewport height instead. + */ + +@media (min-width: 997px) { + .sidebar { + display: flex; + flex-direction: column; + height: 100%; + width: var(--doc-sidebar-width); + } + + .sidebarHidden { + opacity: 0; + visibility: hidden; + } +} + +/* ------------------------------------------------------------- header - */ + +.header { + display: flex; + flex-direction: column; + gap: 0.75rem; + padding: 1rem 0.75rem 0.75rem; +} + +.brand { + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + /* Sits on top of the header's 0.75rem gap, so the logo breathes above search */ + margin-bottom: 1rem; + color: inherit !important; + text-decoration: none !important; +} + +/* + * Outside the navbar the logo has no inherited size, so constrain the + * itself. ThemedImage renders light/dark variants, hence the descendant + * selector rather than relying on imageClassName alone. + */ +.brandImage, +.brand img { + height: 48px; + width: auto; + max-width: 100%; +} + +/* The title is hidden for sighted users but kept in the DOM for crawlers */ +.brandTitle { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +.search { + display: block; +} + +/* + * The search button is sized for a navbar; stretch it to the rail's width. + * `:global` because DocSearch's classes come from the Algolia theme, not this + * CSS module. + */ +.search :global(.DocSearch-Button) { + /* + * DocSearch ships the button as content-box, so a plain width:100% overflows + * its container by the horizontal padding and border. Two classes here also + * out-rank DocSearch's own single-class rule regardless of load order. + */ + box-sizing: border-box; + width: 100%; + margin: 0; +} + +/* ------------------------------------------------------------- footer - */ + +.footer { + margin-top: auto; + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.5rem; + padding: 0.75rem; + border-top: 1px solid var(--border-color); +} + +.footerLinks { + display: flex; + flex-direction: column; + gap: 0.25rem; + min-width: 0; +} + +.footerLink { + font-family: var(--font-display); + font-size: 0.8125rem; + font-weight: 500; + color: var(--text-muted); +} + +.footerLink:hover { + color: var(--accent); + text-decoration: none; +} + +.colorModeToggle { + flex-shrink: 0; +} diff --git a/src/theme/Navbar/Search/index.tsx b/src/theme/Navbar/Search/index.tsx new file mode 100644 index 00000000..a5a86d4d --- /dev/null +++ b/src/theme/Navbar/Search/index.tsx @@ -0,0 +1,27 @@ +import Search from '@theme-original/Navbar/Search'; +import type { Props } from '@theme/Navbar/Search'; +import React, { type ReactNode } from 'react'; +import { useRailSearch } from '@components/RailSearch/RailSearchContext'; + +/** + * Suppresses the navbar's search while the sidebar rail is rendering one. + * + * Hiding the navbar with CSS leaves its SearchBar mounted, and every DocSearch + * instance binds its own Cmd+K listener, so two modals opened at once. Returning + * null keeps the child from mounting at all. + * + * The rail reports its own presence (see RailSearchContext) rather than this + * inferring it: `useDocsSidebar` throws outside its provider, and the route is + * no help either — the docs plugin owns `/docs/**`, so a 404 under that prefix + * is indistinguishable from a real doc. Suppressing on the route would leave + * the 404 with no search at all. + */ +export default function NavbarSearchWrapper(props: Props): ReactNode { + const { railHasSearch } = useRailSearch(); + + if (railHasSearch) { + return null; + } + + return ; +} diff --git a/src/theme/Root/index.tsx b/src/theme/Root/index.tsx new file mode 100644 index 00000000..4d11449f --- /dev/null +++ b/src/theme/Root/index.tsx @@ -0,0 +1,10 @@ +import React, { type ReactNode } from 'react'; +import { RailSearchProvider } from '@components/RailSearch/RailSearchContext'; + +/** + * Root wraps everything, so it is the only place whose context both the navbar + * and the doc sidebar can read. + */ +export default function Root({ children }: { children: ReactNode }): ReactNode { + return {children}; +} diff --git a/src/theme/TOC/index.tsx b/src/theme/TOC/index.tsx new file mode 100644 index 00000000..8f4d0b3f --- /dev/null +++ b/src/theme/TOC/index.tsx @@ -0,0 +1,27 @@ +import TOC from '@theme-original/TOC'; +import type TOCType from '@theme/TOC'; +import React, { useId, type ComponentProps, type ReactNode } from 'react'; +import styles from './styles.module.css'; + +type Props = ComponentProps; + +/** + * Wraps the in-page table of contents with a heading and a nav landmark. + * + * Upstream renders a bare list in a plain
, so the column arrives with no + * label explaining what it is and no landmark for assistive tech to jump to. + * `aria-labelledby` points the landmark at the visible heading rather than + * duplicating the string in an aria-label. + */ +export default function TOCWrapper(props: Props): ReactNode { + const labelId = useId(); + + return ( + + ); +} diff --git a/src/theme/TOC/styles.module.css b/src/theme/TOC/styles.module.css new file mode 100644 index 00000000..ef3981d7 --- /dev/null +++ b/src/theme/TOC/styles.module.css @@ -0,0 +1,30 @@ +/* + * The wrapper owns the sticky positioning so the heading pins while the list + * scrolls beneath it; upstream puts both on the list container itself. + */ +.container { + position: sticky; + top: 1rem; + display: flex; + flex-direction: column; + max-height: calc(100vh - 2rem); +} + +.container > div[class*='tableOfContents'] { + position: static; + top: auto; + max-height: none; + min-height: 0; + overflow-y: auto; +} + +.label { + font-family: var(--font-display); + font-size: var(--label-size); + font-weight: 700; + letter-spacing: var(--label-tracking); + text-transform: uppercase; + color: var(--text-strong); + /* Matches the links' horizontal padding so the whole column shares one edge */ + padding: 0 0 0.5rem 0.75rem; +} diff --git a/static/img/nebula-docs-og.jpg b/static/img/nebula-docs-og.jpg new file mode 100644 index 00000000..4b5fae52 Binary files /dev/null and b/static/img/nebula-docs-og.jpg differ diff --git a/static/img/nebula-docs-og.png b/static/img/nebula-docs-og.png deleted file mode 100644 index 36567062..00000000 Binary files a/static/img/nebula-docs-og.png and /dev/null differ