diff --git a/adev/BUILD.bazel b/adev/BUILD.bazel
index 497301c1ce8..0316eac26e1 100644
--- a/adev/BUILD.bazel
+++ b/adev/BUILD.bazel
@@ -15,6 +15,7 @@ exports_files([
APPLICATION_FILES = [
"//adev/src/assets/images",
+ "//adev/src/assets/images/v21-event:v21-event-images",
"//adev/src/assets/others",
"//adev/src/assets/previews",
"//adev/src/assets:tutorials",
diff --git a/adev/angular.json b/adev/angular.json
index 7368c7a1fad..de7df65a4b1 100644
--- a/adev/angular.json
+++ b/adev/angular.json
@@ -32,7 +32,8 @@
"src/favicon.ico",
"src/robots.txt",
"src/llms.txt",
- "src/assets"
+ "src/assets",
+ "src/assets/images/v21-event"
],
"styles": ["@angular/docs/styles/global-styles.scss", "./src/local-styles.scss"],
"scripts": [],
diff --git a/adev/src/assets/images/angular-v21-hero.jpg b/adev/src/assets/images/angular-v21-hero.jpg
deleted file mode 100644
index fb0dc7e7bde..00000000000
Binary files a/adev/src/assets/images/angular-v21-hero.jpg and /dev/null differ
diff --git a/adev/src/assets/images/v21-event/BUILD.bazel b/adev/src/assets/images/v21-event/BUILD.bazel
new file mode 100644
index 00000000000..9b89c177a55
--- /dev/null
+++ b/adev/src/assets/images/v21-event/BUILD.bazel
@@ -0,0 +1,11 @@
+load("@aspect_rules_js//js:defs.bzl", "js_library")
+
+package(default_visibility = ["//visibility:public"])
+
+js_library(
+ name = "v21-event-images",
+ srcs = glob([
+ "*.png",
+ "*.jpg",
+ ]),
+)
diff --git a/adev/src/assets/images/v21-event/angular-v21-hero.jpg b/adev/src/assets/images/v21-event/angular-v21-hero.jpg
new file mode 100644
index 00000000000..247b88d643b
Binary files /dev/null and b/adev/src/assets/images/v21-event/angular-v21-hero.jpg differ
diff --git a/adev/src/assets/images/v21-event/castle-sign.png b/adev/src/assets/images/v21-event/castle-sign.png
new file mode 100644
index 00000000000..b1e516714cf
Binary files /dev/null and b/adev/src/assets/images/v21-event/castle-sign.png differ
diff --git a/adev/src/assets/images/v21-event/congrats-sign.png b/adev/src/assets/images/v21-event/congrats-sign.png
new file mode 100644
index 00000000000..4768dffe125
Binary files /dev/null and b/adev/src/assets/images/v21-event/congrats-sign.png differ
diff --git a/adev/src/assets/images/v21-event/enter-sign.png b/adev/src/assets/images/v21-event/enter-sign.png
new file mode 100644
index 00000000000..6682fec7e7b
Binary files /dev/null and b/adev/src/assets/images/v21-event/enter-sign.png differ
diff --git a/adev/src/assets/images/v21-event/entry-denied-sign.png b/adev/src/assets/images/v21-event/entry-denied-sign.png
new file mode 100644
index 00000000000..d0a8c07d3f6
Binary files /dev/null and b/adev/src/assets/images/v21-event/entry-denied-sign.png differ
diff --git a/adev/src/assets/images/v21-event/key.png b/adev/src/assets/images/v21-event/key.png
new file mode 100644
index 00000000000..dd83af609bf
Binary files /dev/null and b/adev/src/assets/images/v21-event/key.png differ
diff --git a/adev/src/assets/images/v21-event/mascot-down-1.png b/adev/src/assets/images/v21-event/mascot-down-1.png
new file mode 100644
index 00000000000..76899638fcf
Binary files /dev/null and b/adev/src/assets/images/v21-event/mascot-down-1.png differ
diff --git a/adev/src/assets/images/v21-event/mascot-down-2.png b/adev/src/assets/images/v21-event/mascot-down-2.png
new file mode 100644
index 00000000000..82f3ae41c6a
Binary files /dev/null and b/adev/src/assets/images/v21-event/mascot-down-2.png differ
diff --git a/adev/src/assets/images/v21-event/mascot-left-1.png b/adev/src/assets/images/v21-event/mascot-left-1.png
new file mode 100644
index 00000000000..060551340b1
Binary files /dev/null and b/adev/src/assets/images/v21-event/mascot-left-1.png differ
diff --git a/adev/src/assets/images/v21-event/mascot-left-2.png b/adev/src/assets/images/v21-event/mascot-left-2.png
new file mode 100644
index 00000000000..b9ad270f8ce
Binary files /dev/null and b/adev/src/assets/images/v21-event/mascot-left-2.png differ
diff --git a/adev/src/assets/images/v21-event/mascot-right-1.png b/adev/src/assets/images/v21-event/mascot-right-1.png
new file mode 100644
index 00000000000..8a68248f633
Binary files /dev/null and b/adev/src/assets/images/v21-event/mascot-right-1.png differ
diff --git a/adev/src/assets/images/v21-event/mascot-right-2.png b/adev/src/assets/images/v21-event/mascot-right-2.png
new file mode 100644
index 00000000000..fd574944b83
Binary files /dev/null and b/adev/src/assets/images/v21-event/mascot-right-2.png differ
diff --git a/adev/src/assets/images/v21-event/mascot-up-1.png b/adev/src/assets/images/v21-event/mascot-up-1.png
new file mode 100644
index 00000000000..9f01e0af93b
Binary files /dev/null and b/adev/src/assets/images/v21-event/mascot-up-1.png differ
diff --git a/adev/src/assets/images/v21-event/mascot-up-2.png b/adev/src/assets/images/v21-event/mascot-up-2.png
new file mode 100644
index 00000000000..dde41ede582
Binary files /dev/null and b/adev/src/assets/images/v21-event/mascot-up-2.png differ
diff --git a/adev/src/assets/images/v21-event/mascot.png b/adev/src/assets/images/v21-event/mascot.png
new file mode 100644
index 00000000000..05bff9ffebc
Binary files /dev/null and b/adev/src/assets/images/v21-event/mascot.png differ
diff --git a/adev/src/assets/images/v21-event/welcome-sign.png b/adev/src/assets/images/v21-event/welcome-sign.png
new file mode 100644
index 00000000000..b582f466a54
Binary files /dev/null and b/adev/src/assets/images/v21-event/welcome-sign.png differ
diff --git a/adev/src/assets/images/v21-event/world-map.png b/adev/src/assets/images/v21-event/world-map.png
new file mode 100644
index 00000000000..5c18e9ea96a
Binary files /dev/null and b/adev/src/assets/images/v21-event/world-map.png differ
diff --git a/adev/src/content/events/BUILD.bazel b/adev/src/content/events/BUILD.bazel
index 20895866720..ce4f96ff543 100644
--- a/adev/src/content/events/BUILD.bazel
+++ b/adev/src/content/events/BUILD.bazel
@@ -7,8 +7,9 @@ generate_guides(
]),
api_manifest = "//adev/src/assets:docs_api_manifest",
data = [
- "//adev/src/assets/images:angular-v21-hero.jpg",
+ "//adev/src/assets/images/v21-event:v21-event-images",
"//adev/src/assets/others:angular-v21-release.ics",
+ "//adev/src/content/examples",
],
visibility = ["//adev:__subpackages__"],
)
diff --git a/adev/src/content/events/v21.md b/adev/src/content/events/v21.md
index ae3f6bb5b60..d26d5ce36ca 100644
--- a/adev/src/content/events/v21.md
+++ b/adev/src/content/events/v21.md
@@ -1,29 +1,37 @@
-
+
# Angular v21: The Adventure Begins
+## Release Blog
+
**Angular v21 is live**: check out the [v21 release blog](http://goo.gle/angular-v21-blog) to learn about all of the amazing new features coming your way.
-## Save the Date November 20, 2025
+## Experience the v21 Release
-
-
-
-
+Explore the Angular v21 release with this interactive game world. Curious how we made it? Click the `Show Code` button below to view the source code for this Angular app.
-
+
-Join the Angular team this November for a brand new release adventure. With modern AI tooling, performance updates and more, Angular v21 delivers fantastic new features to improve your developer experience. Whether you’re creating AI-powered apps or scalable enterprise applications, there has never been a better time to build with Angular.
+## Angular v21 Developer Event [Full Version]
+
+Angular v21 is being delivered to you as a brand new release adventure. With modern AI tooling, performance updates and more, Angular v21 delivers fantastic new features to improve your developer experience. Whether you’re creating AI-powered apps or scalable enterprise applications, there has never been a better time to build with Angular.
🔥 What's coming in v21
- New Angular MCP Server tools to improve AI-powered workflows and code generation
- Your first look at Signal Forms, our new streamlined, signal-based approach to forms in Angular
-- Exciting new details about the Angular ARIA package
+- Exciting new details about the Angular Aria package
-That’s just a taste of what’s coming in our upcoming major release event. You won't want to miss it.
-
-
-
-
-
+
+
+
diff --git a/adev/src/content/examples/v21-game-world/e2e/src/app.e2e-spec.ts b/adev/src/content/examples/v21-game-world/e2e/src/app.e2e-spec.ts
new file mode 100644
index 00000000000..2b018bbf1f4
--- /dev/null
+++ b/adev/src/content/examples/v21-game-world/e2e/src/app.e2e-spec.ts
@@ -0,0 +1,172 @@
+import {browser, element, by, Key, ExpectedConditions} from 'protractor';
+
+describe('Angular v21 World', () => {
+ beforeEach(() => browser.get(''));
+
+ async function getCharacterPosition(): Promise<{x: string; y: string}> {
+ const character = element(by.css('.character'));
+ const left = await character.getCssValue('left');
+ const top = await character.getCssValue('top');
+ return {x: left, y: top};
+ }
+
+ it('should display the initial game state correctly', async () => {
+ // Character is visible
+ expect(await element(by.css('.character')).isDisplayed()).toBe(true);
+
+ // Info sign is shown with welcome message
+ const infoSign = element(by.css('.info-sign'));
+ expect(await infoSign.isDisplayed()).toBe(true);
+ expect(await infoSign.getAttribute('src')).toContain('welcome-sign.png');
+
+ // D-pad is visible
+ expect(await element(by.css('.d-pad')).isDisplayed()).toBe(true);
+
+ // Explore button is not visible
+ expect(await element(by.css('.explore-button')).isPresent()).toBe(false);
+
+ // No keys are present
+ expect(await element.all(by.css('.key-icon')).count()).toBe(0);
+ });
+
+ it('should move the character with the D-pad buttons', async () => {
+ const initialPosition = await getCharacterPosition();
+ const leftButton = element(by.css('.d-pad-button.left'));
+
+ // Hold the button down for a short period to simulate walking
+ await browser.actions().mouseDown(leftButton).perform();
+ await browser.sleep(200);
+ await browser.actions().mouseUp(leftButton).perform();
+
+ const newPosition = await getCharacterPosition();
+ expect(newPosition.x).not.toEqual(initialPosition.x);
+ });
+
+ it('should move the character with keyboard arrow keys', async () => {
+ const initialPosition = await getCharacterPosition();
+
+ // Send arrow key press
+ await browser.actions().sendKeys(Key.ARROW_RIGHT).perform();
+ await browser.sleep(200); // Allow time for movement
+ await browser.actions().sendKeys(Key.NULL).perform(); // Release key
+
+ const newPosition = await getCharacterPosition();
+ expect(newPosition.x).not.toEqual(initialPosition.x);
+ });
+
+ it('should show explore button, open dialog, and collect a key when a destination is reached', async () => {
+ const body = element(by.css('body'));
+ const exploreButton = element(by.css('.explore-button'));
+ const dialog = element(by.css('.dialog-overlay'));
+
+ // Move left until we reach the Palm Tree destination
+ for (let i = 0; i < 20; i++) {
+ await body.sendKeys(Key.ARROW_LEFT);
+ await browser.sleep(100);
+ }
+
+ // Wait for the explore button to appear and check info sign
+ await browser.wait(ExpectedConditions.visibilityOf(exploreButton), 5000);
+ expect(await exploreButton.isDisplayed()).toBe(true);
+ expect(await element(by.css('.info-sign')).getAttribute('src')).toContain('enter-sign.png');
+
+ // Click explore button to open dialog
+ await exploreButton.click();
+ await browser.wait(ExpectedConditions.visibilityOf(dialog), 1000);
+ expect(await dialog.isDisplayed()).toBe(true);
+ expect(await dialog.element(by.css('h2')).getText()).toEqual("What's new in Angular AI");
+
+ // Close the dialog
+ await dialog.element(by.css('.close-button')).click();
+ await browser.wait(ExpectedConditions.invisibilityOf(dialog), 1000);
+ expect(await dialog.isPresent()).toBe(false);
+
+ // Check that one key has been collected
+ expect(await element.all(by.css('.key-icon')).count()).toBe(1);
+ });
+
+ it('should show entry denied at castle without all keys', async () => {
+ const body = element(by.css('body'));
+ const exploreButton = element(by.css('.explore-button'));
+
+ // Move to a position near the castle without collecting keys
+ for (let i = 0; i < 20; i++) (await body.sendKeys(Key.ARROW_RIGHT), await browser.sleep(100));
+ for (let i = 0; i < 20; i++) (await body.sendKeys(Key.ARROW_DOWN), await browser.sleep(100));
+
+ await browser.sleep(1000); // Settle
+
+ // Check that the entry denied sign is shown and the button is not present
+ expect(await element(by.css('.info-sign')).getAttribute('src')).toContain(
+ 'entry-denied-sign.png',
+ );
+ expect(await exploreButton.isPresent()).toBe(false);
+ });
+
+ it('should handle the full game flow and show congrats state', async () => {
+ const body = element(by.css('body'));
+ const exploreButton = element(by.css('.explore-button'));
+ const dialog = element(by.css('.dialog-overlay'));
+ const keys = element.all(by.css('.key-icon'));
+ const mascot = element(by.css('.mascot-icon'));
+
+ // **Navigate to Palm Tree (d1) and collect the first key**
+ for (let i = 0; i < 20; i++) {
+ await body.sendKeys(Key.ARROW_LEFT);
+ await browser.sleep(100);
+ }
+ await browser.wait(ExpectedConditions.visibilityOf(exploreButton), 5000);
+ await exploreButton.click();
+ await browser.wait(ExpectedConditions.visibilityOf(dialog), 1000);
+ await dialog.element(by.css('.close-button')).click();
+ await browser.wait(ExpectedConditions.invisibilityOf(dialog), 1000);
+ expect(await keys.count()).toBe(1);
+
+ // **Navigate to Red Door (d2) and collect the second key**
+ for (let i = 0; i < 15; i++) {
+ await body.sendKeys(Key.ARROW_UP);
+ await browser.sleep(100);
+ }
+ await browser.wait(ExpectedConditions.visibilityOf(exploreButton), 5000);
+ await exploreButton.click();
+ await browser.wait(ExpectedConditions.visibilityOf(dialog), 1000);
+ await dialog.element(by.css('.close-button')).click();
+ await browser.wait(ExpectedConditions.invisibilityOf(dialog), 1000);
+ expect(await keys.count()).toBe(2);
+
+ // **Navigate to Volcano (d3) and collect the third key**
+ for (let i = 0; i < 25; i++) {
+ await body.sendKeys(Key.ARROW_RIGHT);
+ await browser.sleep(100);
+ }
+ await browser.wait(ExpectedConditions.visibilityOf(exploreButton), 5000);
+ await exploreButton.click();
+ await browser.wait(ExpectedConditions.visibilityOf(dialog), 1000);
+ await dialog.element(by.css('.close-button')).click();
+ await browser.wait(ExpectedConditions.invisibilityOf(dialog), 1000);
+ expect(await keys.count()).toBe(3);
+
+ // **Navigate to Castle (d4) with all keys**
+ for (let i = 0; i < 15; i++) {
+ await body.sendKeys(Key.ARROW_DOWN);
+ await browser.sleep(100);
+ }
+ await browser.sleep(1000); // Wait for character to settle
+
+ // Check for correct sign and button visibility
+ expect(await element(by.css('.info-sign')).getAttribute('src')).toContain('castle-sign.png');
+ await browser.wait(ExpectedConditions.visibilityOf(exploreButton), 5000);
+
+ // **Open Castle dialog, close it, and see mascot and final sign**
+ await exploreButton.click();
+ await browser.wait(ExpectedConditions.visibilityOf(dialog), 1000);
+ await dialog.element(by.css('.close-button')).click();
+ await browser.wait(ExpectedConditions.invisibilityOf(dialog), 1000);
+
+ // Mascot appears
+ await browser.wait(ExpectedConditions.visibilityOf(mascot), 1000);
+ expect(await mascot.isDisplayed()).toBe(true);
+
+ // Congrats sign appears
+ expect(await element(by.css('.info-sign')).getAttribute('src')).toContain('congrats-sign.png');
+ });
+});
diff --git a/adev/src/content/examples/v21-game-world/example-config.json b/adev/src/content/examples/v21-game-world/example-config.json
new file mode 100644
index 00000000000..ccf472031b4
--- /dev/null
+++ b/adev/src/content/examples/v21-game-world/example-config.json
@@ -0,0 +1,6 @@
+{
+ "tests": [
+ {"cmd": "yarn", "args": ["test", "--browsers=ChromeHeadlessNoSandbox", "--no-watch"]},
+ {"cmd": "yarn", "args": ["e2e", "--configuration=production", "--protractor-config=e2e/protractor-bazel.conf.js", "--no-webdriver-update", "--port=0"]}
+ ]
+}
diff --git a/adev/src/content/examples/v21-game-world/src/app/app.ts b/adev/src/content/examples/v21-game-world/src/app/app.ts
new file mode 100644
index 00000000000..039a95d86db
--- /dev/null
+++ b/adev/src/content/examples/v21-game-world/src/app/app.ts
@@ -0,0 +1,723 @@
+import {ChangeDetectionStrategy, Component, computed, effect, inject, signal} from '@angular/core';
+import {DomSanitizer, SafeResourceUrl} from '@angular/platform-browser';
+
+// 1. INTERFACES
+interface Point {
+ x: number;
+ y: number;
+}
+
+interface Destination {
+ id: string;
+ name: string;
+ position: Point;
+ videoUrl: string;
+ display: string;
+}
+
+interface RoadSegment {
+ id: string;
+ orientation: 'horizontal' | 'vertical';
+ fixedCoordinate: number;
+ start: number;
+ end: number;
+}
+
+type WalkingDirection = 'left' | 'right' | 'up' | 'down' | null;
+
+const STAND = 'assets/images/v21-event/mascot.png';
+const WALK_LEFT_1 = 'assets/images/v21-event/mascot-left-1.png';
+const WALK_LEFT_2 = 'assets/images/v21-event/mascot-left-2.png';
+const WALK_RIGHT_1 = 'assets/images/v21-event/mascot-right-1.png';
+const WALK_RIGHT_2 = 'assets/images/v21-event/mascot-right-2.png';
+const WALK_UP_1 = 'assets/images/v21-event/mascot-up-1.png';
+const WALK_UP_2 = 'assets/images/v21-event/mascot-up-2.png';
+const WALK_DOWN_1 = 'assets/images/v21-event/mascot-down-1.png';
+const WALK_DOWN_2 = 'assets/images/v21-event/mascot-down-2.png';
+
+// Define all unique coordinates as constants for readability and maintanence.
+const START_X = 0.45;
+const LEFT_X = 0.19;
+const RIGHT_X = 0.85;
+const PALM_TREE_X = LEFT_X;
+const RED_DOOR_X = LEFT_X; // Aligned with Palm Tree for a straight vertical path
+const VOLCANO_X = RIGHT_X;
+const CASTLE_X = RIGHT_X;
+
+const BOTTOM_Y = 0.6;
+const TOP_Y = 0.2;
+const RED_DOOR_Y = TOP_Y;
+const VOLCANO_Y = TOP_Y;
+const PALM_TREE_Y = BOTTOM_Y;
+const START_Y = BOTTOM_Y;
+const CASTLE_Y = 0.7;
+
+// 2. GAME DATA CONSTANTS
+const STARTING_POINT: Point = {x: START_X, y: START_Y};
+
+const DESTINATIONS: Destination[] = [
+ {
+ id: 'd1',
+ name: 'Palm Tree',
+ position: {x: PALM_TREE_X, y: PALM_TREE_Y},
+ videoUrl: 'https://www.youtube.com/embed/FteCOhQb4Ow',
+ display: "What's new in Angular AI",
+ },
+ {
+ id: 'd2',
+ name: 'Red Door',
+ position: {x: RED_DOOR_X, y: RED_DOOR_Y},
+ videoUrl: 'https://www.youtube.com/embed/Cegc5JtWbrI',
+ display: 'Meet Angular Aria',
+ },
+ {
+ id: 'd3',
+ name: 'Volcano',
+ position: {x: VOLCANO_X, y: VOLCANO_Y},
+ videoUrl: 'https://www.youtube.com/embed/7v8mIW9_NXw',
+ display: 'Introducing Signal Forms',
+ },
+ {
+ id: 'd4',
+ name: 'Castle',
+ position: {x: CASTLE_X, y: CASTLE_Y},
+ videoUrl: 'https://www.youtube.com/embed/wiWUpCsJ9Os',
+ display: "Say hello to Angular's new Mascot!",
+ },
+];
+
+const ALL_ROAD_SEGMENTS: RoadSegment[] = [
+ // Road 1 (Dest 1 <-> Start)
+ {
+ id: 'r1',
+ orientation: 'horizontal',
+ fixedCoordinate: PALM_TREE_Y,
+ start: PALM_TREE_X,
+ end: START_X,
+ },
+ // Road 2 (Palm Tree <-> Red Door)
+ {
+ id: 'r2',
+ orientation: 'vertical',
+ fixedCoordinate: PALM_TREE_X,
+ start: RED_DOOR_Y,
+ end: PALM_TREE_Y,
+ },
+ // Road 3 (Dest 2 <-> Dest 3)
+ {
+ id: 'r3',
+ orientation: 'horizontal',
+ fixedCoordinate: RED_DOOR_Y,
+ start: RED_DOOR_X,
+ end: VOLCANO_X,
+ },
+ // Road 4 (Dest 3 <-> Dest 4)
+ {id: 'r4', orientation: 'vertical', fixedCoordinate: VOLCANO_X, start: VOLCANO_Y, end: CASTLE_Y},
+];
+
+// 3. GAME MECHANICS CONSTANTS
+const MOVE_STEP = 0.0025;
+const ANIMATION_SPEED = 28; // Higher is slower. Update image every 10 frames.
+// How far off a road's axis the character can be
+const MOVE_TOLERANCE = 0.002;
+// How close to a destination to "arrive" (must be very small)
+const DESTINATION_TOLERANCE = 0.005;
+
+@Component({
+ selector: 'app-root',
+ template: `
+