From d92a0dc412c8e1eceee5b3c4816db0d933d7e9b0 Mon Sep 17 00:00:00 2001 From: root Date: Sat, 4 Jul 2026 14:25:02 +0000 Subject: [PATCH] NEW RELEASE --- .gitignore | 39 ++ .thumbnail | Bin 0 -> 20880 bytes README.md | 288 +++++++++ data.js | 104 ++++ deploy.yml | 23 + index.html | 589 +++++++++++++++++++ script.js | 229 ++++++++ style.css | 462 +++++++++++++++ support.js | 1658 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 3392 insertions(+) create mode 100644 .gitignore create mode 100644 .thumbnail create mode 100644 README.md create mode 100644 data.js create mode 100644 deploy.yml create mode 100644 index.html create mode 100644 script.js create mode 100644 style.css create mode 100644 support.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c71e7ec --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +# --- System / shell files (root home directory ki wajah se) --- +.bash_history +.bashrc +.profile +.lesshst +.wget-hsts +.cloud-locale-test.skip +.Xauthority + +# --- Credentials & secrets (KABHI git mein na jayen) --- +.ssh/ +*.pem +*.key +*.env +.env* + +# --- Caches & local data --- +.cache/ +.local/ +.thumbnail/ + +# --- Editor / IDE files --- +.vscode/ +.idea/ +*.swp +*.swo + +# --- OS files --- +.DS_Store +Thumbs.db + +# --- Node (agar future mein npm use ho) --- +node_modules/ +npm-debug.log* + +# --- Logs & backups --- +*.log +*.bak +*~ diff --git a/.thumbnail b/.thumbnail new file mode 100644 index 0000000000000000000000000000000000000000..8879226d067b94870a7c45e0c6a7cbec55077456 GIT binary patch literal 20880 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 G0002!FYq7$ literal 0 HcmV?d00001 diff --git a/README.md b/README.md new file mode 100644 index 0000000..3e25fe8 --- /dev/null +++ b/README.md @@ -0,0 +1,288 @@ +
+ +# 🌐 Muaz Bhutta β€” Portfolio + +**A blazing-fast, zero-dependency static portfolio with automated CI/CD deployment** + +[![Live Site](https://img.shields.io/badge/Live-muazbhutta.online-2ea44f?style=for-the-badge&logo=googlechrome&logoColor=white)](https://muazbhutta.online) +[![Deploy Status](https://img.shields.io/badge/Deploy-GitHub%20Actions-2088FF?style=for-the-badge&logo=githubactions&logoColor=white)](#-cicd--auto-deploy-on-every-push) +[![License](https://img.shields.io/badge/License-MIT-yellow?style=for-the-badge)](LICENSE) + +![HTML5](https://img.shields.io/badge/HTML5-E34F26?style=flat-square&logo=html5&logoColor=white) +![CSS3](https://img.shields.io/badge/CSS3-1572B6?style=flat-square&logo=css3&logoColor=white) +![JavaScript](https://img.shields.io/badge/JavaScript-F7DF1E?style=flat-square&logo=javascript&logoColor=black) +![Caddy](https://img.shields.io/badge/Caddy-1F88C0?style=flat-square&logo=caddy&logoColor=white) +![GitHub Actions](https://img.shields.io/badge/GitHub_Actions-2088FF?style=flat-square&logo=githubactions&logoColor=white) +![rsync](https://img.shields.io/badge/rsync-CC0000?style=flat-square&logo=gnubash&logoColor=white) + +*No build step. No framework. No node_modules black hole. Just clean HTML/CSS/JS β€” deployed automatically to a self-hosted VPS on every push.* + +[Live Demo](https://muazbhutta.online) Β· [Report Bug](../../issues) Β· [Request Feature](../../issues) + +
+ +--- + +## πŸ“‹ Table of Contents + +- [✨ Features](#-features) +- [πŸ—οΈ Architecture](#️-architecture) +- [πŸ“ File Structure](#-file-structure) +- [✏️ Updating Content](#️-updating-content-the-important-part) +- [πŸš€ Getting Started](#-getting-started) + - [Run Locally](#step-1--run-it-locally) + - [Push to GitHub](#step-2--push-to-github) + - [Manual Deploy](#step-3--manual-deploy-to-the-vps) + - [CI/CD Setup](#step-4--cicd--auto-deploy-on-every-push) +- [πŸ” Deployment Flow](#-deployment-flow) +- [πŸ› οΈ Tech Stack](#️-tech-stack) +- [πŸ“„ License](#-license) +- [πŸ“¬ Contact](#-contact) + +--- + +## ✨ Features + +| Feature | Description | +|---|---| +| ⚑ **Fully Static** | Zero dependencies beyond Google Fonts β€” loads instantly | +| πŸŒ— **Light / Dark / Auto Theme** | Auto mode follows the visitor's system setting and updates **live** if it changes | +| πŸ“œ **Scroll-Reveal Timeline** | Animated career timeline, fully responsive down to mobile | +| πŸ—‚οΈ **Single-File Content** | All content lives in `data.js` β€” edit one file, never touch markup | +| πŸ€– **Auto-Deploy CI/CD** | Every push to `main` deploys to the VPS via GitHub Actions + rsync | +| πŸ”’ **Self-Hosted** | Served by Caddy with automatic HTTPS on a personal VPS | + +--- + +## πŸ—οΈ Architecture + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” git push β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Developer β”‚ ────────────────▢ β”‚ GitHub (main) β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ triggers + β–Ό + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ GitHub Actions β”‚ + β”‚ (deploy.yml) β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ rsync over SSH + β–Ό +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” HTTPS (443) β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Visitor β”‚ ◀──────────────── β”‚ VPS + Caddy β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ auto-SSL πŸ”’ β”‚ /var/www/portfolioβ”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +--- + +## πŸ“ File Structure + +``` +portfolio/ +β”œβ”€β”€ index.html β†’ page structure +β”œβ”€β”€ style.css β†’ design & theme tokens +β”œβ”€β”€ script.js β†’ renders data.js into the page (no need to edit) +β”œβ”€β”€ data.js β†’ ⭐ EDIT THIS FILE to add content +β”œβ”€β”€ .gitignore β†’ keeps secrets & system files out of git +└── .github/ + └── workflows/ + └── deploy.yml β†’ auto-deploy pipeline +``` + +--- + +## ✏️ Updating Content (the important part) + +To add a new project, open **`data.js`** and add an object to the `PROJECTS` array: + +```js +{ + id: "unique-id", + title: "Project Name", + summary: "1–2 line description.", + stack: ["Tech1", "Tech2"], + link: "https://...", // or "#" if there's no link + status: "production", // "production" or "lab" +}, +``` + +> πŸ’‘ **Skills, credentials, About paragraphs, and the career timeline all work the same way** β€” add or edit an object in the matching array. Nothing in `index.html`, `style.css`, or `script.js` ever needs to change. + +--- + +## πŸš€ Getting Started + +### Step 1 β€” Run it locally + +These are static files, so "running" it just means opening it in a browser: + +**Option A (simplest):** +``` +double-click index.html +``` + +**Option B (recommended β€” mirrors production behavior):** +```bash +python3 -m http.server 8000 +``` +Then open **http://localhost:8000**. This matches how Caddy serves the files on the VPS, so local behavior = production behavior. + +--- + +### Step 2 β€” Push to GitHub + +```bash +cd portfolio +git init +git add . +git commit -m "Initial portfolio" +git branch -M main +git remote add origin https://github.com//portfolio.git +git push -u origin main +``` + +Why this matters: +1. πŸ“Œ Your code becomes **version-controlled** β€” every change is tracked +2. πŸ€– It becomes the **trigger source** for the CI/CD pipeline β€” every push to `main` auto-deploys + +--- + +### Step 3 β€” Manual Deploy to the VPS + +*(first time, or without CI/CD)* + +The VPS already runs Caddy serving `muazbhutta.online` β€” just copy files into the directory Caddy points at: + +```bash +# from your local machine +rsync -avz --delete ./portfolio/ user@your-server-ip:/var/www/portfolio/ +``` + +Confirm the Caddyfile block: + +```caddy +muazbhutta.online { + root * /var/www/portfolio + file_server +} +``` + +Then reload: + +```bash +sudo systemctl reload caddy +``` + +βœ… **`https://muazbhutta.online` is now live** β€” Caddy handles SSL automatically. + +> πŸ”€ **Want a subdomain instead?** (e.g. `portfolio.muazbhutta.online`) β€” add a new Caddyfile block with the same directives, using the subdomain as the site address. + +--- + +### Step 4 β€” CI/CD: Auto-Deploy on Every Push + +`.github/workflows/deploy.yml` uses **GitHub Actions** to rsync files to the VPS over SSH on every push to `main`. + +#### πŸ”‘ Required Repository Secrets + +Go to: **Repo β†’ Settings β†’ Secrets and variables β†’ Actions β†’ New repository secret** + +| Secret | Value | +|---|---| +| `DEPLOY_HOST` | Your VPS IP address | +| `DEPLOY_USER` | SSH username on the VPS | +| `DEPLOY_PORT` | `22` (or your custom SSH port) | +| `DEPLOY_PATH` | `/var/www/portfolio/` | +| `DEPLOY_SSH_KEY` | Private SSH deploy key *(see below)* | + +#### πŸ—οΈ Generating a Dedicated Deploy Key *(best practice)* + +```bash +# on your local machine +ssh-keygen -t ed25519 -C "github-deploy" -f ./deploy_key -N "" +``` + +This creates `deploy_key` (private) + `deploy_key.pub` (public). + +Authorize the **public** key on the VPS: + +```bash +ssh-copy-id -i deploy_key.pub user@your-server-ip +``` + +*(or manually append `deploy_key.pub` contents to `~/.ssh/authorized_keys` on the VPS)* + +Then paste the **full contents of `deploy_key`** (the private key) into the `DEPLOY_SSH_KEY` secret on GitHub. + +> ⚠️ **Never commit the private key to the repo.** It lives only in GitHub Secrets. The included `.gitignore` blocks `*.key` and `.ssh/` as a safety net. + +#### πŸ§ͺ Testing the Pipeline + +```bash +# make a small change, e.g. add a project in data.js +git add . +git commit -m "Add new project" +git push +``` + +Watch the **Actions** tab β†’ pipeline runs β†’ within **30–60 seconds** the change is live. No manual rsync. No SSH. ✨ + +--- + +## πŸ” Deployment Flow + +``` +Edit data.js (add a project / skill / timeline entry) + β”‚ + β–Ό +git add . && git commit -m "update" && git push + β”‚ + β–Ό +GitHub Actions triggers automatically + β”‚ + β–Ό +rsync copies files to VPS over SSH + β”‚ + β–Ό +Caddy is already serving β†’ change is LIVE πŸŽ‰ +``` + +**From now on: edit `data.js`, push, done.** Everything else is automatic. + +--- + +## πŸ› οΈ Tech Stack + +| Layer | Technology | Why | +|---|---|---| +| **Frontend** | Vanilla HTML5 / CSS3 / JS | Zero build step, instant loads, easy maintenance | +| **Theming** | CSS custom properties + `prefers-color-scheme` | Native Light/Dark/Auto with live OS sync | +| **Web Server** | [Caddy](https://caddyserver.com/) | Automatic HTTPS, dead-simple config | +| **CI/CD** | GitHub Actions | Free, integrated, zero-maintenance pipeline | +| **Transport** | rsync over SSH | Fast delta transfers β€” only changed files move | +| **Hosting** | Self-hosted VPS | Full control, part of a larger self-hosted infra lab | + +--- + +## πŸ“„ License + +Distributed under the **MIT License**. See `LICENSE` for more information. + +--- + +## πŸ“¬ Contact + +**Muaz Bhutta** β€” Junior Network / Cloud & DevOps Engineer + +[![Website](https://img.shields.io/badge/Website-muazbhutta.online-2ea44f?style=flat-square&logo=googlechrome&logoColor=white)](https://muazbhutta.online) +[![GitHub](https://img.shields.io/badge/GitHub-Follow-181717?style=flat-square&logo=github)](https://github.com/your-username) +[![LinkedIn](https://img.shields.io/badge/LinkedIn-Connect-0A66C2?style=flat-square&logo=linkedin&logoColor=white)](https://linkedin.com/in/your-profile) + +
+ +⭐ **If this repo helped you build your own portfolio pipeline, consider giving it a star!** ⭐ + +*Built with ❀️ and zero frameworks* + +
diff --git a/data.js b/data.js new file mode 100644 index 0000000..4b841e0 --- /dev/null +++ b/data.js @@ -0,0 +1,104 @@ +/* + ============================================================ + DATA FILE β€” this is the only file you should need to touch. + To add a new project, skill, or credential, add a new object + to the relevant array below. The layout/design does not need + to change β€” everything renders automatically from here. + ============================================================ +*/ + +const SITE = { + name: "Muaz Bhutta", + initials: "MB", + role: "Cloud & Network Infrastructure Engineer", + tagline: + "I build infrastructure that runs β€” and I understand exactly why it runs. From routing tables to container orchestration, from VPNs to observability stacks.", + location: "Jeddah / Riyadh, Saudi Arabia", + status: "Open to work", + email: "you@muazbhutta.online", // put your real email here + github: "https://github.com/yourusername", + linkedin: "https://linkedin.com/in/yourusername", + resumeLink: "#", // link to your resume PDF, e.g. /assets/resume.pdf +}; + +// About section β€” short paragraphs, rendered as two columns. +const ABOUT = [ + "I'm a Cloud & Network Infrastructure Engineer based between Jeddah and Riyadh, with a background in Computer Science and hands-on experience across routing, virtualization, and cloud platforms.", + "My foundation is networking β€” CCNA-level routing and switching, VLAN design, and VPN architecture. I extended that into the cloud with AWS and self-managed Linux infrastructure, treating every VPS like a production environment.", + "I learn by building and breaking things on my own servers: VPN dashboards, monitoring stacks, and internal APIs that I design, deploy, and keep running end to end.", + "I'm currently looking for a junior network or cloud engineering role in Saudi Arabia, where I can bring that same hands-on discipline to a team solving real infrastructure problems.", +]; + +// Career path β€” used for the timeline under the About section. +const CAREER_PATH = [ + { step: 1, label: "Multan, Pakistan", detail: "BS in Computer Science" }, + { step: 2, label: "CCNA", detail: "Routing & switching foundations" }, + { step: 3, label: "AWS + Linux", detail: "Cloud platforms & systems administration" }, + { step: 4, label: "Self-hosted labs", detail: "VPNs, monitoring, and APIs β€” running live on a VPS" }, + { step: 5, label: "Jeddah / Riyadh", detail: "Target: Junior Network / Cloud Engineer" }, +]; + +// Skills, grouped by category. +const SKILLS = [ + { group: "Networking", items: ["CCNA (R&S)", "VLANs & Subnetting", "OpenVPN", "WireGuard"] }, + { group: "Cloud", items: ["AWS (EC2, S3, IAM)", "VPS Administration", "Caddy / Nginx", "DNS & Domain Management"] }, + { group: "DevOps", items: ["Docker & Compose", "Prometheus + Grafana", "CI/CD (GitHub Actions)", "Bash Scripting"] }, + { group: "Development", items: ["Linux (Ubuntu/Debian)", "Python + Flask", "REST APIs", "Git / GitHub"] }, +]; + +// Projects. Add a new object here to add a new project card. +// status: "production" or "lab" +const PROJECTS = [ + { + id: "vpn-dashboard", + title: "OpenVPN Management Dashboard", + summary: + "A full VPN control panel built and deployed on my own VPS β€” live client monitoring, connection status, and real-time updates via Server-Sent Events.", + stack: ["Flask", "OpenVPN", "Easy-RSA", "SSE", "Linux"], + link: "#", + status: "production", + }, + { + id: "monitoring-stack", + title: "Prometheus + Grafana Monitoring", + summary: + "A monitoring stack deployed with Docker Compose β€” Node Exporter collects system metrics, visualized on Grafana dashboards in real time.", + stack: ["Docker Compose", "Prometheus", "Grafana", "Node Exporter"], + link: "#", + status: "production", + }, + { + id: "system-api", + title: "Real-Time System Status API", + summary: + "A Flask API that streams live CPU, RAM, and disk metrics via psutil over Server-Sent Events, paired with a dark-themed live dashboard.", + stack: ["Python", "Flask", "psutil", "SSE"], + link: "#", + status: "production", + }, + { + id: "wireguard-lab", + title: "WireGuard VPN Lab", + summary: + "A WireGuard setup with WGDashboard integration, including troubleshooting mobile connectivity issues caused by carrier-level UDP blocking.", + stack: ["WireGuard", "WGDashboard", "Networking"], + link: "#", + status: "lab", + }, + { + id: "this-site", + title: "This Portfolio", + summary: + "A static site that deploys itself to my own VPS through GitHub Actions β€” the CI/CD pipeline is itself part of this project.", + stack: ["HTML/CSS/JS", "GitHub Actions", "Caddy", "VPS"], + link: "https://github.com/yourusername/portfolio", + status: "production", + }, +]; + +// Certifications / education. +const CREDENTIALS = [ + { title: "BS Computer Science", org: "University", year: "" }, + { title: "CCNA (200-301)", org: "Cisco", year: "In progress" }, + { title: "AWS Cloud Practitioner", org: "Amazon Web Services", year: "" }, +]; diff --git a/deploy.yml b/deploy.yml new file mode 100644 index 0000000..699673d --- /dev/null +++ b/deploy.yml @@ -0,0 +1,23 @@ +name: Deploy Portfolio + +on: + push: + branches: [main] + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Deploy via rsync over SSH + uses: burnett01/rsync-deployments@7.0.1 + with: + switches: -avzr --delete --exclude=".git*" --exclude="README.md" + path: ./ + remote_path: ${{ secrets.DEPLOY_PATH }} + remote_host: ${{ secrets.DEPLOY_HOST }} + remote_user: ${{ secrets.DEPLOY_USER }} + remote_key: ${{ secrets.DEPLOY_SSH_KEY }} + remote_port: ${{ secrets.DEPLOY_PORT }} diff --git a/index.html b/index.html new file mode 100644 index 0000000..dbc2ed1 --- /dev/null +++ b/index.html @@ -0,0 +1,589 @@ + + + + + + + + + + + Muaz Bhutta β€” Cloud & Network Infrastructure Engineer + + + + + + +
+ + +
+
+ +
+ + +
+
+ + + +
+ + + +
+ + +
+
+ +
+
+
+ +
+
+

// CLOUD & NETWORK INFRASTRUCTURE

+

Muaz
Bhutta

+

Cloud & Network Infrastructure Engineer

+

From routing tables to cloud pipelines β€” I build infrastructure that runs, and I understand why it runs.

+ +
+ +
+
+ + VPS 152.42.241.74 β€” all systems up +
+
+ + Jeddah / Riyadh, Saudi Arabia +
+
+
+
+ + +
+
+ OSPFβ—† + AWSβ—† + DOCKERβ—† + WIREGUARDβ—† + PROMETHEUSβ—† + GRAFANAβ—† + CI/CDβ—† + LINUXβ—† + CADDYβ—† + VLANβ—† + FLASKβ—† + BASHβ—† + OSPFβ—† + AWSβ—† + DOCKERβ—† + WIREGUARDβ—† + PROMETHEUSβ—† + GRAFANAβ—† + CI/CDβ—† + LINUXβ—† + CADDYβ—† + VLANβ—† + FLASKβ—† + BASHβ—† +
+
+ + +
+
+

$ traceroute muaz --journey

+

The route so far

+ +
+
+ +
+
+
HOP 1
Multan, PK
BS Computer Science
+
+
+
+
HOP 2
CCNA
Routing & Switching foundation
+
+
+
+
HOP 3
AWS + Linux
Cloud & systems skills
+
+
+
+
HOP 4
Self-hosted labs
VPN, monitoring, APIs β€” live on VPS
+
+
+
+
HOP 5
Jeddah / Riyadh
Junior Network / Cloud Engineer β€” target
+
+
+
+
+ + +
+
+

$ show ip interface brief

+

Interfaces β€” all up / up

+ +
+ +
+
Gi0/0 β€” NETWORKING
+
+
CCNA (R&S)
+
OSPF / EIGRP
+
VLAN & Subnetting
+
OpenVPN
+
WireGuard
+
+
+ +
+
Gi0/1 β€” CLOUD
+
+
AWS (EC2, S3, IAM)
+
VPS Administration
+
Caddy / Nginx
+
DNS & Domains
+
+
+ +
+
Gi0/2 β€” DEVOPS
+
+
Docker & Compose
+
Prometheus + Grafana
+
CI/CD (GitHub Actions)
+
Bash scripting
+
+
+ +
+
Gi0/3 β€” DEVELOPMENT
+
+
Linux (Ubuntu/Debian)
+
Python + Flask
+
REST APIs
+
Git / GitHub
+
+
+ +
+
+
+ + +
+
+

$ show ip route β€” selected work

+

Routing table

+

Infrastructure I've designed, deployed, and maintain on my own VPS.

+ +
+ +
+
+ ● LIVE + vpn-dashboard +
+

OpenVPN Management Dashboard

+

Full VPN control panel on my VPS β€” live client monitoring, connection status, and Server-Sent Events for real-time data.

+
+ Flask + OpenVPN + Easy-RSA + SSE + Linux +
+
+ +
+
+ ● LIVE + monitoring-stack +
+

Prometheus + Grafana Monitoring

+

Monitoring stack deployed via Docker Compose β€” Node Exporter collects system metrics, visualized on Grafana dashboards.

+
+ Docker Compose + Prometheus + Grafana + Node Exporter +
+
+ +
+
+ ● LIVE + system-api +
+

Real-time System Status API

+

Flask API streaming live CPU, RAM and disk metrics from psutil over SSE, with a dark-themed dashboard.

+
+ Python + Flask + psutil + SSE +
+
+ +
+
+ ◐ LAB + wireguard-lab +
+

WireGuard VPN Lab

+

WireGuard setup, WGDashboard integration, and troubleshooting mobile connectivity issues (carrier-level UDP blocking).

+
+ WireGuard + WGDashboard + Networking +
+
+ +
+
+ ● LIVE + this-site +
+

This Portfolio

+

Static site that auto-deploys to my own VPS via GitHub Actions β€” the CI/CD pipeline is itself part of the site.

+
+ HTML/CSS/JS + GitHub Actions + Caddy + VPS +
+
+ +
+
+
+ + +
+
+

$ tail -f /var/log/deploy.log

+

Live from the VPS

+ +
+
+ + + + muaz@mj β€” ssh β€” 152.42.241.74 +
+
+
+ +
{{ ln.text }}
+
+ +
+
+
+
+ + +
+
+

$ cat /etc/credentials

+

Education & Certifications

+
+
+
BS Computer Science
University
+ DEGREE +
+
+
CCNA (200-301)
Cisco β€” in progress / planned
+ CERT +
+
+
AWS Cloud Practitioner
Amazon Web Services
+ CERT +
+
+
+
+ + +
+
+

// ESTABLISH CONNECTION

+

Let's connect

+

Open to Junior Network / Cloud Engineer roles in Jeddah & Riyadh.

+ +
+
+ +
+ Β© 2026 Muaz Bhutta +
+
+
+
+ + + diff --git a/script.js b/script.js new file mode 100644 index 0000000..ba1bce5 --- /dev/null +++ b/script.js @@ -0,0 +1,229 @@ +// ============================================================ +// RENDER LOGIC β€” you shouldn't need to touch this file. +// To add or change content, edit data.js instead. +// ============================================================ + +document.addEventListener("DOMContentLoaded", () => { + initThemeToggle(); + renderHero(); + renderAbout(); + renderCareerPath(); + renderSkills(); + renderProjects(); + renderCredentials(); + renderContact(); + renderFooter(); +}); + +// ---------------------------------------------------------------- +// THEME β€” three modes: light, dark, auto (follows system setting) +// ---------------------------------------------------------------- +function initThemeToggle() { + const root = document.documentElement; + const toggle = document.getElementById("theme-toggle"); + const label = document.getElementById("theme-mode-label"); + const media = window.matchMedia("(prefers-color-scheme: light)"); + const modes = ["light", "dark", "auto"]; + + function resolve(mode) { + if (mode === "auto") return media.matches ? "light" : "dark"; + return mode; + } + + function apply(mode) { + root.setAttribute("data-theme-mode", mode); + root.setAttribute("data-theme", resolve(mode)); + if (label) label.textContent = mode.charAt(0).toUpperCase() + mode.slice(1); + localStorage.setItem("themeMode", mode); + } + + let currentMode = localStorage.getItem("themeMode") || "auto"; + apply(currentMode); + + toggle.addEventListener("click", () => { + const next = modes[(modes.indexOf(currentMode) + 1) % modes.length]; + currentMode = next; + apply(currentMode); + }); + + media.addEventListener("change", () => { + if (currentMode === "auto") apply("auto"); + }); +} + +// ---------------------------------------------------------------- +// HERO +// ---------------------------------------------------------------- +function renderHero() { + document.getElementById("hero-title").innerHTML = + `Hi, I'm ${SITE.name}.
${SITE.role}`; + document.getElementById("hero-tagline").textContent = SITE.tagline; + + const cta = document.getElementById("hero-cta"); + cta.appendChild(makeLink("View Projects", "#projects", "btn btn-primary")); + cta.appendChild(makeLink("Get in Touch", "#contact", "btn btn-ghost")); + if (SITE.resumeLink && SITE.resumeLink !== "#") { + cta.appendChild(makeLink("Download Resume", SITE.resumeLink, "btn btn-ghost", true)); + } + + document.getElementById("id-avatar").textContent = SITE.initials; + document.getElementById("id-name").textContent = SITE.name; + document.getElementById("id-role").textContent = SITE.role; + document.getElementById("id-location").textContent = SITE.location; + document.getElementById("id-status").textContent = SITE.status; +} + +function makeLink(text, href, className, external) { + const a = document.createElement("a"); + a.href = href; + a.className = className; + a.textContent = text; + if (external) { + a.target = "_blank"; + a.rel = "noopener"; + } + return a; +} + +// ---------------------------------------------------------------- +// ABOUT +// ---------------------------------------------------------------- +function renderAbout() { + const container = document.getElementById("about-body"); + const mid = Math.ceil(ABOUT.length / 2); + const columns = [ABOUT.slice(0, mid), ABOUT.slice(mid)]; + + columns.forEach((paragraphs) => { + const col = document.createElement("div"); + col.className = "about-col"; + paragraphs.forEach((text) => { + const p = document.createElement("p"); + p.textContent = text; + col.appendChild(p); + }); + container.appendChild(col); + }); +} + +// ---------------------------------------------------------------- +// CAREER PATH (timeline) +// ---------------------------------------------------------------- +function renderCareerPath() { + const track = document.getElementById("path-track"); + CAREER_PATH.forEach((step) => { + const div = document.createElement("div"); + div.className = "path-step"; + div.innerHTML = ` +
${step.label}
+
${step.detail}
+ `; + track.appendChild(div); + }); + + if ("IntersectionObserver" in window) { + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + entry.target.classList.add("in-view"); + observer.unobserve(entry.target); + } + }); + }, + { threshold: 0.3 } + ); + document.querySelectorAll(".path-step").forEach((el) => observer.observe(el)); + } else { + document.querySelectorAll(".path-step").forEach((el) => el.classList.add("in-view")); + } +} + +// ---------------------------------------------------------------- +// SKILLS +// ---------------------------------------------------------------- +function renderSkills() { + const grid = document.getElementById("skills-grid"); + SKILLS.forEach((group) => { + const card = document.createElement("div"); + card.className = "skill-card"; + card.innerHTML = ` +
${group.group}
+ ${group.items.map((item) => `
${item}
`).join("")} + `; + grid.appendChild(card); + }); +} + +// ---------------------------------------------------------------- +// PROJECTS +// ---------------------------------------------------------------- +function renderProjects() { + const list = document.getElementById("proj-list"); + PROJECTS.forEach((p) => { + const card = document.createElement("div"); + card.className = "proj-card"; + card.innerHTML = ` +
+

${p.title}

+ ${p.status} +
+

${p.summary}

+
+ ${p.stack.map((s) => `${s}`).join("")} +
+ ${p.link && p.link !== "#" ? `View project β†’` : ""} + `; + list.appendChild(card); + }); +} + +// ---------------------------------------------------------------- +// CREDENTIALS +// ---------------------------------------------------------------- +function renderCredentials() { + const list = document.getElementById("cred-list"); + CREDENTIALS.forEach((c) => { + const row = document.createElement("div"); + row.className = "cred-row"; + row.innerHTML = ` +
+
${c.title}
+
${c.org}
+
+
${c.year}
+ `; + list.appendChild(row); + }); +} + +// ---------------------------------------------------------------- +// CONTACT +// ---------------------------------------------------------------- +function renderContact() { + document.getElementById("contact-text").textContent = + "Open to job opportunities, collaborations, or just talking networking and cloud β€” reach out any time."; + + const links = document.getElementById("contact-links"); + const items = [ + { label: "Email", href: `mailto:${SITE.email}` }, + { label: "GitHub", href: SITE.github }, + { label: "LinkedIn", href: SITE.linkedin }, + ]; + items.forEach((item, i) => { + const a = document.createElement("a"); + a.className = i === 0 ? "btn btn-primary" : "btn btn-ghost"; + a.href = item.href; + a.target = item.href.startsWith("mailto") ? "_self" : "_blank"; + a.rel = "noopener"; + a.textContent = item.label; + links.appendChild(a); + }); +} + +// ---------------------------------------------------------------- +// FOOTER +// ---------------------------------------------------------------- +function renderFooter() { + document.getElementById("footer-text").textContent = + `Β© ${new Date().getFullYear()} ${SITE.name} β€” built & deployed from scratch.`; +} diff --git a/style.css b/style.css new file mode 100644 index 0000000..1a8a3dd --- /dev/null +++ b/style.css @@ -0,0 +1,462 @@ +/* ============================================================ + THEME TOKENS + [data-theme="dark"] is the default. [data-theme="light"] + overrides the same variable names. The actual color scheme + is always resolved to "light" or "dark" in data-theme; the + user's chosen mode (light / dark / auto) lives separately in + data-theme-mode, used only to drive the toggle icon. + ============================================================ */ +:root, +[data-theme="dark"] { + --bg: #0b0f17; + --panel: #121826; + --panel-2: #182236; + --border: #232e42; + --text: #e9eef5; + --muted: #8b96a8; + --accent: #22b8a3; + --accent-contrast: #04211d; + --accent-soft: rgba(34, 184, 163, 0.12); + --warn: #d1a350; + --warn-soft: rgba(209, 163, 80, 0.12); + --focus: #5b9dff; + --shadow: 0 20px 40px -20px rgba(0, 0, 0, 0.5); +} + +[data-theme="light"] { + --bg: #f7f9fc; + --panel: #ffffff; + --panel-2: #f1f4f9; + --border: #e2e7f0; + --text: #101828; + --muted: #5b6472; + --accent: #0f9488; + --accent-contrast: #ffffff; + --accent-soft: rgba(15, 148, 136, 0.08); + --warn: #9a6b1e; + --warn-soft: rgba(154, 107, 30, 0.1); + --focus: #2563eb; + --shadow: 0 20px 40px -20px rgba(16, 24, 40, 0.15); +} + +:root { + --font-display: "Space Grotesk", sans-serif; + --font-mono: "JetBrains Mono", monospace; + --font-body: "Inter", sans-serif; + --radius: 10px; + --radius-sm: 6px; + --maxw: 1080px; +} + +* { box-sizing: border-box; } + +html { scroll-behavior: smooth; } + +@media (prefers-reduced-motion: reduce) { + html { scroll-behavior: auto; } + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } +} + +body { + margin: 0; + background: var(--bg); + color: var(--text); + font-family: var(--font-body); + line-height: 1.65; + transition: background 0.2s ease, color 0.2s ease; +} + +a { color: inherit; } +img { max-width: 100%; display: block; } +::selection { background: var(--accent); color: var(--accent-contrast); } + +:focus-visible { + outline: 2px solid var(--focus); + outline-offset: 3px; +} + +.wrap { + max-width: var(--maxw); + margin: 0 auto; + padding: 0 24px; +} + +.eyebrow { + font-family: var(--font-mono); + font-size: 12px; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--accent); + margin: 0 0 10px; + font-weight: 500; +} + +.heading-lg { font-family: var(--font-display); font-weight: 600; letter-spacing: -0.01em; margin: 0; } +.heading-md { font-family: var(--font-display); font-weight: 600; letter-spacing: -0.005em; margin: 0; } + +/* ============================================================ + NAV + ============================================================ */ +.site-nav { + position: sticky; + top: 0; + z-index: 50; + backdrop-filter: blur(10px); + background: color-mix(in srgb, var(--bg) 82%, transparent); + border-bottom: 1px solid var(--border); +} +.nav-inner { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px 24px; + gap: 16px; +} +.nav-logo { + font-family: var(--font-display); + font-weight: 600; + font-size: 16px; +} +.nav-logo span { color: var(--accent); } +.nav-links { + display: flex; + gap: 26px; + list-style: none; + margin: 0; padding: 0; + font-size: 13.5px; +} +.nav-links a { + text-decoration: none; + color: var(--muted); + transition: color 0.15s; +} +.nav-links a:hover { color: var(--text); } + +.nav-right { display: flex; align-items: center; gap: 10px; } + +.theme-toggle { + width: 36px; + height: 36px; + border-radius: 50%; + border: 1px solid var(--border); + background: var(--panel); + color: var(--text); + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: border-color 0.15s, transform 0.15s; + flex-shrink: 0; +} +.theme-toggle:hover { border-color: var(--accent); transform: translateY(-1px); } +.theme-toggle svg { width: 17px; height: 17px; } + +/* Icon visibility follows the chosen mode (light / dark / auto), + not the resolved color scheme, so "auto" always shows its own icon. */ +.theme-toggle .icon-sun, +.theme-toggle .icon-moon, +.theme-toggle .icon-auto { display: none; } +[data-theme-mode="light"] .theme-toggle .icon-sun { display: block; } +[data-theme-mode="dark"] .theme-toggle .icon-moon { display: block; } +[data-theme-mode="auto"] .theme-toggle .icon-auto { display: block; } + +.theme-mode-label { + font-family: var(--font-mono); + font-size: 11px; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--muted); + min-width: 34px; +} + +/* ============================================================ + HERO + ============================================================ */ +.hero { + padding: 84px 0 72px; + border-bottom: 1px solid var(--border); +} +.hero-grid { + display: grid; + grid-template-columns: 1.4fr 1fr; + gap: 48px; + align-items: start; +} +.hero h1 { + font-size: clamp(32px, 5vw, 48px); + line-height: 1.12; +} +.hero h1 .accent { color: var(--accent); } +.hero .tagline { + margin-top: 18px; + color: var(--muted); + font-size: 17px; + max-width: 520px; +} +.cta-row { + margin-top: 30px; + display: flex; + gap: 12px; + flex-wrap: wrap; +} +.btn { + font-family: var(--font-body); + font-weight: 600; + font-size: 14px; + padding: 11px 22px; + border-radius: var(--radius-sm); + text-decoration: none; + display: inline-block; + border: 1px solid transparent; + cursor: pointer; + transition: transform 0.15s, border-color 0.15s, background 0.15s, opacity 0.15s; +} +.btn-primary { background: var(--accent); color: var(--accent-contrast); } +.btn-primary:hover { transform: translateY(-1px); opacity: 0.92; } +.btn-ghost { border-color: var(--border); color: var(--text); background: transparent; } +.btn-ghost:hover { border-color: var(--accent); color: var(--accent); } + +/* identity card */ +.id-card { + background: var(--panel); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 26px; + box-shadow: var(--shadow); +} +.id-avatar { + width: 52px; height: 52px; + border-radius: 50%; + background: var(--accent-soft); + color: var(--accent); + border: 1px solid var(--border); + display: flex; align-items: center; justify-content: center; + font-family: var(--font-display); + font-weight: 600; + font-size: 17px; + margin-bottom: 16px; +} +.id-card .id-name { font-family: var(--font-display); font-weight: 600; font-size: 17px; } +.id-card .id-role { color: var(--muted); font-size: 13.5px; margin-top: 3px; } +.id-divider { height: 1px; background: var(--border); margin: 18px 0; } +.id-row { + display: flex; + justify-content: space-between; + gap: 10px; + font-size: 13px; + padding: 7px 0; + color: var(--muted); +} +.id-row span:last-child { color: var(--text); text-align: right; } +.id-status { + margin-top: 16px; + display: flex; + align-items: center; + gap: 8px; + font-size: 12.5px; + color: var(--accent); + background: var(--accent-soft); + border-radius: var(--radius-sm); + padding: 9px 12px; +} +.id-status .dot { width: 7px; height: 7px; border-radius: 50%; background: var(--accent); flex-shrink: 0; } + +/* ============================================================ + SECTION SHELL + ============================================================ */ +.section { padding: 68px 0; border-bottom: 1px solid var(--border); } +.section:last-of-type { border-bottom: none; } +.section-head { margin-bottom: 34px; } +.section-head .heading-md { font-size: 26px; } +.section-head .desc { color: var(--muted); margin-top: 8px; font-size: 15px; max-width: 480px; } + +/* ============================================================ + ABOUT + ============================================================ */ +.about-body { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 40px; +} +.about-col p { color: var(--muted); font-size: 15.5px; margin: 0 0 16px; } +.about-col p:last-child { margin-bottom: 0; } + +/* ============================================================ + PATH (career timeline) + ============================================================ */ +.path-track { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 0; + position: relative; +} +.path-track::before { + content: ""; + position: absolute; + top: 9px; + left: 4%; + right: 4%; + height: 1px; + background: var(--border); +} +.path-step { + position: relative; + padding-top: 30px; + padding-right: 16px; + opacity: 0; + transform: translateY(8px); + transition: opacity 0.5s ease, transform 0.5s ease; +} +.path-step.in-view { opacity: 1; transform: translateY(0); } +.path-step::before { + content: ""; + position: absolute; + top: 0; left: 0; + width: 9px; height: 9px; + border-radius: 50%; + background: var(--bg); + border: 2px solid var(--accent); +} +.path-step .step-label { font-weight: 600; font-size: 14.5px; } +.path-step .step-detail { color: var(--muted); font-size: 13px; margin-top: 4px; line-height: 1.5; } + +/* ============================================================ + SKILLS + ============================================================ */ +.skills-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); + gap: 14px; +} +.skill-card { + background: var(--panel); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 20px; +} +.skill-card .group-title { + font-family: var(--font-mono); + font-size: 12px; + color: var(--muted); + text-transform: uppercase; + letter-spacing: 0.06em; + margin-bottom: 14px; +} +.skill-item { + font-size: 14px; + padding: 6px 0; + color: var(--text); + border-top: 1px solid var(--border); +} +.skill-item:first-of-type { border-top: none; } + +/* ============================================================ + PROJECTS + ============================================================ */ +.proj-list { display: flex; flex-direction: column; gap: 14px; } +.proj-card { + background: var(--panel); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 24px; + transition: border-color 0.15s; +} +.proj-card:hover { border-color: var(--accent); } +.proj-top { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 12px; + flex-wrap: wrap; +} +.proj-top .heading-md { font-size: 17px; } +.status-tag { + font-family: var(--font-mono); + font-size: 11px; + text-transform: uppercase; + padding: 4px 10px; + border-radius: 999px; + white-space: nowrap; +} +.status-tag[data-status="production"] { color: var(--accent); background: var(--accent-soft); } +.status-tag[data-status="lab"] { color: var(--warn); background: var(--warn-soft); } +.proj-card p { color: var(--muted); margin: 10px 0 14px; font-size: 14.5px; } +.stack-row { display: flex; flex-wrap: wrap; gap: 8px; } +.stack-tag { + font-family: var(--font-mono); + font-size: 11px; + color: var(--muted); + background: var(--panel-2); + padding: 4px 9px; + border-radius: 4px; +} +.proj-link { + display: inline-flex; + align-items: center; + gap: 4px; + margin-top: 14px; + font-size: 13px; + font-weight: 600; + color: var(--accent); + text-decoration: none; +} +.proj-link:hover { text-decoration: underline; } + +/* ============================================================ + CREDENTIALS + ============================================================ */ +.cred-list { border-top: 1px solid var(--border); } +.cred-row { + display: flex; + justify-content: space-between; + align-items: baseline; + padding: 16px 0; + border-bottom: 1px solid var(--border); + flex-wrap: wrap; + gap: 6px; +} +.cred-row .title { font-weight: 600; font-size: 15px; } +.cred-row .org { color: var(--muted); font-size: 13.5px; margin-top: 2px; } +.cred-row .year { font-family: var(--font-mono); font-size: 12px; color: var(--muted); } + +/* ============================================================ + CONTACT + ============================================================ */ +.contact-panel { + background: var(--panel); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 44px; + text-align: center; +} +.contact-panel .heading-md { font-size: 25px; margin-bottom: 12px; } +.contact-panel p { color: var(--muted); max-width: 440px; margin: 0 auto 26px; font-size: 15px; } +.contact-links { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; } + +.site-footer { + padding: 30px 0 44px; + text-align: center; + color: var(--muted); + font-size: 13px; +} + +/* ============================================================ + RESPONSIVE + ============================================================ */ +@media (max-width: 860px) { + .hero-grid { grid-template-columns: 1fr; } + .about-body { grid-template-columns: 1fr; } + .path-track { grid-template-columns: repeat(2, 1fr); row-gap: 28px; } + .path-track::before { display: none; } +} +@media (max-width: 640px) { + .nav-links { display: none; } + .theme-mode-label { display: none; } + .hero { padding: 60px 0 48px; } + .section { padding: 48px 0; } + .contact-panel { padding: 30px 22px; } +} diff --git a/support.js b/support.js new file mode 100644 index 0000000..d2f00df --- /dev/null +++ b/support.js @@ -0,0 +1,1658 @@ +// GENERATED from dc-runtime/src/*.ts β€” do not edit. Rebuild with `cd dc-runtime && bun run build`. +"use strict"; +(() => { + var __defProp = Object.defineProperty; + var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; + var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); + + // src/react.ts + function getReact() { + const R = window.React; + if (!R) throw new Error("dc-runtime: window.React is not available yet"); + return R; + } + function getReactDOM() { + const RD = window.ReactDOM; + if (!RD) throw new Error("dc-runtime: window.ReactDOM is not available yet"); + return RD; + } + var h = ((...args) => getReact().createElement( + ...args + )); + + // src/parse.ts + function parseDcDocument(doc) { + const dc = doc.querySelector("x-dc"); + if (!dc) return null; + const scriptEl = doc.querySelector("script[data-dc-script]"); + const { props, preview } = parseDataProps( + scriptEl?.getAttribute("data-props") ?? null + ); + return { + template: dc.innerHTML, + js: scriptEl ? scriptEl.textContent || "" : "", + props, + preview + }; + } + function parseDcText(src) { + const openMatch = /]*)?>/.exec(src); + if (!openMatch) return null; + const close = src.lastIndexOf(""); + if (close === -1 || close < openMatch.index) return null; + const template = src.slice(openMatch.index + openMatch[0].length, close); + const doc = new DOMParser().parseFromString(src, "text/html"); + const scriptEl = doc.querySelector("script[data-dc-script]"); + const { props, preview } = parseDataProps( + scriptEl?.getAttribute("data-props") ?? null + ); + return { + template, + js: scriptEl ? scriptEl.textContent || "" : "", + props, + preview + }; + } + function parseDataProps(raw) { + if (!raw) return { props: null, preview: null }; + let parsed; + try { + parsed = JSON.parse(raw); + } catch { + return { props: null, preview: null }; + } + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { + return { props: null, preview: null }; + } + const obj = parsed; + const preview = obj.$preview && typeof obj.$preview === "object" ? obj.$preview : null; + const rest = {}; + for (const k of Object.keys(obj)) { + if (k[0] !== "$") rest[k] = obj[k]; + } + return { props: Object.keys(rest).length ? rest : null, preview }; + } + function dcNameFromPath(pathname) { + let p = pathname || ""; + try { + p = decodeURIComponent(p); + } catch { + } + const base = p.split("/").pop() || "Root"; + return base.replace(/\.dc\.html$/, "").replace(/\.html?$/, "") || "Root"; + } + + // src/boot.ts + var BASE_CSS = ` + .sc-placeholder{background:color-mix(in srgb,currentColor 8%,transparent); + border:1px solid color-mix(in srgb,currentColor 50%,transparent); + border-radius:2px;box-sizing:border-box;overflow:hidden} + @keyframes sc-shine{0%{background-position:100% 50%}100%{background-position:0% 50%}} + html.sc-dc-streaming .sc-placeholder, + html.sc-dc-streaming .sc-interp.sc-missing{position:relative; + background:color-mix(in srgb,currentColor 5%,transparent); + border-color:transparent} + html.sc-dc-streaming .sc-placeholder::before, + html.sc-dc-streaming .sc-interp.sc-missing::before{content:''; + position:absolute;inset:0;pointer-events:none; + background:linear-gradient(90deg,rgba(217,119,87,0) 25%,rgba(247,225,211,.95) 37%,rgba(217,119,87,0) 63%); + background-size:400% 100%;animation:sc-shine 1.4s ease infinite} + html.sc-dc-streaming .sc-placeholder:nth-child(n+9 of .sc-placeholder)::before, + html.sc-dc-streaming .sc-interp.sc-missing:nth-child(n+9 of .sc-interp.sc-missing)::before{animation:none; + background:color-mix(in srgb,currentColor 8%,transparent)} + .sc-placeholder-error{padding:4px 8px;font:11px/1.4 ui-monospace,monospace; + color:color-mix(in srgb,currentColor 70%,transparent);word-break:break-word} + .sc-interp.sc-missing{display:inline-block;width:2em;height:1em;overflow:hidden; + vertical-align:text-bottom;background:rgba(255,255,255,.3);border:1px solid rgba(0,0,0,.5); + border-radius:2px;box-sizing:border-box;color:transparent; + user-select:none} + .sc-interp.sc-unresolved{font-family:ui-monospace,monospace;font-size:.85em; + color:color-mix(in srgb,currentColor 50%,transparent); + background:color-mix(in srgb,currentColor 10%,transparent);border-radius:3px; + padding:0 3px} + .sc-host.sc-has-error{position:relative} + .sc-logic-error{position:absolute;top:8px;left:8px;z-index:2147483647;max-width:60ch; + padding:6px 10px;background:#b00020;color:#fff;font:12px/1.4 ui-monospace,monospace; + border-radius:4px;white-space:pre-wrap;pointer-events:none} + /* Mirrors PRINT_BASELINE_CSS in apps/web deck-stage-export.ts \u2014 keep both + in sync until dc-runtime regains a build step. */ + @media print { + @page { margin: 0.5cm; } + figure, table { break-inside: avoid; } + #dc-root, #dc-root > .sc-host { height: auto; } + *, *::before, *::after { + print-color-adjust: exact; -webkit-print-color-adjust: exact; + backdrop-filter: none !important; -webkit-backdrop-filter: none !important; + animation-delay: -99s !important; animation-duration: .001s !important; + animation-iteration-count: 1 !important; animation-fill-mode: both !important; + animation-play-state: running !important; transition-duration: 0s !important; + } + } + `; + var FULL_PAGE_CSS = "html,body{height:100%;margin:0}#dc-root,#dc-root>.sc-host{height:100%}"; + function rootNameForDocument(doc, loc) { + let bootPath = loc.pathname || ""; + if (!/\.dc\.html?$/i.test(safeDecode(bootPath))) { + try { + bootPath = new URL(doc.baseURI || "/").pathname; + } catch { + } + } + return dcNameFromPath(bootPath); + } + function safeDecode(s) { + try { + return decodeURIComponent(s); + } catch { + return s; + } + } + function boot(runtime, doc = document) { + const parsed = parseDcDocument(doc); + if (!parsed) return null; + const React = getReact(); + const rootName = rootNameForDocument(doc, location); + runtime.markFetched(rootName); + runtime.setRootName(rootName); + runtime.adoptParsed(rootName, parsed); + fetch(location.href).then((res) => res.ok ? res.text() : "").then((t) => { + const raw = t ? parseDcText(t) : null; + if (raw?.template) runtime.updateHtml(rootName, raw.template); + }).catch(() => { + }); + const dc = doc.querySelector("x-dc"); + const hostEl = doc.createElement("div"); + hostEl.id = "dc-root"; + dc.replaceWith(hostEl); + if (!parsed.preview) { + const s = doc.createElement("style"); + s.textContent = FULL_PAGE_CSS; + doc.head.appendChild(s); + } + const Root = runtime.getDC(rootName); + const entry = runtime.registry.get(rootName); + function StandaloneRoot() { + const [, setTick] = React.useState(0); + React.useEffect(() => { + const sub = () => setTick((n) => n + 1); + entry.subs.add(sub); + return () => { + entry.subs.delete(sub); + }; + }, []); + const defaults = React.useMemo(() => { + const d = {}; + for (const k in entry.propsMeta || {}) { + const v = entry.propsMeta?.[k]?.default; + if (v !== void 0) d[k] = v; + } + return d; + }, [entry.propsMeta]); + return h(Root, { ...defaults, ...entry.propOverrides || {} }); + } + const ReactDOM = getReactDOM(); + if (ReactDOM.createRoot) + ReactDOM.createRoot(hostEl).render(h(StandaloneRoot)); + else ReactDOM.render(h(StandaloneRoot), hostEl); + return rootName; + } + + // src/expr.ts + var IDENT_RE = /^[A-Za-z_$][A-Za-z0-9_$]*/; + var NUMBER_RE = /^-?\d+(\.\d+)?$/; + function resolve(vals, src) { + const expr = String(src).trim(); + if (!expr) return void 0; + if (expr[0] === "(" && expr[expr.length - 1] === ")" && parensWrapWhole(expr)) { + return resolve(vals, expr.slice(1, -1)); + } + const eq = findTopLevelEquality(expr); + if (eq) { + const lv = resolve(vals, expr.slice(0, eq.index)); + const rv = resolve(vals, expr.slice(eq.index + eq.op.length)); + switch (eq.op) { + case "===": + return lv === rv; + case "!==": + return lv !== rv; + case "==": + return lv == rv; + default: + return lv != rv; + } + } + if (expr[0] === "!") return !resolve(vals, expr.slice(1)); + if (expr === "true") return true; + if (expr === "false") return false; + if (expr === "null") return null; + if (expr === "undefined") return void 0; + if (NUMBER_RE.test(expr)) return Number(expr); + if (expr.length >= 2 && (expr[0] === '"' || expr[0] === "'") && expr[expr.length - 1] === expr[0]) { + return expr.slice(1, -1); + } + return resolvePath(vals, expr); + } + function parensWrapWhole(expr) { + let depth = 0; + for (let i = 0; i < expr.length - 1; i++) { + if (expr[i] === "(") depth++; + else if (expr[i] === ")") { + depth--; + if (depth === 0) return false; + } + } + return true; + } + function findTopLevelEquality(expr) { + let depth = 0; + for (let i = 0; i < expr.length; i++) { + const c = expr[i]; + if (c === "[" || c === "(") depth++; + else if (c === "]" || c === ")") depth--; + else if (depth === 0 && (c === "=" || c === "!") && expr[i + 1] === "=") { + if (i > 0 && (expr[i - 1] === "=" || expr[i - 1] === "!")) continue; + if (!expr.slice(0, i).trim()) continue; + const op = expr[i + 2] === "=" ? c + "==" : c + "="; + return { index: i, op }; + } + } + return null; + } + function resolvePath(vals, expr) { + const head = expr.match(IDENT_RE); + if (!head) return void 0; + let cur = vals == null ? void 0 : vals[head[0]]; + let i = head[0].length; + while (i < expr.length) { + if (expr[i] === ".") { + const m = expr.slice(i + 1).match(IDENT_RE) || expr.slice(i + 1).match(/^\d+/); + if (!m) return void 0; + cur = cur == null ? void 0 : cur[m[0]]; + i += 1 + m[0].length; + } else if (expr[i] === "[") { + let depth = 1; + let j = i + 1; + while (j < expr.length && depth > 0) { + if (expr[j] === "[") depth++; + else if (expr[j] === "]") { + depth--; + if (depth === 0) break; + } + j++; + } + if (depth !== 0) return void 0; + const key = resolve(vals, expr.slice(i + 1, j)); + cur = cur == null ? void 0 : cur[key]; + i = j + 1; + } else { + return void 0; + } + } + return cur; + } + + // src/encode.ts + var CAMEL_ATTR = "sc-camel-"; + var INLINE_TEXT_TAGS = new Set( + "a abbr b bdi bdo br cite code del dfn em i ins kbd mark q s samp small span strike strong sub sup u var wbr".split( + " " + ) + ); + var RAW_WRAP = { + select: "sc-raw-select", + table: "sc-raw-table", + tbody: "sc-raw-tbody", + thead: "sc-raw-thead", + tfoot: "sc-raw-tfoot", + tr: "sc-raw-tr", + td: "sc-raw-td", + th: "sc-raw-th", + caption: "sc-raw-caption" + }; + var RAW_UNWRAP = Object.fromEntries( + Object.entries(RAW_WRAP).map(([k, v]) => [v, k]) + ); + var EVENT_MAP = { + onclick: "onClick", + onchange: "onChange", + oninput: "onInput", + onsubmit: "onSubmit", + onkeydown: "onKeyDown", + onkeyup: "onKeyUp", + onkeypress: "onKeyPress", + onmousedown: "onMouseDown", + onmouseup: "onMouseUp", + onmouseenter: "onMouseEnter", + onmouseleave: "onMouseLeave", + onfocus: "onFocus", + onblur: "onBlur", + ondoubleclick: "onDoubleClick", + oncontextmenu: "onContextMenu" + }; + var ATTRS = `(?:[^>"']|"[^"]*"|'[^']*')*`; + var IMPORT_SELF_CLOSE_RE = new RegExp( + "<(x-import|dc-import)(" + ATTRS + ")/>", + "gi" + ); + var CAMEL_ATTR_RE = /(\s)([a-z]+[A-Z][A-Za-z0-9]*)(\s*=)/g; + function encodeCase(html) { + html = html.replace( + IMPORT_SELF_CLOSE_RE, + (_, t, a) => "<" + t + a + ">" + ); + html = html.replace(/)/gi, "/gi, ""); + html = html.replace( + CAMEL_ATTR_RE, + (_, sp, name, eq) => sp + CAMEL_ATTR + name.replace(/[A-Z]/g, (c) => "-" + c.toLowerCase()) + eq + ); + for (const [real, alias] of Object.entries(RAW_WRAP)) { + html = html.replace( + new RegExp("(])", "gi"), + "$1" + alias + ); + } + return html; + } + function kebabToCamel(s) { + return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase()); + } + function cssToObj(css) { + const o = {}; + for (const decl of css.split(";")) { + const i = decl.indexOf(":"); + if (i < 0) continue; + const prop = decl.slice(0, i).trim(); + o[prop.startsWith("--") ? prop : kebabToCamel(prop)] = decl.slice(i + 1).trim(); + } + return o; + } + function compileAttr(raw) { + const whole = raw.match(/^\s*\{\{([\s\S]+?)\}\}\s*$/); + if (whole) { + const path = whole[1]; + return (vals) => resolve(vals, path); + } + if (raw.includes("{{")) { + const parts = raw.split(/\{\{([\s\S]+?)\}\}/g); + return (vals) => parts.map((s, i) => i & 1 ? resolve(vals, s) ?? "" : s).join(""); + } + return () => raw; + } + + // src/compile.ts + function collectProps(node, kind, host) { + const propGetters = []; + const pseudoClasses = []; + let hintSize = null; + for (const { name, value } of [...node.attributes]) { + if (name === "sc-name" || name === "data-dc-tpl") continue; + let key = name; + if (key.startsWith(CAMEL_ATTR)) + key = kebabToCamel(key.slice(CAMEL_ATTR.length)); + if (key === "hint-size") { + hintSize = value; + continue; + } + if (key.startsWith("style-")) { + pseudoClasses.push(host.pseudoClass(key.slice(6), value)); + continue; + } + if (kind !== "dom") { + if (key.includes("-") && !(kind === "x-import" && (key.startsWith("aria-") || key.startsWith("data-")))) + key = kebabToCamel(key); + } else { + if (key === "class") key = "className"; + else if (key === "for") key = "htmlFor"; + else if (key.startsWith("on")) + key = EVENT_MAP[key] || "on" + key[2].toUpperCase() + key.slice(3); + } + propGetters.push([key, compileAttr(value)]); + } + return { propGetters, pseudoClasses, hintSize }; + } + var HOST_STYLE_PROPS = /* @__PURE__ */ new Set([ + "position", + "left", + "right", + "top", + "bottom", + "inset", + "width", + "height", + "z-index", + "transform" + ]); + function hostPositionStyle(style) { + const all = typeof style === "string" ? cssToObj(style) : style != null && typeof style === "object" ? style : null; + if (!all) return void 0; + const out = {}; + for (const [k, v] of Object.entries(all)) { + const kebab = k.replace(/[A-Z]/g, (c) => "-" + c.toLowerCase()); + if (HOST_STYLE_PROPS.has(kebab)) out[k] = v; + } + return Object.keys(out).length ? out : void 0; + } + function compileTemplate(html, host) { + const tpl = document.createElement("template"); + //! nosemgrep: direct-inner-html-assignment + tpl.innerHTML = encodeCase(html); + let tplN = 0; + (function stamp(node) { + if (node.nodeType === Node.ELEMENT_NODE) { + node.setAttribute("data-dc-tpl", String(tplN++)); + } + for (const c of node.childNodes) stamp(c); + })(tpl.content); + const builders = walkChildren(tpl.content, host); + const render = ((vals, ctx) => builders.map((b, i) => b(vals || {}, ctx, i))); + render.__annotated = tpl.innerHTML; + return render; + } + function walkChildren(node, host) { + return [...node.childNodes].map((c) => walk(c, host)).filter((b) => b != null); + } + function walk(node, host) { + if (node.nodeType === Node.TEXT_NODE) return walkText(node); + if (node.nodeType !== Node.ELEMENT_NODE) return null; + const el = node; + const tag = el.tagName.toLowerCase(); + if (tag === "sc-for") return walkFor(el, host); + if (tag === "sc-if") return walkIf(el, host); + if (tag === "x-import") return walkXImport(el, host); + if (tag === "sc-helmet") return host.helmet(el); + if (tag === "dc-import") return walkComponent(el, host); + return walkElement(el, host); + } + var warnedHoles = /* @__PURE__ */ new Set(); + function warnUnresolved(ctx, what) { + const key = (ctx?.__name || "?") + "\0" + what; + if (warnedHoles.has(key)) return; + warnedHoles.add(key); + console.warn("[dc-runtime] " + (ctx?.__name || "template") + ": " + what); + } + function walkText(node) { + const txt = node.nodeValue ?? ""; + if (!txt.includes("{{")) { + if (!txt.trim() && !txt.includes(" ")) return null; + return () => txt; + } + const parts = txt.split(/\{\{([\s\S]+?)\}\}/g); + return (vals, ctx, key) => h( + getReact().Fragment, + { key }, + ...parts.map((p, i) => { + if (!(i & 1)) return p; + const v = resolve(vals, p); + if (v === void 0) { + if (!ctx?.__streamingNow) { + if (document.body?.hasAttribute("data-dc-editor-on")) { + return h( + "span", + { key: i, className: "sc-interp sc-unresolved" }, + "{{ " + p.trim() + " }}" + ); + } + warnUnresolved( + ctx, + "{{ " + p.trim() + " }} never resolved \u2014 rendered as empty" + ); + return null; + } + return h( + "span", + { key: i, className: "sc-interp sc-missing" }, + p.trim() + ); + } + if (getReact().isValidElement(v) || Array.isArray(v)) { + return h(getReact().Fragment, { key: i }, v); + } + if (v === null || typeof v === "boolean") return null; + return h("span", { key: i, className: "sc-interp" }, String(v)); + }) + ); + } + function walkFor(el, host) { + const listGet = compileAttr(el.getAttribute("list") || ""); + const asName = el.getAttribute("as") || "item"; + const hintN = parseInt(el.getAttribute("hint-placeholder-count") || "0", 10); + const kids = walkChildren(el, host); + const listSrc = el.getAttribute("list") || ""; + return (vals, ctx, key) => { + let list = listGet(vals); + if (!Array.isArray(list)) { + if (!ctx?.__streamingNow) { + if (list !== void 0 && list !== null) { + warnUnresolved( + ctx, + 'sc-for list="' + listSrc + '" is not an array (' + typeof list + ")" + ); + } + list = []; + } else { + list = hintN > 0 ? Array(hintN).fill(void 0) : []; + } + } + return h( + getReact().Fragment, + { key }, + list.map((item, i) => { + const sub = { ...vals, [asName]: item, $index: i }; + return h( + getReact().Fragment, + { key: i }, + kids.map((b, j) => b(sub, ctx, j)) + ); + }) + ); + }; + } + function walkIf(el, host) { + const valGet = compileAttr(el.getAttribute("value") || ""); + const hintRaw = el.getAttribute("hint-placeholder-val"); + const hintGet = hintRaw != null ? compileAttr(hintRaw) : null; + const kids = walkChildren(el, host); + return (vals, ctx, key) => { + let v = valGet(vals); + if (v === void 0 && hintGet && ctx?.__streamingNow) v = hintGet(vals); + return v ? h( + getReact().Fragment, + { key }, + kids.map((b, j) => b(vals, ctx, j)) + ) : null; + }; + } + function walkComponent(el, host) { + const name = el.getAttribute("name") || el.getAttribute("component") || ""; + el.removeAttribute("name"); + el.removeAttribute("component"); + const tplId = el.getAttribute("data-dc-tpl"); + const styleRaw = el.getAttribute("style"); + el.removeAttribute("style"); + const styleGet = styleRaw != null ? compileAttr(styleRaw) : null; + const { propGetters, hintSize } = collectProps(el, "dc-import", host); + const kids = walkChildren(el, host); + return (vals, ctx, key) => { + const props = { + key, + __hintSize: hintSize, + __tplId: tplId, + __hostStyle: styleGet ? hostPositionStyle(styleGet(vals)) : void 0 + }; + for (const [k, g] of propGetters) { + const v = g(vals); + if (k === "dcProps") { + if (v && typeof v === "object") Object.assign(props, v); + continue; + } + props[k] = v; + } + if (kids.length) props.children = kids.map((b, j) => b(vals, ctx, j)); + return h(host.component(name), props); + }; + } + function walkXImport(el, host) { + const globalNameGet = compileAttr( + el.getAttribute("component-from-global-scope") || "" + ); + const exportNameGet = compileAttr( + el.getAttribute("component") || el.getAttribute("name") || "" + ); + const fromRaw = el.getAttribute("from") || el.getAttribute("src") || el.getAttribute("import") || ""; + const urls = fromRaw.trim() ? fromRaw.trim().split(/\s+/) : []; + const url = urls.length ? urls[urls.length - 1] : ""; + const kindOf = (u) => /\.(jsx|tsx)(\?|#|$)/i.test(u) ? "jsx" : "js"; + const tplId = el.getAttribute("data-dc-tpl"); + const styleRaw = el.getAttribute("style"); + el.removeAttribute("style"); + const styleGet = styleRaw != null ? compileAttr(styleRaw) : null; + const wrap = tplId != null || styleGet != null; + const { propGetters, hintSize } = collectProps(el, "x-import", host); + const hasContent = el.children.length > 0 || !!(el.textContent || "").trim(); + const kids = hasContent ? walkChildren(el, host) : []; + const urlBindable = fromRaw.includes("{{"); + if (urls.length && !urlBindable) { + let prev; + for (const u of urls) prev = host.loadExternal(kindOf(u), u, prev); + } + const evalName = (g, vals) => { + const v = g(vals); + const s = v == null ? "" : String(v); + return s.includes("{{") ? "" : s; + }; + return (vals, ctx, key) => { + const globalName = evalName(globalNameGet, vals); + const name = globalName || evalName(exportNameGet, vals); + const C = !name || urlBindable ? null : globalName ? host.resolveExternalGlobal(url, globalName) : host.resolveExternal(url, name); + const hostStyle = styleGet ? hostPositionStyle(styleGet(vals)) : void 0; + const wrapper = wrap ? { + key, + className: "sc-host-x", + "data-dc-tpl": tplId, + style: hostStyle || { display: "contents" } + } : null; + if (!C) { + const error = urlBindable ? "x-import `from` cannot contain {{ \u2026 }} \u2014 module URLs are resolved at parse time; use a literal URL" : host.resolveExternalError(url, name); + const ph = host.placeholder({ + key: wrapper ? void 0 : key, + name, + hintSize, + error + }); + return wrapper ? h("div", wrapper, ph) : ph; + } + const props = wrapper ? {} : { key }; + let unresolvedHole = false; + for (const [k, g] of propGetters) { + if (k === "component" || k === "componentFromGlobalScope" || k === "from") { + continue; + } + const v = g(vals); + if (v === void 0) unresolvedHole = true; + if (k === "dcProps") { + if (v && typeof v === "object") Object.assign(props, v); + continue; + } + props[k] = v; + } + if (unresolvedHole && ctx?.__htmlStreamingNow) { + const ph = host.placeholder({ + key: wrapper ? void 0 : key, + name, + hintSize, + error: null + }); + return wrapper ? h("div", wrapper, ph) : ph; + } + if (kids.length) props.children = kids.map((b, j) => b(vals, ctx, j)); + return wrapper ? h("div", wrapper, h(C, props)) : h(C, props); + }; + } + function contentKey(el) { + const clone = el.cloneNode(true); + for (const d of clone.querySelectorAll("*")) { + while (d.attributes.length) d.removeAttribute(d.attributes[0].name); + } + const s = clone.innerHTML; + let h2 = 5381; + for (let i = 0; i < s.length; i++) h2 = (h2 << 5) + h2 + s.charCodeAt(i) | 0; + return s.length + "." + (h2 >>> 0).toString(36); + } + var NEVER_CONTENT_KEYED = new Set( + "script style textarea option title select canvas iframe video audio".split( + " " + ) + ); + var NOT_INLINE_SELECTOR = ":not(" + [...INLINE_TEXT_TAGS].join(",") + ")"; + function walkElement(el, host) { + const realTag = RAW_UNWRAP[el.localName] || el.localName; + const tplId = el.getAttribute("data-dc-tpl"); + const inlineOnly = el.childNodes.length > 0 && !NEVER_CONTENT_KEYED.has(realTag) && el.querySelector(NOT_INLINE_SELECTOR) === null; + const keySuffix = inlineOnly ? "|" + contentKey(el) : ""; + const { propGetters, pseudoClasses } = collectProps(el, "dom", host); + const kids = walkChildren(el, host); + return (vals, ctx, key) => { + const props = { + key: key + keySuffix, + "data-dc-tpl": tplId + }; + for (const [k, g] of propGetters) { + let v = g(vals); + if (k === "style" && typeof v === "string") v = cssToObj(v); + if ((k === "value" || k === "checked") && v === void 0) { + v = k === "checked" ? false : ""; + } + props[k] = v; + } + if (pseudoClasses.length) { + props.className = [props.className, ...pseudoClasses].filter(Boolean).join(" "); + } + return h(realTag, props, ...kids.map((b, j) => b(vals, ctx, j))); + }; + } + + // src/logic.ts + var StreamableLogic = class { + constructor(props) { + __publicField(this, "props"); + __publicField(this, "state", {}); + /** Back-pointer to the wrapper component, installed after construction. */ + __publicField(this, "__host"); + this.props = props || {}; + } + setState(update, cb) { + this.__host && this.__host.__setLogicState(update, cb); + } + forceUpdate() { + this.__host && this.__host.forceUpdate(); + } + componentDidMount() { + } + componentDidUpdate(_prevProps) { + } + componentWillUnmount() { + } + /** The flat object the template renders against (merged over props). */ + renderVals() { + return {}; + } + }; + function evalDcLogic(src) { + //! nosemgrep: eval-and-function-constructor + const fn = new Function( + "DCLogic", + "StreamableLogic", + "React", + src + '\n;return (typeof Component!=="undefined"&&Component)||undefined;' + ); + return fn(StreamableLogic, StreamableLogic, getReact()); + } + + // src/component.ts + function shallowEqual(a, b) { + if (!b) return false; + const ak = Object.keys(a).filter((k) => k !== "children"); + const bk = Object.keys(b).filter((k) => k !== "children"); + if (ak.length !== bk.length) return false; + for (const k of ak) if (a[k] !== b[k]) return false; + return true; + } + function Placeholder({ + name, + hintSize, + streaming, + error + }) { + const [w, hgt] = (hintSize || "100%,60px").split(","); + return h( + "div", + { + className: "sc-placeholder" + (streaming ? " sc-streaming" : ""), + style: { width: w.trim(), height: hgt && hgt.trim() }, + title: name + }, + error ? h( + "div", + { className: "sc-placeholder-error" }, + (name ? name + ": " : "") + error + ) : null + ); + } + function hintToMin(hint) { + if (!hint) return void 0; + const [w, hgt] = hint.split(","); + return { minWidth: w.trim(), minHeight: hgt && hgt.trim() }; + } + function createComponentFactory(registry, ensureFetched) { + const React = getReact(); + const AncestorContext = React.createContext([]); + class StreamableComponent extends React.Component { + constructor(props) { + super(props); + __publicField(this, "__name"); + __publicField(this, "__sub"); + __publicField(this, "__needsDidMount", false); + /** Snapshot of the registry's streaming flags taken at render time β€” + * builders read it off the RenderCtx (this) to pick placeholder vs + * render-nothing for unresolved values. */ + __publicField(this, "__streamingNow", false); + __publicField(this, "__htmlStreamingNow", false); + /** When a construct throws, remember the (class, registry.ver, props) + * triple so render-time reconcile doesn't re-attempt it on every parent + * re-render. A registry bump (new class, template, external module + * resolving via bumpAll) changes `ver` and breaks the memo so an + * env-dependent constructor can self-heal. */ + __publicField(this, "__failedLogic", null); + __publicField(this, "__failedUserProps", null); + __publicField(this, "__failedVer", -1); + /** Per-instance constructor error β€” kept here (not on the registry entry) + * so one instance's successful construct can't hide a sibling's failure, + * and a construct can never wipe an eval error `updateJs` recorded on + * `r.logicError`. */ + __publicField(this, "__ctorError", null); + __publicField(this, "logic"); + this.__name = props.__name; + this.state = { __v: 0, __err: null }; + this.__sub = () => { + if (this.state.__err) this.setState({ __err: null }); + this.forceUpdate(); + }; + this.__makeLogic(registry.get(this.__name).Logic, null); + ensureFetched(this.__name); + } + /** Error-boundary hook: a render crash anywhere in this DC's subtree + * (its own template, an x-import'd component, a child DC without its + * own deeper boundary) lands here instead of unmounting the page. */ + static getDerivedStateFromError(e) { + return { __err: e instanceof Error && e.message ? e.message : String(e) }; + } + componentDidCatch(e, info) { + console.error( + "[dc-runtime] render error in <" + this.__name + ">:", + e, + info?.componentStack || "" + ); + } + /** Instantiate the logic class (or the no-op base) and adopt `prevState` + * over its initial state β€” used both at mount and on hot-swap. */ + __makeLogic(Logic, prevState) { + const L = Logic || StreamableLogic; + try { + this.logic = new L(this.__userProps()); + this.__failedLogic = null; + this.__failedUserProps = null; + this.__ctorError = null; + } catch (e) { + console.error(e); + this.__failedLogic = Logic; + this.__failedUserProps = this.__userProps(); + this.__failedVer = registry.get(this.__name).ver; + this.__ctorError = this.__name + ": " + (e instanceof Error && e.message ? e.message : String(e)); + this.logic = new StreamableLogic( + this.__userProps() + ); + } + this.logic.__host = this; + if (prevState) + this.logic.state = { ...this.logic.state || {}, ...prevState }; + } + /** The props the author's logic + template see β€” internal __-prefixed + * wiring stripped. */ + __userProps() { + const { __name, __hintSize, __tplId, __hostStyle, ...rest } = this.props; + return rest; + } + __setLogicState(update, cb) { + const prev = this.logic.state; + const patch = typeof update === "function" ? update(prev) : update; + this.logic.state = { ...prev, ...patch }; + this.setState((s) => ({ __v: s.__v + 1 }), cb); + } + /** Swap the logic instance when the registry's Logic class changed + * (streaming completion, hot reload). State carries over; didMount + * re-fires after the swap commits so refs exist. */ + __reconcileLogic() { + const r = registry.get(this.__name); + const Next = r.Logic; + const Cur = this.logic.constructor; + if (Next === Cur || !Next && Cur === StreamableLogic || Next === this.__failedLogic && r.ver === this.__failedVer && shallowEqual(this.__userProps(), this.__failedUserProps)) { + return; + } + if (!this.__needsDidMount) { + try { + this.logic.componentWillUnmount(); + } catch (e) { + console.error(e); + } + } + this.__makeLogic(Next, this.logic.state); + this.__needsDidMount = true; + } + componentDidMount() { + registry.get(this.__name).subs.add(this.__sub); + try { + this.logic.componentDidMount(); + } catch (e) { + console.error(e); + } + } + componentDidUpdate(prevProps) { + this.logic.props = this.__userProps(); + if (this.__needsDidMount) { + if (this.state.__err || !registry.get(this.__name).tpl) return; + this.__needsDidMount = false; + try { + this.logic.componentDidMount(); + } catch (e) { + console.error(e); + } + } else { + try { + this.logic.componentDidUpdate(prevProps); + } catch (e) { + console.error(e); + } + } + } + componentWillUnmount() { + registry.get(this.__name).subs.delete(this.__sub); + if (!this.__needsDidMount) { + try { + this.logic.componentWillUnmount(); + } catch (e) { + console.error(e); + } + } + } + render() { + const r = registry.get(this.__name); + const cls = "sc-host" + (r.htmlStreaming ? " sc-streaming-html" : "") + (r.jsStreaming ? " sc-streaming-js" : ""); + const hintStyle = r.htmlStreaming ? hintToMin(this.props.__hintSize) : void 0; + const hostStyle = this.props.__hostStyle || hintStyle ? { ...hintStyle || {}, ...this.props.__hostStyle || {} } : void 0; + const hostBase = { + className: cls, + style: hostStyle, + "data-sc-name": this.__name, + "data-dc-tpl": this.props.__tplId + }; + const chain = Array.isArray(this.context) ? this.context : []; + if (chain.includes(this.__name)) { + const cycle = [ + ...chain.slice(chain.indexOf(this.__name)), + this.__name + ].join(" \u2192 "); + return h( + "div", + { ...hostBase, className: cls + " sc-has-error" }, + h(Placeholder, { + name: this.__name, + hintSize: this.props.__hintSize, + error: "circular import: " + cycle + }) + ); + } + if (this.state.__err) { + return h( + "div", + { ...hostBase, className: cls + " sc-has-error" }, + h( + "div", + { className: "sc-logic-error", "data-omelette-chrome": "" }, + this.__name + ": " + this.state.__err + ), + h(Placeholder, { + name: this.__name, + hintSize: this.props.__hintSize, + error: this.state.__err + }) + ); + } + this.__reconcileLogic(); + if (!r.tpl) { + return h( + "div", + hostBase, + h(Placeholder, { name: this.__name, hintSize: this.props.__hintSize }) + ); + } + const userProps = this.__userProps(); + this.logic.props = userProps; + let vals = userProps; + let renderErr = r.logicError || this.__ctorError; + try { + vals = { ...userProps, ...this.logic.renderVals() || {} }; + } catch (e) { + console.error(e); + renderErr = this.__name + ".renderVals(): " + (e instanceof Error && e.message ? e.message : String(e)); + } + this.__streamingNow = !!(r.htmlStreaming || r.jsStreaming); + this.__htmlStreamingNow = !!r.htmlStreaming; + return h( + "div", + { ...hostBase, className: cls + (renderErr ? " sc-has-error" : "") }, + renderErr && h( + "div", + { className: "sc-logic-error", "data-omelette-chrome": "" }, + renderErr + ), + h( + AncestorContext.Provider, + { value: [...chain, this.__name] }, + r.tpl(vals, this) + ) + ); + } + } + __publicField(StreamableComponent, "contextType", AncestorContext); + const named = /* @__PURE__ */ new Map(); + function getDC(name) { + const hit = named.get(name); + if (hit) return hit; + function Dispatcher(p) { + const [, setTick] = React.useState(0); + React.useEffect(() => { + const sub = () => setTick((n) => n + 1); + registry.get(name).subs.add(sub); + return () => { + registry.get(name).subs.delete(sub); + }; + }, []); + ensureFetched(name); + return h(StreamableComponent, { ...p, __name: name }); + } + Dispatcher.displayName = name; + named.set(name, Dispatcher); + return Dispatcher; + } + return { + getDC, + StreamableComponent + }; + } + + // src/external.ts + var isCustomElementName = (n) => !n.includes(".") && n.includes("-"); + function isRenderableType(g) { + if (typeof g === "function") return !isElementClass(g); + return typeof g === "object" && g !== null && typeof g.$$typeof === "symbol"; + } + function resolveDottedPath(root, name) { + let cur = root; + for (const seg of name.split(".")) { + if (cur == null) return void 0; + cur = cur[seg]; + } + return cur; + } + var BABEL_URL = "https://unpkg.com/@babel/standalone@7.29.0/babel.min.js"; + var BABEL_SRI = "sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y"; + var GLOBAL_POLL_INTERVAL_MS = 50; + var GLOBAL_POLL_TIMEOUT_MS = 3e4; + function createExternalModules(onResolved) { + const cache = /* @__PURE__ */ new Map(); + let babelLoading = null; + const reportedMissing = /* @__PURE__ */ new Map(); + const polling = /* @__PURE__ */ new Set(); + function ensureBabel() { + if (window.Babel) return Promise.resolve(); + if (babelLoading) return babelLoading; + babelLoading = new Promise((res, rej) => { + const s = document.createElement("script"); + s.src = BABEL_URL; + s.integrity = BABEL_SRI; + s.crossOrigin = "anonymous"; + s.onload = () => res(); + s.onerror = rej; + document.head.appendChild(s); + }); + return babelLoading; + } + const pending = /* @__PURE__ */ new Map(); + function load(kind, url, after) { + const existing = pending.get(url); + if (existing) return existing; + cache.set(url, null); + console.info("[dc-runtime] x-import: loading", url, "(" + kind + ")"); + const ready = Promise.all([ + kind === "jsx" ? ensureBabel() : Promise.resolve(), + after ?? Promise.resolve() + ]); + const p = ready.then(() => fetch(url)).then((r) => { + if (!r.ok) throw new Error("HTTP " + r.status); + return r.text(); + }).then((src) => { + const code = kind === "jsx" ? window.Babel.transform(src, { + filename: url, + presets: ["react", "typescript"] + }).code : src; + const module = { exports: {} }; + const before = new Set(Object.keys(window)); + //! nosemgrep: eval-and-function-constructor + new Function("React", "module", "exports", "require", code)( + getReact(), + module, + module.exports, + () => ({}) + ); + const globals = {}; + for (const k of Object.keys(window)) { + if (!before.has(k) && typeof window[k] === "function") { + globals[k] = window[k]; + } + } + cache.set(url, { mod: module.exports, globals }); + console.info( + "[dc-runtime] x-import: loaded", + url, + "\u2014 exports:", + Object.keys(module.exports), + "window globals:", + Object.keys(globals) + ); + onResolved(); + }).catch((e) => { + cache.set(url, { + mod: {}, + globals: {}, + error: "failed to load: " + (e instanceof Error && e.message ? e.message : String(e)) + }); + console.error( + "[dc-runtime] x-import: FAILED to load", + url, + "(" + kind + ")", + e + ); + onResolved(); + }); + pending.set(url, p); + return p; + } + function resolve2(url, name) { + const entry = cache.get(url); + if (!entry) return null; + const { mod, globals } = entry; + const C = mod && mod[name] || globals && globals[name] || typeof window !== "undefined" && window[name] || mod && mod.default; + if (typeof C === "function") return C; + const key = url + "\0" + name; + if (!reportedMissing.has(key)) { + reportedMissing.set( + key, + entry.error || 'no export named "' + name + '" (has: ' + Object.keys(mod).join(", ") + ")" + ); + console.error( + "[dc-runtime] x-import: module", + url, + "loaded but has no component named", + JSON.stringify(name), + "\u2014 available exports:", + Object.keys(mod), + "window globals:", + Object.keys(globals), + ". The module must `module.exports = {" + name + "}` or set `window." + name + "`." + ); + } + return null; + } + function waitForGlobal(name) { + if (polling.has(name)) return; + polling.add(name); + const started = Date.now(); + const isCE = isCustomElementName(name); + const tick = () => { + const found = isCE ? customElements.get(name) : isRenderableType(resolveDottedPath(window, name)); + if (found) { + polling.delete(name); + onResolved(); + return; + } + if (Date.now() - started >= GLOBAL_POLL_TIMEOUT_MS) { + console.warn( + "[dc-runtime] x-import: global", + JSON.stringify(name), + "never appeared on window after " + GLOBAL_POLL_TIMEOUT_MS + "ms" + ); + return; + } + setTimeout(tick, GLOBAL_POLL_INTERVAL_MS); + }; + setTimeout(tick, GLOBAL_POLL_INTERVAL_MS); + } + function resolveGlobal(url, name) { + const isCE = isCustomElementName(name); + if (!url) { + if (isCE) { + if (customElements.get(name)) return name; + waitForGlobal(name); + return null; + } + const g2 = resolveDottedPath(window, name); + if (isRenderableType(g2)) return g2; + waitForGlobal(name); + return null; + } + const entry = cache.get(url); + if (!entry) return null; + if (isCE && customElements.get(name)) return name; + const g = entry.globals[name] ?? resolveDottedPath(window, name); + if (isRenderableType(g)) return g; + if (name.includes(".")) return null; + const key = url + "\0global\0" + name; + if (!reportedMissing.has(key)) { + reportedMissing.set(key, null); + if (isCE && !customElements.get(name)) { + console.warn( + "[dc-runtime] x-import:", + url, + "loaded but no custom element", + JSON.stringify(name), + "is registered and window." + name + " is not a function \u2014 rendering <" + name + "> as an unknown element." + ); + } + } + return name; + } + function getError(url, name) { + const entry = cache.get(url); + if (entry?.error) return entry.error; + return reportedMissing.get(url + "\0" + name) || null; + } + return { load, resolve: resolve2, resolveGlobal, getError }; + } + function isElementClass(g) { + try { + return typeof g === "function" && typeof HTMLElement !== "undefined" && g.prototype instanceof HTMLElement; + } catch { + return false; + } + } + + // src/atomics.ts + var ATOMIC_CSS = ( + // layout + ".fx{display:flex}.col{display:flex;flex-direction:column}.grid{display:grid}.ac{align-items:center}.jc{justify-content:center}.jb{justify-content:space-between}.f1{flex:1}.noshrink{flex-shrink:0}.wrap{flex-wrap:wrap}.fw5{font-weight:500}.fw6{font-weight:600}.fw7{font-weight:700}.fw8{font-weight:800}.fs11{font-size:11px}.fs12{font-size:12px}.fs13{font-size:13px}.fs14{font-size:14px}.fs15{font-size:15px}.fs16{font-size:16px}.fs20{font-size:20px}.fs22{font-size:22px}.upper{text-transform:uppercase}.tc{text-align:center}.nowrap{white-space:nowrap}.gap8{gap:8px}.gap10{gap:10px}.gap12{gap:12px}.gap16{gap:16px}.gap24{gap:24px}.m0{margin:0}.mt8{margin-top:8px}.mt12{margin-top:12px}.mt16{margin-top:16px}.mb8{margin-bottom:8px}.mb12{margin-bottom:12px}.mb16{margin-bottom:16px}.posrel{position:relative}.posabs{position:absolute}.round{border-radius:50%}.ohide{overflow:hidden}.bbox{box-sizing:border-box}.pointer{cursor:pointer}.w100{width:100%}.b0{border:none}" + ); + + // src/helmet.ts + var DESIGN_DOC_MODE_RE = /]*\bname\s*=\s*["']design_doc_mode["'][^>]*\b(?:content|value)\s*=\s*["'](\w+)["']/i; + var CANVAS_BG_LIGHT = "#f0eee6"; + var CANVAS_BG_DARK = "#2e2c26"; + function createHelmetManager(doc, isStreaming) { + const mounted = /* @__PURE__ */ new Set(); + const live = /* @__PURE__ */ new Map(); + let designDocMode = null; + let canvasStyleEl = null; + let appTheme = "light"; + try { + const ds = doc.documentElement.dataset.theme; + appTheme = ds === "dark" || ds === "light" ? ds : new URLSearchParams(doc.defaultView?.location.search ?? "").get( + "theme" + ) === "dark" ? "dark" : "light"; + } catch { + } + function applyCanvasBg() { + if (!canvasStyleEl) return; + const bg = appTheme === "dark" ? CANVAS_BG_DARK : CANVAS_BG_LIGHT; + canvasStyleEl.textContent = `html,body{background:${bg}}#dc-root>.sc-host{position:relative}`; + } + function postDesignMode(mode) { + if (window.parent === window) return; + try { + window.parent.postMessage({ type: "__dc_design_mode", mode }, "*"); + } catch { + } + } + function setDesignDocMode(mode) { + if (mode === designDocMode) return; + designDocMode = mode; + postDesignMode(mode); + if (mode === "canvas") { + doc.documentElement.setAttribute("data-dc-canvas", ""); + canvasStyleEl = doc.createElement("style"); + canvasStyleEl.setAttribute("data-dc-canvas", ""); + applyCanvasBg(); + doc.head.appendChild(canvasStyleEl); + } else { + doc.documentElement.removeAttribute("data-dc-canvas"); + canvasStyleEl?.remove(); + canvasStyleEl = null; + } + } + window.addEventListener("message", (e) => { + const type = e.data && e.data.type; + if (type === "__dc_theme") { + const t = e.data.theme; + if (t === "light" || t === "dark") { + appTheme = t; + doc.documentElement.dataset.theme = t; + applyCanvasBg(); + } + return; + } + if (!designDocMode || type !== "__dc_probe") return; + postDesignMode(designDocMode); + }); + function compile(node) { + const raw = [...node.children]; + const helmetClosed = node.nextSibling != null || node.parentNode?.nextSibling != null; + if (node.hasAttribute("data-dc-atomics") && !mounted.has("__dc-atomics")) { + mounted.add("__dc-atomics"); + const el = doc.createElement("style"); + el.id = "__dc-atomics"; + el.textContent = ATOMIC_CSS; + doc.head.appendChild(el); + } + return (_vals, ctx) => { + const name = ctx && ctx.__name || ""; + const streaming = !!(name && isStreaming(name)); + for (let i = 0; i < raw.length; i++) { + const child = raw[i]; + const tag = child.tagName; + const mayBePartial = streaming && !helmetClosed && i === raw.length - 1; + if (tag === "SCRIPT") { + if (mayBePartial) continue; + const key = "SCRIPT|" + (child.getAttribute("src") || child.textContent || ""); + if (mounted.has(key)) continue; + mounted.add(key); + const el = doc.createElement("script"); + for (const { name: an, value } of [...child.attributes]) + el.setAttribute(an, value); + if (child.textContent) el.textContent = child.textContent; + doc.head.appendChild(el); + } else if (tag === "LINK" || tag === "META") { + if (mayBePartial) continue; + const key = tag + "|" + (child.getAttribute("href") || child.getAttribute("src") || child.outerHTML); + if (mounted.has(key)) continue; + mounted.add(key); + doc.head.appendChild(child.cloneNode(true)); + } else { + const key = name + "|" + i; + let el = live.get(key); + if (!el || el.tagName !== tag) { + if (el) el.remove(); + el = doc.createElement(tag.toLowerCase()); + live.set(key, el); + doc.head.appendChild(el); + } + for (const { name: an, value } of [...child.attributes]) { + if (el.getAttribute(an) !== value) el.setAttribute(an, value); + } + if (el.textContent !== child.textContent) + el.textContent = child.textContent; + } + } + return null; + }; + } + return { compile, setDesignDocMode }; + } + + // src/pseudo.ts + function createPseudoSheet(doc) { + let el = null; + const cache = /* @__PURE__ */ new Map(); + let n = 0; + return (pseudo, css) => { + const k = pseudo + "|" + css; + const hit = cache.get(k); + if (hit) return hit; + if (!el) { + el = doc.createElement("style"); + doc.head.appendChild(el); + } + const cls = "scp" + (n++).toString(36); + const sel = pseudo === "before" || pseudo === "after" ? "." + cls + "::" + pseudo : "." + cls + ":" + pseudo; + el.sheet.insertRule(sel + "{" + css + "}", el.sheet.cssRules.length); + cache.set(k, cls); + return cls; + }; + } + + // src/registry.ts + function createRegistry() { + const entries = /* @__PURE__ */ Object.create(null); + function get(name) { + return entries[name] || (entries[name] = { + html: "", + tpl: null, + Logic: null, + jsStreaming: false, + htmlStreaming: false, + ver: 0, + subs: /* @__PURE__ */ new Set(), + fetched: false + }); + } + function bump(name) { + const r = get(name); + r.ver++; + for (const fn of r.subs) fn(); + } + return { + entries, + get, + bump, + bumpAll() { + for (const n in entries) bump(n); + } + }; + } + + // src/runtime.ts + var COMPONENT_DIR = "."; + function createRuntime(doc = document) { + const registry = createRegistry(); + const pseudoClass = createPseudoSheet(doc); + const helmet = createHelmetManager( + doc, + (name) => registry.get(name).htmlStreaming + ); + const external = createExternalModules(() => registry.bumpAll()); + const factory = createComponentFactory(registry, ensureFetched); + const host = { + component: (name) => factory.getDC(name), + placeholder: (props) => h(Placeholder, props), + helmet: (node) => helmet.compile(node), + loadExternal: (kind, url, after) => external.load(kind, url, after), + resolveExternal: (url, name) => external.resolve(url, name), + resolveExternalGlobal: (url, name) => external.resolveGlobal(url, name), + resolveExternalError: (url, name) => external.getError(url, name), + pseudoClass + }; + function ensureFetched(name) { + const r = registry.get(name); + if (r.fetched) return; + r.fetched = true; + const url = COMPONENT_DIR + "/" + encodeURIComponent(name) + ".dc.html"; + fetch(url).then((res) => { + if (!res.ok) { + console.error( + "[dc-runtime] sibling fetch for <" + name + "/> failed:", + url, + "returned", + res.status, + "\u2014 the reference renders as an empty placeholder." + ); + return ""; + } + return res.text(); + }).then((t) => { + if (!t) return; + const parsed = parseDcText(t); + if (!parsed) { + console.error( + "[dc-runtime] sibling fetch for <" + name + "/>:", + url, + "has no block \u2014 not a Design Component." + ); + return; + } + if (parsed.props) r.propsMeta = parsed.props; + if (parsed.preview) r.preview = parsed.preview; + if (parsed.template && !r.html) updateHtml(name, parsed.template); + if (parsed.js && !r.Logic) updateJs(name, parsed.js); + }).catch( + (e) => console.error( + "[dc-runtime] sibling fetch for <" + name + "/> threw:", + url, + e + ) + ); + } + let rootName = null; + function updateHtml(name, html) { + const r = registry.get(name); + r.html = html; + if (name === rootName) { + const mode = DESIGN_DOC_MODE_RE.exec(html)?.[1] ?? null; + if (mode || !r.htmlStreaming) helmet.setDesignDocMode(mode); + } + try { + r.tpl = compileTemplate(html, host); + } catch (e) { + console.error("[dc-runtime] template compile FAILED for", name, e); + } + registry.bump(name); + } + function updateJs(name, src) { + const r = registry.get(name); + const seq = r.jsSeq = (r.jsSeq || 0) + 1; + try { + const Cls = evalDcLogic(src); + if (r.jsSeq !== seq) return; + if (typeof Cls !== "function") { + r.logicError = name + ".dc.html: