diff --git a/assets/icon.png b/assets/icon.png index 4ea8938..1aeb160 100644 Binary files a/assets/icon.png and b/assets/icon.png differ diff --git a/assets/icon.svg b/assets/icon.svg index 0dc94e1..1ec565f 100644 --- a/assets/icon.svg +++ b/assets/icon.svg @@ -1,12 +1,19 @@ - - + + - - - - - - + + + + + + + + + + + + + diff --git a/assets/logo-stacked.svg b/assets/logo-stacked.svg new file mode 100644 index 0000000..0ec47c9 --- /dev/null +++ b/assets/logo-stacked.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/logo.png b/assets/logo.png index dda71d7..453813e 100644 Binary files a/assets/logo.png and b/assets/logo.png differ diff --git a/assets/logo.svg b/assets/logo.svg index f5bdd89..2d4ce50 100644 --- a/assets/logo.svg +++ b/assets/logo.svg @@ -1,18 +1,23 @@ - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - pidex + + + + + + + diff --git a/assets/mark-inverse.svg b/assets/mark-inverse.svg new file mode 100644 index 0000000..82523da --- /dev/null +++ b/assets/mark-inverse.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/assets/mark-mono.svg b/assets/mark-mono.svg new file mode 100644 index 0000000..441f541 --- /dev/null +++ b/assets/mark-mono.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/assets/mark.svg b/assets/mark.svg new file mode 100644 index 0000000..b634a0a --- /dev/null +++ b/assets/mark.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/bun.lock b/bun.lock index 5157327..aee4565 100644 --- a/bun.lock +++ b/bun.lock @@ -18,6 +18,7 @@ "@types/node": "^26.1.1", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", + "@types/three": "^0.186.0", "@vitejs/plugin-react": "^6.0.0", "concurrently": "^9.1.0", "electron": "^41.0.0", @@ -25,6 +26,7 @@ "jsdom": "^29.1.1", "oxlint": "^1.74.0", "playwright-core": "^1.61.1", + "three": "^0.186.1", "typescript": "^6.0.0", "vite": "^8.0.0", "vitest": "^4.1.10", @@ -65,6 +67,8 @@ "@csstools/css-tokenizer": ["@csstools/css-tokenizer@4.0.0", "", {}, "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA=="], + "@dimforge/rapier3d-compat": ["@dimforge/rapier3d-compat@0.12.0", "", {}, "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow=="], + "@electron/asar": ["@electron/asar@3.4.1", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA=="], "@electron/fuses": ["@electron/fuses@1.8.0", "", { "dependencies": { "chalk": "^4.1.1", "fs-extra": "^9.0.1", "minimist": "^1.2.5" }, "bin": { "electron-fuses": "dist/bin.js" } }, "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw=="], @@ -229,6 +233,8 @@ "@testing-library/react": ["@testing-library/react@16.3.2", "", { "dependencies": { "@babel/runtime": "^7.12.5" }, "peerDependencies": { "@testing-library/dom": "^10.0.0", "@types/react": "^18.0.0 || ^19.0.0", "@types/react-dom": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g=="], + "@tweenjs/tween.js": ["@tweenjs/tween.js@23.1.3", "", {}, "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA=="], + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="], "@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="], @@ -269,8 +275,14 @@ "@types/responselike": ["@types/responselike@1.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw=="], + "@types/stats.js": ["@types/stats.js@0.17.4", "", {}, "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA=="], + + "@types/three": ["@types/three@0.186.0", "", { "dependencies": { "@dimforge/rapier3d-compat": "~0.12.0", "@tweenjs/tween.js": "~23.1.3", "@types/stats.js": "*", "@types/webxr": ">=0.5.17", "fflate": "~0.8.3", "meshoptimizer": "~1.1.1" } }, "sha512-mxYSBpDC+D0pLfSP6sW4WZTcT+nrtmZcimMqnVmy36Hte3XpeYSrvgg4TRdaM1GemGog1AWzI5qL2VoIfMXbJQ=="], + "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], + "@types/webxr": ["@types/webxr@0.5.24", "", {}, "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg=="], + "@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="], "@ungap/structured-clone": ["@ungap/structured-clone@1.3.3", "", {}, "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg=="], @@ -497,6 +509,8 @@ "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + "fflate": ["fflate@0.8.3", "", {}, "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA=="], + "filelist": ["filelist@1.0.6", "", { "dependencies": { "minimatch": "^5.0.1" } }, "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA=="], "follow-redirects": ["follow-redirects@1.16.0", "", {}, "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw=="], @@ -651,6 +665,8 @@ "mdurl": ["mdurl@2.0.0", "", {}, "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w=="], + "meshoptimizer": ["meshoptimizer@1.1.1", "", {}, "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g=="], + "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="], @@ -873,6 +889,8 @@ "temp-file": ["temp-file@3.4.0", "", { "dependencies": { "async-exit-hook": "^2.0.1", "fs-extra": "^10.0.0" } }, "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg=="], + "three": ["three@0.186.1", "", {}, "sha512-blFeqb49wRCSGUGj7gtpfnSGHy2lwDk94RhUmS1c/hTby70kvChbWpkJ4Pm1390LqzzvTmzgXKHPEafJwCb8jA=="], + "tiny-async-pool": ["tiny-async-pool@1.3.0", "", { "dependencies": { "semver": "^5.5.0" } }, "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA=="], "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], diff --git a/docs/adr/0008-pi-dev-design-system.md b/docs/adr/0008-pi-dev-design-system.md index 315c43d..797342b 100644 --- a/docs/adr/0008-pi-dev-design-system.md +++ b/docs/adr/0008-pi-dev-design-system.md @@ -3,11 +3,13 @@ - **Status**: Accepted (2026-07-19) - **Context**: The workbench is a desktop wrapper around the Pi coding agent. The Pi product (pi.dev) has its own distinctive visual language: deep ink canvas `#0d1116`, moonstone text, tidal-blue accent, terracotta rust, Departure Mono labels, Commit Mono code, bracketed buttons `[ ACTION ]`, corner-bracketed figure frames, lowercase `›` and `$` shell prompts. Building a separate design system would feel disjointed. -- **Decision**: Mirror the pi.dev design system in pidex. Bundle `Commit Mono` (OFL) and `Departure Mono` (OFL) locally; fall back to system serifs/mono for the licensed Plantin MT Pro. Tokens live in `src/renderer/design-system/tokens.css` and components in `components.css`. +- **Decision**: Mirror the pi.dev design system in pidex. Bundle `Commit Mono` and `Departure Mono` (both OFL) locally. Body text uses `Newsreader` (OFL), not Plantin MT Pro: Plantin cannot be redistributed, and the previous Georgia fallback was a different pairing on every machine. Tokens live in `src/renderer/design-system/tokens.css` and components in `components.css`. + + The pairing is Newsreader for prose, titles, and the wordmark; Commit Mono for code; Departure Mono for labels and meta. - **Consequences**: - Screenshots from pidex feel like native Pi product surfaces. - Designers familiar with Pi can transfer work without re-learning a design system. - Token names match Pi's published names (`--bg-deep`, `--accent`, `--panel`) — easy to grep and update. - - Local font assets ship ~850KB; acceptable for desktop. - - No licensing risk (both bundled fonts are OFL; Plantin fallback uses system Georgia). \ No newline at end of file + - Local font assets ship about 1.1MB; acceptable for desktop. + - No licensing risk. Newsreader, Commit Mono, and Departure Mono are all OFL and ship with the app. Georgia remains the serif fallback if the face has not loaded. \ No newline at end of file diff --git a/docs/assets/banner.png b/docs/assets/banner.png index 8c681f5..105334c 100644 Binary files a/docs/assets/banner.png and b/docs/assets/banner.png differ diff --git a/package.json b/package.json index fc486b8..19d6dd1 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "@types/node": "^26.1.1", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", + "@types/three": "^0.186.0", "@vitejs/plugin-react": "^6.0.0", "concurrently": "^9.1.0", "electron": "^41.0.0", @@ -43,6 +44,7 @@ "jsdom": "^29.1.1", "oxlint": "^1.74.0", "playwright-core": "^1.61.1", + "three": "^0.186.1", "typescript": "^6.0.0", "vite": "^8.0.0", "vitest": "^4.1.10", diff --git a/scripts/build-banner.py b/scripts/build-banner.py index f2e1d95..699248f 100644 --- a/scripts/build-banner.py +++ b/scripts/build-banner.py @@ -1,13 +1,12 @@ #!/usr/bin/env python -"""Build the README banner: the pidex logo (isometric cube with the -Pi coding agent logo on its top face + wordmark) centered on the -design-system background (bg-canvas with the blueprint grid + vertical -gradient into bg-deep). +"""Build the README banner: the pidex lockup centered on the +design-system background (bg-canvas, blueprint grid, fade into bg-deep). + +The lockup itself comes from scripts/build-logo.py. Outputs: docs/assets/banner.png (1600x600) - assets/logo.png (regenerated as part of the build) - docs/assets/banner-bg.svg (the background SVG, tracked) + docs/assets/banner-bg.svg """ from __future__ import annotations @@ -22,73 +21,6 @@ BANNER_W, BANNER_H = 1600, 600 -INK_BRONZE = "#13110f" -PARCHMENT = "#dacbc2" - - -def pi_path(font_path): - from fontTools.ttLib import TTFont - from fontTools.pens.svgPathPen import SVGPathPen - - font = TTFont(font_path) - cmap = font.getBestCmap() - gname = cmap[ord("π")] - pen = SVGPathPen(font.getGlyphSet()) - font.getGlyphSet()[gname].draw(pen) - glyf = font["glyf"][gname] - bbox = (glyf.xMin, glyf.yMin, glyf.xMax, glyf.yMax) - return pen.getCommands(), bbox, font["head"].unitsPerEm - - -def build_logo_svg(width, height): - d, bbox, _ = pi_path("/System/Library/Fonts/Supplemental/Georgia Italic.ttf") - gx = (bbox[0] + bbox[2]) / 2 - gy = (bbox[1] + bbox[3]) / 2 - e = 200 - dx = e * 0.866 - dy = e * 0.5 - cx, cy = width / 2 - 60, height / 2 - top = (cx, cy - dy) - right = (cx + dx, cy) - bot = (cx, cy + dy) - left = (cx - dx, cy) - a = dx / 470.0 - b = dy / 470.0 - # Scale the Pi logo to 65% of the top face so it fits with padding - pad_scale = 0.65 - a *= pad_scale - b *= pad_scale - transform = f"matrix({a} {b} {-a} {b} {cx} {cy - dy})" - p_d = ( - "M165.29 165.29 H517.36 V400 H400 V517.36 H282.65 V634.72 H165.29 Z " - "M282.65 282.65 V400 H400 V282.65 Z" - ) - i_dot_d = "M517.36 400 H634.72 V634.72 H517.36 Z" - word_x = right[0] + 60 - word_y = cy + 20 - word_size = int(height * 0.22) - parts = [] - parts.append( - f'' - ) - parts.append( - f'' - ) - parts.append( - f'' - ) - parts.append(f'') - parts.append(f'') - parts.append(f'') - parts.append("") - parts.append( - f'pidex' - ) - inner = "".join(parts) - return ( - f'{inner}' - ) - def build_bg_svg(): return ( @@ -126,20 +58,42 @@ def render(svg, out, width): def main(): + logo = ASSETS / "logo.svg" + if not logo.exists(): + raise SystemExit("assets/logo.svg missing; run scripts/build-logo.py first") + bg_svg = build_bg_svg() (DOCS / "banner-bg.svg").write_text(bg_svg) bg_png = DOCS / "banner-bg.png" render(bg_svg, bg_png, BANNER_W) - logo_svg = build_logo_svg(BANNER_W, BANNER_H) - logo_png = ASSETS / "logo.png" - render(logo_svg, logo_png, BANNER_W) - + lockup = DOCS / "lockup.png" + subprocess.run( + [ + "inkscape", + str(logo), + "--export-type=png", + "--export-filename", + str(lockup), + "--export-width", + "760", + ], + check=True, + ) subprocess.run( - ["magick", str(bg_png), str(logo_png), "-composite", str(DOCS / "banner.png")], + [ + "magick", + str(bg_png), + str(lockup), + "-gravity", + "center", + "-composite", + str(DOCS / "banner.png"), + ], check=True, ) bg_png.unlink() + lockup.unlink() print(f"wrote {DOCS / 'banner.png'}") diff --git a/scripts/build-logo.py b/scripts/build-logo.py index 7415c1b..b488823 100644 --- a/scripts/build-logo.py +++ b/scripts/build-logo.py @@ -1,15 +1,17 @@ -"""Build the pidex logo as a composable horizontal lockup SVG. +"""Build the pidex emblem: the Pi logo painted on an isometric cube. -The mark is an isometric cube in the design system's cool/neutral -surfaces (bg-canvas, panel, bg-deep) with the Pi coding agent's logo -(P + i dot) painted on its top face, and the "pidex" wordmark to -the right. +The Pi coding-agent mark (the block P and its square dot) is mapped onto +the top face. Variants are recolors and lockups of that same drawing. Outputs: - assets/logo.svg - horizontal lockup, transparent (composable) - assets/logo.png - rasterized lockup at 800x400 for quick embedding - assets/icon.svg - 1024 square app icon (cube on bg-deep squircle) - assets/icon.png - rasterized icon at 1024x1024 for electron-builder + assets/mark.svg emblem, design-system surfaces + assets/mark-mono.svg single-colour emblem + assets/mark-inverse.svg light-ground emblem + assets/logo.svg horizontal lockup + assets/logo-stacked.svg stacked lockup + assets/icon.svg squircle app icon + assets/icon.png rasterised icon for electron-builder + src/renderer/components/PidexMark.tsx """ from __future__ import annotations @@ -21,91 +23,260 @@ REPO = Path(__file__).resolve().parent.parent ASSETS = REPO / "assets" -SCRIPTS = Path(__file__).resolve().parent - -sys.path.insert(0, str(SCRIPTS)) -from _design import ( # noqa: E402 - INK_BRONZE, - BG_DEEP, - BG_CANVAS, - PANEL, - PARCHMENT, -) +FONT = REPO / "src/renderer/assets/fonts/Newsreader-Variable.woff2" +MARK_TSX = REPO / "src/renderer/components/PidexMark.tsx" + +# Faces sit above both dark grounds (deep #0d1116 and canvas #161d27). +# A light rim holds the silhouette; a dark keyline disappears into the page. +TOP = "#2a3b50" +RIGHT = "#3d536b" +LEFT = "#1c2838" +EDGE = "#6e849c" +INK = "#ebe7e4" +ICON_BG = "#0d1116" + +# Isometric cube. Edge 48, top-face centre (60, 40). +E = 48.0 +DX = E * 0.8660254037844386 +DY = E * 0.5 +CX, CY = 60.0, 40.0 -# Logo canvas -W, H = 240, 120 - -# Isometric cube (edge 48), top face centered around (60, 40) -E = 48 -DX = E * 0.866 # 41.57 -DY = E * 0.5 # 24 -CX, CY = 60, 40 -TOP_X, TOP_Y = CX, CY - DY # (60, 16) -RIGHT_X, RIGHT_Y = CX + DX, CY # (101.57, 40) -BOT_X, BOT_Y = CX, CY + DY # (60, 64) -LEFT_X, LEFT_Y = CX - DX, CY # (18.43, 40) - -# Pi coding agent logo paths (from pi.dev/pi-logo.svg, 800x800 viewBox). -# Content bbox: x,y ∈ [165.29, 634.72] (470x470 region inside the 800x800). +# Pi logo paths (pi.dev, 800 viewBox). Content box is 165.29..634.72. P_D = ( "M165.29 165.29 H517.36 V400 H400 V517.36 H282.65 V634.72 H165.29 Z " "M282.65 282.65 V400 H400 V282.65 Z" ) I_DOT_D = "M517.36 400 H634.72 V634.72 H517.36 Z" +PI_SPAN = 634.72 - 165.29 + +WORD = "pidex" +WORD_SIZE = 46.0 +WORD_WEIGHT = 560.0 +WORD_OPSZ = 48.0 +WORD_GAP = 22.0 -# Affine transform mapping the Pi logo content (165..635, 165..635) onto -# the top-face rhombus. The rhombus is a parallelogram with half-width DX -# and half-height DY (measured from the top-center). The content is a -# 470x470 square. The affine matrix has a = DX/470, b = DY/470, -# c = -a, d = b, and translates so the content center maps to the rhombus -# top-center. -A = DX / 470.0 -B = DY / 470.0 -PI_TRANSFORM = f"matrix({A} {B} {-A} {B} 60 0)" - -# Wordmark -TEXT = "pidex" -TEXT_X = 120 -TEXT_Y = 76 -TEXT_SIZE = 32 -TEXT_FONT = "Georgia, 'Plantin MT Pro', 'Plantin MT Std', serif" -TEXT_WEIGHT = 500 - - -# App icon: macOS-style squircle, cube centred. ICON_SIZE = 1024 ICON_RADIUS = 229 -def _cube_polygons() -> str: - return f""" - - - - - - """ +def face_xy(p: float, q: float) -> tuple[float, float]: + return (CX + q * DX, (CY - DY) + p * DY) -def build_logo_svg() -> str: - return f""" - -{_cube_polygons()} +def poly(points: list[tuple[float, float]]) -> str: + body = " ".join(f"{x:.2f},{y:.2f}" for x, y in points) + return f' - pidex - -""" + +class Palette: + def __init__(self, top: str, right: str, left: str, edge: str, pi: str, stroke_width: float = 1.5): + self.top = top + self.right = right + self.left = left + self.edge = edge + self.pi = pi + self.stroke_width = stroke_width + + +PRIMARY = Palette(TOP, RIGHT, LEFT, EDGE, INK) +# The icon plate is the same ink as the shadow face, so the cube +# needs its own steps or the left plane disappears into the squircle. +ICON = Palette(TOP, RIGHT, LEFT, EDGE, INK, 1.25) +MONO = Palette("none", "none", "none", INK, INK, 1.75) +INVERSE = Palette("#f4ece6", "#e7ddd4", "#d5c9bf", "#b7a99e", "#0d1116") + + +def pi_transform() -> str: + # Map the 470-unit Pi content square onto the top-face rhombus. + a = DX / PI_SPAN + b = DY / PI_SPAN + return f"matrix({a:.8f} {b:.8f} {-a:.8f} {b:.8f} {CX:.2f} 0)" + + +def cube_markup(palette: Palette = PRIMARY, clip_id: str = "pi-face") -> str: + top = [face_xy(0, 0), face_xy(1, 1), face_xy(2, 0), face_xy(1, -1)] + right = [face_xy(1, 1), face_xy(2, 0), (CX, CY + DY + E), (CX + DX, CY + E)] + left = [face_xy(1, -1), face_xy(2, 0), (CX, CY + DY + E), (CX - DX, CY + E)] + edge = ( + f'stroke="{palette.edge}" stroke-width="{palette.stroke_width}" ' + 'stroke-linejoin="round"' + ) + + def face(points: list[tuple[float, float]], fill: str) -> str: + return f'{poly(points)} fill="{fill}" {edge}/>' + + top_points = " ".join(f"{x:.2f},{y:.2f}" for x, y in top) + return "\n".join( + [ + " ", + f' ', + f' ', + " ", + " ", + f" {face(left, palette.left)}", + f" {face(right, palette.right)}", + f" {face(top, palette.top)}", + f' ', + f' ', + f' ', + f' ', + " ", + " ", + ] + ) + + +def wordmark_markup(origin_x: float) -> tuple[str, tuple[float, float, float, float]]: + """Outline WORD in Newsreader. Returns SVG and the ink box in lockup space.""" + from fontTools.ttLib import TTFont + from fontTools.pens.svgPathPen import SVGPathPen + from fontTools.varLib.instancer import instantiateVariableFont + import uharfbuzz as hb + + variable = TTFont(FONT) + variable.flavor = None + tmp = ASSETS / ".newsreader-wordmark.ttf" + variable.save(tmp) + static = instantiateVariableFont(variable, {"wght": WORD_WEIGHT, "opsz": WORD_OPSZ}) + + blob = hb.Blob.from_file_path(str(tmp)) + face = hb.Face(blob) + font = hb.Font(face) + upem = face.upem + font.scale = (upem, upem) + font.set_variations({"wght": WORD_WEIGHT, "opsz": WORD_OPSZ}) + buf = hb.Buffer() + buf.add_str(WORD) + buf.guess_segment_properties() + hb.shape(font, buf) + + scale = WORD_SIZE / upem + glyphset = static.getGlyphSet() + order = static.getGlyphOrder() + paths: list[str] = [] + cursor = 0.0 + ink: list[tuple[float, float]] = [] + for info, pos in zip(buf.glyph_infos, buf.glyph_positions): + name = order[info.codepoint] + pen = SVGPathPen(glyphset) + glyphset[name].draw(pen) + commands = pen.getCommands() + gx = origin_x + (cursor + pos.x_offset) * scale + gy = pos.y_offset * scale + if commands: + paths.append(f'') + glyph = static["glyf"][name] + if glyph.numberOfContours != 0: + x0 = gx + glyph.xMin * scale + x1 = gx + glyph.xMax * scale + y0 = gy - glyph.yMax * scale + y1 = gy - glyph.yMin * scale + ink.extend([(x0, y0), (x1, y1)]) + cursor += pos.x_advance + tmp.unlink(missing_ok=True) + + xs = [p[0] for p in ink] + ys = [p[1] for p in ink] + box = (min(xs), min(ys), max(xs), max(ys)) + body = "\n".join(f" {path}" for path in paths) + return body, box + + +def shift_wordmark(markup: str, dx: float, dy: float) -> str: + # Baseline is applied as a group so the per-glyph transforms stay put. + return f'\n{markup}\n' + + +def build_lockup_from(raw: str, box: tuple[float, float, float, float]) -> str: + cube_right = CX + DX + cube_top = CY - DY + cube_bottom = CY + DY + E + cube_mid = (cube_top + cube_bottom) / 2 + ink_w = box[2] - box[0] + ink_mid = (box[1] + box[3]) / 2 + dx = cube_right + WORD_GAP - box[0] + dy = cube_mid - ink_mid + word = shift_wordmark(raw, dx, dy) + pad = 8 + min_x = (CX - DX) - pad + min_y = cube_top - pad + max_x = cube_right + WORD_GAP + ink_w + pad + max_y = cube_bottom + pad + return svg_wrap( + f"{cube_markup(PRIMARY, 'pi-face-lockup')}\n{word}", + f"{min_x:.2f} {min_y:.2f} {max_x - min_x:.2f} {max_y - min_y:.2f}", + ) + + +def emblem_viewbox() -> str: + """Square frame around the cube so the mark does not stretch.""" + side = 112.0 + return f"{CX - side / 2:.2f} {(CY + E / 2) - side / 2:.2f} {side:.2f} {side:.2f}" + + +def svg_wrap(inner: str, view: str) -> str: + return f'\n{inner}\n\n' + + +def build_mark_svg(palette: Palette = PRIMARY, clip_id: str = "pi-face") -> str: + return svg_wrap(cube_markup(palette, clip_id), emblem_viewbox()) + + +def build_stacked(word: str, box: tuple[float, float, float, float]) -> str: + cube_bottom = CY + DY + E + cube_left = CX - DX + cube_right = CX + DX + ink_w = box[2] - box[0] + dx = CX - (box[0] + box[2]) / 2 + dy = cube_bottom + 18 - box[1] + pad = 8 + min_x = min(cube_left, box[0] + dx) - pad + max_x = max(cube_right, box[2] + dx) + pad + min_y = (CY - DY) - pad + max_y = box[3] + dy + pad + return svg_wrap( + f"{cube_markup(PRIMARY, 'pi-face-stacked')}\n{shift_wordmark(word, dx, dy)}", + f"{min_x:.2f} {min_y:.2f} {max_x - min_x:.2f} {max_y - min_y:.2f}", + ) def build_icon_svg() -> str: - # Cube sits in a ~84x96 box around (60, 64). Scale so it fills ~70% of the squircle. - return f""" - - -{_cube_polygons()} - - + return ( + f'\n' + f' \n' + f' \n' + f"{cube_markup(ICON, 'pi-face-icon')}\n" + f" \n" + "\n" + ) + + +def build_mark_tsx() -> str: + inner = cube_markup(PRIMARY, "pi-face") + indented = "\n".join(f" {line}" if line else "" for line in inner.splitlines()) + return f"""// Generated by scripts/build-logo.py. Do not edit by hand. + +type PidexMarkProps = {{ + size?: number; + className?: string; +}}; + +function PidexMark({{ size = 40, className }}: PidexMarkProps) {{ + return ( + + ); +}} + +export {{ PidexMark }}; """ @@ -128,20 +299,31 @@ def render_png(svg: Path, png: Path, width: int) -> None: def main() -> None: ASSETS.mkdir(exist_ok=True) - svg_path = ASSETS / "logo.svg" - png_path = ASSETS / "logo.png" - svg_path.write_text(build_logo_svg()) - render_png(svg_path, png_path, 800) - print(f"wrote {svg_path}") - print(f"wrote {png_path}") + marks = { + "mark.svg": build_mark_svg(PRIMARY, "pi-face"), + "mark-mono.svg": build_mark_svg(MONO, "pi-face-mono"), + "mark-inverse.svg": build_mark_svg(INVERSE, "pi-face-inverse"), + } + for name, svg in marks.items(): + (ASSETS / name).write_text(svg) + print(f"wrote {ASSETS / name}") + + raw, box = wordmark_markup(0) + logo = ASSETS / "logo.svg" + logo.write_text(build_lockup_from(raw, box)) + render_png(logo, ASSETS / "logo.png", 1200) + stacked = ASSETS / "logo-stacked.svg" + stacked.write_text(build_stacked(raw, box)) + print(f"wrote {logo}") + print(f"wrote {stacked}") - icon_svg = ASSETS / "icon.svg" - icon_png = ASSETS / "icon.png" - icon_svg.write_text(build_icon_svg()) - render_png(icon_svg, icon_png, ICON_SIZE) - print(f"wrote {icon_svg}") - print(f"wrote {icon_png}") + icon = ASSETS / "icon.svg" + icon.write_text(build_icon_svg()) + render_png(icon, ASSETS / "icon.png", ICON_SIZE) + print(f"wrote {icon}") + MARK_TSX.write_text(build_mark_tsx()) + print(f"wrote {MARK_TSX}") if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index d4468be..6071dcc 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -1,6 +1,7 @@ import { useEffect, useMemo, useRef, useState } from 'react'; import { prepare, layout } from '@chenglou/pretext'; import { PiMark } from './components/PiMark'; +import { PidexMark } from './components/PidexMark'; import { FigureFrame } from './components/FigureFrame'; import { BracketButton } from './components/BracketButton'; import { Markdown } from './components/Markdown'; @@ -21,7 +22,7 @@ type SessionTab = { exited: boolean; }; -const BODY_FONT = '450 15px Georgia, serif'; +const BODY_FONT = '450 15px Newsreader, Georgia, serif'; function timeAgo(iso: string): string { const seconds = Math.max(0, (Date.now() - new Date(iso).getTime()) / 1000); @@ -92,6 +93,7 @@ function App() { const [draft, setDraft] = useState(''); const [opening, setOpening] = useState(false); const [slashIndex, setSlashIndex] = useState(0); + const [fontReady, setFontReady] = useState(false); const timelineRef = useRef(null); const bootstrapped = useRef(false); const openSessionRef = useRef<(ws: WorkspaceInfo, info?: SessionInfo) => Promise>( @@ -147,8 +149,28 @@ function App() { timelineRef.current?.scrollTo({ top: timelineRef.current.scrollHeight }); }, [activeTab?.entries.length, activeTab?.entries[activeTab.entries.length - 1]]); + useEffect(() => { + let cancel = false; + const fonts = document.fonts; + if (!fonts?.load) { + setFontReady(true); + return; + } + fonts.load(BODY_FONT).then( + () => { + if (!cancel) setFontReady(true); + }, + () => { + if (!cancel) setFontReady(true); + }, + ); + return () => { + cancel = true; + }; + }, []); + const measuredRows = useMemo(() => { - if (!activeTab) return 0; + if (!activeTab || !fontReady) return 0; let rows = 0; for (const entry of activeTab.entries) { if (entry.kind === 'tool') continue; @@ -158,7 +180,7 @@ function App() { rows += layout(prepared, 780, 24).lineCount; } return rows; - }, [activeTab]); + }, [activeTab, fontReady]); const expandWorkspace = async (ws: WorkspaceInfo) => { const next = expanded === ws.dirName ? null : ws.dirName; @@ -526,7 +548,7 @@ function App() { ) : (
- +

No session open

Pick a session from the sidebar, or start a new one in the expanded workspace. diff --git a/src/renderer/app.css b/src/renderer/app.css index 874ce0c..fcc83c7 100644 --- a/src/renderer/app.css +++ b/src/renderer/app.css @@ -332,6 +332,10 @@ color: var(--faint); } +.empty-mark { + color: var(--text); +} + .empty-title { font-family: var(--serif); font-size: 1.4rem; diff --git a/src/renderer/assets/fonts/Newsreader-Italic-Variable.woff2 b/src/renderer/assets/fonts/Newsreader-Italic-Variable.woff2 new file mode 100644 index 0000000..c53b69f Binary files /dev/null and b/src/renderer/assets/fonts/Newsreader-Italic-Variable.woff2 differ diff --git a/src/renderer/assets/fonts/Newsreader-Variable.woff2 b/src/renderer/assets/fonts/Newsreader-Variable.woff2 new file mode 100644 index 0000000..6ec9ba8 Binary files /dev/null and b/src/renderer/assets/fonts/Newsreader-Variable.woff2 differ diff --git a/src/renderer/components/CubeMark.tsx b/src/renderer/components/CubeMark.tsx new file mode 100644 index 0000000..fbf32bd --- /dev/null +++ b/src/renderer/components/CubeMark.tsx @@ -0,0 +1,238 @@ +import { useEffect, useRef } from 'react'; +import { + BoxGeometry, + CanvasTexture, + Group, + Mesh, + MeshBasicMaterial, + PerspectiveCamera, + PlaneGeometry, + Scene, + SRGBColorSpace, + WebGLRenderer, + type BufferGeometry, +} from 'three'; + +// Face inks from scripts/build-logo.py. At the isometric angle the dark +// face sits on the left and the light face on the right. +const RIGHT = 0x3d536b; +const LEFT = 0x1c2838; +const EDGE = 0x6e849c; +const BOTTOM = 0x10161e; +const SIDE_BACK = 0x243246; +const SIDE_FAR = 0x31475f; + +const P_D = + 'M165.29 165.29 H517.36 V400 H400 V517.36 H282.65 V634.72 H165.29 Z M282.65 282.65 V400 H400 V282.65 Z'; +const DOT_D = 'M517.36 400 H634.72 V634.72 H517.36 Z'; + +const TURN_SECONDS = 16; + +function paintPi(): CanvasTexture { + const size = 1024; + const canvas = document.createElement('canvas'); + canvas.width = size; + canvas.height = size; + const ctx = canvas.getContext('2d'); + if (!ctx) { + throw new Error('pidex cube: 2d canvas unavailable'); + } + + ctx.fillStyle = '#2a3b50'; + ctx.fillRect(0, 0, size, size); + + const min = 165.29; + const span = 634.72 - min; + const margin = size * 0.055; + const scale = (size - margin * 2) / span; + ctx.save(); + ctx.translate(margin, margin); + ctx.scale(scale, scale); + ctx.translate(-min, -min); + ctx.fillStyle = '#ebe7e4'; + ctx.fill(new Path2D(P_D), 'evenodd'); + ctx.fill(new Path2D(DOT_D)); + ctx.restore(); + + const texture = new CanvasTexture(canvas); + texture.colorSpace = SRGBColorSpace; + texture.anisotropy = 8; + return texture; +} + +function paintShadow(): CanvasTexture { + const size = 256; + const canvas = document.createElement('canvas'); + canvas.width = size; + canvas.height = size; + const ctx = canvas.getContext('2d'); + if (!ctx) { + throw new Error('pidex cube: 2d canvas unavailable'); + } + + const ink = ctx.createRadialGradient(size / 2, size / 2, size * 0.05, size / 2, size / 2, size * 0.48); + ink.addColorStop(0, 'rgba(0, 0, 0, 0.5)'); + ink.addColorStop(1, 'rgba(0, 0, 0, 0)'); + ctx.fillStyle = ink; + ctx.fillRect(0, 0, size, size); + + const texture = new CanvasTexture(canvas); + texture.colorSpace = SRGBColorSpace; + return texture; +} + +function CubeMark() { + const mountRef = useRef(null); + + useEffect(() => { + const mount = mountRef.current; + if (!mount) return; + + let renderer: WebGLRenderer; + try { + renderer = new WebGLRenderer({ antialias: true, alpha: true }); + } catch (error) { + console.error('pidex cube: webgl unavailable', error); + return; + } + + renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); + renderer.setClearColor(0x000000, 0); + renderer.outputColorSpace = SRGBColorSpace; + + const scene = new Scene(); + const camera = new PerspectiveCamera(28, 1, 0.1, 20); + camera.position.set(1, 1, 1).multiplyScalar(2.85); + camera.lookAt(0, 0.02, 0); + + const cube = new Group(); + scene.add(cube); + + const pi = paintPi(); + const shadowMap = paintShadow(); + const geometries: BufferGeometry[] = []; + const materials: MeshBasicMaterial[] = []; + + const edgeMaterial = new MeshBasicMaterial({ color: EDGE }); + materials.push(edgeMaterial); + + const thickness = 0.03; + const length = 1 + thickness; + const bars: Array<[number, number, number, number, number, number]> = []; + for (const y of [-0.5, 0.5]) { + for (const z of [-0.5, 0.5]) bars.push([length, thickness, thickness, 0, y, z]); + } + for (const x of [-0.5, 0.5]) { + for (const z of [-0.5, 0.5]) bars.push([thickness, length, thickness, x, 0, z]); + } + for (const x of [-0.5, 0.5]) { + for (const y of [-0.5, 0.5]) bars.push([thickness, thickness, length, x, y, 0]); + } + for (const [w, h, d, x, y, z] of bars) { + const geometry = new BoxGeometry(w, h, d); + geometries.push(geometry); + const mesh = new Mesh(geometry, edgeMaterial); + mesh.position.set(x, y, z); + cube.add(mesh); + } + + const faceGeometry = new PlaneGeometry(1 - thickness, 1 - thickness); + geometries.push(faceGeometry); + const lift = 0.004; + const faces: Array<{ + color: number; + map?: CanvasTexture; + position: [number, number, number]; + rotation: [number, number, number]; + }> = [ + { color: 0xffffff, map: pi, position: [0, 0.5 + lift, 0], rotation: [-Math.PI / 2, 0, 0] }, + { color: BOTTOM, position: [0, -0.5 - lift, 0], rotation: [Math.PI / 2, 0, 0] }, + { color: RIGHT, position: [0.5 + lift, 0, 0], rotation: [0, Math.PI / 2, 0] }, + { color: SIDE_FAR, position: [-0.5 - lift, 0, 0], rotation: [0, -Math.PI / 2, 0] }, + { color: LEFT, position: [0, 0, 0.5 + lift], rotation: [0, 0, 0] }, + { color: SIDE_BACK, position: [0, 0, -0.5 - lift], rotation: [0, Math.PI, 0] }, + ]; + + for (const face of faces) { + const material = new MeshBasicMaterial({ + color: face.color, + ...(face.map ? { map: face.map } : {}), + }); + materials.push(material); + const mesh = new Mesh(faceGeometry, material); + mesh.position.set(...face.position); + mesh.rotation.set(...face.rotation); + cube.add(mesh); + } + + const shadowMaterial = new MeshBasicMaterial({ + map: shadowMap, + transparent: true, + depthWrite: false, + }); + materials.push(shadowMaterial); + const shadowGeometry = new PlaneGeometry(2.6, 2.6); + geometries.push(shadowGeometry); + const shadow = new Mesh(shadowGeometry, shadowMaterial); + shadow.rotation.x = -Math.PI / 2; + shadow.position.y = -0.72; + scene.add(shadow); + + mount.appendChild(renderer.domElement); + + const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches; + let paused = false; + const onEnter = () => { + paused = true; + }; + const onLeave = () => { + paused = false; + }; + mount.addEventListener('pointerenter', onEnter); + mount.addEventListener('pointerleave', onLeave); + + const resize = () => { + const width = mount.clientWidth; + const height = mount.clientHeight; + if (width === 0 || height === 0) return; + renderer.setSize(width, height, false); + camera.aspect = width / height; + camera.updateProjectionMatrix(); + }; + resize(); + const observer = new ResizeObserver(resize); + observer.observe(mount); + + let frame = 0; + let last = performance.now(); + const tick = (now: number) => { + const delta = Math.min((now - last) / 1000, 0.05); + last = now; + if (!reduce && !paused) { + cube.rotation.y += (Math.PI * 2 * delta) / TURN_SECONDS; + } + renderer.render(scene, camera); + frame = requestAnimationFrame(tick); + }; + frame = requestAnimationFrame(tick); + + return () => { + cancelAnimationFrame(frame); + observer.disconnect(); + mount.removeEventListener('pointerenter', onEnter); + mount.removeEventListener('pointerleave', onLeave); + pi.dispose(); + shadowMap.dispose(); + for (const material of materials) material.dispose(); + for (const geometry of geometries) geometry.dispose(); + renderer.dispose(); + renderer.domElement.remove(); + }; + }, []); + + return ( +

+ ); +} + +export { CubeMark }; diff --git a/src/renderer/components/PidexMark.tsx b/src/renderer/components/PidexMark.tsx new file mode 100644 index 0000000..f89b9b1 --- /dev/null +++ b/src/renderer/components/PidexMark.tsx @@ -0,0 +1,36 @@ +// Generated by scripts/build-logo.py. Do not edit by hand. + +type PidexMarkProps = { + size?: number; + className?: string; +}; + +function PidexMark({ size = 40, className }: PidexMarkProps) { + return ( + + ); +} + +export { PidexMark }; diff --git a/src/renderer/demo.css b/src/renderer/demo.css new file mode 100644 index 0000000..52c2fb7 --- /dev/null +++ b/src/renderer/demo.css @@ -0,0 +1,204 @@ +html, +body { + height: auto; + overflow: auto; +} + +.board { + height: auto; + min-height: 100%; + overflow: visible; + padding: var(--space-7) var(--space-6) var(--space-8); +} + +.board-inner { + width: min(1080px, 100%); + margin: 0 auto; + display: grid; + gap: var(--space-7); +} + +.mast h1 { + font-family: var(--serif); + font-weight: 520; + font-size: 3.4rem; + letter-spacing: -0.03em; + line-height: 0.95; + color: var(--text); +} + +.lede { + max-width: 42ch; + margin-top: var(--space-3); + color: var(--copy); + font-size: 1.05rem; +} + +.section { + display: grid; + gap: var(--space-4); +} + +.section h2 { + font-family: var(--accent-mono); + font-size: 0.66rem; + font-weight: 400; + letter-spacing: 0.16em; + text-transform: uppercase; + color: var(--muted); +} + +.logo-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: var(--space-4); +} + +.lockup-grid { + display: grid; + grid-template-columns: 1.4fr 0.8fr; + gap: var(--space-4); +} + +.stage { + display: grid; + place-items: center; + min-height: 200px; + padding: var(--space-5); + background: var(--bg-deep); +} + +.stage--light { + background: #f4ece6; +} + +.stage img { + width: 132px; + height: auto; +} + +.stage--wide img { + width: min(100%, 460px); +} + +.stage--icon { + background: var(--bg-canvas); +} + +.stage--icon img { + width: 148px; +} + +.stage--model { + min-height: 0; + padding: 0; + background: var(--bg-deep); +} + +.cube-mark { + width: 100%; + height: 480px; +} + +.cube-mark canvas { + display: block; + width: 100%; + height: 100%; +} + +.swatches { + display: grid; + grid-template-columns: repeat(6, 1fr); + gap: var(--space-3); +} + +.swatch { + display: grid; + gap: 0.35rem; +} + +.swatch-chip { + height: 72px; + border: 1px solid var(--line-soft); +} + +.swatch-name, +.swatch-hex { + font-family: var(--accent-mono); + font-size: 0.58rem; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.swatch-name { color: var(--muted-strong); } +.swatch-hex { color: var(--muted); } + +.type-grid { + display: grid; + grid-template-columns: 1.2fr 0.8fr; + gap: var(--space-4); +} + +.specimen { + display: grid; + gap: var(--space-3); + padding: var(--space-5); + background: var(--panel-veil); + border: 1px solid var(--line-soft); +} + +.specimen h3 { + font-family: var(--serif); + font-weight: 520; + font-size: 2.4rem; + letter-spacing: -0.02em; + line-height: 1; +} + +.specimen p { + max-width: 38ch; + color: var(--copy); +} + +.specimen em { + font-style: italic; +} + +.mono-sample { + font-family: var(--mono); + font-size: 0.82rem; + color: var(--muted-strong); + line-height: 1.6; +} + +.component-row { + display: flex; + flex-wrap: wrap; + gap: var(--space-3); + align-items: center; +} + +.component-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--space-4); +} + +.frame-body { + display: grid; + gap: var(--space-3); + padding: var(--space-4); +} + +@media (max-width: 860px) { + .board { padding: var(--space-5) var(--space-4) var(--space-7); } + .logo-grid, + .lockup-grid, + .swatches, + .type-grid, + .component-grid { + grid-template-columns: 1fr 1fr; + } + .mast h1 { font-size: 2.6rem; } + .cube-mark { height: 360px; } +} diff --git a/src/renderer/demo.html b/src/renderer/demo.html new file mode 100644 index 0000000..0b3d490 --- /dev/null +++ b/src/renderer/demo.html @@ -0,0 +1,15 @@ + + + + + + pidex design system + + + +
+ + + diff --git a/src/renderer/demo.tsx b/src/renderer/demo.tsx new file mode 100644 index 0000000..cd3f97f --- /dev/null +++ b/src/renderer/demo.tsx @@ -0,0 +1,186 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import { Badge } from './components/Badge'; +import { BracketButton } from './components/BracketButton'; +import { CubeMark } from './components/CubeMark'; +import { FigureFrame } from './components/FigureFrame'; +import { PiMark } from './components/PiMark'; +import mark from '../../assets/mark.svg'; +import markMono from '../../assets/mark-mono.svg'; +import markInverse from '../../assets/mark-inverse.svg'; +import icon from '../../assets/icon.svg'; +import logo from '../../assets/logo.svg'; +import logoStacked from '../../assets/logo-stacked.svg'; +import './design-system/index.css'; +import './demo.css'; + +const SWATCHES = [ + ['Parchment', '#dacbc2', 'var(--parchment)'], + ['Moonstone', '#ebe7e4', 'var(--moonstone)'], + ['Accent', '#6a9fcc', 'var(--accent)'], + ['Tidal', '#4b607c', 'var(--tidal-blue)'], + ['Terracotta', '#844f3b', 'var(--terracotta)'], + ['Sunkissed', '#e1b06e', 'var(--sunkissed)'], + ['Sage', '#a3a473', 'var(--sage)'], + ['Canvas', '#161d27', 'var(--bg-canvas)'], + ['Panel', '#212730', 'var(--panel)'], + ['Deep', '#0d1116', 'var(--bg-deep)'], + ['Success', '#5db87a', 'var(--success)'], + ['Warning', '#e8993a', 'var(--warning)'], +] as const; + +function LogoTile({ + src, + caption, + wide = false, + light = false, + appIcon = false, +}: { + src: string; + caption: string; + wide?: boolean; + light?: boolean; + appIcon?: boolean; +}) { + const stage = ['stage', wide ? 'stage--wide' : '', light ? 'stage--light' : '', appIcon ? 'stage--icon' : ''] + .filter(Boolean) + .join(' '); + return ( + +
+ +
+
+ ); +} + +function Demo() { + return ( +
+
+
+

pidex · design system

+

Workbench, set in type.

+

+ The Pi mark sits on the cube. Newsreader carries the prose, Departure Mono the labels, + Commit Mono the code. +

+
+ +
+

Logo

+ +
+ +
+
+
+ + + + +
+
+ + +
+
+ +
+

Colour

+
+ {SWATCHES.map(([name, hex, fill]) => ( +
+
+ {name} + {hex} +
+ ))} +
+
+ +
+

Type

+
+
+

Newsreader · prose

+

Every repo, in one glance.

+

+ A desktop workbench for the Pi coding agent. Sessions, branches, and what has gone + stale, set as a page rather than a chat. +

+
+
+

Departure Mono · labels

+

workspaces · sessions

+

Commit Mono · code

+

+ prepare(text, font) +
+ layout(prepared, 780, 24) +

+
+
+
+ +
+

Components

+
+ + + + + + agent + +
+
+ Open session + New workspace +
+
+ +
+
+ Branch + main +
+
+ Model + pi +
+
+ Rows + measured +
+
+ › + steer the workbench… +
+
+
+
+ +

+ Inline code sits in Commit Mono, with the accent + kept to the brackets and the live mark. +

+
+
+
+
+
+ ); +} + +const root = document.getElementById('root'); +if (root) { + createRoot(root).render( + + + , + ); +} diff --git a/src/renderer/design-system/fonts.css b/src/renderer/design-system/fonts.css index fda0960..70ce8d7 100644 --- a/src/renderer/design-system/fonts.css +++ b/src/renderer/design-system/fonts.css @@ -1,3 +1,19 @@ +@font-face { + font-family: "Newsreader"; + src: url("../assets/fonts/Newsreader-Variable.woff2") format("woff2"); + font-weight: 200 800; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: "Newsreader"; + src: url("../assets/fonts/Newsreader-Italic-Variable.woff2") format("woff2"); + font-weight: 200 800; + font-style: italic; + font-display: swap; +} + @font-face { font-family: "Commit Mono"; src: url("../assets/fonts/CommitMono-300-Regular.otf") format("opentype"); diff --git a/src/renderer/design-system/tokens.css b/src/renderer/design-system/tokens.css index 4ef5323..8ca45ed 100644 --- a/src/renderer/design-system/tokens.css +++ b/src/renderer/design-system/tokens.css @@ -62,7 +62,7 @@ --grid-major: hsl(218 60% 80% / 0.02625); /* Type */ - --serif: "Plantin MT Pro", "Plantin MT Std", Plantin, Georgia, "Times New Roman", serif; + --serif: "Newsreader", Georgia, "Times New Roman", serif; --mono: "Commit Mono", "SFMono-Regular", ui-monospace, Menlo, Monaco, Consolas, monospace; --accent-mono: "Departure Mono", "Commit Mono", "SFMono-Regular", ui-monospace, Menlo, Monaco, Consolas, monospace;