Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
a0e8bc9
feat: add Christmas mode with festive snow effect and toggle in settings
pFornagiel Dec 12, 2025
ae7bbcf
feat: add ASCII Christmas tree component and integrate into preview l…
pFornagiel Dec 12, 2025
4130599
feat: enhance ASCII Christmas tree with dynamic star generation and r…
pFornagiel Dec 12, 2025
5de2d4f
refactor: simplify AsciiChristmasTree component by removing unused st…
pFornagiel Dec 12, 2025
f311e9f
feat: add licensing information for rossmacarthur/advent package
pFornagiel Dec 12, 2025
176b3e3
feat: organize Christmas components into a dedicated directory and im…
pFornagiel Dec 12, 2025
b1c1a43
feat: unify christmasMode state management across components
pFornagiel Dec 12, 2025
e0ec731
feat: move react-snowfall to devDependencies in package.json and pack…
pFornagiel Dec 12, 2025
8eb2ef6
feat: refactor Christmas components to use centralized christmasMode …
pFornagiel Dec 12, 2025
931a109
feat: move preview-loader-tree-container styles to AsciiChristmasTree…
pFornagiel Dec 12, 2025
0c23425
fix: add missing newline at end of PreviewLoader.css
pFornagiel Dec 12, 2025
539dc9b
feat: reorder imports in PreviewLoader and SettingsDropdown for consi…
pFornagiel Dec 12, 2025
6a55691
feat: integrate christmasMode to conditionally render festive components
pFornagiel Dec 12, 2025
83e14c1
feat: simplify conditional rendering of AsciiChristmasTree in Preview…
pFornagiel Dec 12, 2025
879272d
fix: correct date range for Christmas season in isChristmasSeason fun…
pFornagiel Dec 12, 2025
91250d6
Merge branch 'main' into @pFornagiel/christmas-spirit
pFornagiel Dec 15, 2025
87da971
feat: change the setting to be persistent, remove the time limit
pFornagiel Dec 15, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 22 additions & 5 deletions packages/vscode-extension/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 8 additions & 1 deletion packages/vscode-extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,12 @@
"scope": "window",
"default": true,
"description": "Shows device frame in the IDE panel."
},
"RadonIDE.userInterface.christmasMode": {
"type": "boolean",
"scope": "window",
"default": false,
"description": "Enables festive mode in the IDE panel."
}
}
},
Expand Down Expand Up @@ -1357,7 +1363,8 @@
"vscode-test": "^1.6.1",
"ws": "^8.18.1",
"xml2js": "^0.6.2",
"zod": "^3.25.32"
"zod": "^3.25.32",
"react-snowfall": "^2.4.0"
},
"optionalDependencies": {
"@rollup/rollup-linux-x64-gnu": "4.40.0"
Expand Down
2 changes: 2 additions & 0 deletions packages/vscode-extension/src/common/State.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ export type RadonAISettings = {
export type UserInterfaceSettings = {
panelLocation: PanelLocation;
showDeviceFrame: boolean;
christmasMode?: boolean;
};

export type DeviceControlSettings = {
Expand Down Expand Up @@ -627,6 +628,7 @@ export const initialState: State = {
userInterface: {
panelLocation: "tab",
showDeviceFrame: true,
christmasMode: false,
},
deviceSettings: {
deviceRotation: DeviceRotation.Portrait,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const WorkspaceConfigurationKeyMap = {
userInterface: {
panelLocation: "userInterface.panelLocation",
showDeviceFrame: "userInterface.showDeviceFrame",
christmasMode: "userInterface.christmasMode",
},
deviceSettings: {
deviceRotation: "deviceSettings.deviceRotation",
Expand Down Expand Up @@ -64,6 +65,8 @@ export function getCurrentWorkspaceConfiguration(config: WorkspaceConfiguration)
"tab",
showDeviceFrame:
config.get<boolean>(WorkspaceConfigurationKeyMap.userInterface.showDeviceFrame) ?? true,
christmasMode:
config.get<boolean>(WorkspaceConfigurationKeyMap.userInterface.christmasMode) ?? false,
},
deviceControl: {
startDeviceOnLaunch:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import classNames from "classnames";
import { useEffect, useState } from "react";
import { use$ } from "@legendapp/state/react";

import "./PreviewLoader.css";

Expand All @@ -10,7 +11,6 @@ import { useProject } from "../providers/ProjectProvider";
import Button from "./shared/Button";
import { Output } from "../../common/OutputChannel";
import { useStore } from "../providers/storeProvider";
import { use$ } from "@legendapp/state/react";
import {
DevicePlatform,
DeviceRotation,
Expand All @@ -19,6 +19,8 @@ import {
} from "../../common/State";
import { useSelectedDeviceSessionState } from "../hooks/selectedSession";

import AsciiChristmasTree from "./christmas/AsciiChristmasTree";

const startupStageWeightSum = StartupStageWeight.map((item) => item.weight).reduce(
(acc, cur) => acc + cur,
0
Expand Down Expand Up @@ -111,8 +113,11 @@ function PreviewLoader({ onRequestShowPreview }: { onRequestShowPreview: () => v
}
}

const christmasMode = use$(store$.workspaceConfiguration.userInterface.christmasMode);

return (
<div className={`preview-loader-wrapper ${isLandscape ? "landscape" : "portrait"}`}>
{christmasMode && <AsciiChristmasTree />}
<div className="preview-loader-load-info">
<button className="preview-loader-container" onClick={handleLoaderClick}>
<div className="preview-loader-button-group">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import { ActivateLicenseView } from "../views/ActivateLicenseView";
import { LicenseStatus } from "../../common/License";
import ExportLogsView from "../views/ExportLogsView";

import { ChristmasModeToggle } from "./christmas/ChristmasModeToggle";

interface SettingsDropdownProps {
children: React.ReactNode;
isDeviceRunning: boolean;
Expand Down Expand Up @@ -166,6 +168,7 @@ function SettingsDropdown({ project, isDeviceRunning, children, disabled }: Sett
</DropdownMenu.Item>
{telemetryEnabled && <SendFeedbackItem />}
{shouldShowActivateLicenseItem && <ActivateLicenseItem />}
<ChristmasModeToggle />
<div className="dropdown-menu-item device-settings-version-text">
Radon IDE version: {extensionVersion}
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
.ascii-christmas-tree {
font-family: monospace;
font-size: 10px;
line-height: 1;
margin: 0;
padding: 0;
white-space: pre;
user-select: none;
text-align: center;
}

.tree-row {
display: block;
white-space: pre;
}

.preview-loader-tree-container {
filter: contrast(0.5);
opacity: 0.5;
position: absolute;
bottom: 0;
margin-bottom: 10px;
pointer-events: none;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import { useState, useEffect, useMemo } from "react";
import "./AsciiChristmasTree.css";

interface Pixel {
value: string;
color?: string;
}

const COLORS = {
white: "#ffffff",
green: "#338a3d",
yellow: "#dfc321",
red: "#c22525",
magenta: "#1f52b8",
gray: "#808080",
};

function AsciiChristmasTree({ width = 46, height = 19 }: { width?: number; height?: number }) {
const [baubleColors, setBaubleColors] = useState<string[]>([
COLORS.yellow,
COLORS.red,
COLORS.magenta,
]);

// Rotate bauble colors every 500ms for blinking effect
useEffect(() => {
const interval = setInterval(() => {
setBaubleColors((prev) => {
const rotated = [...prev];
rotated.push(rotated.shift()!);
return rotated;
});
}, 1000);

return () => clearInterval(interval);
}, []);

const treeImage = useMemo(() => {
return generateChristmasScene(width, height, baubleColors);
}, [width, height, baubleColors]);

return (
<div className="preview-loader-tree-container">
<pre className="ascii-christmas-tree" aria-label="ASCII Christmas Tree">
{treeImage.map((row, i) => (
<div key={i} className="tree-row">
{row.map((pixel, j) => (
<span
key={j}
style={{
color: pixel.color || "inherit",
}}>
{pixel.value || " "}
</span>
))}
</div>
))}
</pre>
</div>
);
}

function generateChristmasScene(width: number, _height: number, baubleColors: string[]): Pixel[][] {
// Generate single centered tree with fixed size
const treeWidth = 21;
const tree = generateTree(treeWidth, baubleColors, 0);

// Calculate the actual height needed for the tree
const actualHeight = tree.length;

// Create empty background with calculated height
const image: Pixel[][] = Array(actualHeight)
.fill(null)
.map(() =>
Array(width)
.fill(null)
.map(() => ({ value: " " }))
);

const x = Math.floor((width - treeWidth) / 2);
pasteTree(image, tree, x, 0);

return image;
}

function generateTree(w: number, baubleColors: string[], offset: number): Pixel[][] {
// Ensure odd width
if (w % 2 === 0) {
w -= 1;
}

const tree: Pixel[][] = [];

// Star on top
tree.push(centerPixels(w, pixels(" * ", COLORS.yellow)));

// Tree crown
tree.push(centerPixels(w, pixels(" /_\\ ", COLORS.green)));
tree.push(centerPixels(w, pixels(" /_\\_\\ ", COLORS.green)));

// Tree body
for (let i = 3; i < (w - 2) / 2; i++) {
const left = ` ${"_\\".repeat(i)} `;
const right = ` ${"/_".repeat(i)}\\ `;
tree.push(centerPixels(w, pixels(left, COLORS.green)));
tree.push(centerPixels(w, pixels(right, COLORS.green)));
}

// Add baubles - need to modify tree directly, not slices
for (let rowIdx = 2; rowIdx < tree.length; rowIdx++) {
const row = tree[rowIdx];
const third = Math.floor(w / 3);

const colors = [...baubleColors];
// Rotate colors based on row for variation
for (let r = 0; r < (offset + rowIdx) % 3; r++) {
colors.push(colors.shift()!);
}

// Place baubles in three sections of the row
const sectionRanges = [
{ start: 0, end: third },
{ start: third, end: third * 2 },
{ start: third * 2, end: w },
];

sectionRanges.forEach((range, sectionIdx) => {
// Find all underscore positions in this section
const underscorePositions: number[] = [];
for (let j = range.start; j < range.end && j < row.length; j++) {
if (row[j].value === "_") {
underscorePositions.push(j);
}
}

// Place a bauble at a random underscore position
if (underscorePositions.length > 0) {
const randomIdx = Math.floor(Math.random() * underscorePositions.length);
const pos = underscorePositions[randomIdx];
row[pos] = { value: "*", color: colors[sectionIdx] };
}
});
}

// Add pot
if (w >= 15) {
tree.push(centerPixels(w, pixels(" [_____] ", COLORS.gray)));
tree.push(centerPixels(w, pixels(" \\___/ ", COLORS.gray)));
} else {
const n = w >= 10 ? 3 : 1;
tree.push(centerPixels(w, pixels(` \\${"_".repeat(n)}/ `, COLORS.gray)));
}

return tree;
}

function pixels(str: string, color: string): Pixel[] {
return str.split("").map((c) => ({ value: c, color }));
}

function centerPixels(width: number, pixelArray: Pixel[]): Pixel[] {
const row: Pixel[] = Array(width)
.fill(null)
.map(() => ({ value: " " }));
const start = Math.floor((width - pixelArray.length) / 2);

for (let i = 0; i < pixelArray.length; i++) {
row[start + i] = pixelArray[i];
}

return row;
}

function pasteTree(bg: Pixel[][], fg: Pixel[][], x: number, y: number): void {
const maxY = bg.length - y;

for (let i = 0; i < fg.length; i++) {
const row = fg[fg.length - 1 - i];
for (let j = 0; j < row.length; j++) {
if (row[j].value !== " ") {
const bgRow = maxY - i - 1;
if (bgRow >= 0 && bgRow < bg.length && x + j < bg[0].length) {
bg[bgRow][x + j] = row[j];
}
}
}
}
}

export default AsciiChristmasTree;
Loading
Loading