T45 Website: native captures stand in where a browser cannot run, an OG image, no web notes
No files matched your search
@@ -13,7 +13,8 @@ test("the home page shows the pitch, the scene and the credits", async ({ page }
|
||||
const errors = watch(page);
|
||||
await page.goto("/");
|
||||
await expect(page.getByRole("heading", { level: 1 })).toContainText("component library for GPUI");
|
||||
await expect(page.locator(".hero canvas")).toBeVisible();
|
||||
await expect(page.locator("canvas.iso")).toBeAttached();
|
||||
await expect(page.locator(".hero-iso")).toBeVisible();
|
||||
await expect(page.locator("#foot")).toContainText("Solar Icons by 480 Design");
|
||||
expect(errors).toEqual([]);
|
||||
});
|
||||
@@ -83,3 +84,20 @@ test("an unknown chapter says so", async ({ page }) => {
|
||||
await expect(page.getByRole("heading", { level: 1 })).toHaveText("Not found");
|
||||
expect(errors.join()).toContain("No chapter");
|
||||
});
|
||||
|
||||
test("a story the web cannot run shows its native capture, in the site's mode", async ({ page }) => {
|
||||
await page.emulateMedia({ colorScheme: "light" });
|
||||
await page.goto("/components/forms/pathinput/");
|
||||
const shot = page.locator("img.shot");
|
||||
await expect(shot).toHaveAttribute("srcset", /pathinput-light\.jpg/);
|
||||
await expect(page.locator(".stage iframe")).toHaveCount(0);
|
||||
await page.locator("[data-mode]").click();
|
||||
await expect(shot).toHaveAttribute("srcset", /pathinput-dark\.jpg/);
|
||||
await expect.poll(() => shot.evaluate((img: HTMLImageElement) => img.naturalWidth)).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("a live story that opens a window shows that window natively", async ({ page }) => {
|
||||
await page.goto("/components/shell/aboutdialog/");
|
||||
await expect(page.locator(".stage iframe")).toHaveCount(1);
|
||||
await expect(page.locator(".native img.shot")).toHaveAttribute("srcset", /shell-about-window/);
|
||||
});
|
||||
@@ -5,6 +5,14 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Ely · GPUI components</title>
|
||||
<meta name="description" content="A component library for GPUI, in light and dark. Every component runs live in the browser." />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:title" content="Ely · GPUI components" />
|
||||
<meta property="og:description" content="A component library for GPUI. Every part runs live in the browser, compiled from Rust to WebAssembly." />
|
||||
<meta property="og:image" content="https://ely-site.zhangyanghaha0407.workers.dev/og.png" />
|
||||
<meta property="og:image:width" content="2400" />
|
||||
<meta property="og:image:height" content="1260" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:image" content="https://ely-site.zhangyanghaha0407.workers.dev/og.png" />
|
||||
<script>
|
||||
// The saved mode, or the system's, before first paint.
|
||||
try {
|
||||
|
||||
|
After Width: | Height: | Size: 174 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 9.1 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,17 @@
|
||||
// Renders public/og.png from the hero; needs pnpm preview.
|
||||
import { chromium } from "@playwright/test";
|
||||
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage({ viewport: { width: 1200, height: 630 }, deviceScaleFactor: 2, colorScheme: "light", reducedMotion: "reduce" });
|
||||
await page.goto(process.env.SITE ?? "http://localhost:4173/");
|
||||
await page.addStyleTag({
|
||||
content: ".bar, .cta { display: none } .hero { min-height: 630px; padding: 56px } .words { zoom: 1.5 } .og { font-weight: 600; margin-bottom: 16px }",
|
||||
});
|
||||
await page.evaluate(() => {
|
||||
document.querySelector(".glyphs").insertAdjacentHTML("afterend", '<div class="og">Ely</div>');
|
||||
document.querySelector(".hero p").textContent = "Every component live in the browser, compiled from Rust.";
|
||||
});
|
||||
await page.waitForTimeout(1500);
|
||||
await page.screenshot({ path: "public/og.png", clip: { x: 0, y: 0, width: 1200, height: 630 } });
|
||||
await browser.close();
|
||||
console.log("og: wrote public/og.png");
|
||||
@@ -1,5 +1,6 @@
|
||||
import { all, chapter, chapters, esc, href, storyCount, type Chapter, type Story } from "./data";
|
||||
import { icon } from "./icons";
|
||||
import { isCaptured, native } from "./native";
|
||||
import { preview } from "./preview";
|
||||
|
||||
/** Chapters on the left, a story or the index beside. */
|
||||
@@ -22,7 +23,11 @@ export function components(main: HTMLElement, path: string[]): () => void {
|
||||
}
|
||||
document.title = `${story.title} · ${current.title} · Ely`;
|
||||
body.innerHTML = detail(current, story);
|
||||
return preview(body.querySelector<HTMLElement>(".live")!, current.slug, story.slug, story.title);
|
||||
const live = body.querySelector<HTMLElement>(".live")!;
|
||||
const shots = body.querySelector<HTMLElement>(".native")!;
|
||||
if (isCaptured(current.slug, story.slug)) return native(live, current.slug, story.slug, story.title);
|
||||
const [stopLive, stopShots] = [preview(live, current.slug, story.slug, story.title), native(shots, current.slug, story.slug, story.title)];
|
||||
return () => (stopLive(), stopShots());
|
||||
}
|
||||
|
||||
function sidebar(side: HTMLElement, current?: Chapter, story?: Story): void {
|
||||
@@ -66,6 +71,7 @@ function detail(current: Chapter, story: Story): string {
|
||||
<h1>${esc(story.title)}</h1>
|
||||
<p class="names">${story.components.map(esc).join(" · ")}</p>
|
||||
<div class="live"></div>
|
||||
<div class="native"></div>
|
||||
<nav class="pager" aria-label="More stories">${pager(prev, "prev", "Previous", icon("ArrowLeft"))}${pager(next, "next", "Next", icon("ArrowRight"))}</nav>
|
||||
</article>`;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import shots from "./shots.json";
|
||||
import { esc } from "./data";
|
||||
import { mode, onMode, type Mode } from "./theme";
|
||||
|
||||
const captured = new Set<string>(shots.stories);
|
||||
const windows: Record<string, string[]> = shots.windows;
|
||||
|
||||
/** Whether the site shows this story as a native capture. */
|
||||
export function isCaptured(page: string, story: string): boolean {
|
||||
return captured.has(`${page}/${story}`);
|
||||
}
|
||||
|
||||
const src = (name: string, theme: Mode) => `/shots/${name}-${theme}.jpg`;
|
||||
|
||||
function figure(name: string, alt: string): string {
|
||||
// Captures are at twice the pixels, drawn at their size.
|
||||
return `<img class="shot" data-shot="${name}" srcset="${src(name, mode())} 2x" alt="${esc(alt)}" loading="lazy" />`;
|
||||
}
|
||||
|
||||
/** Draws the story's capture, or the windows it opens, in the site's mode. */
|
||||
export function native(host: HTMLElement, page: string, story: string, title: string): () => void {
|
||||
const key = `${page}/${story}`;
|
||||
if (captured.has(key)) {
|
||||
host.innerHTML = `<p class="label">Native capture · macOS</p>${figure(`${page}--${story}`, `${title}, captured from the native gallery`)}`;
|
||||
} else if (windows[key]) {
|
||||
host.innerHTML = `<p class="label">The windows it opens, natively</p><div class="shots">${windows[key]
|
||||
.map((name) => figure(name, `A window ${title} opens, captured natively`))
|
||||
.join("")}</div>`;
|
||||
} else {
|
||||
return () => {};
|
||||
}
|
||||
return onMode((next) => host.querySelectorAll<HTMLImageElement>("img[data-shot]").forEach((img) => (img.srcset = `${src(img.dataset.shot!, next)} 2x`)));
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"stories": [
|
||||
"files/dragdropfiles",
|
||||
"forms/fileinput-dropzone",
|
||||
"forms/pathinput",
|
||||
"media/imageupload",
|
||||
"shell/dockbadge",
|
||||
"shell/trayicon-traymenu",
|
||||
"shell/windowdragregion",
|
||||
"shell/windowresizeborder",
|
||||
"terminal/terminal-terminaltabs-terminalsplit-terminaltoolbar-shellselector-terminalsearch-terminallink",
|
||||
"theme/themeimporter-syntaxthemes"
|
||||
],
|
||||
"windows": {
|
||||
"shell/splashscreen": [
|
||||
"shell-splash-window"
|
||||
],
|
||||
"shell/aboutdialog": [
|
||||
"shell-about-window"
|
||||
],
|
||||
"shell/multiwindow-manager": [
|
||||
"shell-managed-window"
|
||||
],
|
||||
"shell/miniwindow-compactmode-pictureinpicture": [
|
||||
"shell-mini-window",
|
||||
"shell-pip-window"
|
||||
],
|
||||
"shell/updatedialog": [
|
||||
"shell-update-window"
|
||||
],
|
||||
"shell/crashreporter": [
|
||||
"shell-crash-window"
|
||||
],
|
||||
"navigation/searchpalette-spotlightsearch-quicklauncher": [
|
||||
"navigation-launcher"
|
||||
],
|
||||
"theme/platform-style-iconthemeprovider-vibrancybackground": [
|
||||
"theme-frosted-window"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -126,6 +126,10 @@ canvas.iso { position: fixed; inset: 0; width: 100vw; height: 100vh; pointer-eve
|
||||
.stage-skip { position: absolute; left: -9999px; }
|
||||
.stage-skip:focus { position: static; display: inline-block; margin-bottom: 8px; font-size: 14px; color: var(--accent); }
|
||||
|
||||
img.shot { display: block; max-width: 100%; height: auto; margin-top: 12px; }
|
||||
.native .label { margin-top: 48px; }
|
||||
.shots { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 24px; }
|
||||
.shots img.shot { max-width: min(100%, 560px); }
|
||||
.pager { display: flex; justify-content: space-between; gap: 16px; margin-top: 32px; padding-top: 16px; border-top: 1px solid var(--line); }
|
||||
.pager a { display: flex; flex-direction: column; gap: 4px; max-width: 48%; font-size: 14px; line-height: 20px; }
|
||||
.pager a:hover { color: var(--accent); }
|
||||
|
||||