diff --git a/packages/docusaurus-theme/css/code.css b/packages/docusaurus-theme/css/code.css new file mode 100644 index 0000000..71d13dc --- /dev/null +++ b/packages/docusaurus-theme/css/code.css @@ -0,0 +1,65 @@ +/** + * Code block syntax theme. + * + * Colors are driven by CSS variables (see vars.css for light, vars-dark.css for dark), + * so a consuming site can retune any hue by overriding a single --code-* variable, and + * the change hot-reloads with no server restart. + * + * Pair this with the prism-react-renderer theme exported from + * '@netfoundry/docusaurus-theme/node' (prismTheme), which maps the code block's base + * foreground/background to --code-fg / --code-bg and leaves every token color to these rules. + */ + +/* Soft rounding on the whole block */ +div[class*='codeBlockContainer'] { + border-radius: 10px; + overflow: hidden; +} + +.token.comment, +.token.prolog, +.token.cdata, +.token.doctype { + color: var(--code-comment); + font-style: italic; +} +.token.punctuation { color: var(--code-punctuation); } +.token.namespace { opacity: 0.7; } + +.token.keyword, +.token.atrule, +.token.important, +.token.rule { color: var(--code-keyword); } + +.token.string, +.token.char, +.token.attr-value, +.token.regex, +.token.inserted { color: var(--code-string); } + +.token.function, +.token.deleted { color: var(--code-function); } + +.token.number, +.token.boolean, +.token.constant, +.token.symbol { color: var(--code-number); } + +.token.class-name, +.token.tag, +.token.selector, +.token.builtin { color: var(--code-class); } + +.token.operator, +.token.entity, +.token.url { color: var(--code-operator); } + +.token.property, +.token.attr-name { color: var(--code-property); } + +/* Command-line flags (e.g. -k, --username) and shell variables: muted accent */ +.token.variable, +.token.parameter { color: var(--code-flag); } + +.token.bold { font-weight: 600; } +.token.italic { font-style: italic; } diff --git a/packages/docusaurus-theme/css/theme.css b/packages/docusaurus-theme/css/theme.css index 276aa22..16b7857 100644 --- a/packages/docusaurus-theme/css/theme.css +++ b/packages/docusaurus-theme/css/theme.css @@ -19,6 +19,9 @@ /* Legacy design system variables and comprehensive styling */ @import "./legacy.css"; +/* Code block syntax theme (token colors driven by the --code-* vars) */ +@import "./code.css"; + /* ── Footer social link hover ───────────────────────────────────────────── */ footer a[class*="footerSocialLink"] { transition: all 0.2s ease; diff --git a/packages/docusaurus-theme/css/vars-dark.css b/packages/docusaurus-theme/css/vars-dark.css index 2975de6..44cb06a 100644 --- a/packages/docusaurus-theme/css/vars-dark.css +++ b/packages/docusaurus-theme/css/vars-dark.css @@ -4,4 +4,21 @@ html[data-theme="dark"] { .container { /*background: orange;*/ } +} + +/* Code block syntax palette (dark): Tokyo Night inspired. + Token rules that consume these live in code.css. */ +html[data-theme="dark"] { + --code-bg: #1a1b26; + --code-fg: #a9b1d6; + --code-comment: #565f89; + --code-punctuation: #9aa5ce; + --code-keyword: #bb9af7; /* violet */ + --code-string: #9ece6a; /* green */ + --code-function: #7aa2f7; /* blue */ + --code-number: #ff9e64; /* orange */ + --code-class: #2ac3de; /* cyan */ + --code-operator: #89ddff; /* sky */ + --code-property: #7dcfff; /* light sky */ + --code-flag: #7f9cc0; /* soft steel blue, for CLI flags */ } \ No newline at end of file diff --git a/packages/docusaurus-theme/css/vars.css b/packages/docusaurus-theme/css/vars.css index 77bf260..aa8976a 100644 --- a/packages/docusaurus-theme/css/vars.css +++ b/packages/docusaurus-theme/css/vars.css @@ -22,4 +22,23 @@ .container { /*background: sandybrown;*/ } +} + +/* Code block syntax palette (light): high-contrast, GitHub Light inspired. + Token rules that consume these live in code.css. */ +:root { + --code-bg: #f6f8fa; + --code-fg: #1f2328; + --code-comment: #6e7781; + --code-punctuation: #57606a; + --code-keyword: #cf222e; /* red */ + --code-string: #0a3069; /* navy */ + --code-function: #8250df; /* violet */ + --code-number: #0550ae; /* blue */ + --code-class: #116329; /* green */ + --code-operator: #57606a; /* slate */ + --code-property: #953800; /* amber */ + --code-flag: #4c6b8a; /* steel blue, for CLI flags */ + --prism-background-color: var(--code-bg); + --prism-color: var(--code-fg); } \ No newline at end of file diff --git a/packages/docusaurus-theme/package.json b/packages/docusaurus-theme/package.json index ea70306..f26e6aa 100644 --- a/packages/docusaurus-theme/package.json +++ b/packages/docusaurus-theme/package.json @@ -1,6 +1,6 @@ { "name": "@netfoundry/docusaurus-theme", - "version": "0.15.2", + "version": "0.16.0", "description": "NetFoundry Docusaurus theme with shared layout, footer, and styling", "main": "dist/src/index.js", "types": "dist/src/index.d.ts", diff --git a/packages/docusaurus-theme/src/node.ts b/packages/docusaurus-theme/src/node.ts index 1350193..dcbc238 100644 --- a/packages/docusaurus-theme/src/node.ts +++ b/packages/docusaurus-theme/src/node.ts @@ -4,3 +4,4 @@ * Import from '@netfoundry/docusaurus-theme/node' in Node.js contexts. */ export * from './docusaurus-envhelper'; +export * from './prism'; diff --git a/packages/docusaurus-theme/src/prism.ts b/packages/docusaurus-theme/src/prism.ts new file mode 100644 index 0000000..3c84f7d --- /dev/null +++ b/packages/docusaurus-theme/src/prism.ts @@ -0,0 +1,40 @@ +/** + * Prism (code block) theme for docusaurus.config.ts `themeConfig.prism`. + * + * Intentionally minimal: it only maps the base foreground/background to the --code-fg / + * --code-bg CSS variables. Every token color is driven by css/code.css (auto-loaded via + * theme.css), so the whole syntax palette lives in CSS and hot-reloads with no server restart. + * Light and dark palettes are selected in CSS via [data-theme], so the same object is used for + * both `theme` and `darkTheme`. + * + * Usage: + * import {prismTheme} from '@netfoundry/docusaurus-theme/node'; + * // ... + * prism: { theme: prismTheme, darkTheme: prismTheme, additionalLanguages: [...] } + */ +export const prismTheme = { + plain: { + color: 'var(--code-fg)', + backgroundColor: 'var(--code-bg)', + }, + styles: [] as Array<{types: string[]; style: Record}>, +}; + +/** + * Recommended Prism languages to load beyond the prism-react-renderer defaults + * (typescript, yaml, json, markup, css, etc. are already bundled). + * + * `additionalLanguages` is a site-level themeConfig option, so each site must still + * reference this in its own config. Keeping the list here makes it the single source of truth: + * prism: { theme: prismTheme, darkTheme: prismTheme, additionalLanguages: prismAdditionalLanguages } + * Sites can spread and extend it: [...prismAdditionalLanguages, 'rust']. + */ +export const prismAdditionalLanguages = [ + 'bash', + 'go', + 'python', + 'java', + 'csharp', + 'docker', + 'scala', +]; diff --git a/packages/test-site/docs/code-theme/_category_.yml b/packages/test-site/docs/code-theme/_category_.yml new file mode 100644 index 0000000..0136111 --- /dev/null +++ b/packages/test-site/docs/code-theme/_category_.yml @@ -0,0 +1,2 @@ +label: "Code theme" +position: 9 diff --git a/packages/test-site/docs/code-theme/bash.md b/packages/test-site/docs/code-theme/bash.md new file mode 100644 index 0000000..2b21141 --- /dev/null +++ b/packages/test-site/docs/code-theme/bash.md @@ -0,0 +1,48 @@ +--- +sidebar_label: "Bash" +--- + +# Theme test: Bash + +```bash +#!/usr/bin/env bash +# provision a quickstart overlay and a first service +set -euo pipefail + +CTRL_ADDR="${CTRL_ADDR:-quickstart.demo.openziti.io}" +PORT=1280 +RETRIES=5 + +log() { + local level="$1"; shift + printf '[%s] %s\n' "${level^^}" "$*" >&2 +} + +wait_for_controller() { + local url="https://${CTRL_ADDR}:${PORT}/edge/client/v1/version" + for ((i = 1; i <= RETRIES; i++)); do + if curl -sk -o /dev/null -w '%{http_code}' "$url" | grep -q '^200$'; then + log info "controller ready after ${i} attempt(s)" + return 0 + fi + sleep 2 + done + log error "controller never came up" + return 1 +} + +main() { + ziti run quickstart --zac --ctrl-address "$CTRL_ADDR" & + local pid=$! + trap 'kill "$pid" 2>/dev/null || true' EXIT + + wait_for_controller || exit 1 + + ziti login "localhost:${PORT}" --username admin --password "${ZITI_PWD:-admin}" + local routers + routers=$(ziti list edge-routers 'name contains "quickstart"' --output-json | jq '.data | length') + log info "found ${routers} matching router(s)" +} + +main "$@" +``` diff --git a/packages/test-site/docs/code-theme/config.md b/packages/test-site/docs/code-theme/config.md new file mode 100644 index 0000000..758e194 --- /dev/null +++ b/packages/test-site/docs/code-theme/config.md @@ -0,0 +1,59 @@ +--- +sidebar_label: "Config (YAML / JSON / Docker)" +--- + +# Theme test: config formats + +## YAML + +```yaml +# controller web listener with the console bound +web: + - name: client-management + bindPoints: + - interface: 0.0.0.0:1280 + address: quickstart.demo.openziti.io:1280 + options: + idleTimeout: 5000ms + minTLSVersion: TLS1.2 + apis: + - binding: edge-management + - binding: spa + options: + location: ./console + indexFile: index.html + enabled: true # serve the admin console at /zac/ +``` + +## JSON + +```json +{ + "protocols": ["tcp"], + "addresses": ["first-service.ziti"], + "portRanges": [{ "low": 1280, "high": 1280 }], + "encryptionRequired": true, + "tags": { + "owner": "get-started", + "createdAt": "2026-07-06T00:00:00Z", + "retries": 3, + "ephemeral": false + } +} +``` + +## Dockerfile + +```dockerfile +FROM golang:1.26 AS build +WORKDIR /src +COPY go.mod go.sum ./ +RUN go mod download +COPY . . +RUN CGO_ENABLED=0 go build -o /out/ziti ./cmd/ziti + +FROM gcr.io/distroless/static:nonroot +COPY --from=build /out/ziti /usr/local/bin/ziti +EXPOSE 1280 3022 +ENTRYPOINT ["ziti", "run", "quickstart", "--zac"] +``` diff --git a/packages/test-site/docs/code-theme/go.md b/packages/test-site/docs/code-theme/go.md new file mode 100644 index 0000000..311c981 --- /dev/null +++ b/packages/test-site/docs/code-theme/go.md @@ -0,0 +1,62 @@ +--- +sidebar_label: "Go" +--- + +# Theme test: Go + +```go +// Package overlay dials a Ziti service and echoes a request over it. +package overlay + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/openziti/sdk-golang/ziti" +) + +const ( + defaultTimeout = 5 * time.Second + maxRetries = 3 +) + +// ErrNoService is returned when the named service cannot be reached. +var ErrNoService = errors.New("overlay: service unavailable") + +// Client wraps a Ziti context and a small retry policy. +type Client struct { + ctx ziti.Context + service string + retries int +} + +// Dialer is anything that can open a connection by service name. +type Dialer interface { + Dial(service string) (context.Context, error) +} + +// NewClient builds a Client, defaulting retries when unset. +func NewClient(ctx ziti.Context, service string, opts ...func(*Client)) *Client { + c := &Client{ctx: ctx, service: service, retries: maxRetries} + for _, opt := range opts { + opt(c) + } + return c +} + +func (c *Client) Fetch(ctx context.Context) (n int, err error) { + for attempt := 1; attempt <= c.retries; attempt++ { + conn, dialErr := c.ctx.Dial(c.service) + if dialErr != nil { + err = fmt.Errorf("attempt %d: %w", attempt, ErrNoService) + time.Sleep(time.Duration(attempt) * 250 * time.Millisecond) + continue + } + defer conn.Close() + return conn.Write([]byte("GET /health\r\n")) + } + return 0, err +} +``` diff --git a/packages/test-site/docs/code-theme/java-csharp.md b/packages/test-site/docs/code-theme/java-csharp.md new file mode 100644 index 0000000..1d181ca --- /dev/null +++ b/packages/test-site/docs/code-theme/java-csharp.md @@ -0,0 +1,81 @@ +--- +sidebar_label: "Java & C#" +--- + +# Theme test: Java and C# + +## Java + +```java +package io.openziti.demo; + +import java.time.Duration; +import java.util.List; +import java.util.Objects; + +/** Reaches a dark service over an OpenZiti overlay. */ +public final class OverlayClient { + private static final int DEFAULT_PORT = 1280; + private static final Duration TIMEOUT = Duration.ofSeconds(5); + + private final String service; + private final List roles; + + public OverlayClient(String service, List roles) { + this.service = Objects.requireNonNull(service, "service"); + this.roles = List.copyOf(roles); + } + + public boolean authorized(String role) { + return roles.contains(role) || "#all".equals(role); + } + + public String fetch(int retries) throws InterruptedException { + for (int attempt = 1; attempt <= retries; attempt++) { + try { + return "200 OK from " + service + " on :" + DEFAULT_PORT; + } catch (RuntimeException ex) { + System.err.printf("attempt %d failed: %s%n", attempt, ex.getMessage()); + Thread.sleep(TIMEOUT.toMillis() / retries); + } + } + throw new IllegalStateException("unreachable: " + service); + } +} +``` + +## C# + +```csharp +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace OpenZiti.Demo; + +/// Reaches a dark service over an OpenZiti overlay. +public sealed record OverlayClient(string Service, IReadOnlyList Roles) +{ + private const int DefaultPort = 1280; + + public bool Authorized(string role) => + Roles.Contains(role) || role is "#all"; + + public async Task FetchAsync(int retries = 3) + { + for (var attempt = 1; attempt <= retries; attempt++) + { + try + { + await Task.Delay(TimeSpan.FromMilliseconds(250 * attempt)); + return $"200 OK from {Service} on :{DefaultPort}"; + } + catch (Exception ex) when (attempt < retries) + { + Console.Error.WriteLine($"attempt {attempt} failed: {ex.Message}"); + } + } + throw new InvalidOperationException($"unreachable: {Service}"); + } +} +``` diff --git a/packages/test-site/docs/code-theme/js-ts.md b/packages/test-site/docs/code-theme/js-ts.md new file mode 100644 index 0000000..f111ffd --- /dev/null +++ b/packages/test-site/docs/code-theme/js-ts.md @@ -0,0 +1,49 @@ +--- +sidebar_label: "TypeScript" +--- + +# Theme test: TypeScript + +```typescript +import { connect, type Connection } from '@openziti/ziti-sdk-nodejs'; + +/** Roles that may dial the demo service. */ +type Role = `#${string}` | '@all'; + +interface ServiceSpec { + readonly name: string; + readonly intercept: string; + readonly port: number; + readonly roles: Role[]; +} + +const DEFAULT_PORT = 1280 as const; +const BACKOFF_MS = [250, 500, 1_000]; + +const spec: ServiceSpec = { + name: 'first-service', + intercept: 'first-service.ziti', + port: DEFAULT_PORT, + roles: ['#first-service-clients'], +}; + +async function fetchWithRetry( + svc: ServiceSpec, + retries = 3, +): Promise> { + for (let attempt = 1; attempt <= retries; attempt++) { + try { + const conn: Connection = await connect(svc.name); + const body = await conn.request(`https://${svc.intercept}:${svc.port}/version`); + return { service: svc.name, status: 200, body }; + } catch (err) { + const wait = BACKOFF_MS[attempt - 1] ?? 1_000; + console.warn(`attempt ${attempt} failed: ${(err as Error).message}, waiting ${wait}ms`); + await new Promise((r) => setTimeout(r, wait)); + } + } + throw new Error(`${svc.name} unreachable after ${retries} attempts`); +} + +fetchWithRetry(spec).then((res) => console.log(JSON.stringify(res, null, 2))); +``` diff --git a/packages/test-site/docs/code-theme/python.md b/packages/test-site/docs/code-theme/python.md new file mode 100644 index 0000000..154d54d --- /dev/null +++ b/packages/test-site/docs/code-theme/python.md @@ -0,0 +1,58 @@ +--- +sidebar_label: "Python" +--- + +# Theme test: Python + +```python +"""Minimal async client that reaches a dark service over an OpenZiti overlay.""" +from __future__ import annotations + +import asyncio +from dataclasses import dataclass, field +from enum import Enum +from typing import Final + +DEFAULT_PORT: Final[int] = 1280 +_RETRY_BACKOFF: Final[tuple[float, ...]] = (0.25, 0.5, 1.0) + + +class State(Enum): + IDLE = "idle" + DIALING = "dialing" + READY = "ready" + + +@dataclass(slots=True) +class Service: + name: str + intercept: str + port: int = DEFAULT_PORT + attributes: list[str] = field(default_factory=lambda: ["first-service-clients"]) + + @property + def address(self) -> str: + return f"{self.intercept}:{self.port}" + + def matches(self, role: str) -> bool: + return role in self.attributes or role == "#all" + + +async def fetch(service: Service, *, retries: int = 3) -> dict[str, str]: + state = State.DIALING + for attempt, delay in enumerate(_RETRY_BACKOFF[:retries], start=1): + try: + # pretend this dials the overlay and returns JSON + await asyncio.sleep(delay) + state = State.READY + return {"service": service.name, "state": state.value, "attempt": str(attempt)} + except (ConnectionError, TimeoutError) as exc: + print(f"attempt {attempt} for {service.address!r} failed: {exc}") + raise RuntimeError(f"{service.name} unreachable after {retries} attempts") + + +if __name__ == "__main__": + svc = Service(name="first-service", intercept="first-service.ziti") + result = asyncio.run(fetch(svc)) + print(result) +``` diff --git a/packages/test-site/docusaurus.config.ts b/packages/test-site/docusaurus.config.ts index 5fc009a..df0dffa 100644 --- a/packages/test-site/docusaurus.config.ts +++ b/packages/test-site/docusaurus.config.ts @@ -1,8 +1,8 @@ import type { Config, PluginConfig } from '@docusaurus/types'; import type * as Preset from '@docusaurus/preset-classic'; import path from "node:path" -import {themes as prismThemes} from 'prism-react-renderer'; import {subsitePickerColumns} from "@netfoundry/docusaurus-theme"; +import {prismTheme, prismAdditionalLanguages} from "@netfoundry/docusaurus-theme/node"; import {zrokDocsPluginConfig} from "./remotes/zrok/website/docusaurus-plugin-zrok-docs"; import {frontdoorDocsPluginConfig} from "./remotes/frontdoor/docusaurus/docusaurus-plugin-frontdoor-docs"; import {onpremDocsPluginConfig} from "./remotes/onprem/docusaurus/docusaurus-plugin-onprem-docs"; @@ -259,8 +259,9 @@ frontdoorDocsPluginConfig(`${frontdoor}/docusaurus`, REMARK_MAPPINGS, 'docs/fron ], }, prism: { - theme: prismThemes.github, - darkTheme: prismThemes.dracula, + theme: prismTheme, + darkTheme: prismTheme, + additionalLanguages: prismAdditionalLanguages, }, algolia: { appId: cfg.algolia.appId,