diff --git a/.impeccable/design.json b/.impeccable/design.json index 0191fce..b65afec 100644 --- a/.impeccable/design.json +++ b/.impeccable/design.json @@ -1,6 +1,6 @@ { "schemaVersion": 2, - "generatedAt": "2026-09-07T21:00:00Z", + "generatedAt": "2026-09-09T23:55:00Z", "title": "Design System: uNotch", "extensions": { "colorMeta": { @@ -33,6 +33,8 @@ ] }, "components": [ + {"name": "Brand Mark", "kind": "image", "description": "The mark is a **screen with a side notch**: a rounded rectangular screen outline\nwith a solid mint rail extending inward from its left edge. It describes the\nactual app placement, with no letterform or detached status dot.\nGeometry lives on a 1024 grid (see `logo/mark.svg`):\n\n- Screen: (176, 256), 672 × 512, corner radius 80, stroke 64.\n- Side notch: x=144–336, y=392–632; flush left edge and 56 px inner corners.\n Its left edge aligns with the outside of the screen stroke.\n- Tile variant (`logo/icon.svg`): charcoal rounded square (radius 205 on 928),\n paper screen, mint notch. This is the macOS app icon and the social avatar.\n- Favicon: the same mark on a full-bleed charcoal tile (radius 224 on 1024).\n- Mono variants (`logo/mark-mono-black.svg`, `logo/mark-mono-white.svg`) render\n the screen and notch in the same ink.\n- **Menu bar**: always a macOS *template* image (system tints it). Never draw\n the mint notch in colour in the menu bar.\n\nClear space: keep at least one stroke width (64/1024 of the mark height) free\naround the mark. Minimum size: 16 px. The screen and attached side notch must\nremain distinct at that size; never omit the notch.", "html": "\"uNotch\"", "css": ""}, + {"name": "Social Preview", "kind": "image", "description": "The share card is a 1200 × 630 PNG, composed in `social/card.html` and rendered\nwith `scripts/generate-social.sh`. It uses the backdrop ground, charcoal type,\nthe existing tagline, the new icon, and a screen showing the actual site HUD\nwith illustrative usage values. Provider names, free/open-source status, and\nmacOS requirements come from the site. Keep primary content 64 px from the\ncard edges. Use a new versioned image filename whenever its pixels change,\nsince `/assets/` is cached immutably. Open Graph and Twitter metadata must\nreference the same absolute PNG URL, with dimensions and descriptive alt text.", "html": "\"uNotch", "css": ""}, { "name": "Primary Button", "kind": "button", @@ -84,7 +86,7 @@ ], "narrative": { "northStar": "Tucked", - "overview": "uNotch is a quiet instrument. It lives at the edge of a Mac screen as a seven-point cue and only becomes a readout when the pointer asks. Everything in the system follows from that posture: dark glass that borrows the desktop behind it, ink that is paper at opacity rather than fixed grey, and one accent — the mint status point from the logo — that appears only where something is live or where the user acts. The system is native first: SF Rounded numerals, the system text face, NSVisualEffectView glass, and no loaded web fonts or third-party requests anywhere, on the app or on the site.\n\nThe system explicitly rejects the \"AI tool\" landing-page look: purple-to-blue gradients, neon glows, card grids of features, floating blobs, eyebrow caps labels over every heading, and marketing verbs. It also rejects dashboard density. uNotch shows a handful of numbers; each one gets room.", + "overview": "uNotch is a quiet instrument. It lives at the edge of a Mac screen as a seven-point cue and only becomes a readout when the pointer asks. Everything in the system follows from that posture: dark glass that borrows the desktop behind it, ink that is paper at opacity rather than fixed grey, and one accent — the mint side notch in the logo — that appears only where something is live or where the user acts. The system is native first: SF Rounded numerals, the system text face, NSVisualEffectView glass, and no loaded web fonts or third-party requests anywhere, on the app or on the site.\n\nThe system explicitly rejects the \"AI tool\" landing-page look: purple-to-blue gradients, neon glows, card grids of features, floating blobs, eyebrow caps labels over every heading, and marketing verbs. It also rejects dashboard density. uNotch shows a handful of numbers; each one gets room.", "keyCharacteristics": [ "Dark glass surfaces tinted charcoal; depth from tint, never from stacked shadows", "One accent (mint) rationed to live state and the single primary action", @@ -116,10 +118,10 @@ "Don't put features in a grid of cards. Rows with hairlines.", "Don't add eyebrow caps labels above headings.", "Don't add a second shadow anywhere; the callout is the only surface that casts one.", - "Don't colour the mint point in the menu bar; the menu bar mark is a template image.", + "Don't colour the mint notch in the menu bar; the menu bar mark is a template image.", "Don't use the words seamless, powerful, supercharge, empower, or effortless.", "Don't load a web font. If the display face is unavailable, the system face is correct.", - "Don't let the mark shrink below 16 px or draw it without the status point." + "Don't let the mark shrink below 16 px or draw it without the attached side notch." ] } } diff --git a/CHANGELOG.md b/CHANGELOG.md index 1393132..8ee5822 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ All notable changes to uNotch are recorded here. The format follows ## [Unreleased] +### Changed +- The logo now depicts a screen with a notch tucked into its left edge, across + the app icon, menu bar, settings, website, and favicons. +- Link previews now show a designed share card with the tagline and usage HUD, + with complete Open Graph and Twitter image metadata. + ## [1.2.0] — 2026-09-07 ### Added diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 79072cd..8249807 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -40,6 +40,17 @@ UNOTCH_SNAPSHOT=/tmp/hud.png UNOTCH_SETTINGS_SNAPSHOT=/tmp/settings.png swift te The website is `site/index.html`; `scripts/build-site.sh` assembles it with the brand tokens into `_site/` for a local preview (`python3 -m http.server -d _site`). +Logo geometry lives in `brand/logo/`. After changing it, update the matching +Swift paths and regenerate the web exports with `scripts/generate-favicons.sh` +(requires ImageMagick). The app icon is generated during DMG packaging. + +The share card's editable layout is `brand/social/card.html`. It borrows the +website's HUD and styles when rendered with `scripts/generate-social.sh` +(requires Python 3, Playwright CLI with Chromium, and ImageMagick; render on macOS +for the system font). The PNG is committed, so deployment needs no renderer. +When changing it, choose a new filename in the generator and both image meta +tags in `site/index.html` because `/assets/` has immutable caching. + To test exactly what production serves, build the image from the repo root: ```sh diff --git a/DESIGN.md b/DESIGN.md index d547a25..f555cd5 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -131,7 +131,7 @@ components: --- @@ -145,7 +145,7 @@ components: uNotch is a quiet instrument. It lives at the edge of a Mac screen as a seven-point cue and only becomes a readout when the pointer asks. Everything in the system follows from that posture: dark glass that borrows the desktop behind it, ink that is paper at opacity rather -than fixed grey, and one accent — the mint status point from the logo — that appears only +than fixed grey, and one accent — the mint side notch in the logo — that appears only where something is live or where the user acts. The system is native first: SF Rounded numerals, the system text face, `NSVisualEffectView` glass, and no loaded web fonts or third-party requests anywhere, on the app or on the site. @@ -174,7 +174,7 @@ A charcoal ground, paper ink, and a single mint signal. ### Primary - **Mint** (#3BE29B): the status point. The "remaining" fill of the selected provider ring - and bars, the point on the mark, and the one primary button on a page. 10.5:1 on charcoal. + and bars, the side notch on the mark, and the one primary button on a page. 10.5:1 on charcoal. ### Neutral - **Charcoal** (#15191B): brand ground for the product — icon tile, the tint under glass — and @@ -248,6 +248,40 @@ never thicker than 1 px. ## 5. Components +### Brand mark + +The mark is a **screen with a side notch**: a rounded rectangular screen outline +with a solid mint rail extending inward from its left edge. It describes the +actual app placement, with no letterform or detached status dot. +Geometry lives on a 1024 grid (see `logo/mark.svg`): + +- Screen: (176, 256), 672 × 512, corner radius 80, stroke 64. +- Side notch: x=144–336, y=392–632; flush left edge and 56 px inner corners. + Its left edge aligns with the outside of the screen stroke. +- Tile variant (`logo/icon.svg`): charcoal rounded square (radius 205 on 928), + paper screen, mint notch. This is the macOS app icon and the social avatar. +- Favicon: the same mark on a full-bleed charcoal tile (radius 224 on 1024). +- Mono variants (`logo/mark-mono-black.svg`, `logo/mark-mono-white.svg`) render + the screen and notch in the same ink. +- **Menu bar**: always a macOS *template* image (system tints it). Never draw + the mint notch in colour in the menu bar. + +Clear space: keep at least one stroke width (64/1024 of the mark height) free +around the mark. Minimum size: 16 px. The screen and attached side notch must +remain distinct at that size; never omit the notch. + +### Social preview + +The share card is a 1200 × 630 PNG, composed in `social/card.html` and rendered +with `scripts/generate-social.sh`. It uses the backdrop ground, charcoal type, +the existing tagline, the new icon, and a screen showing the actual site HUD +with illustrative usage values. Provider names, free/open-source status, and +macOS requirements come from the site. Keep primary content 64 px from the +card edges. Use a new versioned image filename whenever its pixels change, +since `/assets/` is cached immutably. Open Graph and Twitter metadata must +reference the same absolute PNG URL, with dimensions and descriptive alt text. + + ### Buttons - **Shape:** pill (999px) on the site; rounded control (11px) inside the HUD. - **Primary (dark glass):** mint fill, charcoal text, 600 weight, 12px 20px padding, 44px @@ -320,9 +354,9 @@ never thicker than 1 px. - **Don't** put features in a grid of cards. Rows with hairlines. - **Don't** add eyebrow caps labels above headings. - **Don't** add a second shadow anywhere; the callout is the only surface that casts one. -- **Don't** colour the mint point in the menu bar; the menu bar mark is a template image. +- **Don't** colour the mint notch in the menu bar; the menu bar mark is a template image. - **Don't** use the words seamless, powerful, supercharge, empower, or effortless. - **Don't** load a web font. If the display face is unavailable, the system face is correct. - **Don't** put ash or slate text over a strong backdrop layer; full charcoal only (The Strong-Layer Rule). Don't fade the render with painted scrims — they seam against the ambient. -- **Don't** let the mark shrink below 16 px or draw it without the status point. +- **Don't** let the mark shrink below 16 px or draw it without the attached side notch. diff --git a/Sources/uNotch/Brand.swift b/Sources/uNotch/Brand.swift index b40452b..16950fb 100644 --- a/Sources/uNotch/Brand.swift +++ b/Sources/uNotch/Brand.swift @@ -48,43 +48,51 @@ enum AppInfo { } } -/// The brand mark drawn in code so the menu bar, HUD, and icon share one geometry. -/// Coordinates follow the 1024 grid in `brand/logo/mark.svg`. +/// The screen and side notch use the 1024 grid in `brand/logo/mark.svg`. +/// MenuBarLogo also draws these paths so the template and HUD stay consistent. struct BrandMarkShape: Shape { + func path(in rect: CGRect) -> Path { + let s = min(rect.width, rect.height) / 1024 + let origin = CGPoint(x: rect.midX - 512 * s, y: rect.midY - 512 * s) + return Path(roundedRect: CGRect( + x: origin.x + 176 * s, y: origin.y + 256 * s, + width: 672 * s, height: 512 * s + ), cornerRadius: 80 * s) + } +} + +struct BrandNotchShape: Shape { func path(in rect: CGRect) -> Path { let s = min(rect.width, rect.height) / 1024 let ox = rect.midX - 512 * s let oy = rect.midY - 512 * s - func p(_ x: CGFloat, _ y: CGFloat) -> CGPoint { CGPoint(x: ox + x * s, y: oy + y * s) } - + func p(_ x: CGFloat, _ y: CGFloat) -> CGPoint { + CGPoint(x: ox + x * s, y: oy + y * s) + } var path = Path() - path.move(to: p(310, 308)) - path.addLine(to: p(310, 558)) - path.addCurve(to: p(714, 558), control1: p(310, 760), control2: p(714, 760)) - path.addLine(to: p(714, 308)) + path.move(to: p(144, 392)) + path.addLine(to: p(280, 392)) + path.addCurve(to: p(336, 448), control1: p(311, 392), control2: p(336, 417)) + path.addLine(to: p(336, 576)) + path.addCurve(to: p(280, 632), control1: p(336, 607), control2: p(311, 632)) + path.addLine(to: p(144, 632)) + path.closeSubpath() return path } } struct BrandMark: View { var ink: Color = Brand.paper - var point: Color = Brand.mint + var notch: Color = Brand.mint var body: some View { GeometryReader { proxy in - let side = min(proxy.size.width, proxy.size.height) - let s = side / 1024 - // Optical floor: below ~48 pt the point and stroke thin out, so clamp them - // (BRAND.md: "if the point disappears at 16 px, the mark has been drawn wrong"). - let stroke = max(84 * s, 1.6) - let radius = max(39 * s, 1.4) + let s = min(proxy.size.width, proxy.size.height) / 1024 ZStack { BrandMarkShape() - .stroke(ink, style: StrokeStyle(lineWidth: stroke, lineCap: .round)) - Circle() - .fill(point) - .frame(width: radius * 2, height: radius * 2) - .position(x: proxy.size.width / 2 + (714 - 512) * s, y: proxy.size.height / 2 + (256 - 512) * s) + .stroke(ink, lineWidth: max(64 * s, 1.25)) + BrandNotchShape() + .fill(notch) } } .accessibilityHidden(true) diff --git a/Sources/uNotch/UNotchApp.swift b/Sources/uNotch/UNotchApp.swift index 923d61a..b25a5f3 100644 --- a/Sources/uNotch/UNotchApp.swift +++ b/Sources/uNotch/UNotchApp.swift @@ -154,28 +154,19 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { } } -/// Menu bar mark: the brand geometry as a template image so macOS tints it. -/// The status point is drawn in the same ink here by design (see brand/BRAND.md). +/// The shared screen-and-notch paths as a template image that macOS tints. private enum MenuBarLogo { static func makeImage() -> NSImage { let side: CGFloat = 18 let image = NSImage(size: NSSize(width: side, height: side), flipped: true) { rect in - let s = side / 1024 - func p(_ x: CGFloat, _ y: CGFloat) -> NSPoint { NSPoint(x: x * s, y: y * s) } - - NSColor.black.setStroke() - NSColor.black.setFill() - let mark = NSBezierPath() - mark.lineWidth = max(1.8, 84 * s * 1.3) - mark.lineCapStyle = .round - mark.move(to: p(310, 308)) - mark.line(to: p(310, 558)) - mark.curve(to: p(714, 558), controlPoint1: p(310, 760), controlPoint2: p(714, 760)) - mark.line(to: p(714, 308)) - mark.stroke() - - let r = max(1.1, 39 * s * 1.5) - NSBezierPath(ovalIn: NSRect(x: 714 * s - r, y: 256 * s - r, width: r * 2, height: r * 2)).fill() + guard let context = NSGraphicsContext.current?.cgContext else { return false } + context.setStrokeColor(NSColor.black.cgColor) + context.setFillColor(NSColor.black.cgColor) + context.setLineWidth(max(1.25, 64 * side / 1024)) + context.addPath(BrandMarkShape().path(in: rect).cgPath) + context.strokePath() + context.addPath(BrandNotchShape().path(in: rect).cgPath) + context.fillPath() return true } image.isTemplate = true diff --git a/brand/BRAND.md b/brand/BRAND.md index c704742..bb16b22 100644 --- a/brand/BRAND.md +++ b/brand/BRAND.md @@ -22,23 +22,36 @@ One word to keep in mind while designing: *tucked*. ## Mark -The mark is a **u-shaped notch** with a **status point** at its top-right terminal. +The mark is a **screen with a side notch**: a rounded rectangular screen outline +with a solid mint rail extending inward from its left edge. It describes the +actual app placement, with no letterform or detached status dot. Geometry lives on a 1024 grid (see `logo/mark.svg`): -- Stroke 84, round caps; the arms rise from y=308 to the bowl at y=558. -- The status point is a 78 px circle centred at (714, 256) — it sits *above* the - right arm, like a light on a rail. +- Screen: (176, 256), 672 × 512, corner radius 80, stroke 64. +- Side notch: x=144–336, y=392–632; flush left edge and 56 px inner corners. + Its left edge aligns with the outside of the screen stroke. - Tile variant (`logo/icon.svg`): charcoal rounded square (radius 205 on 928), - paper mark, mint point. This is the macOS app icon and the social avatar. -- Mono variants (`logo/mark-mono-black.svg`, `logo/mark-mono-white.svg`) drop the - colour and render the point in the same ink. Use them for print, favicons in - monochrome contexts, and the menu bar. -- **Menu bar**: the mark is always a macOS *template* image (system tints it). - Never draw the mint point in colour in the menu bar. - -Clear space: keep at least one stroke width (84/1024 of the mark height) free -around the mark. Minimum size: 16 px (the 16 px mono render is the survival floor — -if the point disappears at 16 px, the mark has been drawn wrong). + paper screen, mint notch. This is the macOS app icon and the social avatar. +- Favicon: the same mark on a full-bleed charcoal tile (radius 224 on 1024). +- Mono variants (`logo/mark-mono-black.svg`, `logo/mark-mono-white.svg`) render + the screen and notch in the same ink. +- **Menu bar**: always a macOS *template* image (system tints it). Never draw + the mint notch in colour in the menu bar. + +Clear space: keep at least one stroke width (64/1024 of the mark height) free +around the mark. Minimum size: 16 px. The screen and attached side notch must +remain distinct at that size; never omit the notch. + +## Social preview + +The share card is a 1200 × 630 PNG, composed in `social/card.html` and rendered +with `scripts/generate-social.sh`. It uses the backdrop ground, charcoal type, +the existing tagline, the new icon, and a screen showing the actual site HUD +with illustrative usage values. Provider names, free/open-source status, and +macOS requirements come from the site. Keep primary content 64 px from the +card edges. Use a new versioned image filename whenever its pixels change, +since `/assets/` is cached immutably. Open Graph and Twitter metadata must +reference the same absolute PNG URL, with dimensions and descriptive alt text. ## Colour @@ -49,10 +62,10 @@ if the point disappears at 16 px, the mark has been drawn wrong). | `ash` | `#2A3135` | Hairlines and dividers on dark. | | `smoke` | `#8B9498` | Secondary text on dark (5.7:1 on charcoal). | | `paper` | `#F4F7F6` | Primary text on dark; the mark itself. | -| `mint` | `#3BE29B` | **The status point.** The only accent. | +| `mint` | `#3BE29B` | **The live signal.** Side notch, status point, and remaining usage. | **The Status Point Rule.** Mint is a signal, not a theme. It appears where there is -live state or the primary action: the point on the mark, the "remaining" fill of the +live state or the primary action: the side notch on the mark, the "remaining" fill of the selected provider, the one primary action on a page (it may repeat — a download button at the top and bottom — but it is always the *same* action). Never on headings, borders, bullets, backgrounds, or decoration. If a screen has mint on two diff --git a/brand/logo/favicon.svg b/brand/logo/favicon.svg index 9b188f8..08975a3 100644 --- a/brand/logo/favicon.svg +++ b/brand/logo/favicon.svg @@ -1,5 +1,7 @@ - + + uNotch + A screen with a notch tucked into its left edge. - - + + diff --git a/brand/logo/icon.svg b/brand/logo/icon.svg index 7caf81c..76ba4e1 100644 --- a/brand/logo/icon.svg +++ b/brand/logo/icon.svg @@ -1,7 +1,7 @@ uNotch - A paper u-shaped notch mark with a mint status point on a charcoal tile. + A screen with a notch tucked into its left edge. - - + + diff --git a/brand/logo/mark-mono-black.svg b/brand/logo/mark-mono-black.svg index 162fb5b..5f40802 100644 --- a/brand/logo/mark-mono-black.svg +++ b/brand/logo/mark-mono-black.svg @@ -1,5 +1,6 @@ - - uNotch mark, monochrome black - - + + uNotch + A screen with a notch tucked into its left edge. + + diff --git a/brand/logo/mark-mono-white.svg b/brand/logo/mark-mono-white.svg index bb7e6d1..7447627 100644 --- a/brand/logo/mark-mono-white.svg +++ b/brand/logo/mark-mono-white.svg @@ -1,5 +1,6 @@ - - uNotch mark, monochrome white - - + + uNotch + A screen with a notch tucked into its left edge. + + diff --git a/brand/logo/mark.svg b/brand/logo/mark.svg index ddc9661..4e1bdbf 100644 --- a/brand/logo/mark.svg +++ b/brand/logo/mark.svg @@ -1,5 +1,6 @@ - - uNotch mark - - + + uNotch + A screen with a notch tucked into its left edge. + + diff --git a/brand/social/card.html b/brand/social/card.html new file mode 100644 index 0000000..cff7f98 --- /dev/null +++ b/brand/social/card.html @@ -0,0 +1,44 @@ + + + + + + uNotch social preview + + + + + +
uNotch
+

unotch.sethmedina.com

+

Live AI usage,
quietly tucked
into the edge
of your Mac.

+

Claude · Codex · Cursor Agent

+
+ + {{HUD}} +
+

Illustrative usage

+ + + diff --git a/scripts/generate-favicons.sh b/scripts/generate-favicons.sh new file mode 100755 index 0000000..6029093 --- /dev/null +++ b/scripts/generate-favicons.sh @@ -0,0 +1,16 @@ +#!/bin/sh +# Raster favicon exports all derive from the canonical SVG. Requires ImageMagick. +set -eu +repo_root="$(cd "$(dirname "$0")/.." && pwd)" +source="$repo_root/brand/logo/favicon.svg" +for size in 16 32 180 192; do + case "$size" in + 16|32) name="favicon-$size.png" ;; + 180) name="apple-touch-icon.png" ;; + 192) name="icon-192.png" ;; + esac + magick -background none -density 384 "$source" -resize "${size}x${size}" \ + -set comment 'Source: brand/logo/favicon.svg, uNotch screen-and-side-notch mark.' \ + "$repo_root/site/$name" +done +magick -background none -density 384 "$source" -define icon:auto-resize=48,32,16 "$repo_root/site/favicon.ico" diff --git a/scripts/generate-icon.swift b/scripts/generate-icon.swift index 2ddf880..2b43cda 100644 --- a/scripts/generate-icon.swift +++ b/scripts/generate-icon.swift @@ -2,7 +2,7 @@ import AppKit import Foundation // Renders the app icon from the brand geometry in brand/logo/icon.svg -// (1024 grid, charcoal tile, paper mark, mint status point). brand/BRAND.md is canon. +// (1024 grid, charcoal tile, paper screen, mint side notch). brand/BRAND.md is canon. guard CommandLine.arguments.count == 2 else { fputs("usage: swift generate-icon.swift \n", stderr) @@ -67,30 +67,31 @@ func renderIcon(pixels: Int) throws -> Data { NSColor(red: 0.082, green: 0.098, blue: 0.106, alpha: 1).setFill() tile.fill() - let mark = NSBezierPath() - mark.lineWidth = scaled(84, by: scale) - mark.lineCapStyle = .round - mark.move(to: NSPoint(x: scaled(310, by: scale), y: scaled(716, by: scale))) - mark.line(to: NSPoint(x: scaled(310, by: scale), y: scaled(466, by: scale))) - mark.curve( - to: NSPoint(x: scaled(714, by: scale), y: scaled(466, by: scale)), - controlPoint1: NSPoint(x: scaled(310, by: scale), y: scaled(264, by: scale)), - controlPoint2: NSPoint(x: scaled(714, by: scale), y: scaled(264, by: scale)) + let screen = NSBezierPath( + roundedRect: NSRect( + x: scaled(176, by: scale), y: scaled(256, by: scale), + width: scaled(672, by: scale), height: scaled(512, by: scale) + ), + xRadius: scaled(80, by: scale), yRadius: scaled(80, by: scale) ) - mark.line(to: NSPoint(x: scaled(714, by: scale), y: scaled(716, by: scale))) - NSColor(red: 0.96, green: 0.97, blue: 0.97, alpha: 1).setStroke() - mark.stroke() + screen.lineWidth = scaled(64, by: scale) + NSColor(red: 244.0 / 255, green: 247.0 / 255, blue: 246.0 / 255, alpha: 1).setStroke() + screen.stroke() - let status = NSBezierPath( - ovalIn: NSRect( - x: scaled(675, by: scale), - y: scaled(729, by: scale), - width: scaled(78, by: scale), - height: scaled(78, by: scale) - ) - ) - NSColor(red: 0.231, green: 0.886, blue: 0.608, alpha: 1).setFill() - status.fill() + // AppKit's y axis is inverted relative to the SVG's top-left origin. + func p(_ x: CGFloat, _ y: CGFloat) -> NSPoint { + NSPoint(x: x * scale, y: (1024 - y) * scale) + } + let notch = NSBezierPath() + notch.move(to: p(144, 392)) + notch.line(to: p(280, 392)) + notch.curve(to: p(336, 448), controlPoint1: p(311, 392), controlPoint2: p(336, 417)) + notch.line(to: p(336, 576)) + notch.curve(to: p(280, 632), controlPoint1: p(336, 607), controlPoint2: p(311, 632)) + notch.line(to: p(144, 632)) + notch.close() + NSColor(red: 59.0 / 255, green: 226.0 / 255, blue: 155.0 / 255, alpha: 1).setFill() + notch.fill() guard let data = bitmap.representation(using: .png, properties: [:]) else { throw CocoaError(.fileWriteUnknown) diff --git a/scripts/generate-social.sh b/scripts/generate-social.sh new file mode 100755 index 0000000..66b947d --- /dev/null +++ b/scripts/generate-social.sh @@ -0,0 +1,30 @@ +#!/bin/sh +# Render the share card from its template and the site's actual HUD. +# Requires Python 3, Playwright CLI + Chromium, and ImageMagick (magick). +# On macOS, Chromium uses the same installed system fonts as the site. +set -eu +repo_root="$(cd "$(dirname "$0")/.." && pwd)" +output="${1:-$repo_root/site/assets/unotch-social-v2.png}" +work="$(mktemp -d "${TMPDIR:-/tmp}/unotch-social.XXXXXX")" +trap 'rm -rf "$work"' EXIT HUP INT TERM +"$repo_root/scripts/build-site.sh" "$work/site" >/dev/null +python3 - "$repo_root" "$work/site" <<'PY' +from pathlib import Path +import re +import sys +root, out = map(Path, sys.argv[1:]) +site = (root / 'site/index.html').read_text() +styles = re.search(r'', site, re.S).group(1) +start = site.index('
') +end = site.index('

', start) +card = (root / 'brand/social/card.html').read_text() +card = card.replace('{{SITE_STYLES}}', styles).replace('{{HUD}}', site[start:end]) +(out / 'social.html').write_text(card) +PY +npx --no-install playwright screenshot --browser chromium --viewport-size='1200,630' \ + --wait-for-timeout=500 "file://$work/site/social.html" "$work/card.png" +mkdir -p "$(dirname "$output")" +# Save an opaque RGB PNG; no runtime rendering is required on the server. +magick "$work/card.png" -alpha off -define png:color-type=2 \ + -set comment 'Source: brand/social/card.html; HUD from site/index.html; backdrop from the existing Aetheria emerald render, seed 805214.' "$output" +printf '%s\n' "$output" diff --git a/site/Caddyfile b/site/Caddyfile index 0784b04..761728a 100644 --- a/site/Caddyfile +++ b/site/Caddyfile @@ -30,7 +30,7 @@ @brand path /brand/* header @brand Cache-Control "public, max-age=604800, stale-while-revalidate=86400" - # Backdrop renders carry their seed in the file name; a new render is a new URL. + # Backdrops and social cards use versioned file names; changed pixels need a new URL. @assets path /assets/* header @assets Cache-Control "public, max-age=31536000, immutable" diff --git a/site/apple-touch-icon.png b/site/apple-touch-icon.png index ecb87b0..b144180 100644 Binary files a/site/apple-touch-icon.png and b/site/apple-touch-icon.png differ diff --git a/site/assets/unotch-social-v2.png b/site/assets/unotch-social-v2.png new file mode 100644 index 0000000..e031632 Binary files /dev/null and b/site/assets/unotch-social-v2.png differ diff --git a/site/favicon-16.png b/site/favicon-16.png index 64e77fd..4a18e1d 100644 Binary files a/site/favicon-16.png and b/site/favicon-16.png differ diff --git a/site/favicon-32.png b/site/favicon-32.png index 31990f2..33af6db 100644 Binary files a/site/favicon-32.png and b/site/favicon-32.png differ diff --git a/site/favicon.ico b/site/favicon.ico index e9ca47f..39d0b2a 100644 Binary files a/site/favicon.ico and b/site/favicon.ico differ diff --git a/site/icon-192.png b/site/icon-192.png index 80444ab..4b66565 100644 Binary files a/site/icon-192.png and b/site/icon-192.png differ diff --git a/site/index.html b/site/index.html index b47fc90..39283a4 100644 --- a/site/index.html +++ b/site/index.html @@ -8,21 +8,29 @@ uNotch — Live AI usage, quietly tucked into the edge of your Mac - - - - - - - + + + + + + + - - + + + + - - - + + + + + + + + +