diff --git a/.impeccable/design.json b/.impeccable/design.json new file mode 100644 index 0000000..a8d5cd6 --- /dev/null +++ b/.impeccable/design.json @@ -0,0 +1,445 @@ +{ + "schemaVersion": 2, + "generatedAt": "2026-11-02T15:17:38+00:00", + "title": "Design System: Crosswind", + "extensions": { + "colorMeta": { + "bg": { + "role": "neutral", + "displayName": "Graphite Ground", + "canonical": "#191b1d", + "tonalRamp": [ + "oklch(15% 0.0050 248.1)", + "oklch(26% 0.0050 248.1)", + "oklch(37% 0.0050 248.1)", + "oklch(48% 0.0050 248.1)", + "oklch(60% 0.0050 248.1)", + "oklch(71% 0.0050 248.1)", + "oklch(83% 0.0050 248.1)", + "oklch(95% 0.0050 248.1)" + ] + }, + "surface": { + "role": "neutral", + "displayName": "Panel Graphite", + "canonical": "#202225", + "tonalRamp": [ + "oklch(15% 0.0064 258.4)", + "oklch(26% 0.0064 258.4)", + "oklch(37% 0.0064 258.4)", + "oklch(48% 0.0064 258.4)", + "oklch(60% 0.0064 258.4)", + "oklch(71% 0.0064 258.4)", + "oklch(83% 0.0064 258.4)", + "oklch(95% 0.0064 258.4)" + ] + }, + "elevated": { + "role": "neutral", + "displayName": "Raised Graphite", + "canonical": "#282b2e", + "tonalRamp": [ + "oklch(15% 0.0070 248.1)", + "oklch(26% 0.0070 248.1)", + "oklch(37% 0.0070 248.1)", + "oklch(48% 0.0070 248.1)", + "oklch(60% 0.0070 248.1)", + "oklch(71% 0.0070 248.1)", + "oklch(83% 0.0070 248.1)", + "oklch(95% 0.0070 248.1)" + ] + }, + "border": { + "role": "neutral", + "displayName": "Hairline", + "canonical": "#34373b", + "tonalRamp": [ + "oklch(15% 0.0081 255.6)", + "oklch(26% 0.0081 255.6)", + "oklch(37% 0.0081 255.6)", + "oklch(48% 0.0081 255.6)", + "oklch(60% 0.0081 255.6)", + "oklch(71% 0.0081 255.6)", + "oklch(83% 0.0081 255.6)", + "oklch(95% 0.0081 255.6)" + ] + }, + "border-strong": { + "role": "neutral", + "displayName": "Hover Hairline", + "canonical": "#484c51", + "tonalRamp": [ + "oklch(15% 0.0098 253.9)", + "oklch(26% 0.0098 253.9)", + "oklch(37% 0.0098 253.9)", + "oklch(48% 0.0098 253.9)", + "oklch(60% 0.0098 253.9)", + "oklch(71% 0.0098 253.9)", + "oklch(83% 0.0098 253.9)", + "oklch(95% 0.0098 253.9)" + ] + }, + "text": { + "role": "neutral", + "displayName": "Soft White", + "canonical": "#e6e7e8", + "tonalRamp": [ + "oklch(15% 0.0017 247.8)", + "oklch(26% 0.0017 247.8)", + "oklch(37% 0.0017 247.8)", + "oklch(48% 0.0017 247.8)", + "oklch(60% 0.0017 247.8)", + "oklch(71% 0.0017 247.8)", + "oklch(83% 0.0017 247.8)", + "oklch(95% 0.0017 247.8)" + ] + }, + "text-2": { + "role": "neutral", + "displayName": "Secondary Grey", + "canonical": "#92979e", + "tonalRamp": [ + "oklch(15% 0.0118 256.7)", + "oklch(26% 0.0118 256.7)", + "oklch(37% 0.0118 256.7)", + "oklch(48% 0.0118 256.7)", + "oklch(60% 0.0118 256.7)", + "oklch(71% 0.0118 256.7)", + "oklch(83% 0.0118 256.7)", + "oklch(95% 0.0118 256.7)" + ] + }, + "label": { + "role": "neutral", + "displayName": "Label Grey", + "canonical": "#7f858c", + "tonalRamp": [ + "oklch(15% 0.0127 252.2)", + "oklch(26% 0.0127 252.2)", + "oklch(37% 0.0127 252.2)", + "oklch(48% 0.0127 252.2)", + "oklch(60% 0.0127 252.2)", + "oklch(71% 0.0127 252.2)", + "oklch(83% 0.0127 252.2)", + "oklch(95% 0.0127 252.2)" + ] + }, + "muted": { + "role": "neutral", + "displayName": "Decoration Grey", + "canonical": "#686d74", + "tonalRamp": [ + "oklch(15% 0.0125 256.7)", + "oklch(26% 0.0125 256.7)", + "oklch(37% 0.0125 256.7)", + "oklch(48% 0.0125 256.7)", + "oklch(60% 0.0125 256.7)", + "oklch(71% 0.0125 256.7)", + "oklch(83% 0.0125 256.7)", + "oklch(95% 0.0125 256.7)" + ] + }, + "accent": { + "role": "primary", + "displayName": "Terminal Teal", + "canonical": "#67cdbb", + "tonalRamp": [ + "oklch(15% 0.0995 180.6)", + "oklch(26% 0.0995 180.6)", + "oklch(37% 0.0995 180.6)", + "oklch(48% 0.0995 180.6)", + "oklch(60% 0.0995 180.6)", + "oklch(71% 0.0995 180.6)", + "oklch(83% 0.0995 180.6)", + "oklch(95% 0.0995 180.6)" + ] + }, + "purple": { + "role": "secondary", + "displayName": "Muted Command Purple", + "canonical": "#a99fd6", + "tonalRamp": [ + "oklch(15% 0.0794 292.7)", + "oklch(26% 0.0794 292.7)", + "oklch(37% 0.0794 292.7)", + "oklch(48% 0.0794 292.7)", + "oklch(60% 0.0794 292.7)", + "oklch(71% 0.0794 292.7)", + "oklch(83% 0.0794 292.7)", + "oklch(95% 0.0794 292.7)" + ] + }, + "amber": { + "role": "tertiary", + "displayName": "Low-Saturation Amber", + "canonical": "#d0ae74", + "tonalRamp": [ + "oklch(15% 0.0852 80.3)", + "oklch(26% 0.0852 80.3)", + "oklch(37% 0.0852 80.3)", + "oklch(48% 0.0852 80.3)", + "oklch(60% 0.0852 80.3)", + "oklch(71% 0.0852 80.3)", + "oklch(83% 0.0852 80.3)", + "oklch(95% 0.0852 80.3)" + ] + }, + "code-function": { + "role": "neutral", + "displayName": "Pale Function Blue", + "canonical": "#b9d4e6", + "tonalRamp": [ + "oklch(15% 0.0383 237.1)", + "oklch(26% 0.0383 237.1)", + "oklch(37% 0.0383 237.1)", + "oklch(48% 0.0383 237.1)", + "oklch(60% 0.0383 237.1)", + "oklch(71% 0.0383 237.1)", + "oklch(83% 0.0383 237.1)", + "oklch(95% 0.0383 237.1)" + ] + } + }, + "typographyMeta": { + "display": { + "displayName": "Display", + "purpose": "The home page heading only. 1.6rem at 960px wide and below." + }, + "headline": { + "displayName": "Headline", + "purpose": "The H1 of each doc page." + }, + "title": { + "displayName": "Title", + "purpose": "H2 sections, each under a 1px top rule." + }, + "subtitle": { + "displayName": "Subtitle", + "purpose": "H3, and the label column on home page sections." + }, + "body": { + "displayName": "Body", + "purpose": "Prose, capped at 72ch." + }, + "chrome": { + "displayName": "Chrome", + "purpose": "Tabs, sidebar links, buttons and the search input." + }, + "label": { + "displayName": "Label", + "purpose": "Sidebar section names and small captions." + }, + "code": { + "displayName": "Code", + "purpose": "Code panels and the command." + }, + "mono-label": { + "displayName": "Mono label", + "purpose": "File names, callout labels and the copied toast." + }, + "mono-micro": { + "displayName": "Mono micro", + "purpose": "Badges and keycaps." + } + }, + "shadows": [ + { + "name": "float", + "value": "0 8px 24px rgb(0 0 0 / 28%)", + "purpose": "Open search, the copied toast and the diagram tooltip." + }, + { + "name": "palette-contact", + "value": "0 1px 2px rgb(0 0 0 / 20%)", + "purpose": "The package palette at rest. The only resting shadow." + } + ], + "motion": [ + { + "name": "fast", + "value": "140ms", + "purpose": "Colour, border and background state changes." + }, + { + "name": "ease-out", + "value": "cubic-bezier(0.23, 1, 0.32, 1)", + "purpose": "Palette pane entry and diagram messages." + }, + { + "name": "pane-in", + "value": "160ms", + "purpose": "Palette pane entry, with a 2px rise." + }, + { + "name": "press", + "value": "120ms", + "purpose": "Opacity on button press and scale on the copy icon." + } + ], + "breakpoints": [ + { + "name": "diagram-compact", + "value": "720px" + }, + { + "name": "tablet", + "value": "960px" + }, + { + "name": "desktop", + "value": "1220px" + } + ] + }, + "components": [ + { + "name": "Primary Button", + "kind": "button", + "refersTo": "button-primary", + "description": "The main action in a group. A teal tint, never a solid fill.", + "html": "Get started", + "css": ".ds-btn-primary{display:inline-block;padding:5.6px 13px;border:1px solid var(--cw-border);border-radius:var(--cw-radius-m);font:500 12.5px/1.45 \"Geist\", sans-serif;text-decoration:none;cursor:pointer;transition:background-color var(--cw-fast),border-color var(--cw-fast),color var(--cw-fast),opacity 120ms;border-color:color-mix(in srgb,var(--cw-accent) 42%,transparent);background:color-mix(in srgb,var(--cw-accent) 14%,var(--cw-bg));color:var(--cw-accent)} .ds-btn-primary:hover,.ds-btn-primary:focus-visible{border-color:color-mix(in srgb,var(--cw-accent) 70%,transparent);background:color-mix(in srgb,var(--cw-accent) 20%,var(--cw-bg))} .ds-btn-primary:focus-visible{outline:1px solid var(--cw-accent);outline-offset:2px} .ds-btn-primary:active{opacity:.75}" + }, + { + "name": "Default Button", + "kind": "button", + "refersTo": "button", + "description": "Every other action.", + "html": "Browse docs", + "css": ".ds-btn{display:inline-block;padding:5.6px 13px;border:1px solid var(--cw-border);border-radius:var(--cw-radius-m);font:500 12.5px/1.45 \"Geist\", sans-serif;text-decoration:none;cursor:pointer;transition:background-color var(--cw-fast),border-color var(--cw-fast),color var(--cw-fast),opacity 120ms;background:var(--cw-surface);color:var(--cw-text)} .ds-btn:hover,.ds-btn:focus-visible{border-color:var(--cw-border-strong);background:var(--cw-elevated)} .ds-btn:focus-visible{outline:1px solid var(--cw-accent);outline-offset:2px} .ds-btn:active{opacity:.75}" + }, + { + "name": "Search Field", + "kind": "input", + "refersTo": "input-search", + "description": "Header search with its shortcut shown as a keycap. Ctrl K opens it as a command palette.", + "html": "", + "css": ".ds-search{display:flex;align-items:center;gap:8px;width:220px;height:28px;padding:0 5px 0 7px;border:1px solid var(--cw-border);border-radius:var(--cw-radius-m);background:var(--cw-surface);color:var(--cw-text-2);transition:border-color var(--cw-fast)} .ds-search:hover,.ds-search:focus-within{border-color:var(--cw-border-strong)} .ds-search svg{width:14px;height:14px;flex-shrink:0;fill:none;stroke:currentcolor;stroke-linecap:round;stroke-width:1.25} .ds-search input{flex:1;min-width:0;border:0;outline:0;background:none;color:var(--cw-text);caret-color:var(--cw-accent);font:400 12.5px \"Geist\", sans-serif} .ds-search input::placeholder{color:var(--cw-text-2)} .ds-search kbd{display:inline-block;min-width:17px;padding:0 3.6px;border:1px solid var(--cw-border);border-radius:var(--cw-radius-s);background:var(--cw-elevated);color:var(--cw-text-2);font:400 10px/16px \"Geist Mono\", monospace;text-align:center}" + }, + { + "name": "Sidebar Navigation", + "kind": "nav", + "refersTo": "nav-link", + "description": "Section label on the page ground, Secondary Grey links, a teal square in the gutter on the active link.", + "html": "", + "css": ".ds-nav{display:grid;gap:1px;width:220px;font:400 12.5px/1.35 \"Geist\", sans-serif} .ds-nav__label{padding:2px 14px;color:var(--cw-label);font-size:11px;font-weight:500} .ds-nav a{position:relative;padding:4px 8px 4px 14px;border-radius:var(--cw-radius-m);color:var(--cw-text-2);text-decoration:none;transition:color var(--cw-fast),background-color var(--cw-fast)} .ds-nav a:hover{background:var(--cw-surface);color:var(--cw-text)} .ds-nav a:focus-visible{outline:1px solid var(--cw-accent);outline-offset:2px} .ds-nav a.is-active{background:var(--cw-elevated);color:var(--cw-text);font-weight:500} .ds-nav a.is-active::before{content:\"\";position:absolute;top:calc(50% - 2.5px);left:4px;width:5px;height:5px;border-radius:1px;background:var(--cw-accent)}" + }, + { + "name": "Badge", + "kind": "chip", + "refersTo": "badge", + "description": "Compact outlined rectangle naming a package kind. Purple only for the CLI.", + "html": "SDK CLI", + "css": ".ds-badge{display:inline-flex;align-items:center;padding:0 4px;border:1px solid var(--cw-border);border-radius:var(--cw-radius-s);color:var(--cw-text-2);font:400 10px/16px \"Geist Mono\", monospace} .ds-badge--cli{border-color:color-mix(in srgb,var(--cw-purple) 34%,transparent);color:var(--cw-purple)}" + }, + { + "name": "Code Panel", + "kind": "card", + "refersTo": "code-panel", + "description": "Real code in a bordered panel, with the file name in a raised title bar.", + "html": "
src/entry.py
class Counter(Agent):\n    initial_state = {\"count\": 0}
", + "css": ".ds-code{overflow:hidden;margin:0;border:1px solid var(--cw-border);border-radius:var(--cw-radius-l);background:var(--cw-surface)} .ds-code figcaption{padding:6px 16px;border-bottom:1px solid var(--cw-border);background:var(--cw-elevated);color:var(--cw-text-2);font:400 11px/1.5 \"Geist Mono\", monospace} .ds-code pre{margin:0;padding:13px 16px;color:var(--cw-text);font:400 12.5px/1.6 \"Geist Mono\", monospace;font-variant-ligatures:none} .ds-code .k{color:var(--cw-purple)} .ds-code .s{color:var(--cw-accent)} .ds-code .n{color:var(--cw-amber)} .ds-code .f{color:#b9d4e6}" + }, + { + "name": "Callout", + "kind": "card", + "refersTo": "callout", + "description": "Label left, text right, stacked on phones. Teal for notes, amber for warnings, red for danger.", + "html": "", + "css": ".ds-callout{--c:var(--cw-accent);display:grid;grid-template-columns:72px minmax(0,1fr);margin:0 0 8px;padding:10px 14px;border:1px solid color-mix(in srgb,var(--c) 38%,var(--cw-border));border-radius:var(--cw-radius-l);background:color-mix(in srgb,var(--c) 7%,var(--cw-surface));color:var(--cw-text);font:400 14px/1.6 \"Geist\", sans-serif} .ds-callout p{margin:0} .ds-callout .ds-callout__label{display:flex;align-items:center;gap:5px;height:22px;color:var(--c);font:400 11px/1 \"Geist Mono\", monospace} .ds-callout__label::before{content:\"\";width:6px;height:6px;border-radius:1px;background:currentcolor} .ds-callout--warn{--c:var(--cw-amber)}" + }, + { + "name": "Command", + "kind": "custom", + "refersTo": "command", + "description": "A copyable shell command in purple Geist Mono with an unselectable prompt.", + "html": "
$ uvx wynd init
", + "css": ".ds-command{display:inline-flex;align-items:center;border:1px solid var(--cw-border);border-radius:var(--cw-radius-m);background:var(--cw-surface)} .ds-command code{padding:0 3px 0 11px;color:var(--cw-purple);font:400 12.5px \"Geist Mono\", monospace} .ds-command code span{color:var(--cw-muted);user-select:none} .ds-command button{display:grid;place-items:center;width:26px;height:26px;padding:0;border:0;border-radius:var(--cw-radius-m);background:none;color:var(--cw-text-2);cursor:pointer;transition:color var(--cw-fast),background-color var(--cw-fast)} .ds-command button:hover,.ds-command button:focus-visible{background:var(--cw-elevated);color:var(--cw-text)} .ds-command button:focus-visible{outline:1px solid var(--cw-accent);outline-offset:2px} .ds-command svg{width:14px;height:14px;fill:none;stroke:currentcolor;stroke-linecap:round;stroke-width:1.25;transition:transform 120ms} .ds-command button:active svg{transform:scale(.88)}" + }, + { + "name": "Package Palette", + "kind": "custom", + "refersTo": "palette", + "description": "The signature component. Filter and arrow through packages, and the code preview swaps. Keycaps show the keys.", + "html": "
↑ ↓ preview↵ open docs
", + "css": ".ds-palette{width:340px;overflow:hidden;border:1px solid var(--cw-border);border-radius:var(--cw-radius-xl);background:var(--cw-surface);box-shadow:0 1px 2px rgb(0 0 0 / 20%);font-family:\"Geist\", sans-serif} .ds-palette__filter{display:flex;align-items:center;gap:8px;padding:0 14px;border-bottom:1px solid var(--cw-border);color:var(--cw-text-2);transition:color var(--cw-fast)} .ds-palette__filter:focus-within{color:var(--cw-accent)} .ds-palette__filter svg{width:14px;height:14px;flex-shrink:0;fill:none;stroke:currentcolor;stroke-linecap:round;stroke-width:1.25} .ds-palette__filter input{width:100%;height:40px;border:0;outline:0;background:none;color:var(--cw-text);caret-color:var(--cw-accent);font:400 13px \"Geist\", sans-serif} .ds-palette__filter input::placeholder{color:var(--cw-text-2)} .ds-palette__rows{display:grid;gap:1px;padding:6px} .ds-row{display:grid;grid-template-columns:6px auto minmax(0,1fr) auto;align-items:center;gap:8px;width:100%;padding:7.6px 8px;border:0;border-radius:var(--cw-radius-m);background:none;color:var(--cw-text-2);font:400 13px \"Geist\", sans-serif;text-align:left;cursor:pointer;transition:color var(--cw-fast),background-color var(--cw-fast)} .ds-row:hover,.ds-row.is-on{background:var(--cw-elevated);color:var(--cw-text)} .ds-row:focus-visible{outline:1px solid var(--cw-accent);outline-offset:-1px} .ds-row i{display:inline-block;width:6px;height:6px;border-radius:1px;background:var(--cw-muted)} .ds-row.is-on i{background:var(--cw-accent)} .ds-row b{font-weight:500} .ds-row code{color:var(--cw-text-2);font:400 11px \"Geist Mono\", monospace} .ds-tag{padding:0 4px;border:1px solid var(--cw-border);border-radius:var(--cw-radius-s);font:400 10px/16px \"Geist Mono\", monospace} .ds-tag--cli{border-color:color-mix(in srgb,var(--cw-purple) 34%,transparent);color:var(--cw-purple)} .ds-palette__hints{display:flex;gap:16px;padding:8px 14px;border-top:1px solid var(--cw-border);color:var(--cw-text-2);font-size:11px} .ds-palette__hints kbd{display:inline-block;min-width:17px;padding:0 3.6px;border:1px solid var(--cw-border);border-radius:var(--cw-radius-s);background:var(--cw-elevated);color:var(--cw-text-2);font:400 10px/16px \"Geist Mono\", monospace;text-align:center}" + }, + { + "name": "Status Bar", + "kind": "custom", + "description": "The footer's last row. Alpha state on the left in Geist Mono with an amber square, links on the right.", + "html": "
alphaIndependent project. Not affiliated with Cloudflare.Discord
", + "css": ".ds-status{display:flex;flex-wrap:wrap;align-items:center;gap:3px 16px;padding:7px 16px;border-top:1px solid var(--cw-border);background:var(--cw-bg);color:var(--cw-text-2);font:400 11px/1.5 \"Geist\", sans-serif} .ds-status__state{display:inline-flex;align-items:center;gap:5px;color:var(--cw-text);font-family:\"Geist Mono\", monospace} .ds-status__state i{display:inline-block;width:6px;height:6px;border-radius:1px;background:var(--cw-amber)} .ds-status__end{margin-left:auto} .ds-status a{color:var(--cw-text-2);transition:color var(--cw-fast)} .ds-status a:hover{color:var(--cw-text)} .ds-status a:focus-visible{outline:1px solid var(--cw-accent);outline-offset:2px}" + } + ], + "narrative": { + "northStar": "The Native Utility", + "overview": "The docs behave like one developer tool that a reader already has open. Slim chrome, keyboard shortcuts shown as keycaps, a status bar for a footer, and search that opens as a command palette all come from native desktop tooling. Real code is the main visual material, and the home page previews it inside a package palette.\n\nThe system is dark only and dense. Three graphite steps and 1px borders carry all structure. Colour appears in small amounts, and each colour has a job. Tiny square markers show position and state, so no icon set or illustration is needed beyond the puffin logo in the header.\n\nThe world rejects the SaaS landing page, meaning a hero with a mascot and a grid of feature cards. The site runs on MkDocs Material, and every token maps onto Material's variables, so generated API pages inherit the same look.", + "keyCharacteristics": [ + "Dark only, with no theme toggle.", + "Geist for text and chrome, Geist Mono for anything a developer would type or read as a machine value.", + "1px borders and tonal steps instead of shadows at rest.", + "Square markers, compact rectangular badges and keycaps.", + "A 140ms state transition on almost every interactive element." + ], + "rules": [ + { + "name": "The Label Ground Rule", + "body": "Label Grey sits on Graphite Ground only. Small text on a panel or a raised step uses Secondary Grey, because Label Grey drops below 4.5:1 there.", + "section": "colors" + }, + { + "name": "The Decoration Rule", + "body": "Decoration Grey never carries information a reader needs to read. If removing it would lose meaning, the element needs Secondary Grey.", + "section": "colors" + }, + { + "name": "The Purple Rule", + "body": "Purple means a command, the CLI, or a Python keyword. Links and buttons stay teal or neutral.", + "section": "colors" + }, + { + "name": "The Mono Rule", + "body": "Geist Mono is for code, commands, shortcuts, statuses and identifiers. Everything else, including every label a person wrote, is Geist.", + "section": "typography" + }, + { + "name": "The Root Rule", + "body": "The html element is pinned at 125%, which is 20px at the browser default, and Material's wide-screen growth is off. So .05rem is 1px, and every rem value in this file assumes that root.", + "section": "typography" + }, + { + "name": "The Float Rule", + "body": "A shadow means the element sits above the page. Panels in the document flow, including the header, stay flat.", + "section": "elevation" + }, + { + "name": "The Square Rule", + "body": "State is marked with a tiny square, not a dot, stripe or icon. Teal is current, Decoration Grey is idle, amber is a warning or alpha status.", + "section": "shapes" + } + ], + "dos": [ + "Do keep the page dark, using only the three graphite steps for grounds.", + "Do use 1px Hairline borders (#34373b) and thin rules to separate things.", + "Do set commands, shortcuts, statuses, package names and file names in Geist Mono.", + "Do mark the current item with a 5px or 6px teal square.", + "Do show a keycap only when the shortcut works.", + "Do write rem values against the 20px root, where .05rem is 1px.", + "Do use Secondary Grey (#92979e) for small text on Panel Graphite or Raised Graphite.", + "Do map new colours through the `--cw-*` tokens and Material's `--md-*` variables, so generated API pages inherit them." + ], + "donts": [ + "Don't add a light theme or a theme toggle.", + "Don't build a hero with a mascot or a grid of feature cards. The puffin stays in the header as the logo.", + "Don't use Decoration Grey (#686d74) for text a reader needs.", + "Don't put Label Grey (#7f858c) on Panel Graphite or Raised Graphite.", + "Don't use purple for links, buttons or general emphasis.", + "Don't raise amber's saturation, or use it for anything other than warnings, alpha status and syntax values.", + "Don't divide page sections with background bands.", + "Don't put a shadow on a panel in the document flow, other than the palette's contact shadow.", + "Don't set uppercase or tracked labels in HTML chrome.", + "Don't use radii above 10px, or pill shapes." + ] + } +} diff --git a/.impeccable/review/api-d.png b/.impeccable/review/api-d.png new file mode 100644 index 0000000..3a32c38 Binary files /dev/null and b/.impeccable/review/api-d.png differ diff --git a/.impeccable/review/desktop.png b/.impeccable/review/desktop.png new file mode 100644 index 0000000..e32e720 Binary files /dev/null and b/.impeccable/review/desktop.png differ diff --git a/.impeccable/review/guide-d.png b/.impeccable/review/guide-d.png new file mode 100644 index 0000000..ff67c5e Binary files /dev/null and b/.impeccable/review/guide-d.png differ diff --git a/.impeccable/review/guide-m.png b/.impeccable/review/guide-m.png new file mode 100644 index 0000000..c8ef50c Binary files /dev/null and b/.impeccable/review/guide-m.png differ diff --git a/.impeccable/review/home-d.png b/.impeccable/review/home-d.png new file mode 100644 index 0000000..06f2b96 Binary files /dev/null and b/.impeccable/review/home-d.png differ diff --git a/.impeccable/review/mobile.png b/.impeccable/review/mobile.png new file mode 100644 index 0000000..92fa083 Binary files /dev/null and b/.impeccable/review/mobile.png differ diff --git a/.impeccable/review/search.png b/.impeccable/review/search.png new file mode 100644 index 0000000..b212987 Binary files /dev/null and b/.impeccable/review/search.png differ diff --git a/.impeccable/surfaces/docs.md b/.impeccable/surfaces/docs.md new file mode 100644 index 0000000..2f4cb5a --- /dev/null +++ b/.impeccable/surfaces/docs.md @@ -0,0 +1,20 @@ +--- +version: 1 +slug: "docs" +primary_target: "docs" +related_targets: [] +--- + +Scope: docs home page and every doc page (MkDocs Material). Mode: Read. +Reader: Python developer evaluating Crosswind. Job: see packages and code, pick one, start. +Constraints: keep content and generated API pages. Dark only. Keep the diagram, drop the mascot. Restyle Material search with Ctrl+K. + +## Direction contract + +THESIS: The docs behave like one native developer utility. Refuses the SaaS hero with a mascot and a feature-card grid. +OWN-WORLD: User-pinned palette #191B1D, #202225, #282B2E, #34373B, #E6E7E8, #92979E, #686D74, accent #67CDBB, muted purple for CLI. Geist and Geist Mono. 1px borders, 8px radii, tiny square markers, compact rectangular badges, keycaps. +STORY: The reader learns what Crosswind is, previews each package's real code in a palette, opens that package's docs, and reads a calm column framed by native chrome. +FIRST VIEWPORT: Slim header and package tabs. Left: 44px H1, one paragraph, buttons, command with copy, alpha line. Right: small inspector of facts. Below at full grid width: the package palette with filter and rows on the left, code preview on the right. +FORM: Pinned by the brief, so no roll ran. Position: user-pinned. Seed key: none. +Signature interaction: filter and arrow through packages, code preview swaps, Enter opens docs. Status bar footer, P and N page shortcuts shown as keycaps. +FINISH: unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, DESIGN.md, and every shipping raster carrying its provenance diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..89d67a9 --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,368 @@ +--- +name: Crosswind +description: Docs for typed, async Python SDKs on Cloudflare Workers, built to behave like one native developer utility. +colors: + bg: "#191b1d" + surface: "#202225" + elevated: "#282b2e" + border: "#34373b" + border-strong: "#484c51" + text: "#e6e7e8" + text-2: "#92979e" + label: "#7f858c" + muted: "#686d74" + accent: "#67cdbb" + purple: "#a99fd6" + amber: "#d0ae74" + red: "#d98a82" + code-function: "#b9d4e6" +typography: + display: + fontFamily: "Geist, -apple-system, BlinkMacSystemFont, Helvetica, Arial, sans-serif" + fontSize: "2.1rem" + fontWeight: 600 + lineHeight: 1.08 + letterSpacing: "-0.034em" + headline: + fontFamily: "Geist, -apple-system, BlinkMacSystemFont, Helvetica, Arial, sans-serif" + fontSize: "1.5rem" + fontWeight: 600 + lineHeight: 1.15 + letterSpacing: "-0.028em" + title: + fontFamily: "Geist, -apple-system, BlinkMacSystemFont, Helvetica, Arial, sans-serif" + fontSize: "1rem" + fontWeight: 600 + lineHeight: 1.3 + letterSpacing: "-0.016em" + subtitle: + fontFamily: "Geist, -apple-system, BlinkMacSystemFont, Helvetica, Arial, sans-serif" + fontSize: "0.8rem" + fontWeight: 600 + lineHeight: 1.4 + letterSpacing: "-0.008em" + body: + fontFamily: "Geist, -apple-system, BlinkMacSystemFont, Helvetica, Arial, sans-serif" + fontSize: "0.75rem" + fontWeight: 400 + lineHeight: 1.65 + chrome: + fontFamily: "Geist, -apple-system, BlinkMacSystemFont, Helvetica, Arial, sans-serif" + fontSize: "0.625rem" + fontWeight: 400 + lineHeight: 1.35 + label: + fontFamily: "Geist, -apple-system, BlinkMacSystemFont, Helvetica, Arial, sans-serif" + fontSize: "0.55rem" + fontWeight: 500 + lineHeight: 1.35 + code: + fontFamily: "Geist Mono, SFMono-Regular, Consolas, Menlo, monospace" + fontSize: "0.625rem" + fontWeight: 400 + lineHeight: 1.6 + mono-label: + fontFamily: "Geist Mono, SFMono-Regular, Consolas, Menlo, monospace" + fontSize: "0.55rem" + fontWeight: 400 + lineHeight: 1.5 + mono-micro: + fontFamily: "Geist Mono, SFMono-Regular, Consolas, Menlo, monospace" + fontSize: "0.5rem" + fontWeight: 400 + lineHeight: "0.8rem" +rounded: + marker: "1px" + s: "4px" + m: "6px" + l: "8px" + xl: "10px" +spacing: + xs: "0.2rem" + s: "0.3rem" + m: "0.4rem" + l: "0.6rem" + xl: "0.8rem" + 2xl: "1.4rem" +components: + button: + backgroundColor: "{colors.surface}" + textColor: "{colors.text}" + typography: "{typography.chrome}" + rounded: "{rounded.m}" + padding: "0.28rem 0.65rem" + button-hover: + backgroundColor: "{colors.elevated}" + textColor: "{colors.text}" + button-primary: + backgroundColor: "color-mix(in srgb, #67cdbb 14%, #191b1d)" + textColor: "{colors.accent}" + typography: "{typography.chrome}" + rounded: "{rounded.m}" + padding: "0.28rem 0.65rem" + button-primary-hover: + backgroundColor: "color-mix(in srgb, #67cdbb 20%, #191b1d)" + textColor: "{colors.accent}" + input-search: + backgroundColor: "{colors.surface}" + textColor: "{colors.text}" + typography: "{typography.chrome}" + rounded: "{rounded.m}" + height: "1.4rem" + width: "11rem" + input-search-open: + backgroundColor: "{colors.surface}" + textColor: "{colors.text}" + rounded: "{rounded.xl}" + height: "2rem" + width: "34rem" + nav-link: + textColor: "{colors.text-2}" + typography: "{typography.chrome}" + rounded: "{rounded.m}" + padding: "0.2rem 0.4rem 0.2rem 0.7rem" + nav-link-hover: + backgroundColor: "{colors.surface}" + textColor: "{colors.text}" + nav-link-active: + backgroundColor: "{colors.elevated}" + textColor: "{colors.text}" + badge: + textColor: "{colors.text-2}" + typography: "{typography.mono-micro}" + rounded: "{rounded.s}" + padding: "0 0.2rem" + badge-cli: + textColor: "{colors.purple}" + typography: "{typography.mono-micro}" + rounded: "{rounded.s}" + padding: "0 0.2rem" + keycap: + backgroundColor: "{colors.elevated}" + textColor: "{colors.text-2}" + typography: "{typography.mono-micro}" + rounded: "{rounded.s}" + padding: "0 0.18rem" + inline-code: + backgroundColor: "{colors.surface}" + rounded: "{rounded.s}" + padding: "0.08em 0.32em" + code-panel: + backgroundColor: "{colors.surface}" + textColor: "{colors.text}" + typography: "{typography.code}" + rounded: "{rounded.l}" + padding: "0.65rem 0.8rem" + callout: + backgroundColor: "{colors.surface}" + textColor: "{colors.text}" + rounded: "{rounded.l}" + padding: "0.5rem 0.7rem" + command: + backgroundColor: "{colors.surface}" + textColor: "{colors.purple}" + typography: "{typography.code}" + rounded: "{rounded.m}" + palette: + backgroundColor: "{colors.surface}" + textColor: "{colors.text}" + rounded: "{rounded.xl}" + palette-row: + textColor: "{colors.text-2}" + rounded: "{rounded.m}" + padding: "0.38rem 0.4rem" + palette-row-selected: + backgroundColor: "{colors.elevated}" + textColor: "{colors.text}" +--- + +# Design System: Crosswind + +## Overview + +**Creative North Star: "The Native Utility"** + +The docs behave like one developer tool that a reader already has open. Slim chrome, keyboard shortcuts shown as keycaps, a status bar for a footer, and search that opens as a command palette all come from native desktop tooling. Real code is the main visual material, and the home page previews it inside a package palette. + +The system is dark only and dense. Three graphite steps and 1px borders carry all structure. Colour appears in small amounts, and each colour has a job. Tiny square markers show position and state, so no icon set is needed. The only illustrations are the puffin logo in the header and the package diagram. + +The world rejects the SaaS landing page, meaning a generic hero and a grid of feature cards. The site runs on MkDocs Material, and every token maps onto Material's variables, so generated API pages inherit the same look. + +**Key Characteristics:** +- Dark only, with no theme toggle. +- Geist for text and chrome, Geist Mono for anything a developer would type or read as a machine value. +- 1px borders and tonal steps instead of shadows at rest. +- Square markers, compact rectangular badges and keycaps. +- A 140ms state transition on almost every interactive element. + +## Colors + +Near-neutral graphite with a cool cast, one teal accent, and two quiet secondary hues with narrow jobs. The palette is user-pinned. Label Grey and amber are added steps. + +### Primary +- **Terminal Teal** (#67cdbb): Marks where the reader is and what they can act on. It colours links, the primary button, the active square marker, the focus ring, search matches, the text caret and code strings. Tints use `color-mix` at 10% to 42%. + +### Secondary +- **Muted Command Purple** (#a99fd6): The colour of commands and the CLI badge. Python keywords in code use it as a secondary use. + +### Tertiary +- **Low-Saturation Red** (#d98a82): An added danger colour, used only on danger, error and failure callouts. +- **Low-Saturation Amber** (#d0ae74): An added warning colour. It marks warning and caution callouts, quote callout labels, and the alpha status. Syntax highlighting also uses it for numbers, constants and builtins. + +### Neutral +- **Graphite Ground** (#191b1d): The page, header, tabs, footer and sidebars. +- **Panel Graphite** (#202225): Code panels, callouts, the palette, inline code, default buttons and the sidebar hover. +- **Raised Graphite** (#282b2e): Title bars of code panels, keycaps, the selected row and the active sidebar link. +- **Hairline** (#34373b): Every resting border and divider. +- **Hover Hairline** (#484c51): Borders on hover, and on open search and the diagram tooltip. +- **Soft White** (#e6e7e8): Headings, body text and the current item. +- **Secondary Grey** (#92979e): Secondary text at any size, on any of the three graphite steps. Idle navigation, captions, table headers and code comments use it. +- **Label Grey** (#7f858c): An added step for small labels, such as sidebar section names and the fact table's terms. It reaches 4.64:1 on Graphite Ground, 4.28:1 on Panel Graphite and 3.82:1 on Raised Graphite. +- **Decoration Grey** (#686d74): Decoration only, because it fails 4.5:1 on every step (3.31:1 on Graphite Ground). Use it for command prompts, breadcrumb separators, idle square markers and unchecked task boxes. +- **Pale Function Blue** (#b9d4e6): Function names in syntax highlighting. It appears nowhere else. + +### Named Rules +**The Label Ground Rule.** Label Grey sits on Graphite Ground only. Small text on a panel or a raised step uses Secondary Grey, because Label Grey drops below 4.5:1 there. + +**The Decoration Rule.** Decoration Grey never carries information a reader needs to read. If removing it would lose meaning, the element needs Secondary Grey. + +**The Purple Rule.** Purple means a command, the CLI, or a Python keyword. Links and buttons stay teal or neutral. + +## Typography + +**Display Font:** Geist (with -apple-system, BlinkMacSystemFont, Helvetica, Arial, sans-serif) +**Body Font:** Geist (with the same fallback) +**Label/Mono Font:** Geist Mono (with SFMono-Regular, Consolas, Menlo, monospace) + +**Character:** One neutral grotesque carries headings, prose and chrome, tightened as sizes grow. Geist Mono separates machine values from prose at the same small sizes. The site loads Geist at 400, 500 and 600, and Geist Mono at 400 and 500. + +### Hierarchy + +Sizes are rem at a 20px root. Pixel values below are the rendered result. + +- **Display** (600, 42px, 1.08): The home page heading only. It drops to 32px at 960px wide and below. The direction contract asked for 44px, and the build ships 42px. +- **Headline** (600, 30px, 1.15): The H1 of each doc page. +- **Title** (600, 20px, 1.3): H2 sections, each under a 1px top rule. +- **Subtitle** (600, 16px, 1.4): H3, and the label column on home page sections. H4 is 14px in Secondary Grey. +- **Body** (400, 15px, 1.65): Prose, capped at 72ch. The home lede is 16px at 1.55 in Secondary Grey. Callouts are 14px. +- **Chrome** (400, 12.5px, 1.35): Tabs, sidebar links, buttons and the search input. Active items step up to 500. Tables are 13px with 12px headers. +- **Label** (500, 11px): Sidebar section names and small captions. Sentence case, no tracking. +- **Code** (Geist Mono 400, 12.5px, 1.6): Code panels with tabular numerals and no ligatures. Inline code is 0.84em. +- **Mono label** (Geist Mono 400, 11px, 1.5): File names, callout labels and the copied toast. +- **Mono micro** (Geist Mono 400, 10px): Badges and keycaps. + +### Named Rules +**The Mono Rule.** Geist Mono is for code, commands, shortcuts, statuses and identifiers. Everything else, including every label a person wrote, is Geist. + +**The Root Rule.** The html element is pinned at 125%, which is 20px at the browser default, and Material's wide-screen growth is off. So .05rem is 1px, and every rem value in this file assumes that root. + +## Layout + +The main grid is capped at 66rem (1320px). On doc pages the primary sidebar is 11rem (220px), the secondary sidebar is 10rem (200px), and the content column is capped at 42rem (840px) from 1220px wide. Code and tables take the column's full width while prose keeps its 72ch measure. + +The home hero is a text column beside a 16rem (320px) fact table with a 3rem gap, aligned to the bottom edge. The palette below it takes the full grid width, with a 17rem (340px) side column. Later home sections use a 13rem (260px) label column with the heading absolutely placed in it. + +Spacing clusters on 4, 6, 8, 12 and 16px. The 0.8rem (16px) inset is the standard horizontal padding for panels, title bars and result rows. Rows stay compact, with 0.2rem to 0.4rem of vertical padding. + +Hero, palette and section columns collapse to one at 960px. The sidebars become Material's drawer below 1220px, where each link grows to 14px with 0.5rem of vertical padding. On the home page, the packages table turns into a stack of rows at 960px. The diagram's caption text moves out of the scene at 720px. + +## Elevation & Depth + +The system is flat and tonal. Depth comes from the three graphite steps and 1px borders, so a raised thing is lighter and a title bar is one step above its panel. Page sections divide with a thin rule, never with a background band. + +### Shadow Vocabulary +- **Float** (`box-shadow: 0 8px 24px rgb(0 0 0 / 28%)`): Elements that float above the page, which are open search, the copied toast and the diagram tooltip. +- **Palette contact** (`box-shadow: 0 1px 2px rgb(0 0 0 / 20%)`): The package palette at rest. It is the only resting shadow. + +Open search also dims the page behind it with `rgb(12 13 14 / 64%)`. + +### Named Rules +**The Float Rule.** A shadow means the element sits above the page. Panels in the document flow, including the header, stay flat. + +## Shapes + +Small, even radii on rectangles. There are four steps: 4px for badges, keycaps and inline code, 6px for buttons, nav links, and the search field, 8px for panels and tables, and 10px for the largest containers, which are the palette, open search and the diagram. The contract named 8px radii, and the build added the other steps around it. Square markers are 5px or 6px with a 1px radius. All strokes are 1px, and panels clip their contents with `overflow: hidden`. + +### Named Rules +**The Square Rule.** State is marked with a tiny square, not a dot, stripe or icon. Teal is current, Decoration Grey is idle, amber is a warning or alpha status. + +## Components + +Components are quiet and tool-like. State changes are colour, border or background shifts at 140ms. Palette pane entry and diagram messages ease with `cubic-bezier(0.23, 1, 0.32, 1)`. `prefers-reduced-motion` turns transitions and the pane animation off. Focus everywhere is a 1px teal outline with a 2px offset. + +### Buttons +- **Shape:** Gently rounded rectangle (6px) with a 1px border. +- **Primary:** Teal text on a 14% teal tint of Graphite Ground, with a 42% teal border. It does not fill solid. +- **Default:** Soft White on Panel Graphite with a Hairline border. +- **Hover / Focus:** Default moves to Raised Graphite and Hover Hairline. Primary moves to a 20% tint and a 70% border. +- **Active:** Opacity 0.75. + +### Badges and keycaps +- **Badge:** Mono micro text in a 4px outlined rectangle with no fill, reading SDK or CLI. The CLI badge is purple with a 34% purple border. +- **Keycap:** Mono micro text on Raised Graphite with a Hairline border and no shadow. It labels real shortcuts only, which are Ctrl K, the arrows, Enter, Esc, and P and N on the previous and next links, shown only to hover-capable pointers. + +### Cards / Containers +- **Corner Style:** 8px for code panels, callouts, tables and previous and next links. +- **Background:** Panel Graphite, with title bars on Raised Graphite. +- **Shadow Strategy:** Flat, as in Elevation & Depth. +- **Border:** 1px Hairline, moving to Hover Hairline on interactive panels. +- **Internal Padding:** 0.65rem by 0.8rem in code, 0.5rem by 0.7rem in callouts. + +### Inputs / Fields +- **Style:** The search field is a 1.4rem tall Panel Graphite field with a 6px radius and a keycap at its right edge. The palette filter is borderless inside its own row. +- **Focus:** The search border moves to Hover Hairline, and the palette filter icon turns teal. Inputs drop the global focus ring because the container shows focus. The caret is teal. +- **Open search:** Ctrl K or Cmd K opens a centred 34rem field at 14vh, 2rem tall with a 10px radius. Once a query exists, it joins the results as one panel. Each result is one title and one line of context, with a hint row of keycaps underneath. + +### Navigation +- **Header and tabs:** Two thin rows on Graphite Ground, each with a 1px bottom border and no shadow. The repository name is Geist Mono. +- **Tabs:** 12.5px Secondary Grey. The active tab is Soft White at 500 with a 5px teal square. +- **Sidebar:** Secondary Grey links with a 6px radius. Hover uses Panel Graphite. The active link uses Raised Graphite with a 5px teal square in the gutter, so the label does not shift. The active table of contents entry turns teal with no square. +- **Mobile:** Material's drawer with full-width rows, each divided by a 1px border. + +### Code panels +Panel Graphite, 8px radius, with a 1px border. A file name or tab row sits in a Raised Graphite title bar, with a 1px teal indicator under the active tab. A copy icon sits at the top right. Inline code is a small 4px outlined chip on Panel Graphite. + +### Package field +The home hero's right column: an 8px grid of tiny squares on Panel Graphite, drawn on a canvas, with the package name, position squares and a pause button in a footer row. It morphs to one recognisable scene per package every 6 seconds, in step with the heading carousel: a robot broadcasting to clients for Agents, a terminal inside marching walls for Sandboxes, shipping containers on a crane for Containers, a cloud serving requests for Workers, and the wind symbol blowing blocks off a stack for Wynd. Squares near the pointer brighten. The package preview below follows it, and picking a package in the preview moves the hero and stops the carousel. It holds on hover or focus of the hero or the preview and stops off screen. Reduced motion starts it stopped on a still frame. It stays flat, so it never reads as the isometric package diagram. + +### Callouts +A panel with a 6px square and mono label in a 72px left column, text in the right column, stacked on phones. Each type tints its label, marker, border (38%) and background (7%): teal for notes, tips and other information, amber for warnings, cautions and quote callouts with a bold label, red for danger, errors and failures. + +### Command +A bordered inline group holding a purple Geist Mono command, a Decoration Grey prompt the reader cannot select, and an icon button that copies the command. + +### Package palette +The signature component. A 10px panel with a filter field, rows and a keycap hint row on the left, and the selected package's summary and code on the right. Each row holds a square, a name, a mono package id and a badge. The selected row uses Raised Graphite and a teal square. Arrow keys swap the preview, and Enter opens the docs. The pane enters over 160ms with a 2px rise. Without JavaScript, the panes stay stacked and readable. + +### Fact table +A bordered definition list with 1px row rules, 12px text, Label Grey terms and Soft White values. Status rows carry a square marker. + +### Status bar +The footer's last row, in 11px Secondary Grey on Graphite Ground, with an amber square and the alpha state in Geist Mono on the left and links on the right. It holds the independence statement that PRODUCT.md requires. + +### Ecosystem diagram +An SVG isometric scene in a 10px panel, with Geist Mono labels, 1px hairline strokes and teal for the active path. Its faces use the graphite steps, plus three in-between greys scoped to the diagram (`#1e2022`, `#26292c`, `#1c1e20`). Uppercase tracked labels belong to this SVG only. + +## Do's and Don'ts + +### Do: +- **Do** keep the page dark, using only the three graphite steps for grounds. +- **Do** use 1px Hairline borders (#34373b) and thin rules to separate things. +- **Do** set commands, shortcuts, statuses, package names and file names in Geist Mono. +- **Do** mark the current item with a 5px or 6px teal square. +- **Do** show a keycap only when the shortcut works. +- **Do** write rem values against the 20px root, where .05rem is 1px. +- **Do** use Secondary Grey (#92979e) for small text on Panel Graphite or Raised Graphite. +- **Do** map new colours through the `--cw-*` tokens and Material's `--md-*` variables, so generated API pages inherit them. + +### Don't: +- **Don't** add a light theme or a theme toggle. +- **Don't** build a hero with a mascot or a grid of feature cards. The puffin stays in the header as the logo. +- **Don't** use Decoration Grey (#686d74) for text a reader needs. +- **Don't** put Label Grey (#7f858c) on Panel Graphite or Raised Graphite. +- **Don't** use purple for links, buttons or general emphasis. +- **Don't** raise amber's saturation, or use it for anything other than warnings, alpha status and syntax values. +- **Don't** divide page sections with background bands. +- **Don't** put a shadow on a panel in the document flow, other than the palette's contact shadow. +- **Don't** set uppercase or tracked labels in HTML chrome. +- **Don't** use radii above 10px, or pill shapes. diff --git a/PRODUCT.md b/PRODUCT.md new file mode 100644 index 0000000..6228f5f --- /dev/null +++ b/PRODUCT.md @@ -0,0 +1,43 @@ +# Product + + + +## Platform + +web + +## Users + +Python developers evaluating Crosswind because they want Cloudflare's TypeScript-only developer kits in Python. They need to see the packages and real code, pick a package, and start. People already using Crosswind are the second audience, and they use the docs as reference. + +## Product Purpose + +Crosswind is an unofficial set of typed, async Python SDKs and tools for Cloudflare Python Workers. Each package ports one TypeScript kit or official tool, so developers subclass Python classes and never write Pyodide or FFI calls. + +## Positioning + +Each SDK keeps the wire protocol of its official counterpart, so TypeScript clients such as the `useAgent` React hook connect to a Python Worker unchanged. + +## Capabilities and Constraints + +- Packages: `cf-agents`, `containers-py`, `crosswind-sandboxes`, `crosswind-workers` and the `wynd` CLI. +- The docs site is MkDocs Material, deployed on Cloudflare Pages, with API pages generated by mkdocstrings. +- Every package is alpha and pre-1.0. The docs must keep telling readers to pin exact versions. +- Python Workers run on Pyodide, so not every PyPI package imports. + +## Brand Commitments + +- The name is Crosswind and the puffin logo stays. +- Crosswind is independent and not affiliated with or endorsed by Cloudflare. The docs must say so. +- The user pinned the visual direction: dark, minimal, native tooling. See `DESIGN.md`. + +## Evidence on Hand + +- Mosslight Valley, a demo game whose server uses `cf-agents`. +- No customers, benchmarks or testimonials exist. Do not invent them. + +## Product Principles + +- Show code before claims. +- Keep Python types at every public boundary. +- State what is alpha or missing. diff --git a/docs/assets/hero-img.png b/docs/assets/hero-img.png deleted file mode 100644 index 4530f3d..0000000 Binary files a/docs/assets/hero-img.png and /dev/null differ diff --git a/docs/docs/assets/hero-img.png b/docs/docs/assets/hero-img.png deleted file mode 100644 index 18afba9..0000000 Binary files a/docs/docs/assets/hero-img.png and /dev/null differ diff --git a/docs/docs/containers/outbound-traffic.md b/docs/docs/containers/outbound-traffic.md index 4936580..1f2ad0d 100644 --- a/docs/docs/containers/outbound-traffic.md +++ b/docs/docs/containers/outbound-traffic.md @@ -60,12 +60,11 @@ characters, so `*.example.com`, `api.*.com` and `goo*gle` are all valid. container needs both. The SDK ignores a trailing dot on the hostname. Blocked requests get a `520` response with the body `Origin is disallowed`. -> **Note** -> -> Host lists apply to HTTP traffic, and to HTTPS once -> [HTTPS interception](#https) is on. See the -> [platform docs](https://developers.cloudflare.com/containers/configuration/outbound-traffic/) -> for how the platform handles other ports and protocols. +!!! note + Host lists apply to HTTP traffic, and to HTTPS once + [HTTPS interception](#https) is on. See the + [platform docs](https://developers.cloudflare.com/containers/configuration/outbound-traffic/) + for how the platform handles other ports and protocols. ## Outbound handlers @@ -332,10 +331,9 @@ rule that applies decides what happens: `allowed_hosts` or `enable_internet` is `True`. Otherwise the SDK blocks it. -> **Note** -> -> If your only rules are class-level `outbound_by_host` entries, only requests -> to those hosts go through `ContainerProxy`. Anything else, such as a -> catch-all, a host list or any runtime change, routes all of the -> container's HTTP traffic through it. That stays in place until the container -> restarts. +!!! note + If your only rules are class-level `outbound_by_host` entries, only requests + to those hosts go through `ContainerProxy`. Anything else, such as a + catch-all, a host list or any runtime change, routes all of the + container's HTTP traffic through it. That stays in place until the container + restarts. diff --git a/docs/docs/index.md b/docs/docs/index.md index f574b7e..ac9090f 100644 --- a/docs/docs/index.md +++ b/docs/docs/index.md @@ -6,103 +6,171 @@ hide: - toc --- -=== "Agents" +
- ```python - from workers import Request, Response, WorkerEntrypoint - from agents import Agent, callable, route_agent_request +
+
+
+**Agents** `cf-agents` +Stateful agents with WebSocket clients, schedules, durable tasks and streaming chat. +
+ +
+ +```python +from workers import Request, Response, WorkerEntrypoint +from agents import Agent, callable, route_agent_request + + +class Counter(Agent): + initial_state = {"count": 0} + + @callable # clients call this over a WebSocket + async def increment(self, by: int = 1) -> int: + self.set_state({"count": self.state["count"] + by}) # saved, sent to every client + return self.state["count"] + + +class Default(WorkerEntrypoint): + async def fetch(self, request: Request) -> Response: + # /agents/counter/ reaches the counter called + return await route_agent_request(request, self.env) or Response("Not found", status=404) +``` + +
+ +
+
+
+**Sandboxes** `crosswind-sandboxes` + +Read and write files, mount S3 buckets and back up directories in a sandbox container. +
+ +
+ +```python +from sandboxes import Files +from workers import DurableObject, Request, Response, WorkerEntrypoint + + +class Sandbox(DurableObject): + async def save(self, text: str) -> str: + if not self.ctx.container.running: + self.ctx.container.start() + + files = Files(self.ctx.container) + await files.mkdir("/workspace", recursive=True) + await files.write_file("/workspace/notes.txt", text) # still there on the next request + entries = await files.read_directory("/workspace") + return ", ".join(entry.name for entry in entries) - class Counter(Agent): - initial_state = {"count": 0} - @callable # clients call this over a WebSocket - async def increment(self, by: int = 1) -> int: - self.set_state({"count": self.state["count"] + by}) # saved, sent to every client - return self.state["count"] +class Default(WorkerEntrypoint): + async def fetch(self, request: Request) -> Response: + # One sandbox per user, with its own container and files + user_id = request.url.rsplit("/", 1)[-1] + sandbox = self.env.SANDBOX.getByName(user_id) + return Response(await sandbox.save(await request.text())) +``` +
- class Default(WorkerEntrypoint): - async def fetch(self, request: Request) -> Response: - # /agents/counter/ reaches the counter called - return await route_agent_request(request, self.env) or Response("Not found", status=404) - ``` +
+
+
+**Containers** `containers-py` -=== "Containers" +Start, stop and route requests to Cloudflare Containers, and filter their outbound traffic. +
+ +
+ +```python +from workers import Request, Response, WorkerEntrypoint +from containers import Container, get_container - ```python - from workers import Request, Response, WorkerEntrypoint - from containers import Container, get_container +class Transcoder(Container): + default_port = 8080 + sleep_after = "5m" + enable_internet = False - class Transcoder(Container): - default_port = 8080 - sleep_after = "5m" - enable_internet = False +class Default(WorkerEntrypoint): + async def fetch(self, request: Request) -> Response: + # One container per video, started on the first request + video_id = request.url.rsplit("/", 1)[-1] + container = get_container(self.env.TRANSCODER, video_id) + return await container.fetch(request) +``` - class Default(WorkerEntrypoint): - async def fetch(self, request: Request) -> Response: - # One container per video, started on the first request - video_id = request.url.rsplit("/", 1)[-1] - container = get_container(self.env.TRANSCODER, video_id) - return await container.fetch(request) - ``` +
-=== "Sandboxes" +
+
+
+**Workers** `crosswind-workers` - ```python - from sandboxes import Files - from workers import DurableObject, Request, Response, WorkerEntrypoint +Drop-in replacement for the official `workers` package, with type hints on bindings, requests and Durable Objects. +
+ +
+```python +from env import Env # generated by wynd from wrangler.jsonc +from workers import DurableObject, Request, Response, WorkerEntrypoint - class Sandbox(DurableObject): - async def save(self, text: str) -> str: - if not self.ctx.container.running: - self.ctx.container.start() - files = Files(self.ctx.container) - await files.mkdir("/workspace", recursive=True) - await files.write_file("/workspace/notes.txt", text) # still there on the next request - entries = await files.read_directory("/workspace") - return ", ".join(entry.name for entry in entries) +class Counter(DurableObject[Env]): + async def increment(self, by: int) -> int: + count = (await self.ctx.storage.get("count") or 0) + by + await self.ctx.storage.put("count", count) + return count - class Default(WorkerEntrypoint): - async def fetch(self, request: Request) -> Response: - # One sandbox per user, with its own container and files - user_id = request.url.rsplit("/", 1)[-1] - sandbox = self.env.SANDBOX.getByName(user_id) - return Response(await sandbox.save(await request.text())) - ``` +class Default(WorkerEntrypoint[Env]): + async def fetch(self, request: Request) -> Response: + # self.env.COUNTER is typed, so a misspelled method fails the type check + count = await self.env.COUNTER.get_by_name("global").increment(1) + return Response.json({"count": count}) +``` -=== "Workers" +
- ```python - from env import Env # generated by wynd from wrangler.jsonc - from workers import DurableObject, Request, Response, WorkerEntrypoint +
+
+
+**Wynd** `wynd` +Replaces pywrangler. It drops vendored files your Worker never imports before each upload. +
+ +
- class Counter(DurableObject[Env]): - async def increment(self, by: int) -> int: - count = (await self.ctx.storage.get("count") or 0) + by - await self.ctx.storage.put("count", count) - return count +```sh +uvx wynd init # write a new crosswind-workers project +uvx wynd dev # install packages for the Worker's Python, then wrangler dev +uvx wynd deploy # prune unused packages, then wrangler deploy +uvx wynd tail # anything else goes to Wrangler unchanged +``` +
- class Default(WorkerEntrypoint[Env]): - async def fetch(self, request: Request) -> Response: - # self.env.COUNTER is typed, so a misspelled method fails the type check - count = await self.env.COUNTER.get_by_name("global").increment(1) - return Response.json({"count": count}) - ``` +
-> **Warning** -> -> Every Crosswind package is in alpha. A minor release can rename or remove -> an API, so an unpinned dependency can break a deploy that worked yesterday. -> Pin exact versions, test each upgrade before you deploy it, and expect to -> change your code when you upgrade. See [Status](#status). +
## What Crosswind is @@ -115,18 +183,26 @@ Each Crosswind package ports one of those kits to Python. You subclass a Python class, override `async` methods and call typed functions, and the package makes the JavaScript calls. +
+ +
+ ## Packages | Project | Package | What it does | Start here | Version | | ------------------------------------- | ------------------- | -------------------------------------------------------------------------------------------- | ---------- | --------------------------------------------------------------------- | -| [Agents](agents/overview.md) | `cf-agents` | Stateful agents with WebSocket clients, schedules, durable tasks and streaming chat. | [Build an agent](agents/getting-started.md) | ![cf-agents on PyPI](https://img.shields.io/pypi/v/cf-agents) | -| [Containers](containers/overview.md) | `containers-py` | Start, stop and route requests to Cloudflare Containers, and filter their outbound traffic. | [Run a container](containers/getting-started.md) | ![containers-py on PyPI](https://img.shields.io/pypi/v/containers-py) | -| [Sandboxes](sandboxes/overview.md) | `crosswind-sandboxes` | Read and write files, mount S3 buckets and back up directories in a sandbox container. | [Create a sandbox](sandboxes/getting-started.md) | ![crosswind-sandboxes on PyPI](https://img.shields.io/pypi/v/crosswind-sandboxes) | -| [Workers](workers/overview.md) | `crosswind-workers` | Drop-in replacement for the official `workers` package, with type hints on bindings, requests and Durable Objects. | [Write a Worker](workers/getting-started.md) | ![crosswind-workers on PyPI](https://img.shields.io/pypi/v/crosswind-workers) | -| [Wynd](wynd/overview.md) | `wynd` | Replaces pywrangler. It drops vendored files your Worker never imports before each upload. | [Switch from pywrangler](wynd/getting-started.md) | ![wynd on PyPI](https://img.shields.io/pypi/v/wynd) | +| [Agents](agents/overview.md) | `cf-agents` | Stateful agents with WebSocket clients, schedules, durable tasks and streaming chat. | [Build an agent](agents/getting-started.md) | ![cf-agents on PyPI](https://img.shields.io/pypi/v/cf-agents?style=flat-square&label=&color=282b2e) | +| [Containers](containers/overview.md) | `containers-py` | Start, stop and route requests to Cloudflare Containers, and filter their outbound traffic. | [Run a container](containers/getting-started.md) | ![containers-py on PyPI](https://img.shields.io/pypi/v/containers-py?style=flat-square&label=&color=282b2e) | +| [Sandboxes](sandboxes/overview.md) | `crosswind-sandboxes` | Read and write files, mount S3 buckets and back up directories in a sandbox container. | [Create a sandbox](sandboxes/getting-started.md) | ![crosswind-sandboxes on PyPI](https://img.shields.io/pypi/v/crosswind-sandboxes?style=flat-square&label=&color=282b2e) | +| [Workers](workers/overview.md) | `crosswind-workers` | Drop-in replacement for the official `workers` package, with type hints on bindings, requests and Durable Objects. | [Write a Worker](workers/getting-started.md) | ![crosswind-workers on PyPI](https://img.shields.io/pypi/v/crosswind-workers?style=flat-square&label=&color=282b2e) | +| [Wynd](wynd/overview.md) | `wynd` | Replaces pywrangler. It drops vendored files your Worker never imports before each upload. | [Switch from pywrangler](wynd/getting-started.md) | ![wynd on PyPI](https://img.shields.io/pypi/v/wynd?style=flat-square&label=&color=282b2e) | For a new Python Worker, use `crosswind-workers` and deploy it with Wynd. +
+ +
+ ## How the packages work - **Python classes.** You subclass a class and override `async` methods. @@ -140,31 +216,65 @@ For a new Python Worker, use `crosswind-workers` and deploy it with Wynd. Starlette, and WSGI apps such as Flask. See [Serve FastAPI or Flask](workers/web-frameworks.md). +
+
+ How Agents, Containers, Workers and Wynd connect + + Python sits at the centre of Crosswind. Agents, Containers, Wynd and Workers connect to it. Wynd drops the dependencies a Worker never imports, packs the rest into a bundle and deploys it to Workers. Select a package in the diagram to open its docs. +
+ +
+ + + + + +
+ +
+
    +
  1. +
  2. +
  3. +
  4. +
+ +
+
+ +
+ +
+ ## Demo
- - Mosslight Valley is a small game that runs on Python Workers. Its server is written with `cf-agents`. [Open it in a new tab](https://agents-py-mosslight-valley.reachvishm8605.workers.dev/){ target="_blank" } +
+ +
+ ## Status -Every package is alpha and pre-1.0. A minor release can break your code, so -pin exact versions: +Every package is alpha and pre-1.0. A minor release can rename or remove an +API, so an unpinned dependency can break a deploy that worked yesterday. Pin +exact versions, test each upgrade before you deploy it, and expect to change +your code when you upgrade: ```sh uv add "crosswind-workers==" @@ -173,3 +283,5 @@ uv add "crosswind-workers==" Each SDK's overview lists the features it doesn't port yet. Crosswind is an independent community project. It isn't affiliated with or endorsed by Cloudflare. + +
diff --git a/docs/docs/javascripts/field.js b/docs/docs/javascripts/field.js new file mode 100644 index 0000000..5941fcd --- /dev/null +++ b/docs/docs/javascripts/field.js @@ -0,0 +1,452 @@ +// Home hero: a grid of tiny squares that morphs into one scene per package, in +// step with the heading carousel. Each scene is a small picture of what the package does. +(() => { + const rotator = document.querySelector(".cw-rotator"); + const panel = document.querySelector(".cw-field"); + if (!rotator || !panel) return; + + const reduced = matchMedia("(prefers-reduced-motion: reduce)").matches; + const EVERY = 6000; + const MORPH = 0.9; // seconds + const CELL = 8; // CSS pixels between squares + const words = [...rotator.children]; + + panel.innerHTML = ` +
+
+ +
+
+ +
+
`; + const canvas = panel.querySelector("canvas"); + const ctx = canvas.getContext("2d"); + const pkg = panel.querySelector(".cw-field__pkg"); + const dots = panel.querySelector(".cw-field__dots"); + const pause = panel.querySelector(".cw-field__pause"); + + // Colour ramps from the site tokens, so a palette change reaches the canvas + const token = (name) => { + const hex = getComputedStyle(document.documentElement).getPropertyValue(name).trim(); + return [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16)); + }; + const ramp = (to) => { + const from = token("--cw-border"); + return Array.from({ length: 33 }, (_, i) => + `rgb(${from.map((c, k) => Math.round(c + (to[k] - c) * (i / 32) ** 0.8)).join()})`); + }; + const TEAL = ramp(token("--cw-accent")); + const PURPLE = ramp(token("--cw-purple")); + + const clamp = (n) => Math.min(1, Math.max(0, n)); + const smooth = (a, b, n) => { + const p = clamp((n - a) / (b - a)); + return p * p * (3 - 2 * p); + }; + const hash = (a, b = 0) => { + const s = Math.sin(a * 127.1 + b * 311.7) * 43758.5453; + return s - Math.floor(s); + }; + + // Scenes draw into the current buffer through these helpers + let cols = 0; + let rows = 0; + let values = new Float32Array(0); + let tones = new Uint8Array(0); + const put = (x, y, value, purple = false) => { + const cx = Math.round(x); + const cy = Math.round(y); + if (cx < 0 || cy < 0 || cx >= cols || cy >= rows) return; + const i = cy * cols + cx; + if (value < values[i]) return; + values[i] = value; + tones[i] = purple ? 1 : 0; + }; + const fill = (x, y, w, h, value, purple) => { + for (let j = 0; j < h; j++) for (let i = 0; i < w; i++) put(x + i, y + j, value, purple); + }; + const outline = (x, y, w, h, value, purple) => { + fill(x, y, w, 1, value, purple); + fill(x, y + h - 1, w, 1, value, purple); + fill(x, y, 1, h, value, purple); + fill(x + w - 1, y, 1, h, value, purple); + }; + const sprite = (lines, x, y, shade) => lines.forEach((line, j) => { + for (let i = 0; i < line.length; i++) if (line[i] === "#") put(x + i, y + j, shade(x + i, y + j)); + }); + + const CLOUD = [ + "..........#####..........", + ".......###.....##........", + "......#..........#.......", + "....###...........###....", + "...#.................##..", + "..#....................#.", + ".#......................#", + ".#......................#", + "..#....................#.", + "...####################..", + ]; + const BOLT = ["....##", "...##.", "..##..", ".#####", "...##.", "..##..", ".##..."]; + const WIND = [ + "...........####...........", + "..........#....#..........", + "...............#..........", + "...............#....####..", + "###############....#....#.", + "........................#.", + "########################..", + "..........................", + "################..........", + "................#.........", + "................#.........", + "..........#....#..........", + "...........####...........", + ]; + + const SCENES = { + // A robot that broadcasts its state to the clients on either side + Agents(t) { + const cx = Math.floor(cols / 2); + const cy = Math.floor(rows / 2) + 2; + const mid = Math.floor(rows / 2); + const blink = t % 3.4 < 0.14; + fill(cx - 7, cy - 5, 15, 11, 0.2); + outline(cx - 7, cy - 5, 15, 11, 0.9); + fill(cx - 8, cy - 2, 1, 4, 0.7); + fill(cx + 8, cy - 2, 1, 4, 0.7); + fill(cx, cy - 8, 1, 3, 0.7); + fill(cx - 1, cy - 10, 3, 2, 0.5 + 0.5 * Math.max(0, Math.sin(t * 3.2)), true); + fill(cx - 4, blink ? cy - 1 : cy - 2, 3, blink ? 1 : 2, 1); + fill(cx + 2, blink ? cy - 1 : cy - 2, 3, blink ? 1 : 2, 1); + fill(cx - 2, cy + 3, 5, 1, 0.7); + const reach = Math.min(23, cols / 2 - 4); + const r = 10 + ((t * 9) % (reach + 4)); + for (const side of [-1, 1]) { + for (let j = -9; j <= 9; j++) { + const dx = Math.sqrt(Math.max(0, r * r - j * j)); + if (dx > 9.5) put(cx + side * dx, mid + j, 0.55 * (1 - (r - 10) / (reach + 4)) + 0.2); + } + for (const off of [-7, 0, 7]) { + const x = cx + side * reach; + const hit = Math.max(0, 1 - Math.abs(r - Math.hypot(reach, off)) / 2.5); + fill(x - 2, mid + off - 2, 5, 4, 0.15 + hit * 0.45, true); + outline(x - 2, mid + off - 2, 5, 4, 0.5 + hit * 0.5, true); + } + } + }, + + // Code running in a terminal, inside walls that nothing crosses + Sandboxes(t) { + const half = Math.min(25, Math.floor(cols / 2) - 2); + const cx = Math.floor(cols / 2); + const [left, right, top, bottom] = [cx - half, cx + half, 1, rows - 2]; + const march = Math.floor(t * 5); + const dash = (n) => ((n + march) % 3 === 0 ? 0.2 : 0.8); + for (let x = left; x <= right; x++) { + put(x, top, dash(x), true); + put(x, bottom, dash(right - x), true); + } + for (let y = top; y <= bottom; y++) { + put(left, y, dash(y), true); + put(right, y, dash(bottom - y), true); + } + const termW = 27; + const termH = 13; + const tx = cx - 13; + const ty = Math.floor((rows - termH) / 2); + fill(tx, ty, termW, termH, 0.14); + outline(tx, ty, termW, termH, 0.85); + fill(tx + 1, ty + 1, termW - 2, 1, 0.4); + fill(tx + 2, ty + 1, 2, 1, 0.8); + const lens = [14, 9, 18, 6]; + const typed = (t * 11) % 62; + let used = 0; + lens.forEach((len, line) => { + const y = ty + 3 + line * 2; + put(tx + 2, y, 0.6, true); + const shown = Math.min(len, Math.max(0, typed - used)); + fill(tx + 4, y, shown, 1, 1); + if (shown > 0 && shown < len && t % 0.5 < 0.3) put(tx + 4 + shown, y, 1, true); + used += len; + }); + // One grain bouncing around the terminal, turned back at the wall + const span = (n) => (a, b) => { + const p = (t * n) % (2 * (b - a)); + return a + (p < b - a ? p : 2 * (b - a) - p); + }; + if (half > 18) { + put(span(9)(left + 2, tx - 2), span(6)(top + 2, bottom - 2), 1); + put(span(8)(tx + termW + 1, right - 2), span(7)(top + 2, bottom - 2), 1); + } + }, + + // Shipping containers: a crane lowers one onto the stack and lifts it off again + Containers(t) { + const W = 13; + const H = 5; + const box = (x, y, value, led, purple) => { + fill(x, y, W, H, value * 0.22, purple); + outline(x, y, W, H, value, purple); + for (let i = 2; i < W - 3; i += 2) fill(x + i, y + 1, 1, H - 2, value * 0.5, purple); + fill(x + W - 3, y + 1, 1, H - 2, value * 0.8, purple); + put(x + W - 2, y + 2, led); + }; + const cx = Math.floor(cols / 2); + const bottom = rows - 1 - H; + const x0 = cx - Math.floor((W * 3 + 2) / 2); + for (let i = 0; i < 3; i++) box(x0 + i * (W + 1), bottom, 0.7, hash(i, Math.floor(t * 0.7)) > 0.4 ? 1 : 0.2); + for (let i = 0; i < 2; i++) box(x0 + i * (W + 1), bottom - H - 1, 0.7, hash(i + 5, Math.floor(t * 0.9)) > 0.4 ? 1 : 0.2, i === 1); + const slot = x0 + 2 * (W + 1); + const rest = bottom - H - 1; + const c = t % 7; + const lift = smooth(0, 2.2, c) * (1 - smooth(4.6, 6.8, c)); + const y = 3 + (rest - 3) * lift; + fill(0, 0, cols, 1, 0.3); + const hookX = slot + Math.floor(W / 2); + fill(hookX, 1, 1, Math.max(0, Math.round(y) - 1), 0.55); + box(slot, y, 1, lift > 0.98 ? 1 : 0.2); + }, + + // Requests fly into the Cloudflare cloud, and responses leave the other side + Workers(t) { + const x = Math.floor((cols - CLOUD[0].length) / 2); + const y = Math.floor((rows - CLOUD.length) / 2); + const beat = Math.max(0, Math.sin(t * 5)) ** 4; + CLOUD.forEach((line, j) => fill(x + line.indexOf("#"), y + j, line.lastIndexOf("#") - line.indexOf("#") + 1, 1, 0.16)); + sprite(CLOUD, x, y, () => 0.95); + sprite(BOLT, x + 9, y + 2, () => 0.7 + beat * 0.3); + [3, 6, 9].forEach((lane, i) => { + const inEnd = x + 2; + const outStart = x + CLOUD[0].length - 2; + for (let k = 0; k < 2; k++) { + const p = (t * 0.55 + i * 0.31 + k * 0.5) % 1; + fill(p * inEnd - 1, y + lane - 1, 2, 2, 0.5 + 0.5 * smooth(0, 0.3, p)); + fill(outStart + p * (cols - outStart), y + lane - 1, 2, 2, 1 - 0.5 * smooth(0.7, 1, p), true); + } + }); + }, + + // Wind blowing the packages nothing imports off the stack + Wynd(t) { + const total = WIND[0].length + 16; + const x = Math.max(1, Math.floor((cols - total) / 2)); + const y = Math.floor((rows - WIND.length) / 2); + const head = (t * 22) % 46 - 6; + sprite(WIND, x, y, (px) => 0.72 + 0.28 * Math.max(0, 1 - Math.abs(px - x - head) / 7)); + const sx = x + WIND[0].length + 2; + const cycle = t % 5; + for (let row = 0; row < 4; row++) { + for (let col = 0; col < 2; col++) { + const bx = sx + col * 5; + const by = y + 1 + row * 3; + const unused = hash(row, col + 1) > 0.5; + if (!unused) { + fill(bx, by, 4, 2, 0.9); + continue; + } + const gone = smooth(0.8 + row * 0.15, 3 + row * 0.15, cycle) * (1 - smooth(4.6, 5, cycle)); + const fade = 1 - smooth(0.3, 1, gone); + fill(bx + gone * 18, by - Math.sin(gone * 3) * 2, 4, 2, 0.8 * fade, true); + } + } + }, + }; + const NAMES = { + Agents: "cf-agents", + Sandboxes: "crosswind-sandboxes", + Containers: "containers-py", + Workers: "crosswind-workers", + Wynd: "wynd", + }; + const order = words.slice(0, -1).map((w) => w.textContent); + + let scale = 1; + let current = 0; + let previous = 0; + let morphStart = -9; + let clock = reduced ? 8 : 0; + let stopped = reduced; + let held = 0; + let pointer = null; + + function render(name) { + values = new Float32Array(cols * rows); + tones = new Uint8Array(cols * rows); + SCENES[name](clock); + return [values, tones]; + } + + function draw() { + const m = reduced ? 1 : smooth(0, MORPH, clock - morphStart); + const [to, toTones] = render(order[current]); + const [from, fromTones] = m < 1 ? render(order[previous]) : [to, toTones]; + ctx.clearRect(0, 0, canvas.width, canvas.height); + for (let y = 0; y < rows; y++) { + for (let x = 0; x < cols; x++) { + const i = y * cols + x; + // Sweep the change across the grid so it reads as one motion + const local = m < 1 ? smooth(0, 1, m * 1.6 - (x / cols) * 0.6) : 1; + let value = from[i] + (to[i] - from[i]) * local; + const purple = local < 0.5 ? fromTones[i] : toTones[i]; + if (pointer) value += Math.max(0, 1 - Math.hypot(x - pointer.x, y - pointer.y) / 5) * 0.6; + value = clamp(value); + const size = Math.round((1.2 + value * 4.6) * scale); + ctx.fillStyle = (purple ? PURPLE : TEAL)[Math.round(value * 32)]; + ctx.fillRect( + Math.round((x * CELL + CELL / 2) * scale - size / 2), + Math.round((y * CELL + CELL / 2) * scale - size / 2), + size, + size, + ); + } + } + } + + function resize() { + const box = canvas.parentElement.getBoundingClientRect(); + scale = devicePixelRatio || 1; + canvas.width = Math.round(box.width * scale); + canvas.height = Math.round(box.height * scale); + cols = Math.floor(box.width / CELL); + rows = Math.floor(box.height / CELL); + ctx.setTransform(1, 0, 0, 1, (canvas.width - cols * CELL * scale) / 2, (canvas.height - rows * CELL * scale) / 2); + draw(); + } + + // Runs only while the panel is on screen and the carousel isn't paused + let visible = true; + let raf = 0; + let last = 0; + function frame(now) { + clock += Math.min(0.05, (now - last) / 1000); + last = now; + draw(); + raf = requestAnimationFrame(frame); + } + function sync() { + const run = visible && !stopped && !document.hidden; + if (run && !raf) { + last = performance.now(); + raf = requestAnimationFrame(frame); + } else if (!run && raf) { + cancelAnimationFrame(raf); + raf = 0; + } + } + + // The word list ends with a copy of the first word, so the loop always moves upward + const moveWords = (index) => words.forEach((w) => (w.style.transform = `translateY(${-index * 100}%)`)); + words[0].addEventListener("transitionend", () => { + if (!words[0].style.transform.includes(`-${order.length * 100}%`)) return; + words.forEach((w) => (w.style.transition = "none")); + moveWords(0); + void rotator.offsetHeight; // commit the jump before transitions come back + words.forEach((w) => (w.style.transition = "")); + }); + + const buttons = order.map((name, i) => { + const b = document.createElement("button"); + b.type = "button"; + b.className = "cw-field__dot"; + b.setAttribute("aria-label", `Show ${name}`); + b.title = name; + b.addEventListener("click", () => { + stopped = true; + renderPause(); + show(i); + sync(); + }); + dots.append(b); + return b; + }); + + function show(index, wrap) { + previous = current; + current = index; + morphStart = clock; + moveWords(wrap ? order.length : index); + pkg.textContent = NAMES[order[index]]; + document.dispatchEvent(new CustomEvent("cw:package", { detail: order[index] })); + buttons.forEach((b, i) => b.setAttribute("aria-pressed", i === index)); + // A paused canvas still has to finish the change, so play out the morph + if (stopped && !reduced) { + const until = performance.now() + MORPH * 1000; + let at = performance.now(); + const finish = (now) => { + clock += (now - at) / 1000; + at = now; + draw(); + if (now < until && stopped) requestAnimationFrame(finish); + }; + requestAnimationFrame(finish); + } else { + draw(); + } + } + + function renderPause() { + pause.setAttribute("aria-label", stopped ? "Play the animation" : "Pause the animation"); + pause.title = stopped ? "Play" : "Pause"; + pause.innerHTML = stopped + ? '' + : ''; + } + + pause.addEventListener("click", () => { + stopped = !stopped; + renderPause(); + sync(); + }); + + canvas.parentElement.addEventListener("pointermove", (event) => { + if (reduced) return; + const box = canvas.getBoundingClientRect(); + pointer = { x: (event.clientX - box.left) / CELL - 0.5, y: (event.clientY - box.top) / CELL - 0.5 }; + if (!raf) draw(); + }); + canvas.parentElement.addEventListener("pointerleave", () => { + pointer = null; + if (!raf) draw(); + }); + + // Hold on one package while the reader points at or tabs through the hero's moving parts + const preview = document.querySelector(".cw-palette"); + for (const target of [panel, preview, rotator.parentElement]) { + target?.addEventListener("pointerenter", () => (held += 1)); + target?.addEventListener("pointerleave", () => (held = Math.max(0, held - 1))); + } + for (const target of [panel, preview]) { + target?.addEventListener("focusin", () => (held += 1)); + target?.addEventListener("focusout", () => (held = Math.max(0, held - 1))); + } + + // Picking a package in the preview moves the hero too, and stops the carousel there + document.addEventListener("cw:pick", (event) => { + stopped = true; + renderPause(); + show(order.indexOf(event.detail)); + sync(); + }); + + setInterval(() => { + if (stopped || held || document.hidden || !visible) return; + const next = (current + 1) % order.length; + show(next, next === 0); + }, EVERY); + + new IntersectionObserver(([entry]) => { + visible = entry.isIntersecting; + sync(); + }).observe(panel); + document.addEventListener("visibilitychange", sync); + new ResizeObserver(resize).observe(canvas.parentElement); + + panel.hidden = false; + pause.hidden = reduced; + renderPause(); + resize(); + show(0); + sync(); +})(); diff --git a/docs/docs/javascripts/home.js b/docs/docs/javascripts/home.js new file mode 100644 index 0000000..7b0995a --- /dev/null +++ b/docs/docs/javascripts/home.js @@ -0,0 +1,132 @@ +// Home page package palette. Without this script the panes stay stacked and readable. +// Rows are vertical tabs: arrows move and preview, Enter opens the package docs. +(() => { + const palette = document.querySelector(".cw-palette"); + if (!palette) return; + + const panes = [...palette.querySelectorAll(".cw-pane")]; + const side = document.createElement("div"); + side.className = "cw-palette__side"; + side.innerHTML = ` + +
+ + `; + const input = side.querySelector("input"); + const list = side.querySelector("[role=tablist]"); + const empty = side.querySelector(".cw-palette__empty"); + + const rows = panes.map((pane) => { + const { name, pkg, kind } = pane.dataset; + const row = document.createElement("button"); + row.type = "button"; + row.className = "cw-palette__row"; + row.id = `row-${pane.id}`; + row.setAttribute("role", "tab"); + row.setAttribute("aria-controls", pane.id); + row.innerHTML = ``; + row.querySelector(".cw-palette__name").textContent = name; + row.querySelector("code").textContent = pkg; + row.querySelector(".cw-badge").textContent = kind; + pane.setAttribute("role", "tabpanel"); + pane.setAttribute("aria-labelledby", row.id); + list.append(row); + return { row, pane, text: pane.querySelector(".cw-pane__head").textContent.toLowerCase() }; + }); + + const visible = () => rows.filter((r) => !r.row.hidden); + const docsLink = (pane) => pane.querySelector(".cw-pane__links a"); + + // `follow` is true when the hero carousel asked for the change, so it isn't sent back + function select(item, follow) { + if (selected() && selected() !== item) { + palette.classList.add("has-switched"); + if (!follow) document.dispatchEvent(new CustomEvent("cw:pick", { detail: item.pane.dataset.name })); + } + for (const r of rows) { + const on = r === item; + r.row.setAttribute("aria-selected", on); + r.row.tabIndex = on ? 0 : -1; + r.pane.hidden = !on; + } + } + const selected = () => rows.find((r) => !r.pane.hidden); + + function filter() { + const q = input.value.trim().toLowerCase(); + for (const r of rows) r.row.hidden = !r.text.includes(q); + const shown = visible(); + empty.hidden = shown.length > 0; + empty.textContent = shown.length ? "" : `No package matches "${input.value.trim()}".`; + if (shown.length && selected().row.hidden) select(shown[0]); + } + + input.addEventListener("input", filter); + input.addEventListener("keydown", (event) => { + if (event.key === "ArrowDown" && visible().length) { + event.preventDefault(); + selected().row.focus(); + } else if (event.key === "Enter" && visible().length) { + docsLink(selected().pane).click(); + } else if (event.key === "Escape") { + input.value = ""; + filter(); + } + }); + + list.addEventListener("focusin", (event) => { + const item = rows.find((r) => r.row === event.target); + if (item) select(item); + }); + list.addEventListener("click", (event) => { + const item = rows.find((r) => r.row === event.target.closest("button")); + if (item) select(item); + }); + list.addEventListener("keydown", (event) => { + const shown = visible(); + const i = shown.indexOf(selected()); + const target = { + ArrowDown: shown[i + 1], + ArrowUp: i ? shown[i - 1] : null, + Home: shown[0], + End: shown.at(-1), + }[event.key]; + if (event.key === "Enter") { + event.preventDefault(); + docsLink(selected().pane).click(); + } else if (event.key === "ArrowUp" && !i) { + event.preventDefault(); + input.focus(); + } else if (target) { + event.preventDefault(); + target.row.focus(); + } + }); + + const main = document.createElement("div"); + main.className = "cw-palette__main"; + main.append(...panes); + palette.append(side, main); + palette.classList.add("is-ready"); + select(rows[0]); + + document.addEventListener("cw:package", (event) => { + const item = rows.find((r) => r.pane.dataset.name === event.detail); + if (item && !item.row.hidden) select(item, true); + }); +})(); + +// The game focuses its input when it loads, which scrolls this page, so load it on click +document.querySelector(".cw-demo button")?.addEventListener("click", (event) => { + const frame = document.createElement("iframe"); + frame.src = "https://agents-py-mosslight-valley.reachvishm8605.workers.dev/"; + frame.title = "Mosslight Valley demo game"; + frame.allow = "fullscreen; autoplay; gamepad"; + event.currentTarget.parentElement.replaceChildren(frame); +}); diff --git a/docs/docs/javascripts/shortcuts.js b/docs/docs/javascripts/shortcuts.js new file mode 100644 index 0000000..97a33e9 --- /dev/null +++ b/docs/docs/javascripts/shortcuts.js @@ -0,0 +1,19 @@ +// Ctrl+K or Cmd+K opens Material's search. Material already handles Esc, the arrow keys, and P and N. +(() => { + const query = document.querySelector('[data-md-component="search-query"]'); + if (!query) return; + const mac = /Mac|iPhone|iPad/.test(navigator.platform); + query.form.dataset.shortcut = mac ? "⌘K" : "Ctrl K"; + + const hints = document.createElement("div"); + hints.className = "cw-search-hints"; + hints.setAttribute("aria-hidden", "true"); + hints.innerHTML = "↑↓ select↵ openesc close"; + document.querySelector(".md-search__output")?.append(hints); + + addEventListener("keydown", (event) => { + if (!(event.ctrlKey || event.metaKey) || event.key.toLowerCase() !== "k") return; + event.preventDefault(); + query.focus(); // Material opens the search panel when its input takes focus + }); +})(); diff --git a/docs/docs/javascripts/system.js b/docs/docs/javascripts/system.js index 54870e7..4f211e2 100644 --- a/docs/docs/javascripts/system.js +++ b/docs/docs/javascripts/system.js @@ -19,19 +19,19 @@ const MODULES = { agents: { - n: "01", name: "Agents", pkg: "cf-agents", href: "agents/overview/", + name: "Agents", pkg: "cf-agents", href: "agents/overview/", desc: "Stateful agents with WebSocket clients, schedules, durable tasks and streaming chat.", }, workers: { - n: "02", name: "Workers", pkg: "crosswind-workers", href: "workers/overview/", + name: "Workers", pkg: "crosswind-workers", href: "workers/overview/", desc: "Typed replacement for the workers package, with typed bindings, requests and Durable Objects.", }, containers: { - n: "03", name: "Containers", pkg: "containers-py", href: "containers/overview/", + name: "Containers", pkg: "containers-py", href: "containers/overview/", desc: "Start, stop and route requests to Cloudflare Containers, and filter their outbound traffic.", }, wynd: { - n: "04", name: "Wynd", pkg: "wynd", href: "wynd/overview/", + name: "Wynd", pkg: "wynd", href: "wynd/overview/", desc: "Replaces pywrangler. Before each upload it drops vendored files your Worker never imports.", }, }; @@ -348,7 +348,6 @@ const [lx, ly] = iso(corner); const [dx, dy, anchor] = side ? [-14, -4, "end"] : [0, 22, "middle"]; const t1 = make(name, "text", { x: r1(lx + dx), y: r1(ly + dy), class: "t b", "text-anchor": anchor }); - make(t1, "tspan", { class: "num" }).textContent = m.n + " "; t1.append(m.name.toUpperCase()); label(name, corner, m.pkg, "m", dx, dy + 14, anchor); assemble(pad, ASSEMBLE_AT(key), 0.5); diff --git a/docs/docs/stylesheets/home.css b/docs/docs/stylesheets/home.css index c2d7671..7c36dee 100644 --- a/docs/docs/stylesheets/home.css +++ b/docs/docs/stylesheets/home.css @@ -1,189 +1,595 @@ +/* Hero: text on the left, a small inspector of facts on the right */ .cw-hero { - padding: 2rem 0 0.5rem; + padding: 2rem 0 1.2rem; } .cw-hero .md-grid { - display: flex; - align-items: center; - justify-content: space-between; - gap: 2rem; + display: grid; + grid-template-columns: minmax(0, 1fr) 24rem; + align-items: end; + gap: 3rem; padding: 0 0.8rem; } .cw-hero h1 { - margin-bottom: 0.5rem; - color: var(--md-default-fg-color); - font-weight: 600; + max-width: 17em; + margin: 0 0 0.6rem; + font-size: 2.1rem; + letter-spacing: -0.034em; + line-height: 1.08; } -.cw-hero p { - max-width: 40rem; +.cw-hero__text p { + max-width: 34rem; + margin: 0; color: var(--cw-text-2); font-size: 0.8rem; + line-height: 1.55; } -/* Buttons, "or" divider and command share one column as wide as the buttons */ .cw-hero__actions { - display: inline-grid; - gap: 0.6rem; - max-width: 100%; - margin-top: 0.8rem; + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.4rem; + margin-top: 1rem; } -.cw-hero__buttons { +.cw-hero__text .cw-hero__status { display: flex; - flex-wrap: wrap; - gap: 0.5rem; + align-items: center; + gap: 0.3rem; + margin-top: 0.7rem; + font-size: 0.6rem; + line-height: 1.4; } -.cw-hero .md-button { - margin: 0; +.cw-hero__status a { + color: var(--cw-accent); + text-decoration: underline; + text-decoration-color: color-mix(in srgb, var(--cw-accent) 36%, transparent); + text-underline-offset: 0.18em; } -.cw-or { +/* field.js draws a grid of tiny squares here, one abstract pattern per package */ +.cw-field { display: flex; - align-items: center; - gap: 0.75rem; - color: var(--cw-meta); - font-size: 0.7rem; + flex-direction: column; + align-self: stretch; + overflow: hidden; + border: 1px solid var(--cw-border); + border-radius: var(--cw-radius-l); + background-color: var(--cw-surface); } -.cw-or::before, -.cw-or::after { - content: ""; +.cw-field[hidden] { + display: none; +} + +.cw-field__stage { + position: relative; flex: 1; + min-height: 8rem; +} + +.cw-field__stage canvas { + position: absolute; + inset: 0; + width: 100%; + height: 100%; +} + +.cw-field__foot { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.6rem; + padding: 0.2rem 0.3rem 0.2rem 0.7rem; border-top: 1px solid var(--cw-border); } -.cw-hero .cw-command { - margin: 0; +.cw-field__pkg { + color: var(--cw-text-2); + font-family: var(--md-code-font-family); + font-size: 0.55rem; + line-height: 1.5; } -.cw-hero .cw-command code { - padding: 0.6rem 1rem; - font-size: 0.8rem; +.cw-field__controls, +.cw-field__dots { + display: flex; + align-items: center; } -.cw-command__comment, -.cw-command__prompt { - color: var(--cw-meta); - user-select: none; +.cw-field__dot { + display: grid; + place-items: center; + width: 0.9rem; + height: 1.1rem; + cursor: pointer; } -.cw-hero__mascot { - flex-shrink: 0; - width: 20rem; - height: auto; +.cw-field__dot::before { + content: ""; + width: 6px; + height: 6px; + border-radius: 1px; + background-color: var(--cw-muted); + transition: background-color var(--cw-fast); } -/* Phones: stack the image under the text */ -@media screen and (max-width: 44.984375em) { - .cw-hero .md-grid { - flex-direction: column; - align-items: flex-start; - } +.cw-field__dot:hover::before { + background-color: var(--cw-text-2); +} - .cw-hero__mascot { - width: 100%; - max-width: 20rem; - } +.cw-field__dot[aria-pressed="true"]::before { + background-color: var(--cw-accent); +} + +.cw-field__pause { + display: grid; + place-items: center; + width: 1.1rem; + height: 1.1rem; + margin-left: 0.15rem; + border-radius: var(--cw-radius-s); + color: var(--cw-text-2); + cursor: pointer; + transition: color var(--cw-fast), background-color var(--cw-fast); +} + +.cw-field__pause[hidden] { + display: none; +} + +.cw-field__pause:is(:hover, :focus-visible) { + background-color: var(--cw-elevated); + color: var(--cw-text); +} + +.cw-field__pause svg { + width: 0.65rem; + height: 0.65rem; + fill: currentcolor; + stroke: currentcolor; + stroke-linecap: round; + stroke-linejoin: round; + stroke-width: 1.5; +} + +.cw-palette__filter svg { + width: 0.7rem; + height: 0.7rem; + fill: none; + stroke: currentcolor; + stroke-linecap: round; + stroke-width: 1.25; } -/* Rotating package name: a one-line window over a column of words that steps - up every 2.5s. */ +/* inline-flex takes its baseline from the first word, so the window sits on the heading's baseline */ .cw-rotator { - display: inline-block; + display: inline-flex; + flex-direction: column; overflow: hidden; - height: 1.3em; - line-height: 1.3em; - vertical-align: bottom; - color: #3b82f6; /* blue-500 in both themes; 3.7:1 on white, fine at heading size */ + height: 1.25em; + color: var(--cw-accent); + line-height: 1.25em; + vertical-align: baseline; } .cw-rotator > span { - display: block; - animation: cw-rotate 10s var(--cw-ease-out) infinite; + flex-shrink: 0; + transition: transform 420ms var(--cw-ease-out); +} + +@media (prefers-reduced-motion: reduce) { + .cw-rotator > span { + transition: none; + } } -.cw-hero h1:hover .cw-rotator > span { - animation-play-state: paused; +/* Package palette: the main demonstration on the page */ +.cw-home { + margin-top: -0.5rem; } -@keyframes cw-rotate { - 0%, 20% { transform: translateY(0); } - 25%, 45% { transform: translateY(-100%); } - 50%, 70% { transform: translateY(-200%); } - 75%, 95% { transform: translateY(-300%); } - 100% { transform: translateY(-400%); } +.cw-home .cw-pane__about p, +.cw-home .cw-section p { + max-width: none; } -@media (prefers-reduced-motion: reduce) { - .cw-rotator > span { - animation: none; +.cw-palette { + border: 1px solid var(--cw-border); + border-radius: var(--cw-radius-xl); + background-color: var(--cw-surface); + box-shadow: 0 1px 2px rgb(0 0 0 / 20%); +} + +.cw-palette.is-ready { + display: grid; + grid-template-columns: 17rem minmax(0, 1fr); + overflow: hidden; +} + +.cw-palette__side { + display: flex; + flex-direction: column; + border-right: 1px solid var(--cw-border); +} + +.cw-palette__filter { + display: flex; + align-items: center; + gap: 0.4rem; + padding: 0 0.7rem; + border-bottom: 1px solid var(--cw-border); + color: var(--cw-text-2); + transition: color var(--cw-fast); +} + +.cw-palette__filter:focus-within { + color: var(--cw-accent); +} + +.cw-palette__filter input { + width: 100%; + height: 2rem; + padding: 0; + border: 0; + outline: 0; + background: none; + color: var(--cw-text); + font: inherit; + font-size: 0.65rem; +} + +.cw-palette__filter input::placeholder { + color: var(--cw-text-2); +} + +.cw-palette__rows { + display: grid; + gap: 1px; + padding: 0.3rem; +} + +.cw-palette__row { + display: grid; + grid-template-columns: 6px auto minmax(0, 1fr) auto; + align-items: center; + gap: 0.4rem; + width: 100%; + padding: 0.38rem 0.4rem; + border-radius: var(--cw-radius-m); + color: var(--cw-text-2); + font-size: 0.65rem; + text-align: left; + cursor: pointer; + transition: color var(--cw-fast), background-color var(--cw-fast); +} + +.cw-palette__row[hidden] { + display: none; +} + +.cw-palette__row:hover { + background-color: var(--cw-elevated); + color: var(--cw-text); +} + +.cw-palette__row[aria-selected="true"] { + background-color: var(--cw-elevated); + color: var(--cw-text); +} + +.cw-palette__row[aria-selected="true"] .cw-square { + background-color: var(--cw-accent); +} + +.cw-palette__row:focus-visible { + outline-offset: -1px; +} + +.cw-palette__name { + font-weight: 500; +} + +.md-typeset .cw-palette__row code { + justify-self: start; + overflow: hidden; + max-width: 100%; + padding: 0; + border: 0; + background: none; + color: var(--cw-text-2); + font-family: var(--md-code-font-family); + font-size: 0.55rem; + text-overflow: ellipsis; + white-space: nowrap; +} + +.cw-palette__empty { + margin: 0; + padding: 0.5rem 0.8rem; + color: var(--cw-text-2); + font-size: 0.6rem; +} + +.cw-palette__hints { + display: flex; + gap: 0.8rem; + margin-top: auto; + padding: 0.4rem 0.7rem; + border-top: 1px solid var(--cw-border); + color: var(--cw-text-2); + font-size: 0.55rem; +} + +.cw-palette__hints kbd + kbd { + margin-left: 2px; +} + +.cw-palette__main { + min-width: 0; +} + +.cw-pane + .cw-pane { + border-top: 1px solid var(--cw-border); +} + +/* Only after the first switch, so the code is visible as soon as the page paints */ +.cw-palette.has-switched .cw-pane:not([hidden]) { + animation: cw-pane-in 160ms var(--cw-ease-out); +} + +@keyframes cw-pane-in { + from { + opacity: 0; + transform: translateY(2px); } } -/* "Why Crosswind" cards: 1 column on phones, 2 on tablets, 4 on desktop */ -.cw-home .grid.cards { - grid-template-columns: 1fr; +.cw-pane__head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1.2rem; + padding: 0.55rem 0.8rem; + border-bottom: 1px solid var(--cw-border); +} + +.cw-home .cw-pane__about p { + margin: 0; + color: var(--cw-text-2); + font-size: 0.6rem; + line-height: 1.45; +} + +.cw-home .cw-pane__about p:first-child { + margin-bottom: 0.1rem; + color: var(--cw-text); + font-size: 0.7rem; +} + +.cw-pane__about strong { + font-weight: 600; +} + +.cw-home .cw-pane__links p { + display: flex; + flex-shrink: 0; + gap: 0.3rem; + margin: 0; +} + +.cw-pane__links { + flex-shrink: 0; +} + +.cw-pane__links .md-button { + white-space: nowrap; +} + +.cw-pane > .highlight { + margin: 0; + border: 0; + border-radius: 0; + background: none; +} + +.cw-palette.is-ready .cw-pane > .highlight { + height: 21.6rem; + overflow: auto; + scrollbar-color: var(--cw-border) transparent; + scrollbar-width: thin; +} + +/* Sections: a label column and a content column, split by thin rules */ +.cw-section { + position: relative; + padding: 1.4rem 0 0 13rem; + border-top: 1px solid var(--cw-border); +} + +.cw-palette + .cw-section { + margin-top: 2.4rem; +} + +.cw-section + .cw-section { + margin-top: 1.4rem; +} + +.cw-section > h2 { + position: absolute; + top: 1.4rem; + left: 0; + width: 11rem; + margin: 0; + padding: 0; + border: 0; + font-size: 0.8rem; + letter-spacing: -0.008em; + line-height: 1.45; +} + +.cw-section > :first-child + * { + margin-top: 0; +} + +.cw-section > :last-child { + margin-bottom: 0; +} + +.cw-home .cw-section img { + display: block; + height: 1rem; + border-radius: var(--cw-radius-s); +} + +.cw-home table code { + white-space: nowrap; +} + +.cw-demo { + display: grid; + place-items: center; + overflow: hidden; + width: 100%; + aspect-ratio: 16 / 9; + border: 1px solid var(--cw-border); + border-radius: var(--cw-radius-l); + background-color: var(--cw-surface); +} + +/* Stays short until the visitor loads the game */ +.cw-demo:has(button) { + place-items: center start; + padding: 0.5rem 0.7rem; + aspect-ratio: auto; +} + +.cw-demo iframe { + width: 100%; + height: 100%; + border: 0; } -@media screen and (min-width: 48em) { - .cw-home .grid.cards { - grid-template-columns: repeat(2, 1fr); +@media screen and (max-width: 60em) { + /* iOS zooms the page when a smaller input takes focus */ + .cw-palette__filter input { + font-size: 0.8rem; + } + + .cw-hero { + padding-top: 1.4rem; + } + + .cw-hero .md-grid { + grid-template-columns: minmax(0, 1fr); + gap: 1.2rem; + } + + + .cw-hero h1 { + font-size: 1.6rem; + } + + + .cw-palette.is-ready { + grid-template-columns: minmax(0, 1fr); + } + + .cw-palette__side { + border-right: 0; + border-bottom: 1px solid var(--cw-border); + } + + .cw-palette__hints { + display: none; + } + + .cw-pane__head { + flex-direction: column; + align-items: flex-start; + gap: 0.5rem; + } + + .cw-palette.is-ready .cw-pane > .highlight { + height: auto; + max-height: 22rem; + } + + .cw-section { + padding-left: 0; + } + + .cw-section > h2 { + position: static; + width: auto; + margin-bottom: 0.6rem; + } + + /* The packages table reads as a stack of rows */ + .cw-home table:not([class]), + .cw-home table:not([class]) :is(tbody, tr, td) { + display: block; + } + + .cw-home table:not([class]) thead { + display: none; + } + + .cw-home table:not([class]) tr { + padding: 0.5rem 0.6rem; + } + + .cw-home table:not([class]) tr + tr { + border-top: 1px solid var(--cw-border); + } + + .cw-home table:not([class]) td { + padding: 0.1rem 0; + border: 0; } } -@media screen and (min-width: 64em) { - .cw-home .grid.cards { - grid-template-columns: repeat(4, 1fr); +@media (prefers-reduced-motion: reduce) { + .cw-palette.has-switched .cw-pane:not([hidden]) { + animation: none; } } /* Ecosystem diagram under the hero. javascripts/system.js draws the SVG; the - classes below style its strokes, faces and text. It uses the site's zinc - tokens with blue as the only accent, and no shadows. The --sx-* values that - theme.css has no token for sit between its zinc steps. */ + classes below style its strokes, faces and text. The --sx-* values that + theme.css has no token for sit between its surface steps. */ .cw-system { --sx-bg: var(--cw-bg); --sx-rule: var(--cw-border); --sx-line: var(--cw-text-2); --sx-text: var(--cw-text); - --sx-meta: var(--cw-meta); + --sx-meta: var(--cw-text-2); --sx-accent: var(--cw-accent); --sx-accent-soft: color-mix(in srgb, var(--cw-accent) 12%, transparent); - --sx-grid: #f4f4f5; /* zinc-100 */ - --sx-grid-2: #e4e4e7; /* zinc-200 */ - --sx-line-2: #a1a1aa; /* zinc-400 */ - --sx-face-t: #fafafa; /* zinc-50 */ - --sx-face-l: #f4f4f5; /* zinc-100 */ - --sx-face-r: #e4e4e7; /* zinc-200 */ -} - -[data-md-color-scheme="slate"] .cw-system { - --sx-grid: #212124; - --sx-grid-2: #2c2c30; - --sx-line-2: #52525b; /* zinc-600 */ - --sx-face-t: #27272a; /* zinc-800 */ - --sx-face-l: #212124; - --sx-face-r: #1d1d20; -} - -.cw-system-section { - padding: 1.5rem 0 0; + --sx-grid: #1e2022; + --sx-grid-2: #26292c; + --sx-line-2: var(--cw-border-strong); + --sx-face-t: var(--cw-elevated); + --sx-face-l: var(--cw-surface); + --sx-face-r: #1c1e20; } -.cw-system-section .md-grid { - padding: 0 0.8rem; -} - -.cw-system { +.md-typeset .cw-system { + display: block; position: relative; - margin: 0; + overflow: hidden; + width: 100%; + max-width: none; + margin: 1.2rem 0 0; border: 1px solid var(--cw-border); + border-radius: var(--cw-radius-xl); background-color: var(--sx-bg); color: var(--sx-text); - font-family: var(--md-text-font-family); + font-family: var(--md-code-font-family); } .cw-system__head, @@ -192,11 +598,17 @@ align-items: center; justify-content: space-between; gap: 1rem; - padding: 0.45rem 0.75rem; - color: var(--sx-meta); - font-size: 0.6rem; - letter-spacing: 0.06em; - text-transform: uppercase; + padding: 0.35rem 0.7rem; + background-color: var(--cw-surface); + color: var(--cw-text-2); + font-family: var(--md-text-font-family); + font-size: 0.55rem; +} + +.md-typeset figcaption.cw-system__head { + max-width: none; + margin: 0; + font-style: normal; } .cw-system__head { @@ -231,23 +643,25 @@ pointer-events: none; } -.cw-system__message { +.md-typeset .cw-system__message { grid-area: 1 / 1; margin: 0; color: var(--sx-text); - font-size: 0.95rem; + font-family: var(--md-text-font-family); + font-size: 0.8rem; font-weight: 500; + letter-spacing: -0.008em; line-height: 1.35; opacity: 0; transform: translateY(0.3rem); - transition: opacity 200ms, transform 200ms; + transition: opacity var(--cw-fast), transform var(--cw-fast); } /* The outgoing line clears before the next one rises in */ -.cw-system__message.is-on { +.md-typeset .cw-system__message.is-on { opacity: 1; transform: none; - transition: opacity 500ms var(--cw-ease-out) 250ms, transform 500ms var(--cw-ease-out) 250ms; + transition: opacity 500ms var(--cw-ease-out) var(--cw-fast), transform 500ms var(--cw-ease-out) var(--cw-fast); } /* Positioned, so each layer keeps its own paint record and an unchanged layer is @@ -257,7 +671,7 @@ position: relative; width: 100%; height: auto; - transition: opacity 250ms; + transition: opacity var(--cw-fast); } .cw-system__scene svg + svg { @@ -280,7 +694,7 @@ } /* Stage buttons double as a progress readout */ -.cw-system__stages { +.md-typeset ol.cw-system__stages:not([hidden]) { display: flex; gap: 1rem; margin: 0; @@ -288,6 +702,10 @@ list-style: none; } +.md-typeset ol.cw-system__stages li { + margin: 0; +} + .cw-system__stages button { padding: 0; border: 0; @@ -297,7 +715,7 @@ letter-spacing: inherit; text-transform: inherit; cursor: pointer; - transition: color 200ms; + transition: color var(--cw-fast); } .cw-system__stages button { @@ -336,22 +754,18 @@ /* Same look as .md-button in theme.css, at the size of the footer text */ .cw-system__replay { - padding: 0.25rem 0.6rem; + padding: 0 0.35rem; border: 1px solid var(--cw-border); - border-radius: 2px; - background-color: var(--cw-surface); + border-radius: var(--cw-radius-s); + background-color: var(--cw-elevated); color: var(--cw-text); font: inherit; - font-weight: 500; - letter-spacing: inherit; - text-transform: inherit; cursor: pointer; - transition: color 200ms, border-color 200ms, opacity 100ms; + transition: color var(--cw-fast), border-color var(--cw-fast), opacity 100ms; } .cw-system__replay:is(:hover, :focus-visible) { - border-color: var(--cw-accent); - color: var(--cw-accent); + border-color: var(--cw-border-strong); } .cw-system__replay:active { @@ -371,10 +785,13 @@ z-index: 1; width: 15rem; padding: 0.55rem 0.7rem; - border: 1px solid var(--cw-border); - background-color: var(--cw-surface); - color: var(--sx-meta); - font-size: 0.62rem; + border: 1px solid var(--cw-border-strong); + border-radius: var(--cw-radius-m); + background-color: var(--cw-elevated); + box-shadow: var(--cw-shadow); + color: var(--cw-text-2); + font-family: var(--md-text-font-family); + font-size: 0.6rem; line-height: 1.5; pointer-events: none; } @@ -396,7 +813,7 @@ padding: 0; background: none; color: var(--sx-accent); - font-family: inherit; + font-family: var(--md-code-font-family); font-size: inherit; } @@ -424,7 +841,7 @@ stroke-width: 1.15; stroke-linecap: round; stroke-linejoin: round; - transition: stroke 300ms; + transition: stroke var(--cw-fast); } .cw-system .e.l2 { @@ -586,7 +1003,7 @@ } .cw-system [data-mod] { - transition: opacity 200ms; + transition: opacity var(--cw-fast); } .cw-system .is-dim { @@ -621,8 +1038,8 @@ padding: 0.7rem 0.75rem 0; } - .cw-system__message { - font-size: 0.85rem; + .md-typeset .cw-system__message { + font-size: 0.7rem; } .cw-system__stages button span:first-child { @@ -633,10 +1050,14 @@ .cw-system__stage-name { display: none; } + + .md-typeset figcaption.cw-system__head { + text-align: left; + } } @media (prefers-reduced-motion: reduce) { - .cw-system__message, + .md-typeset .cw-system__message, .cw-system svg, .cw-system .e, .cw-system [data-mod] { diff --git a/docs/docs/stylesheets/theme.css b/docs/docs/stylesheets/theme.css index 431b251..725475e 100644 --- a/docs/docs/stylesheets/theme.css +++ b/docs/docs/stylesheets/theme.css @@ -1,38 +1,39 @@ -/* viswa.space style: flat, zinc greys, blue as the only accent, square boxes, no shadows. - --cw-* are the raw tokens; the --md-* lines map them onto Material. */ +/* Design system. --cw-* are the raw tokens; the --md-* lines map them onto Material. + Material sizes everything in rem, and html is pinned to 20px, so .05rem = 1px. */ :root, -[data-md-color-scheme="default"] { - --cw-bg: #ffffff; /* white */ - --cw-surface: #f4f4f5; /* zinc-100 */ - --cw-code: #e4e4e7; /* zinc-200 */ - --cw-border: #d4d4d8; /* zinc-300 */ - --cw-text: #18181b; /* zinc-900 */ - --cw-text-2: #52525b; /* zinc-600 */ - --cw-meta: #71717a; /* zinc-500 */ - --cw-accent: #2563eb; /* blue-600; blue-500 is under 4.5:1 on white */ - --cw-accent-hover: #1d4ed8; /* blue-700 */ -} - [data-md-color-scheme="slate"] { - --cw-bg: #18181b; /* zinc-900 */ - --cw-surface: #27272a; /* zinc-800 */ - --cw-code: #27272a; /* zinc-800 */ - --cw-border: #3f3f46; /* zinc-700 */ - --cw-text: #ffffff; - --cw-text-2: #a1a1aa; /* zinc-400 */ - --cw-meta: #a1a1aa; /* zinc-400 */ - --cw-accent: #60a5fa; /* blue-400 */ - --cw-accent-hover: #3b82f6; /* blue-500 */ -} + color-scheme: dark; -:root, -[data-md-color-scheme="default"], -[data-md-color-scheme="slate"] { + --cw-bg: #191b1d; + --cw-surface: #202225; + --cw-elevated: #282b2e; + --cw-border: #34373b; + --cw-border-strong: #484c51; + --cw-text: #e6e7e8; + --cw-text-2: #92979e; + --cw-label: #7f858c; /* small labels; 4.6:1 on --cw-bg, where --cw-muted is too faint */ + --cw-muted: #686d74; /* decoration only, such as prompts and separators */ + --cw-accent: #67cdbb; + --cw-purple: #a99fd6; + --cw-amber: #d0ae74; + --cw-red: #d98a82; + + --cw-radius-s: 4px; + --cw-radius-m: 6px; + --cw-radius-l: 8px; + --cw-radius-xl: 10px; + + --cw-fast: 140ms; --cw-ease-out: cubic-bezier(0.23, 1, 0.32, 1); + --cw-shadow: 0 8px 24px rgb(0 0 0 / 28%); + + --md-text-font: "Geist"; + --md-code-font: "Geist Mono"; + --md-default-bg-color: var(--cw-bg); --md-default-fg-color: var(--cw-text); --md-default-fg-color--light: var(--cw-text-2); - --md-default-fg-color--lighter: var(--cw-meta); + --md-default-fg-color--lighter: var(--cw-label); --md-default-fg-color--lightest: var(--cw-border); --md-primary-fg-color: var(--cw-bg); @@ -40,19 +41,42 @@ --md-primary-fg-color--dark: var(--cw-bg); --md-primary-bg-color: var(--cw-text); --md-primary-bg-color--light: var(--cw-text-2); - --md-accent-fg-color: var(--cw-accent-hover); - --md-accent-fg-color--transparent: var(--cw-surface); + --md-accent-fg-color: var(--cw-accent); + --md-accent-fg-color--transparent: color-mix(in srgb, var(--cw-accent) 10%, transparent); + --md-accent-bg-color: var(--cw-bg); --md-typeset-color: var(--cw-text); --md-typeset-a-color: var(--cw-accent); - --md-code-bg-color: var(--cw-surface); - --md-code-fg-color: var(--cw-text); --md-typeset-table-color: var(--cw-border); + --md-typeset-mark-color: color-mix(in srgb, var(--cw-accent) 24%, transparent); --md-footer-bg-color: var(--cw-bg); - --md-footer-bg-color--dark: var(--cw-bg); + --md-footer-bg-color--dark: var(--cw-surface); --md-footer-fg-color: var(--cw-text); --md-footer-fg-color--light: var(--cw-text-2); - --md-footer-fg-color--lighter: var(--cw-meta); + --md-footer-fg-color--lighter: var(--cw-text-2); + + --md-code-bg-color: var(--cw-surface); + --md-code-fg-color: var(--cw-text); + --md-code-hl-color: var(--cw-accent); + --md-code-hl-color--light: color-mix(in srgb, var(--cw-accent) 12%, transparent); + --md-code-hl-keyword-color: var(--cw-purple); + --md-code-hl-string-color: var(--cw-accent); + --md-code-hl-number-color: var(--cw-amber); + --md-code-hl-constant-color: var(--cw-amber); + --md-code-hl-special-color: var(--cw-amber); + --md-code-hl-function-color: #b9d4e6; + --md-code-hl-name-color: var(--cw-text); + --md-code-hl-variable-color: var(--cw-text); + --md-code-hl-operator-color: var(--cw-text-2); + --md-code-hl-punctuation-color: var(--cw-text-2); + --md-code-hl-comment-color: var(--cw-text-2); + --md-code-hl-generic-color: var(--cw-text-2); +} + +html { + font-size: 125%; /* Material grows this on wide screens; a native tool keeps its density */ + scrollbar-color: var(--cw-border) transparent; + scrollbar-width: thin; } body { @@ -61,168 +85,1267 @@ body { -moz-osx-font-smoothing: grayscale; } -/* Header and footer: page background with a 1px rule, no shadow */ +::selection { + background-color: color-mix(in srgb, var(--cw-accent) 28%, transparent); + color: var(--cw-text); +} + +input { + caret-color: var(--cw-accent); +} + +:focus-visible { + outline: 1px solid var(--cw-accent); + outline-offset: 2px; +} + +.md-sidebar__scrollwrap, +.md-search__scrollwrap, +.highlight code, +.md-typeset__scrollwrap { + scrollbar-color: var(--cw-border) transparent; + scrollbar-width: thin; +} + +.md-grid { + max-width: 66rem; +} + +/* Header */ .md-header, .md-header--shadow { border-bottom: 1px solid var(--cw-border); box-shadow: none; } +.md-header[data-md-state="shadow"], +.md-header--shadow { + box-shadow: none; +} + +.md-tabs + * { + border-top: 0; +} + +.md-header__inner { + padding: 0 0.3rem; +} + +.md-header__button.md-logo { + margin: 0 0.1rem 0 0; + padding: 0.3rem; +} + +.md-header__button.md-logo :is(img, svg) { + height: 1.1rem; + width: auto; +} + +.md-header__title { + height: 2.2rem; + margin-left: 0.2rem; + font-size: 0.7rem; + line-height: 2.2rem; +} + .md-header__topic:first-child { font-weight: 600; + letter-spacing: -0.01em; } -.md-footer { - border-top: 1px solid var(--cw-border); +.md-header__topic + .md-header__topic { + color: var(--cw-text-2); + font-weight: 400; +} + +.md-header__button { + padding: 0.3rem; +} + +.md-header__source { + width: auto; + max-width: none; + margin-left: 0.6rem; + padding-right: 0.3rem; +} + +.md-source__repository { + max-width: none; +} + +/* A single line; the fetched release tag names a different package on most pages */ +.md-source__facts { + display: none; +} + +.md-source { + color: var(--cw-text-2); + font-size: 0.55rem; + transition: color var(--cw-fast); +} + +.md-source:hover { + color: var(--cw-text); + opacity: 1; +} + +.md-source__icon { + width: 1.4rem; + height: 1.4rem; +} + +.md-source__icon svg { + width: 0.8rem; + height: 0.8rem; + margin-top: 0.3rem; + margin-left: 0.3rem; +} + +.md-source__icon + .md-source__repository { + margin-left: -1.2rem; + padding-left: 1.3rem; + font-family: var(--md-code-font-family); +} + +/* Package tabs. The square marks where the reader is, here and in the sidebar. */ +.md-tabs { + border-bottom: 1px solid var(--cw-border); + background-color: var(--cw-bg); + color: var(--cw-text-2); +} + +.md-tabs__list { + margin-left: 0.15rem; +} + +.md-tabs__item { + height: 1.7rem; + padding: 0 0.45rem; +} + +.md-tabs__link { + display: inline-flex; + align-items: center; + margin-top: 0.42rem; + font-size: 0.625rem; + opacity: 1; + transition: color var(--cw-fast); +} + +.md-tabs__link:is(:hover, :focus) { + color: var(--cw-text); +} + +.md-tabs__item--active .md-tabs__link { + color: var(--cw-text); + font-weight: 500; +} + +.md-tabs__item--active .md-tabs__link::before, +.md-nav__link--active::before { + content: ""; + width: 5px; + height: 5px; + border-radius: 1px; + background-color: var(--cw-accent); +} + +.md-tabs__item--active .md-tabs__link::before { + margin-right: 0.3rem; +} + +/* In the gutter, so the marker doesn't push the label sideways */ +.md-nav__link--active::before { + position: absolute; + top: calc(50% - 2.5px); + left: 0.2rem; } +/* Search, as a command palette */ .md-search__form { + height: 1.4rem; border: 1px solid var(--cw-border); - border-radius: 2px; + border-radius: var(--cw-radius-m); + background-color: var(--cw-surface); + transition: border-color var(--cw-fast); +} + +.md-search__form:hover { + border-color: var(--cw-border-strong); background-color: var(--cw-surface); } -/* Headings: Material's default weights */ -.md-typeset :is(h1, h2, h3) { +.md-search__input:focus-visible { + outline: 0; +} + +.md-search__form:focus-within { + border-color: var(--cw-border-strong); +} + +.md-search__form::after { + content: attr(data-shortcut); + position: absolute; + top: 0.2rem; + right: 0.25rem; + padding: 0 0.2rem; + border: 1px solid var(--cw-border); + border-radius: var(--cw-radius-s); + background-color: var(--cw-elevated); + color: var(--cw-text-2); + font: 400 0.5rem/0.85rem var(--md-code-font-family); + pointer-events: none; +} + +.md-search__form:not([data-shortcut])::after, +.md-search__form:has(input:not(:placeholder-shown))::after { + display: none; +} + +@media screen and (min-width: 60em) { + .md-search__inner { + width: 11rem; + } + + .md-search__input { + height: 1.3rem; + padding-left: 1.5rem; + font-size: 0.625rem; + } + + .md-search__input::placeholder { + color: var(--cw-text-2); + } + + .md-search__icon[for="__search"] { + top: 0.2rem; + left: 0.35rem; + width: 0.9rem; + height: 0.9rem; + } + + .md-search__icon svg { + width: 0.9rem; + height: 0.9rem; + } + + .md-search__options { + top: 0.25rem; + right: 0.25rem; + } + + .md-search__output { + top: 1.75rem; + border: 1px solid var(--cw-border); + border-radius: var(--cw-radius-xl); + background-color: var(--cw-surface); + box-shadow: var(--cw-shadow); + } + + [data-md-toggle="search"]:checked ~ .md-header .md-search__inner { + position: fixed; + top: 14vh; + left: 50%; + width: 34rem; + margin: 0; + padding: 0; + z-index: 2; + float: none; + transform: translateX(-50%); + } + + [data-md-toggle="search"]:checked ~ .md-header .md-search__form { + height: 2rem; + border-color: var(--cw-border-strong); + border-radius: var(--cw-radius-xl); + background-color: var(--cw-surface); + box-shadow: var(--cw-shadow); + } + + [data-md-toggle="search"]:checked ~ .md-header .md-search__form::after { + content: "esc"; + top: 0.5rem; + right: 0.5rem; + } + + [data-md-toggle="search"]:checked ~ .md-header .md-search__input { + height: 1.9rem; + padding-left: 2rem; + font-size: 0.75rem; + } + + [data-md-toggle="search"]:checked ~ .md-header .md-search__icon[for="__search"] { + top: 0.5rem; + left: 0.6rem; + } + + /* Input and results read as one panel */ + [data-md-toggle="search"]:checked ~ .md-header .md-search__form:has(input:not(:placeholder-shown)) { + border-radius: var(--cw-radius-xl) var(--cw-radius-xl) 0 0; + box-shadow: none; + } + + [data-md-toggle="search"]:checked ~ .md-header .md-search__output { + top: 2rem; + border-top: 0; + border-color: var(--cw-border-strong); + border-radius: 0 0 var(--cw-radius-xl) var(--cw-radius-xl); + } + + [data-md-toggle="search"]:checked ~ .md-header .md-search__options { + top: 0.45rem; + right: 0.5rem; + } + + .md-search__scrollwrap { + max-height: 52vh; + background-color: transparent; + } +} + +.md-search__overlay { + background-color: rgb(12 13 14 / 64%); +} + +html .md-search-result .md-search-result__meta { + padding: 0 0.8rem; + border-bottom: 1px solid var(--cw-border); + background-color: transparent; + color: var(--cw-text-2); + font-size: 0.55rem; + line-height: 1.5rem; +} + +.md-search-result__item { + box-shadow: 0 -1px var(--cw-border); +} + +.md-search-result__item:first-child { + box-shadow: none; +} + +.md-search-result__link { + transition: background-color var(--cw-fast); +} + +.md-search-result__link:is(:focus, :hover) { + background-color: var(--cw-elevated); +} + +/* One compact row per result: icon, title, one line of context */ +/* Material indents results for its icons and nests API results, so flatten both */ +.md-search-result__icon, +.md-search-result__article hr, +.md-search-result__article > :is(h1, h2) ~ :is(h1, h2), +.md-search-result__article > :is(h1, h2) ~ * ~ * { + display: none; +} + +.md-search-result__link { + display: block; +} + +/* Results can hold bare text nodes that no selector can hide, so clip to title plus one line */ +html .md-search-result__link .md-search-result__article { + max-height: 2.25rem; + margin-bottom: 0.3rem; + padding: 0.3rem 0.8rem 0; +} + +html .md-search-result__link .md-search-result__article :is(h1, h2) { + margin: 0; + padding: 0; + border: 0; + font-size: 0.65rem; + font-weight: 500; + line-height: 1.45; +} + +html .md-search-result__link .md-search-result__article > p ~ p { + display: none; +} + +html .md-search-result__link .md-search-result__article > :is(h1, h2) ~ :is(h1, h2) { + display: none; +} + +[dir] .md-search-result__more > summary > div { + padding: 0 0.8rem 0.3rem; + font-size: 0.55rem; +} + +.md-search-result__article { + margin: 0; + padding: 0.3rem 0.8rem; + font-size: 0.6rem; + line-height: 1.45; +} + +.md-search-result__article :is(h1, h2) { + margin: 0; + padding: 0; + border: 0; color: var(--cw-text); + font-size: 0.65rem; + font-weight: 500; + letter-spacing: 0; + line-height: 1.45; +} + +.md-search-result__article > :not(h1, h2, p), +.md-search-result__article > p ~ * { + display: none; +} + +.md-search-result__article > p { + display: -webkit-box; + overflow: hidden; + max-width: none; + margin: 0; + color: var(--cw-text-2); + -webkit-box-orient: vertical; + -webkit-line-clamp: 1; + line-clamp: 1; +} + +.cw-search-hints { + display: flex; + gap: 0.8rem; + padding: 0.3rem 0.8rem; + border-top: 1px solid var(--cw-border); + color: var(--cw-text-2); + font-size: 0.55rem; +} + +.cw-search-hints kbd + kbd { + margin-left: 2px; +} + +.md-search-result mark { + background: none; + color: var(--cw-accent); + text-decoration: none; +} + +/* Sidebars */ +@media screen and (min-width: 76.25em) { + .md-sidebar--primary { + width: 11rem; + } + + .md-sidebar--secondary { + width: 10rem; + } + + /* The title is only needed for back navigation in the mobile drawer */ + .md-nav--primary .md-nav__title { + display: none; + } + + .md-nav--primary .md-nav__item--section { + margin: 0.9rem 0 0; + } + + .md-nav--lifted > .md-nav__list > .md-nav__item > [for] { + margin: 0 0 0.1rem; + box-shadow: none; + color: var(--cw-label); + font-size: 0.55rem; + font-weight: 500; + } + + .md-nav--primary .md-nav__item--section > .md-nav { + margin: 0; + } + + .md-nav--primary .md-nav__item--section > .md-nav__link { + padding: 0.1rem 0.7rem; + color: var(--cw-label); + font-size: 0.55rem; + font-weight: 500; + } +} + +.md-nav { + font-size: 0.625rem; + line-height: 1.35; +} + +.md-nav__link { + position: relative; + align-items: center; + margin: 1px 0 0; + padding: 0.2rem 0.4rem 0.2rem 0.7rem; + border-radius: var(--cw-radius-m); + color: var(--cw-text-2); + transition: color var(--cw-fast), background-color var(--cw-fast); +} + +.md-nav__link[href]:hover, +.md-nav__link[for]:hover { + background-color: var(--cw-surface); + color: var(--cw-text); +} + +.md-nav__item .md-nav__link--active, +.md-nav__item .md-nav__link--active code { + color: var(--cw-text); +} + +.md-nav--primary .md-nav__link--active { + background-color: var(--cw-elevated); + font-weight: 500; +} + +.md-nav--secondary .md-nav__title { + padding: 0 0.4rem 0.2rem 0.7rem; + background: none; + box-shadow: none; + color: var(--cw-label); + font-size: 0.55rem; + font-weight: 500; +} + +.md-nav--secondary .md-nav__link--active::before { + display: none; +} + +.md-nav--secondary .md-nav__link--active { + color: var(--cw-accent); +} + +.md-nav--secondary .md-nav__link { + padding-block: 0.12rem; +} + +.md-nav--secondary .md-nav__list .md-nav__list { + padding-left: 0.5rem; +} + +.md-sidebar__inner { + padding-right: 0.2rem; +} + +/* Breadcrumb */ +.md-path { + margin: 0 0.8rem; + padding-top: 1rem; + color: var(--cw-text-2); + font-size: 0.55rem; + line-height: 1.4; +} + +@media screen and (min-width: 76.25em) { + .md-path { + margin-left: 1.2rem; + } +} + +.md-path__item:not(:first-child)::before { + color: var(--cw-muted); +} + +.md-path__link { + transition: color var(--cw-fast); +} + +.md-path__link:hover { + color: var(--cw-text); +} + +/* The reading column. Code and tables take the full width; prose keeps its measure. */ +@media screen and (min-width: 76.25em) { + .md-content:not(:has(.cw-home)) { + max-width: 42rem; + } +} + +.md-content__inner { + padding-top: 0.5rem; +} + +.md-typeset { + font-size: 0.75rem; + line-height: 1.65; + overflow-wrap: break-word; +} + +.md-typeset :is(p, li) { + max-width: 72ch; +} + +.md-typeset :is(h1, h2, h3, h4) { + color: var(--cw-text); + font-weight: 600; + text-wrap: balance; +} + +.md-typeset h1 { + margin: 0 0 0.9rem; + font-size: 1.5rem; + letter-spacing: -0.028em; + line-height: 1.15; +} + +/* Thin dividers mark sections, not background changes */ +.md-typeset h2 { + margin: 2.2rem 0 0.7rem; + padding-top: 1.1rem; + border-top: 1px solid var(--cw-border); + font-size: 1rem; + letter-spacing: -0.016em; + line-height: 1.3; +} + +.md-typeset h1 + h2 { + margin-top: 1rem; } .md-typeset h3 { + margin: 1.5rem 0 0.4rem; + font-size: 0.8rem; + letter-spacing: -0.008em; + line-height: 1.4; +} + +.md-typeset h4 { + margin: 1.2rem 0 0.3rem; + color: var(--cw-text-2); + font-size: 0.7rem; + letter-spacing: 0; +} + +.md-typeset a { text-decoration: underline; - text-underline-offset: 0.2em; + text-decoration-color: color-mix(in srgb, var(--cw-accent) 36%, transparent); + text-decoration-thickness: 1px; + text-underline-offset: 0.18em; + transition: color var(--cw-fast), text-decoration-color var(--cw-fast); } -/* Links */ -.md-typeset a:not(.md-button, .headerlink) { - transition: color 200ms; +.md-typeset a:is(:hover, :focus) { + color: var(--cw-accent); + text-decoration-color: var(--cw-accent); } -.md-typeset a:not(.md-button, .headerlink):is(:hover, :focus) { - color: var(--cw-accent-hover); +.md-typeset .headerlink { + color: var(--cw-muted); + font-weight: 400; + text-decoration: none; } -.md-nav__link--active, -.md-nav__link--active code { +.md-typeset .headerlink:is(:hover, :focus) { color: var(--cw-accent); } -/* Boxes: square, 1px zinc border, no shadow */ -.md-typeset pre > code, -.md-typeset table:not([class]), -.md-typeset .grid.cards > ul > li, -.md-typeset .admonition, -.md-typeset details, -.cw-demo { +.md-typeset hr { + border-bottom-color: var(--cw-border); +} + +.md-typeset :is(ul, ol) li { + margin-bottom: 0.3em; +} + +.md-typeset [type="checkbox"]:checked + .task-list-indicator::before { + background-color: var(--cw-accent); +} + +.md-typeset .task-list-indicator::before { + background-color: var(--cw-muted); +} + +/* Inline code */ +.md-typeset code { + font-size: 0.84em; + font-variant-ligatures: none; +} + +.md-typeset :not(pre) > code { + padding: 0.08em 0.32em; + border: 1px solid var(--cw-border); + border-radius: var(--cw-radius-s); + background-color: var(--cw-surface); +} + +.md-typeset a > code { + color: inherit; + transition: border-color var(--cw-fast); +} + +.md-typeset a:is(:hover, :focus) > code { + border-color: var(--cw-accent); + background-color: var(--cw-surface); + color: inherit; +} + +/* Code panels */ +.md-typeset .highlight { + overflow: hidden; + margin: 1em 0; border: 1px solid var(--cw-border); + border-radius: var(--cw-radius-l); + background-color: var(--cw-surface); +} + +.md-typeset .highlight pre { + margin: 0; +} + +.md-typeset pre > code { + padding: 0.65rem 0.8rem; border-radius: 0; - box-shadow: none; + background: none; + font-size: 0.625rem; + line-height: 1.6; + font-variant-numeric: tabular-nums; } -.md-typeset .grid { - gap: 0.5rem; +.highlight span.filename { + margin: 0; + padding: 0.3rem 0.8rem; + border-bottom: 1px solid var(--cw-border); + border-radius: 0; + background: var(--cw-elevated); + color: var(--cw-text-2); + font: 400 0.55rem/1.5 var(--md-code-font-family); } -.md-typeset .grid.cards > ul > li { - padding: 1rem; +.highlight span.filename + pre > code { + border-radius: 0; +} + +.md-clipboard { + top: 0.3rem; + right: 0.3rem; + width: 1.1rem; + height: 1.1rem; + border: 1px solid transparent; + border-radius: var(--cw-radius-s); + color: var(--cw-text-2); + transition: color var(--cw-fast), border-color var(--cw-fast), background-color var(--cw-fast), opacity var(--cw-fast); +} + +.md-clipboard::after { + width: 0.8rem; + height: 0.8rem; + margin: 0.1rem; +} + +.md-clipboard:is(:hover, :focus-visible) { + border-color: var(--cw-border-strong); + background-color: var(--cw-elevated); + color: var(--cw-text); +} + +.md-code__nav { + top: 0.3rem; + right: 0.3rem; + padding: 0; + background: none; +} + +.md-code__button { + color: var(--cw-text-2); + border-radius: var(--cw-radius-s); + transition: color var(--cw-fast), background-color var(--cw-fast); +} + +.md-code__button:hover { + background-color: var(--cw-elevated); +} + +/* Tabs become the title bar of the code panel */ +.md-typeset .tabbed-set { + overflow: hidden; + margin: 1em 0; + border: 1px solid var(--cw-border); + border-radius: var(--cw-radius-l); background-color: var(--cw-surface); } -.md-typeset .grid.cards > ul > li:hover { - border-color: var(--cw-border); +.md-typeset .tabbed-labels { + border-bottom: 1px solid var(--cw-border); + background-color: var(--cw-elevated); box-shadow: none; } -.md-typeset :not(pre) > code { - padding: 0 5px; - border-radius: 0; - background-color: var(--cw-code); +.md-typeset .tabbed-labels > label { + padding: 0.45rem 0.8rem 0.4rem; + color: var(--cw-text-2); + font-size: 0.6rem; + font-weight: 500; + transition: color var(--cw-fast); +} + +.md-typeset .tabbed-labels > label:hover { + color: var(--cw-text); +} + +.md-typeset .tabbed-set > input:checked + label, +.md-typeset .tabbed-labels > label:has(+ *) { + color: inherit; +} + +.md-typeset .tabbed-set > input:nth-child(n):checked ~ .tabbed-labels > label:nth-child(n):hover { + color: var(--cw-text); } .md-typeset .tabbed-labels::before { background: var(--cw-accent); } -.md-typeset blockquote { - border-left: 2px solid var(--cw-accent); - color: var(--cw-text-2); +.md-typeset .tabbed-content { + box-shadow: none; +} + +.md-typeset .tabbed-block { + padding: 0.1px 0.8rem; +} + +.md-typeset .tabbed-block:has(> .highlight:only-child) { + padding: 0; +} + +.md-typeset .tabbed-block > .highlight:only-child { + margin: 0; + border: 0; + border-radius: 0; +} + +/* Callouts: label left, text right. --cw-callout tints each + type: teal informs, amber warns, red means danger. + [dir] and the doubled class outrank Material's per-type colours, icons and title bars. */ +[dir] .md-typeset .admonition.admonition, +[dir] .md-typeset blockquote { + --cw-callout: var(--cw-accent); + + overflow: visible; + margin: 1.2em 0; + padding: 0.5rem 0.7rem; + border: 1px solid color-mix(in srgb, var(--cw-callout) 38%, var(--cw-border)); + border-radius: var(--cw-radius-l); + background-color: color-mix(in srgb, var(--cw-callout) 7%, var(--cw-surface)); + box-shadow: none; + color: var(--cw-text); + font-size: 0.7rem; + line-height: 1.6; +} + +[dir] .md-typeset .admonition.admonition:is(.warning, .caution, .attention), +[dir] .md-typeset blockquote:has(> p:first-child > strong:only-child) { + --cw-callout: var(--cw-amber); +} + +[dir] .md-typeset .admonition.admonition:is(.danger, .error, .failure) { + --cw-callout: var(--cw-red); } -.cw-demo { +[dir] .md-typeset .admonition.admonition, +[dir] .md-typeset blockquote:has(> p:first-child > strong:only-child) { display: grid; - place-items: center; - overflow: hidden; + grid-template-columns: 3.6rem minmax(0, 1fr); + gap: 0.4rem 0; +} + +[dir] .md-typeset .admonition.admonition > *, +[dir] .md-typeset blockquote > * { + grid-column: 2; + min-width: 0; + max-width: none; + margin: 0; +} + +[dir] .md-typeset .admonition > .admonition-title, +[dir] .md-typeset blockquote > p:first-child:has(> strong:only-child), +[dir] .md-typeset blockquote > p:first-child > strong:only-child { + display: flex; + grid-column: 1; + align-items: center; + gap: 0.25rem; + height: 1.12rem; /* one line of callout text, so the label sits on its first line */ + margin: 0; + padding: 0; + border: 0; + background: none; + color: var(--cw-callout); + font: 400 0.55rem/1 var(--md-code-font-family); +} + +[dir] .md-typeset .admonition > .admonition-title::before, +[dir] .md-typeset blockquote > p:first-child > strong:only-child::before { + content: ""; + position: static; + flex-shrink: 0; + width: 6px; + height: 6px; + border-radius: 1px; + background-color: var(--cw-callout); + -webkit-mask: none; + mask: none; +} + +@media screen and (max-width: 44.984375em) { + [dir] .md-typeset .admonition.admonition, + [dir] .md-typeset blockquote:has(> p:first-child > strong:only-child) { + grid-template-columns: minmax(0, 1fr); + gap: 0.15rem; + } + + [dir] .md-typeset .admonition.admonition > *, + [dir] .md-typeset blockquote > * { + grid-column: 1; + } +} + +/* Tables */ +.md-typeset__scrollwrap { + margin: 1em 0; +} + +.md-typeset__table { + display: block; + margin: 0; + padding: 0; +} + +.md-typeset table:not([class]) { + display: table; width: 100%; - aspect-ratio: 16 / 9; + overflow: hidden; + border: 1px solid var(--cw-border); + border-radius: var(--cw-radius-l); + border-collapse: separate; + border-spacing: 0; + font-size: 0.65rem; + line-height: 1.5; + font-variant-numeric: tabular-nums; +} + +.md-typeset table:not([class]) th { + padding: 0.35rem 0.6rem; + border-bottom: 1px solid var(--cw-border); background-color: var(--cw-surface); + color: var(--cw-text-2); + font-size: 0.6rem; + font-weight: 500; + white-space: nowrap; } -.cw-demo iframe { - width: 100%; - height: 100%; +.md-typeset table:not([class]) td { + padding: 0.4rem 0.6rem; + border-top: 1px solid var(--cw-border); + vertical-align: top; +} + +.md-typeset table:not([class]) tr:first-child td { + border-top: 0; +} + +.md-typeset table:not([class]) tbody tr { + transition: background-color var(--cw-fast); +} + +.md-typeset table:not([class]) tbody tr:hover { + background-color: var(--cw-surface); + box-shadow: none; +} + +/* Generated API pages */ +.md-typeset .doc-heading { + --md-code-hl-string-color: var(--cw-text-2); + font-size: 0.7rem; + letter-spacing: 0; +} + +.md-typeset .doc-heading code { + padding: 0; border: 0; + background: none; + font-size: 1em; + font-weight: 500; +} + +.md-typeset .doc-label code { + padding: 0 0.2rem; + border: 1px solid var(--cw-border); + border-radius: var(--cw-radius-s); + color: var(--cw-text-2); + font-size: 0.5rem; + font-weight: 400; +} + +.md-typeset .doc-symbol { + border-radius: var(--cw-radius-s); } -/* Buttons: small radius, colour change on hover, opacity on press */ +.doc-contents > .highlight, +.doc-signature { + font-size: 0.65rem; +} + +/* Buttons */ .md-typeset .md-button { + margin: 0; + padding: 0.28rem 0.65rem; border: 1px solid var(--cw-border); - border-radius: 2px; + border-radius: var(--cw-radius-m); background-color: var(--cw-surface); color: var(--cw-text); + font-size: 0.625rem; font-weight: 500; - transition: color 200ms, background-color 200ms, border-color 200ms, opacity 100ms; + line-height: 1.45; + text-decoration: none; + transition: background-color var(--cw-fast), border-color var(--cw-fast), color var(--cw-fast), opacity 120ms; } .md-typeset .md-button:is(:hover, :focus-visible) { - border-color: var(--cw-accent); - background-color: var(--cw-surface); - color: var(--cw-accent); + border-color: var(--cw-border-strong); + background-color: var(--cw-elevated); + color: var(--cw-text); } .md-typeset .md-button--primary { - border-color: #2563eb; - background-color: #2563eb; /* blue-600: white text stays above 4.5:1 in both themes */ - color: #ffffff; + border-color: color-mix(in srgb, var(--cw-accent) 42%, transparent); + background-color: color-mix(in srgb, var(--cw-accent) 14%, var(--cw-bg)); + color: var(--cw-accent); } .md-typeset .md-button--primary:is(:hover, :focus-visible) { - border-color: #1d4ed8; - background-color: #1d4ed8; - color: #ffffff; + border-color: color-mix(in srgb, var(--cw-accent) 70%, transparent); + background-color: color-mix(in srgb, var(--cw-accent) 20%, var(--cw-bg)); + color: var(--cw-accent); } .md-typeset .md-button:active { opacity: 0.75; } -@media (prefers-reduced-motion: reduce) { - .md-typeset .md-button, - .md-typeset a { - transition: none; +/* Badges and keycaps */ +.cw-badge { + display: inline-flex; + align-items: center; + gap: 0.2rem; + padding: 0 0.2rem; + border: 1px solid var(--cw-border); + border-radius: var(--cw-radius-s); + color: var(--cw-text-2); + font: 400 0.5rem/0.8rem var(--md-code-font-family); +} + +.cw-badge--cli { + border-color: color-mix(in srgb, var(--cw-purple) 34%, transparent); + color: var(--cw-purple); +} + +.cw-square { + display: inline-block; + flex-shrink: 0; + width: 6px; + height: 6px; + border-radius: 1px; + background-color: var(--cw-muted); +} + +.cw-square--amber { + background-color: var(--cw-amber); +} + +.md-typeset kbd, +.cw-search-hints kbd { + display: inline-block; + min-width: 0.85rem; + padding: 0 0.18rem; + border: 1px solid var(--cw-border); + border-radius: var(--cw-radius-s); + background-color: var(--cw-elevated); + box-shadow: none; + color: var(--cw-text-2); + font: 400 0.5rem/0.8rem var(--md-code-font-family); + text-align: center; + vertical-align: 0.05em; +} + +/* Previous and next, with the keys Material already binds */ +.md-footer__inner { + gap: 0.5rem; + padding: 0.8rem; +} + +.md-footer__link { + flex: 0 1 18rem; + align-items: center; + margin: 0; + padding: 0.5rem 0.7rem; + border: 1px solid var(--cw-border); + border-radius: var(--cw-radius-l); + background-color: var(--cw-surface); + opacity: 1; + transition: border-color var(--cw-fast), background-color var(--cw-fast); +} + +.md-footer__link:is(:hover, :focus) { + border-color: var(--cw-border-strong); + background-color: var(--cw-elevated); + opacity: 1; +} + +.md-footer__link--next { + justify-content: flex-end; + margin-left: auto; + text-align: right; +} + +.md-footer__button { + display: none; +} + +.md-footer__title { + max-width: 100%; + margin: 0; + padding: 0; + font-size: 0.7rem; + font-weight: 500; + line-height: 1.4; +} + +.md-footer__direction { + position: static; + display: block; + margin-bottom: 0.1rem; + color: var(--cw-text-2); + font-size: 0.55rem; + font-weight: 400; + opacity: 1; +} + +@media (hover: hover) and (pointer: fine) { + .md-footer__direction::after { + margin-left: 0.3rem; + padding: 0 0.16rem; + border: 1px solid var(--cw-border); + border-radius: var(--cw-radius-s); + background-color: var(--cw-elevated); + font: 400 0.48rem/0.75rem var(--md-code-font-family); + } + + .md-footer__link--prev .md-footer__direction::after { + content: "P"; + } + + .md-footer__link--next .md-footer__direction::after { + content: "N"; } } -/* Primary nav, after docs.astral.sh/uv: smaller text and tighter spacing on wide screens */ -@media screen and (min-width: 76.25em) { - /* The title is only needed for back navigation in the mobile drawer */ +/* Footer as a status bar */ +.md-footer { + border-top: 1px solid var(--cw-border); +} + +.md-footer-meta { + border-top: 1px solid var(--cw-border); +} + +.md-footer:not(:has(.md-footer__inner)) .md-footer-meta { + border-top: 0; +} + +.md-footer-meta__inner { + padding: 0; +} + +.md-footer-meta.md-typeset a { + color: var(--cw-text-2); +} + +.md-footer-meta.md-typeset a:is(:hover, :focus) { + color: var(--cw-text); +} + +.cw-statusbar { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.15rem 0.8rem; + width: 100%; + margin: 0; + padding: 0.35rem 0.8rem; + color: var(--cw-text-2); + font-size: 0.55rem; + line-height: 1.5; +} + +.cw-statusbar__state { + display: inline-flex; + align-items: center; + gap: 0.25rem; + color: var(--cw-text); + font-family: var(--md-code-font-family); +} + +.cw-statusbar__end { + display: flex; + gap: 0.8rem; + margin-left: auto; +} + +/* Copied-to-clipboard toast */ +.md-dialog { + right: 0.8rem; + bottom: 0.8rem; + left: auto; + padding: 0.3rem 0.6rem; + border: 1px solid var(--cw-border); + border-radius: var(--cw-radius-m); + background-color: var(--cw-elevated); + box-shadow: var(--cw-shadow); + color: var(--cw-text); + font: 400 0.55rem/1.5 var(--md-code-font-family); +} + +/* Phone and tablet navigation */ +.md-nav__source { + background-color: var(--cw-surface); + color: var(--cw-text-2); +} + +@media screen and (max-width: 76.234375em) { .md-nav--primary .md-nav__title { - display: none; + border-bottom: 1px solid var(--cw-border); + background-color: var(--cw-surface); + color: var(--cw-text); + font-size: 0.7rem; } - .md-nav--primary, - .md-nav--primary .md-nav { - font-size: 0.75rem; + .md-nav--primary .md-nav__item { + border-top: 1px solid var(--cw-border); } - /* Space items with the item margin, not the link margin, so gaps stay even */ .md-nav--primary .md-nav__link { margin: 0; + padding: 0.5rem 0.8rem; + border-radius: 0; + font-size: 0.7rem; } - .md-nav--primary .md-nav__item { - margin-top: 0.35em; + .md-nav--primary .md-nav__link--active { + background-color: var(--cw-surface); + } + + .md-nav--secondary .md-nav__link { + padding: 0.35rem 0.8rem; + } + + .md-sidebar--primary { + background-color: var(--cw-bg); } } -.md-nav__link--active { - font-weight: 700; +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + scroll-behavior: auto; + transition-duration: 0.01ms; + } } diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 02b4061..4934c88 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -8,23 +8,22 @@ theme: logo: assets/logo.png favicon: assets/logo.png palette: - - media: "(prefers-color-scheme: light)" - scheme: default - primary: custom - accent: custom - toggle: - icon: material/weather-night - name: Switch to dark mode - - media: "(prefers-color-scheme: dark)" - scheme: slate - primary: custom - accent: custom - toggle: - icon: material/weather-sunny - name: Switch to light mode + scheme: slate + primary: custom + accent: custom + features: + - content.code.copy + - navigation.footer + - navigation.path + - navigation.sections + - navigation.tabs + - search.highlight + - toc.follow extra_css: - stylesheets/theme.css - stylesheets/home.css +extra_javascript: + - javascripts/shortcuts.js plugins: - search - mkdocstrings: diff --git a/docs/overrides/home.html b/docs/overrides/home.html index c652048..ddac565 100644 --- a/docs/overrides/home.html +++ b/docs/overrides/home.html @@ -1,79 +1,34 @@ {% extends "main.html" %} -{#- The landing page uses its own fonts; docs pages keep Material's defaults #} -{% block fonts %} - - - -{% endblock %} +{#- Material puts tabs after the hero; a native header stays above the content #} +{% block tabs %}{% endblock %} {% block hero %} + {% include "partials/tabs.html" %}
-
-
-

+
+
+

Crosswind {#- The last word repeats the first so the loop restarts without a jump #} - +

-

Crosswind is a set of typed, async Python packages and tools for Python Workers. It ports the Agents and Containers SDKs, replaces the workers package with typed APIs, and replaces pywrangler with wynd.

+

Crosswind is a set of typed, async Python packages and tools for Python Workers. It ports the Agents, Containers and Sandboxes SDKs, replaces the workers package with typed APIs, and replaces pywrangler with wynd.

- -
or
- {#- The comment and prompt can't be selected, so copying gives just the command #} -
uvx wynd init
+ Get started + Browse docs
+

Every package is alpha. Pin exact versions.

- -
-

- {#- javascripts/system.js draws and animates the SVG #} -
-
-
-
- Fig. 1 · How the packages fit together - - Python sits at the centre of Crosswind. Agents, Containers, Wynd and Workers connect to it. Wynd drops the dependencies a Worker never imports, packs the rest into a bundle and deploys it to Workers. Select a package in the diagram to open its docs. -
- - {#- Stacked layers, back to front (grid, routes, packages, logo, moving parts), so a frame only repaints the layers that changed #} -
- - - - - -
- -
-
    -
  1. -
  2. -
  3. -
  4. -
- -
-
+
{% endblock %} {% block scripts %} {{ super() }} + + {% endblock %} diff --git a/docs/overrides/main.html b/docs/overrides/main.html index 3bd60ea..7953298 100644 --- a/docs/overrides/main.html +++ b/docs/overrides/main.html @@ -1,5 +1,11 @@ {% extends "base.html" %} +{#- Material's font link asks for italics that Geist doesn't ship, so load the weights the theme uses #} +{% block fonts %} + + +{% endblock %} + {% block extrahead %}