From 9bb3c509ef87e0a6eb2d04afffb0bf495855d49b Mon Sep 17 00:00:00 2001 From: Pascal Garber Date: Sat, 4 Jul 2026 11:09:42 +0200 Subject: [PATCH 1/2] Web: app-web Learn navigation + Examples list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 5 of the app-web Adwaita rewrite: turn the single-scroll Learn view into the master→detail structure of app-gnome's `Learn` (`learn.blp`) — an `AdwNavigationView` with a landing page that pushes to the Tutorial or the Examples list. This completes the four real views (Learn, Editor, Debugger, GameConsole). - `AdwLearnView` (`widgets/learn/learn-view.ts`): rewritten as the navigation container. A landing page (a boxed list of "Tutorial" / "Examples" rows) pushes the Tutorial page (the generated `@learn6502/learn` HTML, unchanged) or the Examples page. Exposes `hasVisibleSubpage` / `navigateBack()` and a `subpage-changed` event — the web stand-in for the GNOME twin's `hasVisibleSubpage` property + `navigation.push`/`pop`. - `ExamplesList` + `ExampleListItem` (`widgets/learn/`): the web twins of app-gnome's `ExamplesList` / `ExampleListItem`, rendering `@learn6502/examples`. Each card REUSES the widgets from the earlier phases — the GameConsole `Display` for the memory-snapshot thumbnail (parsed from the example's `displayMemory` hex, same as GNOME) and the Editor `SourceView` (read-only, copyable) for the code. Copy routes through `learnController.dispatch("copy")` to load the program into the editor. - Shell: the header back button (stubbed since phase 1) is wired to `learnView.navigateBack()` and revealed via `updateLearnBackButton()` when a Learn subpage is open AND the Learn view is on screen (the whole sidebar on desktop, the active tab on mobile) — matching how the GNOME twin gates it. - `SourceView` gained a `fillHeight` property (default true for the editor; false for the example cards, which size to content, capped in CSS). - app-web now depends on `@learn6502/examples`; adds the `*.asm` text loader + ambient module declaration (mirroring app-gnome) so example sources import as strings. No lockfile change (workspace-only dep). Verified in a headless browser on desktop + mobile: the landing rows render, pushing Examples shows all three example cards with painted thumbnails and copyable source, copying an example loads it into the editor (+ toast), the back button reveals/hides correctly and pops to the landing, and the Tutorial page renders the full tutorial. --- packages/app-web/package.json | 4 +- packages/app-web/src/app/main-window.ts | 19 +- packages/app-web/src/types/asm-module.d.ts | 12 ++ .../app-web/src/widgets/editor/source-view.ts | 13 +- .../src/widgets/learn/example-list-item.ts | 102 ++++++++++ .../src/widgets/learn/examples-list.ts | 45 +++++ packages/app-web/src/widgets/learn/index.ts | 2 + .../app-web/src/widgets/learn/learn-view.ts | 180 ++++++++++++++---- packages/app-web/src/widgets/learn/styles.css | 119 +++++++++++- 9 files changed, 457 insertions(+), 39 deletions(-) create mode 100644 packages/app-web/src/types/asm-module.d.ts create mode 100644 packages/app-web/src/widgets/learn/example-list-item.ts create mode 100644 packages/app-web/src/widgets/learn/examples-list.ts diff --git a/packages/app-web/package.json b/packages/app-web/package.json index 434aede6..b73436f5 100644 --- a/packages/app-web/package.json +++ b/packages/app-web/package.json @@ -22,7 +22,8 @@ "license": "GPL-3.0", "gjsify": { "loaders": { - ".html": "text" + ".html": "text", + ".asm": "text" } }, "devDependencies": { @@ -36,6 +37,7 @@ "@gjsify/adwaita-web": "^0.16.3", "@learn6502/common-ui": "^0.7.0", "@learn6502/core": "^0.7.0", + "@learn6502/examples": "^0.7.0", "@learn6502/learn": "^0.7.0" } } diff --git a/packages/app-web/src/app/main-window.ts b/packages/app-web/src/app/main-window.ts index dbf15f5f..c1d56353 100644 --- a/packages/app-web/src/app/main-window.ts +++ b/packages/app-web/src/app/main-window.ts @@ -293,7 +293,12 @@ export class MainWindow implements MainView { this.configureIconButton(this.learnBackButton, "go-previous", "Back"); this.learnBackButton.setAttribute("slot", "start"); - this.learnBackButton.hidden = true; // no Learn subpages until the Learn phase + this.learnBackButton.hidden = true; // shown only when a Learn subpage is open + this.learnBackButton.addEventListener("click", () => this.learnView.navigateBack()); + // The Learn view reports when its visible sub-page changes (Tutorial / + // Examples pushed or popped) so the shell can reveal its back button — + // the web stand-in for the GNOME twin's `hasVisibleSubpage` property. + this.learnView.addEventListener("subpage-changed", () => this.updateLearnBackButton()); this.runControls.className = "shell-run-controls"; this.runControls.setAttribute("slot", "start"); @@ -500,6 +505,7 @@ export class MainWindow implements MainView { if (previous === ViewType.EDITOR && slot.type !== ViewType.EDITOR) this.editorView.saveState(); if (slot.type === ViewType.LEARN) this.learnView.restoreScrollPosition(); if (slot.type === ViewType.DEBUGGER) this.updateDebugger(); + this.updateLearnBackButton(); }); // Initial layout is applied by mount() once the tree is connected. @@ -510,6 +516,17 @@ export class MainWindow implements MainView { private applyLayout(): void { if (this.isDesktop()) this.mountThreeColumnLayout(); else this.mountSingleLayout(); + this.updateLearnBackButton(); + } + + /** + * Reveal the header back button when the Learn view has a sub-page open AND + * the Learn view is on screen — the whole sidebar on desktop, or the active + * tab on mobile (the GNOME twin gates its back button the same way). + */ + private updateLearnBackButton(): void { + const learnOnScreen = this.isDesktop() || this.currentView === ViewType.LEARN; + this.learnBackButton.hidden = !(learnOnScreen && this.learnView.hasVisibleSubpage); } private isDesktop(): boolean { diff --git a/packages/app-web/src/types/asm-module.d.ts b/packages/app-web/src/types/asm-module.d.ts new file mode 100644 index 00000000..00e7d58b --- /dev/null +++ b/packages/app-web/src/types/asm-module.d.ts @@ -0,0 +1,12 @@ +// File loader for `gjsify build`: `*.asm` is claimed by the text-loader plugin +// (configured via `loaders` in package.json#gjsify, mirroring app-gnome's +// `*.asm` loader). It produces a JS string default export — used when +// `@learn6502/examples` imports each example's `*.asm` source as a string. +// +// This ambient module declaration lives in a non-module file (no top-level +// import/export) so `declare module "*.asm"` is treated globally rather than +// as a local module-augmentation scope. +declare module "*.asm" { + const content: string; + export default content; +} diff --git a/packages/app-web/src/widgets/editor/source-view.ts b/packages/app-web/src/widgets/editor/source-view.ts index 24369b0e..b4e7aa4e 100644 --- a/packages/app-web/src/widgets/editor/source-view.ts +++ b/packages/app-web/src/widgets/editor/source-view.ts @@ -37,6 +37,7 @@ export class SourceView extends HTMLElement implements SourceViewWidget { private pendingLineNumbers = true; private pendingSelectable = true; private pendingCopyable = false; + private pendingFillHeight = true; private readonly onCodeChanged = (event: Event): void => { const detail = (event as CustomEvent<{ code: string }>).detail; @@ -117,6 +118,16 @@ export class SourceView extends HTMLElement implements SourceViewWidget { if (this.editor) this.editor.language = this.pendingLanguage; } + /** Whether the editor fills its host's height (vs. sizing to its content). */ + get fillHeight(): boolean { + return this.editor ? this.editor.fillHeight : this.pendingFillHeight; + } + + set fillHeight(value: boolean) { + this.pendingFillHeight = !!value; + if (this.editor) this.editor.fillHeight = this.pendingFillHeight; + } + /** Move keyboard focus into the CodeMirror editor. */ public focus(): boolean { this.editor?.view?.focus(); @@ -134,7 +145,7 @@ export class SourceView extends HTMLElement implements SourceViewWidget { editor.lineNumbers = this.pendingLineNumbers; editor.selectable = this.pendingSelectable; editor.copyable = this.pendingCopyable; - editor.fillHeight = true; + editor.fillHeight = this.pendingFillHeight; editor.code = this.pendingCode; editor.addEventListener("code-changed", this.onCodeChanged); diff --git a/packages/app-web/src/widgets/learn/example-list-item.ts b/packages/app-web/src/widgets/learn/example-list-item.ts new file mode 100644 index 00000000..02f86c47 --- /dev/null +++ b/packages/app-web/src/widgets/learn/example-list-item.ts @@ -0,0 +1,102 @@ +import type { ExampleMeta } from "@learn6502/examples"; +import { DisplayAddressRange, EventDispatcher, Memory } from "@learn6502/core"; + +import { Display } from "../game-console/display.js"; +import { SourceView } from "../editor/source-view.js"; + +/** Events emitted by an example list item. */ +interface ExampleListItemEventMap { + copy: { code: string }; +} + +/** + * ExampleListItem — one entry in the Learn view's Examples list. + * + * Web twin of app-gnome's `ExampleListItem extends Gtk.Box` + * (`example-list-item.blp`): a card with the example's memory-snapshot + * thumbnail (reusing the GameConsole `Display`), its title/author/description, + * and the source code in a read-only `SourceView` whose copy button loads the + * program into the editor. Mirrors the GNOME item's `copy-code` signal via the + * shared `EventDispatcher`. + * + * @emits copy - when the source view's copy button is pressed + */ +export class ExampleListItem extends HTMLElement { + readonly events: EventDispatcher = new EventDispatcher(); + + private readonly thumbnail = new Display(); + private readonly sourceView = new SourceView(); + private example: ExampleMeta | null = null; + private built = false; + + connectedCallback(): void { + this.ensureBuilt(); + } + + /** Bind an example: fill the text, load the code, paint the thumbnail. */ + public setExample(example: ExampleMeta): void { + this.ensureBuilt(); + this.example = example; + + const title = this.querySelector(".example-title"); + const author = this.querySelector(".example-author"); + const description = this.querySelector(".example-description"); + if (title) title.textContent = example.title; + if (author) author.textContent = `by ${example.author}`; + if (description) description.textContent = example.description; + + this.sourceView.code = example.code; + this.paintThumbnail(example.displayMemory); + } + + /** + * Paint the thumbnail from the example's display-memory hex snapshot — the + * same 2-hex-chars-per-byte parse app-gnome's `ExampleListItem` uses. + */ + private paintThumbnail(displayMemoryHex: string): void { + const memory = new Memory(); + let addr = DisplayAddressRange.START; + for (let i = 0; i < displayMemoryHex.length && addr <= DisplayAddressRange.END; i += 2) { + const value = parseInt(displayMemoryHex.substring(i, i + 2), 16); + if (!Number.isNaN(value)) { + memory.set(addr, value); + addr++; + } + } + this.thumbnail.initialize(memory); + } + + private ensureBuilt(): void { + if (this.built) return; + this.built = true; + + const header = document.createElement("div"); + header.className = "example-header"; + + this.thumbnail.classList.add("example-thumbnail"); + + const text = document.createElement("div"); + text.className = "example-text"; + const title = document.createElement("span"); + title.className = "example-title title-4"; + const author = document.createElement("span"); + author.className = "example-author dim-label"; + const description = document.createElement("span"); + description.className = "example-description body"; + text.append(title, author, description); + + header.append(this.thumbnail, text); + + // Read-only, copyable source view — the copy button loads it into the editor. + // Size to content (capped in CSS) rather than filling like the main editor. + this.sourceView.classList.add("example-source-view"); + this.sourceView.editable = false; + this.sourceView.copyable = true; + this.sourceView.fillHeight = false; + this.sourceView.events.on("copy", (event) => this.events.dispatch("copy", { code: event.code })); + + this.replaceChildren(header, this.sourceView); + } +} + +customElements.define("learn-example-list-item", ExampleListItem); diff --git a/packages/app-web/src/widgets/learn/examples-list.ts b/packages/app-web/src/widgets/learn/examples-list.ts new file mode 100644 index 00000000..d91fcc63 --- /dev/null +++ b/packages/app-web/src/widgets/learn/examples-list.ts @@ -0,0 +1,45 @@ +import * as Examples from "@learn6502/examples/examples"; +import type { ExampleMeta } from "@learn6502/examples"; +import { EventDispatcher } from "@learn6502/core"; + +import { ExampleListItem } from "./example-list-item.js"; + +/** Events emitted by the examples list. */ +interface ExamplesListEventMap { + copy: { code: string }; +} + +/** + * ExamplesList — the Learn view's Examples page content. + * + * Web twin of app-gnome's `ExamplesList extends Gtk.Box` (`examples-list.blp`): + * one `ExampleListItem` per bundled example (`@learn6502/examples`), forwarding + * each item's copy to a single `copy` event the Learn view routes to + * `learnController` (which loads the code into the editor). + * + * @emits copy - when any example's code is copied to the editor + */ +export class ExamplesList extends HTMLElement { + readonly events: EventDispatcher = new EventDispatcher(); + + private readonly examples: ExampleMeta[] = Object.values(Examples); + private built = false; + + connectedCallback(): void { + this.ensureBuilt(); + } + + private ensureBuilt(): void { + if (this.built) return; + this.built = true; + + for (const example of this.examples) { + const item = new ExampleListItem(); + item.setExample(example); + item.events.on("copy", (event) => this.events.dispatch("copy", { code: event.code })); + this.appendChild(item); + } + } +} + +customElements.define("learn-examples-list", ExamplesList); diff --git a/packages/app-web/src/widgets/learn/index.ts b/packages/app-web/src/widgets/learn/index.ts index b19ff8e2..322b6370 100644 --- a/packages/app-web/src/widgets/learn/index.ts +++ b/packages/app-web/src/widgets/learn/index.ts @@ -2,3 +2,5 @@ import "@gjsify/adwaita-web/source-view"; // registers (CodeMi import "./styles.js"; export * from "./learn-view.js"; +export * from "./examples-list.js"; +export * from "./example-list-item.js"; diff --git a/packages/app-web/src/widgets/learn/learn-view.ts b/packages/app-web/src/widgets/learn/learn-view.ts index 32a61219..aca8026d 100644 --- a/packages/app-web/src/widgets/learn/learn-view.ts +++ b/packages/app-web/src/widgets/learn/learn-view.ts @@ -1,36 +1,38 @@ -import { AdwClamp } from "@gjsify/adwaita-web"; +import { AdwActionRow, AdwClamp, AdwNavigationPage, AdwNavigationView, AdwPreferencesGroup } from "@gjsify/adwaita-web"; import type { LearnView } from "@learn6502/common-ui"; import { learnController } from "@learn6502/common-ui"; import tutorialHtml from "@learn6502/learn/dist/tutorial.html"; +import { ExamplesList } from "./examples-list.js"; + +/** Learn6502 project repository — the "share your example" destination. */ +const SHARE_URL = "https://github.com/JumpLink/Learn6502"; + /** - * AdwLearnView — the `LearnView` from `@learn6502/common-ui` implemented as a - * class over `@gjsify/adwaita-web` custom elements, mirroring the ADR 0007 - * spike's `AdwDebuggerView` (`views/adw-debugger.ts`). + * AdwLearnView — the `LearnView` from `@learn6502/common-ui` implemented over + * `@gjsify/adwaita-web`, the web twin of app-gnome's `Learn extends Adw.Bin` + * (`learn.blp`) and app-android's `Learn`. * - * Unlike the debugger, there is no per-widget composition here: the whole - * tutorial is one generated HTML fragment from `@learn6502/learn`'s HTML - * render target (`dist/tutorial.html`, built alongside the GTK `.ui` XML and - * NativeScript XML from the SAME `tutorial.mdx` — see - * `packages/learn/tsx/index.tsx`). This view's job is narrow: mount that - * fragment, upgrade its `` code blocks to real CodeMirror - * editors (the `@gjsify/adwaita-web/source-view` opt-in subpath, imported as - * a side effect by `./index.ts`), and forward the source views' `copy` - * events to `learnController` — exactly the seam GNOME's `TutorialView`/ - * `MdxView` and Android's `Learn` widget use - * (`sourceView.events.on("copy", ...) -> learnController.dispatch("copy", ...)`). + * Structure mirrors `learn.blp`: an `AdwNavigationView` with a landing page + * (a boxed list of "Tutorial" and "Examples" rows) that pushes to either the + * Tutorial page (the generated `@learn6502/learn` HTML with `` + * upgrades) or the Examples page (`ExamplesList` + a share prompt). The shell's + * header back button drives `navigateBack()`; a `subpage-changed` event lets it + * show/hide that button as the visible page changes (mirroring the GNOME twin's + * `hasVisibleSubpage` property + `navigation.push`/`navigation.pop`). * - * Phase 2 scope: the tutorial only, as a single scrolling document (no - * Examples list / navigation subpages — see GNOME's `Learn`/`learn.blp` for - * that structure, which a later phase can port). Quick-help - * (`dist/quick-help.html`) is generated by the same learn-package target but - * has no slot in the current shell chrome. + * All copy actions (tutorial code blocks, example cards) route through + * `learnController.dispatch("copy", …)` — the same seam the GNOME/Android twins + * use to load code into the editor. */ export class AdwLearnView extends HTMLElement implements LearnView { private built = false; private lastScrollPosition = 0; + private navigationView: AdwNavigationView | null = null; + private tutorialScroller: HTMLDivElement | null = null; - private readonly onCopy = (event: Event): void => { + /** A tutorial code block's copy button bubbles a `copy` CustomEvent up here. */ + private readonly onTutorialCopy = (event: Event): void => { const detail = (event as CustomEvent<{ code: string }>).detail; if (!detail?.code) return; learnController.dispatch("copy", { code: detail.code }); @@ -40,35 +42,143 @@ export class AdwLearnView extends HTMLElement implements LearnView { this.ensureBuilt(); } - // --- LearnView interface --- + // --- LearnView interface (scroll applies to the Tutorial page) --- - /** Save the current scroll position (this element is its own scroll - * container — see `styles.ts`'s `learn-view { overflow-y: auto }`). */ public saveScrollPosition(): void { - this.lastScrollPosition = this.scrollTop; + if (this.tutorialScroller) this.lastScrollPosition = this.tutorialScroller.scrollTop; } - /** Restore the previously saved scroll position. */ public restoreScrollPosition(): void { - if (this.lastScrollPosition > 0) this.scrollTop = this.lastScrollPosition; + if (this.tutorialScroller && this.lastScrollPosition > 0) { + this.tutorialScroller.scrollTop = this.lastScrollPosition; + } + } + + // --- Navigation surface for the shell (mirrors the GNOME twin) --- + + /** Whether a subpage (Tutorial/Examples) is open — drives the back button. */ + public get hasVisibleSubpage(): boolean { + const tag = this.navigationView?.visiblePageTag ?? null; + return tag !== null && tag !== "main"; + } + + /** Pop back to the landing page (the shell's header back button). */ + public navigateBack(): void { + this.navigationView?.pop(); } - /** Build the widget tree (once): the tutorial fragment in a clamp, matching - * the GNOME twin's `Adw.Clamp { maximum-size: 600 }` around `TutorialView`. */ + // --- DOM construction --- + private ensureBuilt(): void { if (this.built) return; this.built = true; + const nav = new AdwNavigationView(); + nav.append(this.buildLandingPage(nav), this.buildTutorialPage(), this.buildExamplesPage()); + nav.addEventListener("notify::visible-page", () => { + this.dispatchEvent( + new CustomEvent("subpage-changed", { bubbles: true, detail: { hasSubpage: this.hasVisibleSubpage } }) + ); + }); + + this.navigationView = nav; + this.replaceChildren(nav); + } + + /** Landing page: a boxed list with Tutorial + Examples rows (learn.blp). */ + private buildLandingPage(nav: AdwNavigationView): AdwNavigationPage { + const page = new AdwNavigationPage(); + page.setAttribute("title", "Learn"); + page.setAttribute("tag", "main"); + + const group = new AdwPreferencesGroup(); + group.appendChild(this.buildNavRow("Tutorial", "Step-by-step guide to 6502 assembly", () => nav.push("tutorial"))); + group.appendChild(this.buildNavRow("Examples", "Try out example programs", () => nav.push("examples"))); + + const column = document.createElement("div"); + column.className = "learn-landing"; + column.appendChild(group); + + page.appendChild(this.pageScroller(column)); + return page; + } + + private buildNavRow(title: string, subtitle: string, onActivate: () => void): AdwActionRow { + const row = new AdwActionRow(); + row.setAttribute("title", title); + row.setAttribute("subtitle", subtitle); + row.setAttribute("activatable", ""); + // Declare the chevron as a suffix slot child — AdwActionRow consumes + // `slot="suffix"` children at connect time (its `suffixSection` is not + // available before then). + const chevron = document.createElement("span"); + chevron.className = "learn-row-chevron"; + chevron.textContent = "›"; + chevron.setAttribute("slot", "suffix"); + row.appendChild(chevron); + row.addEventListener("activated", onActivate); + return row; + } + + /** Tutorial page: the generated tutorial HTML in a clamp, scrollable. */ + private buildTutorialPage(): AdwNavigationPage { + const page = new AdwNavigationPage(); + page.setAttribute("title", "Tutorial"); + page.setAttribute("tag", "tutorial"); + const clamp = new AdwClamp(); clamp.setAttribute("maximum-size", "600"); - // The fragment is trusted build output (the learn package's own MDX - // render target), not user input. + // Trusted build output (the learn package's own MDX render target). clamp.innerHTML = tutorialHtml; - this.replaceChildren(clamp); - // Fenced code blocks are `` tags in the markup above; - // the `copy` event bubbles from any of them up through this element. - this.addEventListener("copy", this.onCopy); + const scroller = this.pageScroller(clamp); + // A tutorial code block's copy button bubbles a `copy` CustomEvent here. + scroller.addEventListener("copy", this.onTutorialCopy); + this.tutorialScroller = scroller; + page.appendChild(scroller); + return page; + } + + /** Examples page: the examples list + a "share your example" prompt. */ + private buildExamplesPage(): AdwNavigationPage { + const page = new AdwNavigationPage(); + page.setAttribute("title", "Examples"); + page.setAttribute("tag", "examples"); + + const list = new ExamplesList(); + list.events.on("copy", ({ code }) => learnController.dispatch("copy", { code })); + + const share = document.createElement("div"); + share.className = "learn-share"; + const prompt = document.createElement("p"); + prompt.className = "body"; + prompt.textContent = "Got a cool 6502 example? Share it with the community!"; + const shareLink = document.createElement("a"); + shareLink.className = "learn-share-button"; + shareLink.textContent = "Share Your Example"; + shareLink.href = SHARE_URL; + shareLink.target = "_blank"; + shareLink.rel = "noopener noreferrer"; + share.append(prompt, shareLink); + + const column = document.createElement("div"); + column.className = "learn-examples"; + column.append(list, share); + + const clamp = new AdwClamp(); + clamp.setAttribute("maximum-size", "600"); + clamp.appendChild(column); + + page.appendChild(this.pageScroller(clamp)); + return page; + } + + /** A vertically-scrolling container that fills the navigation page. */ + private pageScroller(child: HTMLElement): HTMLDivElement { + const scroller = document.createElement("div"); + scroller.className = "learn-page-scroller"; + scroller.appendChild(child); + return scroller; } } diff --git a/packages/app-web/src/widgets/learn/styles.css b/packages/app-web/src/widgets/learn/styles.css index 9617ef0c..458210f4 100644 --- a/packages/app-web/src/widgets/learn/styles.css +++ b/packages/app-web/src/widgets/learn/styles.css @@ -15,11 +15,28 @@ * self-injected on import (see ./styles.ts). */ +/* learn-view is `display: block; height: 100%` via the shell's `.shell-view-slot` + class (whose specificity beats a bare `learn-view` rule); just clip the + absolutely-positioned nav pages to it. */ learn-view { - display: block; + overflow: hidden; + box-sizing: border-box; +} + +/* The navigation view fills the Learn pane. Its own stylesheet makes it a flex + column whose visible page fills (position: absolute; inset: 0) — do NOT + override its `display`; only give it a height for that flex layout to fill. */ +learn-view adw-navigation-view { height: 100%; +} + +/* The page's direct child fills (`.adw-navigation-page > *` in the nav's own + CSS); this makes it the internal scroll container. */ +.learn-page-scroller { + min-height: 0; overflow-y: auto; box-sizing: border-box; + padding: 12px; } /* The rendered fragment's root carries BOTH "adw-box adw-box--vertical" (its @@ -98,3 +115,103 @@ learn-view adw-source-view { border-radius: 12px; overflow: hidden; } + +/* --- Landing page (Tutorial / Examples boxed list) --- */ + +.learn-landing { + padding-block: 12px; +} + +.learn-row-chevron { + color: var(--dim-label-color, rgb(0 0 0 / 45%)); + font-size: 1.2em; +} + +/* --- Examples page --- */ + +.learn-examples { + display: flex; + flex-direction: column; + gap: 24px; + padding-block: 12px; +} + +learn-examples-list { + display: flex; + flex-direction: column; + gap: 18px; +} + +.learn-share { + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + text-align: center; + padding: 12px 24px 24px; +} + +.learn-share-button { + display: inline-block; + padding: 8px 20px; + border-radius: 9999px; + background: var(--accent-bg-color, #3584e4); + color: var(--accent-fg-color, #fff); + font-weight: 700; + text-decoration: none; +} + +/* --- Example list item card --- */ + +learn-example-list-item { + display: flex; + flex-direction: column; + gap: 12px; + padding: 12px; + border-radius: 12px; + background: var(--card-bg-color, rgb(0 0 0 / 4%)); +} + +.example-header { + display: flex; + gap: 12px; + align-items: center; +} + +.example-thumbnail { + flex: 0 0 auto; + inline-size: 72px; + block-size: 72px; + border-radius: 6px; + overflow: hidden; +} + +.example-thumbnail .game-console-screen { + inline-size: 72px; + max-inline-size: 72px; + block-size: 72px; +} + +.example-text { + display: flex; + flex-direction: column; + gap: 2px; + min-inline-size: 0; +} + +.example-title { + font-weight: 700; +} + +.example-author, +.dim-label { + color: var(--dim-label-color, rgb(0 0 0 / 55%)); + font-size: 0.9em; +} + +.example-source-view { + display: block; + max-block-size: 220px; + overflow: auto; + border-radius: 8px; +} From e3588c3a0b6a23af5d45a8f1b5e01ae371753276 Mon Sep 17 00:00:00 2001 From: Pascal Garber Date: Sat, 4 Jul 2026 11:28:07 +0200 Subject: [PATCH 2/2] Web: register asm-module.d.ts in app-web tsconfig MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit app-web's tsconfig uses an explicit `files` list, not `include`, so an ambient `.d.ts` with no import/export (unreachable via the import closure) is only in the program when listed there. `src/types/asm-module.d.ts` was missing from the list, so the `*.asm` declaration was absent under a clean install and the examples' `import … from "./snake.asm"` failed type-check on CI (TS2307) while passing locally off a stale node_modules where the examples package's own `env.d.ts` happened to be reachable. Register it beside `html-module.d.ts`. --- packages/app-web/tsconfig.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/app-web/tsconfig.json b/packages/app-web/tsconfig.json index 412e60de..e506ee18 100644 --- a/packages/app-web/tsconfig.json +++ b/packages/app-web/tsconfig.json @@ -19,6 +19,7 @@ "src/app/main.ts", "src/types/adwaita-fonts.d.ts", "src/types/html-module.d.ts", + "src/types/asm-module.d.ts", "src/types/css.d.ts" ] }