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 0000000..8879226 Binary files /dev/null and b/.thumbnail differ 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: