
Create Site
- 123 installs
- 572 repo stars
- Updated July 28, 2026
- microsoft/power-platform-skills
>-.
About
>-. > **Plugin check**: Run `node "${PLUGIN_ROOT}/scripts/check-version.js"` — if it outputs a message, show it to the user before proceeding. The create-site skill documents workflows, constraints, and examples from SKILL.md for agent-assisted execution.
- > **Plugin check**: Run `node "${PLUGIN_ROOT}/scripts/check-version.js"` — if it outputs a message, show it to the user
- # Create Power Pages Code Site
- **Use TaskCreate/TaskUpdate**: Track all progress throughout all phases — create the todo list upfront with all phases b
- **Git checkpoints**: Commit after every individual page and component — each gets its own commit so breaking changes can
- **Constraint**: Only static SPA frameworks are supported (React, Vue, Angular, Astro). NOT supported: Next.js, Nuxt.js,
Create Site by the numbers
- 123 all-time installs (skills.sh)
- Ranked #504 of 1,453 DevOps & CI/CD skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
create-site capabilities & compatibility
- Capabilities
- > **plugin check**: run `node "${plugin_root}/sc · # create power pages code site · **use taskcreate/taskupdate**: track all progres · **git checkpoints**: commit after every individu
- Use cases
- documentation
What create-site says it does
>-
npx skills add https://github.com/microsoft/power-platform-skills --skill create-siteAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 123 |
|---|---|
| repo stars | ★ 572 |
| Security audit | 2 / 3 scanners passed |
| Last updated | July 28, 2026 |
| Repository | microsoft/power-platform-skills ↗ |
How do I apply create-site using the workflow in its SKILL.md?
>-
Who is it for?
Developers following the create-site skill for the tasks it documents.
Skip if: Tasks outside the create-site scope described in SKILL.md.
When should I use this skill?
User mentions create-site or related triggers from the skill description.
What you get
Working create-site setup aligned with the documented patterns and constraints.
Files
Plugin check: Run node "${PLUGIN_ROOT}/scripts/check-version.js" — if it outputs a message, show it to the user before proceeding.Create Power Pages Code Site
Guide the user through creating a complete, production-quality Power Pages code site from initial concept to deployed site. Follow a systematic approach: discover requirements, scaffold and launch immediately, plan components and design, implement with design applied, validate, review, and deploy.
Core Principles
- Use best judgement for design details: Once the user picks an aesthetic direction and mood, make confident decisions about specific fonts, colors, page layouts, and component behavior. Do not ask the user to specify every detail — use the design reference and your own taste to make creative, distinctive choices.
- Use TaskCreate/TaskUpdate: Track all progress throughout all phases — create the todo list upfront with all phases before starting any work.
- Scaffold early, design with intention: Get the dev server running immediately after discovery so the user has something to look at. Then plan the design and features while the scaffold is live — apply the chosen aesthetic during implementation.
- Live preview feedback loop: The dev server MUST be running before any customization begins. Browse the site via Playwright (
browser_navigate+browser_snapshot) to verify every significant change. Do NOT take screenshots — only use accessibility snapshots to check page structure and content. - Keep the scaffold loader in sync with reality: The scaffold loader polls
public/scaffold-status.json. Update this file before everyAskUserQuestion(to raise the "waiting for your input" banner so the user doesn't miss a terminal prompt) and before each implementation step in Phase 5 (so the progress-bar label matches what you're actually doing while the decorative spinner continues its default cycle). See Live Preview Status Protocol. - Use real images: Source high-quality photos from Unsplash wherever pages need visual content — hero sections, feature cards, about pages, backgrounds, etc. Use
https://images.unsplash.com/photo-{id}?w={width}&h={height}&fit=cropURLs with specific photo IDs found viaWebSearch. Never leave image placeholders or broken<img>tags pointing to nonexistent files. - Git checkpoints: Commit after every individual page and component — each gets its own commit so breaking changes can be reverted.
Constraint: Only static SPA frameworks are supported (React, Vue, Angular, Astro). NOT supported: Next.js, Nuxt.js, Remix, SvelteKit, Liquid.
Initial request: $ARGUMENTS
---
Live Preview Status Protocol
<!-- not-a-gate: prose-only section — mentions of AskUserQuestion here describe the live-status protocol that wraps every real prompt in Phases 3/4/8; the actual gates are catalogued in §6.13 of references/approval-gates.md and marked at their call sites below -->
While the scaffold loading screen is visible (from Phase 2.6 until the Home page itself is replaced in Phase 5), the loader polls GET /scaffold-status.json every 1.5 seconds. The message you write into <PROJECT_ROOT>/public/scaffold-status.json appears as the label under the progress bar, and awaitingInput controls the "waiting for your input" banner. The decorative spinner above the progress bar continues its built-in phrase cycle; keep the progress-bar label current so the loader still reflects what is actually happening.
Why this matters: When the browser with the loader takes over the user's screen, a prompt in the terminal can sit unanswered for a long time because the user doesn't realize anything is waiting. The banner makes it obvious.
File shape (all fields optional — omit any field you don't want to change):
{
"message": "Creating Contact page",
"awaitingInput": false,
"inputPrompt": "Please check your terminal to respond."
}message— one short present-participle phrase shown as the status line under the progress bar in the loader (replacing the default "Getting started…" / "Setting up infrastructure…" cycle). Include the grouping context inline when it helps (e.g.,"Creating Footer component (shared components)").awaitingInput— whentrue, a prominent pulsing banner appears at the top of the loader and stays visible until this field is cleared. Set this before everyAskUserQuestioncall and clear it (false) immediately after the user answers.inputPrompt— short context for the banner (e.g.,"Choose a framework"). Optional.
When to update the file:
1. After scaffold launches (end of Phase 2): write an initial status like { "message": "Planning your site", "awaitingInput": false }. 2. Before any `AskUserQuestion` that runs while the scaffold is visible (Phases 3, 4, and any in-scaffold prompt in Phase 5): set awaitingInput: true with a short inputPrompt. After the user answers, write again with awaitingInput: false. 3. Before each implementation step in Phase 5 — applying design tokens, creating each shared component, creating each page, updating the router, updating navigation — update message to the specific action. Examples: "Applying design tokens", "Creating Navbar component", "Creating Contact page". 4. At the end of Phase 5, after the Home page has been replaced: delete public/scaffold-status.json so it isn't deployed with the site.
Write the file with the Write tool (atomic overwrite). You do not need to read it first.
---
Phase 1: Discovery
Goal: Understand what site needs to be built and what problem it solves
Actions:
<!-- gate: create-site:1.purpose | category=plan | cancel-leaves=nothing -->
🚦 Gate (plan · create-site:1.purpose): Multi-question prompt collecting site name, framework, purpose, audience, and target directory. Determines what gets scaffolded. Fires only on the "site purpose unclear" branch (step 3 below).
>
Trigger: Phase 1 when site purpose was not provided in $ARGUMENTS.Why we ask: Wrong framework picked → wrong template copied into the wrong directory; cleanup is annoying.
Cancel leaves: Nothing — no scaffolding has started yet.
1. Create todo list with all 8 phases (see Progress Tracking table) 2. If site purpose is clear from arguments:
- Summarize understanding
- Identify site type (portal, dashboard, landing page, blog, etc.)
3. If site purpose is unclear, use AskUserQuestion:
| Question | Header | Options |
|---|---|---|
| What should the site be called? (e.g., "Contoso Portal", "HR Dashboard") | Site Name | (free text — use a single generic option so the user types a custom name via "Other") |
| Which frontend framework? | Framework | React (Recommended), Vue, Angular, Astro |
| What is the site's purpose? | Purpose | Company Portal, Blog/Content, Dashboard, Landing Page |
| Who is the target audience? | Audience | Internal (employees, partners), External (public-facing customers) |
| Where should the project be created? | Location | Current directory, New folder in current directory (Recommended), Any other directory |
4. Resolve the project location:
- If "Current directory": Project root =
<cwd>. - If "New folder in current directory": Create a folder named
__SITE_NAME__inside the cwd. Project root =<cwd>/__SITE_NAME__/. - If "Any other directory": Ask for the full path. Verify/create it. Project root = provided path.
After resolving, confirm: "The site will be created at <resolved path>."
Store this as PROJECT_ROOT.
5. From the user's answers, derive:
__SITE_NAME__(Title Case, e.g.,Contoso Portal)__SITE_SLUG__(kebab-case derived from site name, e.g.,contoso-portal)__SITE_DESCRIPTION__(one-line description based on name + purpose)
6. Summarize understanding and confirm with user before proceeding
Audience influences site generation:
- Internal: Prioritize data tables, dashboards, authentication, navigation depth, functional over flashy design
- External: Prioritize landing page appeal, SEO-friendly structure, contact forms, clean marketing-oriented layout
Output: Clear statement of site purpose, framework, audience, derived naming values, and project location
---
Phase 2: Scaffold & Launch Dev Server
Goal: Get a running site immediately so the user has something to preview while features and design are planned
The scaffold is a temporary branded loading screen — it shows a Power Pages animated "Building your site" experience with orbiting elements, status messages, and feature cards. Its only purpose is to get the dev server running quickly so the user has something to look at while you plan and build. During Phase 5 (Implementation), the entire scaffold — including theme.css, Layout, Home page, and all placeholder components — is completely replaced with the user's actual site: their chosen typography, color palette, pages, components, and navigation. Do NOT try to build on top of the loading screen; replace it entirely.
See ${PLUGIN_ROOT}/references/framework-conventions.md for the full framework → build tool → router → output path mapping.Actions:
2.1 Copy Template
${PLUGIN_ROOT} is already resolved to the plugin's absolute path at runtime. Use it directly in Glob/Read paths — do NOT search for the plugin directory.Read and copy all files from the matching asset template to the project directory:
| Framework | Asset Directory |
|---|---|
| React | ${PLUGIN_ROOT}/skills/create-site/assets/react/ |
| Vue | ${PLUGIN_ROOT}/skills/create-site/assets/vue/ |
| Angular | ${PLUGIN_ROOT}/skills/create-site/assets/angular/ |
| Astro | ${PLUGIN_ROOT}/skills/create-site/assets/astro/ |
Use Glob to discover all files in the asset directory, Read each file, then Write to the project directory preserving the relative path structure.
Also copy the shared loader icon that the scaffold references from its CSS (url('/power-pages-icon.png')):
Read the binary file ${PLUGIN_ROOT}/skills/create-site/assets/shared/power-pages-icon.png and Write it to <PROJECT_ROOT>/public/power-pages-icon.png. (All four supported frameworks serve public/ at the web root, so the same /power-pages-icon.png URL works for every framework.)
Seed the live status file so the loader shows a real message the moment it mounts. Write <PROJECT_ROOT>/public/scaffold-status.json:
{ "message": "Planning your site", "awaitingInput": false }See Live Preview Status Protocol for the full contract — from here on, update this file before every AskUserQuestion and before each Phase 5 implementation step.
2.2 Replace Placeholders
After copying, replace all __PLACEHOLDER__ tokens in every file. Use Edit with replace_all: true on each file.
- Name/slug/description placeholders: Use the actual values from Phase 1 (
__SITE_NAME__,__SITE_SLUG__,__SITE_DESCRIPTION__).
Note: The scaffold loading screen uses hardcoded Power Pages branding colors — there are no color placeholders (__PRIMARY_COLOR__, etc.) to replace. The user's chosen color palette is applied fresh during Phase 5 when the scaffold is completely replaced.2.3 Rename gitignore
Rename gitignore → .gitignore in the project root (stored without dot prefix to avoid git interference in the plugin repo).
2.4 Install Dependencies
Run npm install before initializing git so that package-lock.json is included in the initial commit:
cd "<PROJECT_ROOT>"
npm install2.5 Initialize Git Repository
Initialize a git repo and make the first commit. This captures all template files AND package-lock.json in one clean baseline:
cd "<PROJECT_ROOT>"
git init
git add -A
git commit -m "Initial scaffold: __SITE_NAME__ (__FRAMEWORK__)"From this point, commit after every significant milestone so any breaking change can be reverted.
2.6 Start Dev Server
This MUST happen now — before any planning or customization begins. The dev server gives the user a live preview while features and design are being planned:
cd "<PROJECT_ROOT>"
npm run devRun npm run dev in the background using Bash with run_in_background: true. Note the local URL (typically http://localhost:5173 for Vite or http://localhost:4200 for Angular or http://localhost:4321 for Astro).
2.7 Verify in Playwright & Share URL
Immediately after the dev server starts, verify the scaffold is working:
1. Use mcp__plugin_power-pages_playwright__browser_navigate to open the dev server URL 2. Use mcp__plugin_power-pages_playwright__browser_snapshot to verify the page loaded correctly (do NOT take screenshots — only use accessibility snapshots) 3. Share the dev server URL with the user so they can preview the site in their own browser (e.g., "Your site is running at http://localhost:5173 — open it in your browser to follow along as I build.")
GATE: Do NOT proceed to Phase 3 until ALL of the following are true:
>
1. Template files copied and placeholders replaced
2. Git repo initialized with initial scaffold commit
3. npm install completed successfully4. Dev server is running in the background (npm run dev)5. Playwright has opened the site and verified it loads via browser_snapshot6. The dev server URL has been shared with the user
>
If any of these are not done, complete them now before moving on.
Output: Running dev server with verified scaffold, URL shared with user
---
Phase 3: Component Planning
Goal: Determine what pages, components, and design elements the site needs — while the user previews the running scaffold
<!-- gate: create-site:3.requirements | category=plan | cancel-leaves=nothing -->
🚦 Gate (plan · create-site:3.requirements): Three sub-prompts (features multi-select, aesthetic, mood) — shape the Phase 4 plan and the Phase 5 implementation. Fires at step 2 of the action list below.
>
Trigger: Phase 3 entry; scaffold loader is up.
Why we ask: Wrong feature set / aesthetic gets baked into the rendered plan — the Phase 4.7 gate would still catch most errors, but it's wasteful to defer the catch.
Cancel leaves: Nothing — scaffold loader files are throwaway artifacts replaced wholesale in Phase 5.
Actions:
1. Raise the "awaiting input" banner so the user notices the terminal prompt even while the browser loader is full-screen. Write <PROJECT_ROOT>/public/scaffold-status.json:
{ "message": "Planning your site", "awaitingInput": true, "inputPrompt": "Features, aesthetic, and mood — please answer in the terminal." }Immediately after the user answers, Write the same file again with "awaitingInput": false so the banner disappears.
2. Use AskUserQuestion to collect feature and design requirements:
| Question | Header | Options |
|---|---|---|
| Which features? (multi-select) | Features | (generate 3-4 context-aware options based on the site name, purpose, and audience from Phase 1) |
| What aesthetic direction do you want? | Aesthetic | Minimal & Clean (Recommended), Bold & Vibrant, Dark & Moody, Warm & Organic |
| What's the overall mood? | Mood | Professional & Trustworthy (Recommended), Creative & Playful, Technical & Precise, Elegant & Premium |
Feature options are NOT hardcoded. Infer relevant features from Phase 1 answers. For example:
- "HR Dashboard" + Internal → Employee Directory, Leave Requests, Announcements, Org Chart
- "Contoso Portal" + External → Contact Form, Service Catalog, Knowledge Base, FAQ
- "Partner Hub" + Internal → Document Library, Partner Directory, Deal Tracker, Notifications
>
Always generate options that make sense for the specific site — never reuse a fixed list.
>
If you include an Authentication feature option, describe it generically as "Login/signup for tracking application status" or similar. Do NOT mention a specific identity provider (e.g., "Entra ID", "SAML", "Google") in the feature description — the /power-pages:setup-auth skill will ask the user which provider they want.3. AI Component Planning — Based on Phase 1 answers (site name, purpose, audience) and the feature selection above, propose which of the Power Pages generative-AI summarization APIs the site might use. The site itself does not depend on them — the page ships with reserved slots and runs without AI; /add-ai-webapi populates the slots later when the user is ready. Use AskUserQuestion with multi-select to let the user opt in:
| Question | Header | Options |
|---|---|---|
Which AI summarization features should the site have? (multi-select — each can be added later with /add-ai-webapi) | AI Summaries | (generate 2-4 context-aware options plus "None for now") |
Options are NOT hardcoded. Infer relevant AI summary features from Phase 1 and the features picked above. Examples:
- "HR Dashboard" + Leave Requests feature → "Data summarization for leave requests", "Search summary on the knowledge base"
- "Contoso Portal" + Knowledge Base → "Search summary on site-wide search", "Data summarization for articles"
- "Customer Self-Service" + Support Cases / Incidents → "Data summarization for support cases (Microsoft-shipped recipe)", "Data summarization for attached knowledge articles"
>
Treat the standard incident table like any other Dataverse table — propose DataSummarization for it when the site handles support cases, but don't force the Microsoft-shipped
recipe ($select=description,title + the portal-comments expand) unless that genuinely fitsthe user's UX. A custom case-like table or a different facet of the standard incident is a
regular Data Summarization pick. Always include None for now so the user can defer. Do
NOT integrate the APIs in this skill — only record the user's picks so Phase 4's plan can
mention them and Phase 8 can suggest /add-ai-webapi as a recommended next step.>
Capture the selection in memory asAI_SUMMARY_PICKS— a list of one or more of:search-summary,data-summarization.
4. Map picks to target pages. For each entry in AI_SUMMARY_PICKS, decide which page will carry the AI surface and store the mapping as AI_SUMMARY_PLACEMENTS. This is what Phase 4 shows the user and what Phase 5 reserves slots for. Use the feature selection from step 2 — and treat this mapping as an input to the page list Claude proposes in step 7: if a pick has no natural target page, add one to the plan so the summary has a home:
| Pick | Default target page | If no matching page is planned |
|---|---|---|
search-summary | A search / search-results page (e.g., SearchResults, Search) | Add a search page to the plan so the summary has a home |
data-summarization | The detail page of the table the user called out (e.g., ProductDetail for products, CaseDetail for support cases) — ask the user if ambiguous | Propose adding a detail page; if rejected, fall back to a list/dashboard page |
AI_SUMMARY_PLACEMENTS shape: one record per placement, e.g. [{ pick: "data-summarization", targetPage: "CaseDetail", marker: "POWERPAGES:AI-SLOT kind=data-summarization" }].
The marker string is the comment tag Phase 5 emits into the page source as a reserved anchor that /add-ai-webapi later finds. Keep the shape uniform — one marker per placement, always the same tag, so the follow-up skill's explore step can grep for them deterministically.
5. Read the design aesthetics reference: ${PLUGIN_ROOT}/skills/create-site/references/design-aesthetics.md 6. Map aesthetic + mood to design choices using the Aesthetic x Mood Mapping table from the design reference. Record the chosen font direction, color direction, and motion direction. 7. Analyze requirements and determine needed components. If AI_SUMMARY_PLACEMENTS from step 4 implies a page that wasn't already in the plan (e.g., a CaseDetail page for a data-summarization pick on the support-case table), add it to the page list now. Present the component plan to the user as a table:
| Component Type | Count | Details |
|---------------------|-------|---------|
| Pages | 4 | Home, About, Services, Contact |
| Shared Components | 3 | Navbar, Footer, ContactForm |
| Design Elements | 4 | Google Fonts (Playfair Display + Source Sans Pro), Color palette (6 CSS vars), Page transitions, Gradient backgrounds |
| Routes | 4 | /, /about, /services, /contact |8. Use best judgement to determine the final color palette based on the chosen aesthetic + mood. These will be written fresh into a new theme.css during Implementation (Phase 5) when the scaffold loading screen is completely replaced:
| CSS Variable | Description | Value |
|---|---|---|
--color-primary | Primary hex color | (choose based on aesthetic + mood) |
--color-secondary | Complementary hex color | (choose based on aesthetic + mood) |
--color-bg | Background color | (choose based on aesthetic + mood) |
--color-surface | Surface/card color | (choose based on aesthetic + mood) |
--color-text | Main text color | (choose based on aesthetic + mood) |
--color-text-muted | Muted text color | (choose based on aesthetic + mood) |
Output: Confirmed list of pages, components, design elements, and routes to create
---
Phase 4: Plan Approval
Goal: Render the implementation plan as an HTML document, open it in the user's default browser, and get approval before starting implementation.
Why HTML instead of a chat message: A structured HTML plan (like the ones produced by/integrate-backend,/add-server-logic, and/add-cloud-flow) lets the user skim sections, compare swatches, and preview typography — all impossible in a terminal. The scaffold loader in their browser may also be full-screen, so surfacing the plan in a new tab puts it where they can actually read it.
4.1 Read the Design Reference
Read the design aesthetics reference: ${PLUGIN_ROOT}/skills/create-site/references/design-aesthetics.md. Every field you populate below should be justified by the chosen aesthetic + mood from Phase 3.
AI Readiness in the plan. IfAI_SUMMARY_PLACEMENTSfrom Phase 3 is non-empty, reflect each placement in the matchingPAGES_DATAentry'sdescriptionorcontent— e.g., "Reserved slot for an AI summary card; populated later by `/add-ai-webapi`. The page ships without AI." This keeps the user's expectation honest: the site does not depend on generative-AI features being enabled on the tenant, and there is no "Run /add-ai-webapi" placeholder visible to end-users. IfAI_SUMMARY_PLACEMENTSis empty, omit any AI references from the plan.
4.2 Build the Plan Data
Assemble a single JSON object with the following keys. The plan template rejects any data that's missing a required key, so include all of them.
| Key | Type | Content |
|---|---|---|
SITE_NAME | string | Title-case site name from Phase 1 |
PLAN_TITLE | string | Always "Implementation Plan" |
FRAMEWORK | string | React / Vue / Angular / Astro |
AESTHETIC | string | Chosen aesthetic (e.g., Minimal & Clean) |
MOOD | string | Chosen mood (e.g., Professional & Trustworthy) |
SUMMARY | string | One paragraph describing what the site is and who it serves |
TYPOGRAPHY_DATA | object | { primary: { name, sample, reason }, secondary: { name, sample, reason } } — name must be a real Google Font family |
PALETTE_DATA | array | [{ var, hex, description }] — one entry per CSS variable (primary, secondary, bg, surface, text, text-muted) |
MOTION_DATA | array | [{ label, description }] — page transitions, hover states, etc. |
BACKGROUNDS_DATA | array | [{ label, description }] — hero backgrounds, section treatments, patterns |
PAGES_DATA | array | [{ name, route, description, content: [...], components: [...] }] — content is an outline of what's on the page, components is shared component names used |
COMPONENTS_DATA | array | [{ name, purpose, usedBy: [...] }] — shared components with the page names that consume them |
ROUTES_DATA | array | [{ path, page }] — every route the router will register |
REVIEW_DATA | array of strings | Verification checklist items (e.g., "All pages load without console errors") |
DEPLOYMENT_DATA | array | [{ title, description, recommended?: boolean }] — mark exactly one as recommended: true |
Write the data for the user, not for internal tooling — phrase description and reason fields in plain language.
4.3 Render the HTML Plan
Pick an output path under <PROJECT_ROOT>/docs/. Default is create-site-plan.html; if that file already exists, pick a descriptive variant like create-site-plan-v2.html (the render script refuses to overwrite existing files).
node "${PLUGIN_ROOT}/scripts/render-createsite-plan.js" --output "<PROJECT_ROOT>/docs/create-site-plan.html" --data-inline '<json-string>'Use --data-inline so no temp JSON file is written. If the JSON is too large for a single shell argument, write it to a temp file and use --data <path> instead, then delete the temp file after the render succeeds.
The script prints {"status":"ok","output":"<path>"} on success. Capture and use that actual output path for the next step.
4.4 Open the Plan in the Default Browser
Open <OUTPUT_PATH> in the default browser using the platform-appropriate file opener for the current environment. For example, use open on macOS, xdg-open on Linux, or the equivalent default-browser opener available on Windows.
4.5 Present a Brief Summary in the Terminal
Keep the terminal message short — the full plan lives in the HTML file now. Include:
- One sentence confirming the plan was rendered and where (the output path).
- A 3-5 line bullet summary: framework, page count, component count, palette primary + mood.
- A pointer: "See the open browser tab for pages, color swatches, typography samples, and deployment options."
Do NOT dump the full plan contents into the terminal — that defeats the purpose of the HTML view.
4.6 Raise the "Awaiting Input" Banner
The user may still be looking at the full-screen scaffold loader when you ask for approval. Write <PROJECT_ROOT>/public/scaffold-status.json:
{ "message": "Ready to build", "awaitingInput": true, "inputPrompt": "Plan approval needed — review the plan in your browser and answer in the terminal." }Immediately after the user answers, Write the same file again with "awaitingInput": false.
4.7 Ask for Approval
<!-- gate: create-site:4.7.plan-approval | category=plan | cancel-leaves=nothing -->
🚦 Gate (plan · create-site:4.7.plan-approval): Final sign-off on the rendered HTML plan before Phase 5 starts replacing the scaffold with real pages, components, and design tokens.
>
Trigger: Phase 4.3 rendered docs/create-site-plan.html; Phase 4.4 opened it in the browser.Why we ask: Phase 5 rewrites the entire scaffold (theme.css, Layout, Home page, components, routes) — undoing that touches every commit in the implementation phase.
Cancel leaves: Nothing destructive — the scaffold itself can be deleted with the project directory; no Dataverse / deploy fired.
Use AskUserQuestion:
| Question | Header | Options |
|---|---|---|
| Does this plan look good? | Plan | Approve and start building (Recommended), I'd like to make changes |
- If "Approve": Proceed to Phase 5.
- If "I'd like to make changes": Ask what they want changed, update the JSON, and re-render to a new filename (the render script won't overwrite). Re-open that new file in the browser and repeat 4.5–4.7.
Output: Approved implementation plan, with an HTML copy committed alongside the project for the user to reference during and after implementation.
---
Phase 5: Implementation
Goal: Build all pages, components, and design elements with the chosen aesthetic applied from the start
Prerequisite: The dev server MUST already be running and verified via Playwright (completed in Phase 2). If it is not, go back and complete Phase 2.
>
Design reference: Read ${PLUGIN_ROOT}/skills/create-site/references/design-aesthetics.md and apply its principles throughout this phase. All pages and components should be built with the chosen typography, color palette, motion, and backgrounds from the start — do NOT build with neutral styling first and redesign later.Actions:
5.1 Create Todos for All Work
Before writing any code, use TaskCreate to create a todo for every piece of work. This gives the user full visibility into what will be built:
- One todo per page — e.g., "Create Contact page (
/contact)", "Create Dashboard page (/dashboard)" - One todo per shared component — e.g., "Create ContactForm component", "Create DataTable component"
- One todo for routing — "Update router with all new routes"
- One todo for navigation — "Update Layout/Header with navigation links"
- One todo for design foundations — "Apply design tokens (fonts, colors, motion, backgrounds)"
Each todo should have a clear subject, activeForm, and description that includes the file path and what the page/component does. Then work through the todos in order, marking each in_progress → completed.
5.2 Replace the Scaffold & Build
The scaffold is a temporary loading screen — it must be completely replaced during this phase. Do NOT build on top of it or try to modify the loading animation into a real page. Start fresh with the user's chosen design.
Narrate progress in the loader: Before each of the steps below, update<PROJECT_ROOT>/public/scaffold-status.jsonso the user — who may still be watching the Home page loader — sees what's actually happening instead of the hardcoded placeholder cycle. Use a short present-participlemessage(e.g.,"Creating Navbar component","Creating Contact page"). Include any useful grouping context inline in the message itself. The loader picks up changes within ~1.5 seconds. Updates become no-ops once step 4 replaces the Home page.
1. Design foundations — Completely rewrite theme.css (or styles.css for Angular) from scratch with the chosen color palette as CSS custom properties, Google Fonts, motion/animation utilities, and background treatments. The scaffold's loading screen CSS is discarded entirely. Commit after this step. Before starting, set the loader status to `{ "message": "Applying design tokens" }`. 2. Layout — Rewrite the Layout component (and Header/Footer for Astro) with proper navigation, header, and footer that reflect the chosen design. The scaffold's passthrough Layout is replaced with a real layout structure. Set status to `{ "message": "Rewriting Layout" }`. 3. Shared components — Build reusable components (Navbar, Footer, ContactForm, etc.) that pages will use. For each component, set status to `{ "message": "Creating <Component> component" }`. 4. Pages — Create route components for each requested page, replacing the scaffold Home page and About placeholder entirely. Each page component must update document.title on mount to reflect the current page (e.g., "Contact — Contoso Portal"). Use the framework's idiomatic lifecycle hook: useEffect (React), onMounted (Vue), ngOnInit (Angular), or a <title> tag in the frontmatter (Astro). Format: "<Page Name> — <Site Name>", with the home page using just "<Site Name>". For each page, set status to `{ "message": "Creating <Page> page" }` before writing the file. The loader disappears when the Home page itself is replaced — no further status updates are needed after that. 5. Router — Register all new routes (the scaffold only has / and /about — add all requested routes) 6. Navigation — Add links to the new Layout/Header component 7. Entry HTML — Update index.html (or Layout.astro for Astro) to load the chosen Google Fonts instead of the scaffold's DM Sans + Outfit 8. Reserve AI summary slots — only if AI_SUMMARY_PLACEMENTS from Phase 3 is non-empty. For each placement, insert a single comment marker in the target page source at the intended insertion point. No visible placeholder UI, no stub components, no extra routes — just a grep-able anchor that /add-ai-webapi will later find and replace. Syntax depends on the framework:
| Framework | Marker syntax |
|---|---|
| React (JSX) | {/* POWERPAGES:AI-SLOT kind=<pick> */} inside the component's return JSX |
| Vue (SFC template) | <!-- POWERPAGES:AI-SLOT kind=<pick> --> inside <template> |
| Angular (HTML template) | <!-- POWERPAGES:AI-SLOT kind=<pick> --> inside the component template |
| Astro | <!-- POWERPAGES:AI-SLOT kind=<pick> --> inside the component's HTML |
Where <pick> is one of search-summary, data-summarization — verbatim from the placement record.
Placement within the page:
- `data-summarization` (record detail): directly after the page heading, above the detail content — this is where a Copilot-style summary card naturally reads in the reading order.
- `data-summarization` (list page): directly above the list / table, below the page heading and any filter bar.
- `search-summary`: directly above the search-results list, below the search input — the summary paragraph reads before the keyword hits.
One marker per placement, exactly as defined in the marker field of the AI_SUMMARY_PLACEMENTS record. Do NOT add stub components (<CopilotSummaryCard />, etc.), CSS classes, or empty <aside> elements — the slot is just a comment. The site must ship as if AI is not a consideration; the follow-up skill does the real work.
Important: Build real, functional UI with distinctive design applied — not placeholder "coming soon" pages, and not generic unstyled markup. Every page and component should reflect the chosen aesthetic from the moment it's created. The scaffold loading screen should be completely gone after this phase — no trace of the Power Pages branded animation should remain.
5.3 Source Real Images
Use high-quality photos from Unsplash wherever the site needs visual content. Do NOT use placeholder services (e.g., placeholder.com, placehold.co), broken <img> tags, or leave empty image slots.
How to find images:
1. Use WebSearch to search Unsplash for relevant photos (e.g., site:unsplash.com modern office workspace) 2. Pick specific photos and use their direct URL with sizing parameters: https://images.unsplash.com/photo-{id}?w={width}&h={height}&fit=crop 3. Choose images that match the site's aesthetic and mood
Where to use images:
- Hero sections — Striking, high-resolution photos that set the tone for the site
- Feature/service cards — Relevant photos that illustrate each feature or service
- About/team sections — Professional or contextual photos matching the site's purpose
- Backgrounds — Atmospheric photos used as full-bleed or overlay backgrounds
- Content sections — Supporting photos that break up text and add visual interest
Guidelines:
- Pick images that feel cohesive together — consistent style, lighting, and color tone
- Use appropriate sizing (
w=800for cards,w=1600for heroes/backgrounds) to avoid slow loads - Add descriptive
alttext to every<img>for accessibility - For icons and logos, use inline SVGs instead of photos
5.4 Git Commit Checkpoints
Commit after every individual page and component so breaking changes can be reverted. Each page and each component gets its own commit — do NOT batch multiple pages or components into a single commit.
git add -A
git commit -m "<short description of what was added/changed>"When to commit:
- After applying design foundations (fonts, colors, motion)
- After creating each page (e.g., "Add Home page", "Add Contact page")
- After creating each shared component (e.g., "Add Navbar component", "Add Footer component")
- After updating routing and navigation
- Before attempting anything risky or experimental
If something breaks, revert to the last good commit:
git revert HEAD5.5 Live Verification
After each significant change (new page or component), browse the site via Playwright to ensure everything is up to the mark:
1. Use mcp__plugin_power-pages_playwright__browser_navigate to reload or navigate to the updated page 2. Use mcp__plugin_power-pages_playwright__browser_snapshot to verify the page structure and content are correct — do NOT take screenshots 3. If something looks wrong in the snapshot, fix it before proceeding
The user is previewing in their own browser via the dev server URL shared in Phase 2.7.
5.6 Clean Up the Live Status File
Once the scaffold loader is gone, public/scaffold-status.json is just dead weight that would ship with the deployed site. Delete the file from <PROJECT_ROOT>/public/ and commit the removal alongside the final implementation.
GATE: Do NOT proceed to Phase 6 until ALL customization is complete with design applied. The site must have distinctive typography (Google Fonts — no generic Inter/Roboto/Arial), a cohesive color palette (CSS variables), motion/animations, and all requested pages/features before moving to accessibility verification.
Output: All pages, components, and design elements implemented and verified
---
Phase 6: Accessibility Verification
Goal: Verify the site meets WCAG 2.2 AA standards using axe-core automated testing and fix any violations
Prerequisite: All pages and components must be fully implemented (Phase 5 complete). The dev server MUST be running.
Actions:
6.1 Install Playwright Dependency
Install playwright as a dev dependency in the project so the audit script can launch a headless browser. This uses the system-installed browser (Edge/Chrome) — no browser download is needed:
cd "<PROJECT_ROOT>"
npm install --save-dev playwright6.2 Run axe-core Audit on Every Page
Run the audit script via Bash, passing the dev server URL and all site routes:
node "${PLUGIN_ROOT}/skills/create-site/scripts/axe-audit.js" --url <DEV_SERVER_URL> --routes /,/about,/services,/contact --project-root "<PROJECT_ROOT>"The script launches a headless browser, navigates to each route, injects axe-core from CDN, runs the analysis, and outputs a JSON array of per-route results to stdout. Each result contains violations (with id, impact, description, helpUrl, and affected nodes), passes count, and incomplete count. The script exits with code 1 if any critical or serious violations are found.
Parse the JSON output and record all violations.
6.3 Fix Accessibility Violations
For each violation found, identify the source file and apply the fix:
| Violation | Fix |
|---|---|
Missing alt text on images | Add descriptive alt attributes to <img> tags |
| Insufficient color contrast | Adjust CSS color variables to meet 4.5:1 (normal text) or 3:1 (large text) ratios |
| Missing form labels | Add <label> elements or aria-label attributes |
| Missing landmark regions | Wrap content in <main>, <nav>, <header>, <footer> |
| Skipped heading levels | Correct heading hierarchy (h1 → h2 → h3, no gaps) |
| Missing link text | Add descriptive text or aria-label to links |
Missing lang attribute | Add lang="en" to the <html> tag |
| Inadequate focus indicators | Add visible outline styles to interactive elements |
After fixing each group of related violations, commit:
git add -A
git commit -m "Fix accessibility: <violation description>"6.4 Re-verify After Fixes
After all fixes are applied, re-run the audit script (same command as 6.2) to confirm violations are resolved:
1. If new violations appear (e.g., a fix introduced a regression), repeat 6.3–6.4 2. Continue until the script exits with code 0 (zero critical and serious violations)
Present a summary table to the user:
| Page | Route | Violations Found | Violations Fixed | Status |
|------|-------|-----------------|-----------------|--------|
| Home | / | 3 | 3 | Pass |
| About | /about | 1 | 1 | Pass |
| Contact | /contact | 2 | 2 | Pass |
| **Total** | | **6** | **6** | **All passing** |GATE: Do NOT proceed to Phase 7 until all pages pass axe-core with zero `critical` and `serious` violations. Minor and moderate violations should also be fixed where possible, but are not blocking.
Output: Accessibility-verified site with zero critical/serious axe-core violations
---
Phase 7: Review & User Testing
Goal: Ensure the site meets user expectations and all pages work correctly
<!-- gate: create-site:7.review | category=plan | cancel-leaves=nothing -->
🚦 Gate (plan · create-site:7.review): Live-site review — last chance to request changes before the deploy prompt. Cancel branch lets the user keep iterating. Fires at step 4 of the action list below.
>
Trigger: Phase 7 has verified all pages render via Playwright.
Why we ask: User loses the chance to spot UI issues before deploy; broken pages get pushed.
Cancel leaves: Nothing — site files stay as-is on disk.
Actions:
1. Browse through each page via Playwright (browser_navigate + browser_snapshot) to verify all pages load correctly — do NOT take screenshots 2. Present a summary of what was built:
| Component Type | Count | Details |
|---------------------|-------|---------|
| Pages | 4 | Home (/), About (/about), Services (/services), Contact (/contact) |
| Shared Components | 3 | Navbar, Footer, ContactForm |
| Design Elements | 4 | Playfair Display + Source Sans Pro, 6 CSS variables, fade-in transitions, gradient backgrounds |
| Git Commits | 7 | scaffold + 6 feature commits |3. Share the dev server URL with the user and list all available routes 4. Ask the user to review using AskUserQuestion:
"The site is ready for review at <dev server URL>. Please check it out in your browser. Would you like any changes?"5. If the user requests changes, apply them and re-verify by browsing via browser_snapshot
Output: User-approved site ready for deployment
---
Phase 8: Deployment & Next Steps
Goal: Deploy the site and suggest enhancements
This phase is MANDATORY. Do NOT end the session without asking about deployment.
<!-- gate: create-site:8.deploy | category=plan | cancel-leaves=nothing -->
🚦 Gate (plan · create-site:8.deploy): Deploy prompt — invokes /deploy-site on Yes. Skipping leaves the site files on disk for the user to deploy later. Fires at step 2 of the action list below.>
Trigger: Phase 8 entry; Phase 7 review approved.
Why we ask: Auto-deploy picks whatever env PAC CLI happens to be pointing at — wrong-env first deploy is messy to undo.
Cancel leaves: Nothing — site files stay on disk; no deploy fired.
Actions:
1. Record skill usage:
Reference: ${PLUGIN_ROOT}/references/skill-tracking-reference.mdFollow the skill tracking instructions in the reference to record this skill's usage. Use --skillName "CreateSite". Note: .powerpages-site may not exist for first-time sites — the script exits silently.
2. Use AskUserQuestion with options: Deploy now (Recommended), Skip for now:
"Would you like to deploy your site to Power Pages now?"
3. If the user chooses to deploy, invoke the /deploy-site skill. 4. Mark all todos complete 5. Present a final summary:
- Site name and purpose
- Framework and project location
- Components created (X pages, Y components, Z design elements)
- Key files and their purposes
- Total file count and git commit count
6. Suggest optional enhancement skills:
/setup-datamodel— Create Dataverse tables for dynamic content/add-seo— Add meta tags, robots.txt, sitemap.xml, favicon/add-tests— Add unit tests (Vitest) and E2E tests (Playwright)/add-ai-webapi— Add generative-AI summaries (Search Summary and Data Summarization). Recommend first when `AI_SUMMARY_PLACEMENTS` from Phase 3 is non-empty — the pages already carryPOWERPAGES:AI-SLOTcomment markers at the intended insertion points, so the follow-up skill's explore step finds them deterministically and the user gets the AI surface they picked during discovery without any page redesign.
Output: Deployed (or deployment-ready) site with clear next steps
---
Important Notes
Throughout All Phases
- Use TaskCreate/TaskUpdate to track progress at every phase
- Ask for user confirmation at key decision points (see list below)
- Use best judgement for design details — make confident, creative choices based on the user's aesthetic + mood selection without asking for every specific font, color, or layout decision
- Apply design from the start — never build neutral then restyle
- Verify via Playwright after every significant change
- Commit after every page and component — each gets its own dedicated commit, never batch multiple together
- No screenshots — only use
browser_snapshot(accessibility snapshots) to verify pages; never usebrowser_take_screenshotas it clutters the user's directory. Give the user the dev server URL for visual preview.
Key Decision Points (Wait for User)
1. After Phase 1: Confirm site purpose, framework, and project location 2. After Phase 4: Approve implementation plan 3. After Phase 7: Accept site or request changes 4. At Phase 8: Deploy or skip
Progress Tracking
Before starting Phase 1, create a task list with all phases using TaskCreate:
| Task subject | activeForm | Description |
|---|---|---|
| Discover site requirements | Discovering requirements | Collect site name, framework, purpose, audience, and project location |
| Scaffold and launch dev server | Scaffolding project | Copy template, replace placeholders with defaults, git init, npm install, start dev server, share URL |
| Plan site components | Planning components | Determine pages, components, design direction, and routes while user previews scaffold |
| Approve implementation plan | Getting plan approval | Present implementation plan covering design and pages, get user approval |
| Implement pages and components | Building site | Apply chosen design tokens, create all pages, components, routing, navigation |
| Verify accessibility with axe-core | Verifying accessibility | Run axe-core on every page, fix all critical/serious violations, re-verify until passing |
| Review with user | Reviewing site | Navigate all pages, share URL, get user feedback, apply changes |
| Deploy and wrap up | Deploying site | Ask about deployment, present summary, suggest next steps |
Mark each task in_progress when starting it and completed when done via TaskUpdate. This gives the user visibility into progress and keeps the workflow deterministic.
Quality Standards
Every site must meet these standards before completion:
- Distinctive typography via Google Fonts (no generic Inter/Roboto/Arial)
- Cohesive color palette via CSS variables
- Motion/animations (page transitions, hover states)
- All requested pages and features implemented (not placeholders)
- All routes working and navigation complete
- Accessibility verified via axe-core — zero critical/serious violations on all pages
- Git commits at key milestones
- Verified via Playwright
- User reviewed and approved
- Deployment offered
---
Example Workflow
User Request
"Create a partner portal for our consultants"
Phase 1: Discovery
- Name: Partner Portal
- Framework: React
- Purpose: Company Portal
- Audience: Internal (partners, consultants)
- Location: New folder
partner-portalin current directory
Phase 2: Scaffold & Launch
- React template copied, default placeholders replaced
- Git initialized, npm installed, dev server running at
http://localhost:5173 - Playwright verified scaffold loads
- URL shared with user — they can preview immediately
Phase 3: Component Planning
- Features: Consultant Directory, Project Tracker, Document Library, Announcements
- Aesthetic: Minimal & Clean
- Mood: Professional & Trustworthy
- Component table presented and approved
- Design choices made: DM Sans + Space Grotesk,
#1e3a5fprimary, blue-gray palette
Phase 4: Plan Approval
- Plan data assembled as a single JSON object
- Rendered to
docs/create-site-plan.htmlviarender-createsite-plan.js - Opened in the user's default browser
- Brief summary shown in terminal with a pointer to the browser tab
- User approved via AskUserQuestion
Phase 5: Implementation
- Todos created for each page, component, routing, navigation, design foundations
- Built in order: design tokens (replace defaults with chosen palette) → shared components → pages → router → nav
- Git commits after each major piece
- Playwright verified each page
Phase 6: Accessibility Verification
- axe-core injected and run on all 4 pages via
browser_evaluate - Found 5 violations: 2 missing alt text, 1 insufficient contrast, 1 missing lang attribute, 1 skipped heading level
- All violations fixed in source code and committed
- Re-run confirmed zero critical/serious violations across all pages
Phase 7: Review
- Summary table presented
- User reviewed at
http://localhost:5173, requested minor color adjustment - Adjustment applied, re-verified
Phase 8: Deploy
- User chose to deploy → invoked
/deploy-site - Final summary presented with next step suggestions
---
Begin with Phase 1: Discovery
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"__SITE_SLUG__": {
"projectType": "application",
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:application",
"options": {
"outputPath": "dist/__SITE_SLUG__",
"index": "src/index.html",
"browser": "src/main.ts",
"tsConfig": "tsconfig.app.json",
"assets": [
{ "glob": "**/*", "input": "public" }
],
"styles": ["src/styles.css"]
},
"configurations": {
"production": {
"budgets": [
{ "type": "initial", "maximumWarning": "500kB", "maximumError": "1MB" },
{ "type": "anyComponentStyle", "maximumWarning": "4kB", "maximumError": "8kB" }
],
"outputHashing": "all"
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": { "buildTarget": "__SITE_SLUG__:build:production" },
"development": { "buildTarget": "__SITE_SLUG__:build:development" }
},
"defaultConfiguration": "development"
}
}
}
}
}
# Dependencies
node_modules
# Build output
dist
tmp
out-tsc
# Angular
.angular/cache
# TypeScript
*.tsbuildinfo
# Environment variables
.env
.env.local
.env*.local
# Logs
*.log
npm-debug.log*
yarn-debug.log*
pnpm-debug.log*
# Testing
coverage
# Editor
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Miscellaneous
.sass-cache
testem.log
.playwright-mcp/
# OS
.DS_Store
Thumbs.db
{
"name": "__SITE_SLUG__",
"private": true,
"version": "0.0.0",
"scripts": {
"dev": "ng serve",
"build": "ng build",
"watch": "ng build --watch"
},
"dependencies": {
"@angular/animations": "^19.1.0",
"@angular/common": "^19.1.0",
"@angular/compiler": "^19.1.0",
"@angular/core": "^19.1.0",
"@angular/forms": "^19.1.0",
"@angular/platform-browser": "^19.1.0",
"@angular/platform-browser-dynamic": "^19.1.0",
"@angular/router": "^19.1.0",
"rxjs": "~7.8.0",
"tslib": "^2.6.0",
"zone.js": "~0.15.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "^19.1.0",
"@angular/cli": "^19.1.0",
"@angular/compiler-cli": "^19.1.0",
"typescript": "~5.7.0"
}
}
{
"$schema": "https://www.schemastore.org/powerpages.config.json",
"siteName": "__SITE_NAME__",
"compiledPath": "dist/__SITE_SLUG__/browser",
"defaultLandingPage": "index.html"
}
import { Component } from '@angular/core'
import { LayoutComponent } from './components/layout.component'
import { RouterOutlet } from '@angular/router'
@Component({
selector: 'app-root',
standalone: true,
imports: [LayoutComponent, RouterOutlet],
template: `
<app-layout>
<router-outlet />
</app-layout>
`,
})
export class AppComponent {}
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core'
import { provideRouter } from '@angular/router'
import { routes } from './app.routes'
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
],
}
import { Routes } from '@angular/router'
import { HomeComponent } from './pages/home.component'
import { AboutComponent } from './pages/about.component'
export const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'about', component: AboutComponent },
]
import { Component } from '@angular/core'
@Component({
selector: 'app-layout',
standalone: true,
template: '<ng-content />',
})
export class LayoutComponent {}
import { Component } from '@angular/core'
@Component({
selector: 'app-about',
standalone: true,
template: '',
})
export class AboutComponent {}
import { Component, AfterViewInit, OnDestroy, ViewEncapsulation } from '@angular/core'
@Component({
selector: 'app-home',
standalone: true,
encapsulation: ViewEncapsulation.None,
styles: [`
:root {
--pp-violet: #7B5EA7;
--pp-purple: #8B6FC0;
--pp-lavender: #A78BDB;
--pp-periwinkle: #8088D9;
--pp-sky: #5A9BD5;
--pp-deep-violet: #5C3D8F;
--pp-bg: #F7F5FA;
--pp-surface: #FFFFFF;
--pp-text: #2D1B4E;
--pp-text-secondary: #6B5A82;
--pp-text-muted: #9B8FB5;
--pp-border: rgba(139, 111, 192, 0.12);
--pp-glow: rgba(139, 111, 192, 0.2);
}
html, body { overflow: hidden; background: var(--pp-bg); color: var(--pp-text); }
.loading-wrapper { position: fixed; inset: 0; display: flex; align-items: center; justify-content: center; background: var(--pp-bg); }
.ambient { position: fixed; inset: 0; z-index: 0; overflow: hidden; }
.ambient::before {
content: ''; position: absolute; width: 160%; height: 160%; top: -30%; left: -30%;
background:
radial-gradient(ellipse 600px 600px at 25% 30%, rgba(167, 139, 219, 0.12) 0%, transparent 70%),
radial-gradient(ellipse 500px 500px at 75% 70%, rgba(90, 155, 213, 0.08) 0%, transparent 70%),
radial-gradient(ellipse 800px 400px at 50% 50%, rgba(139, 111, 192, 0.06) 0%, transparent 60%);
animation: ambientDrift 25s ease-in-out infinite alternate;
}
@keyframes ambientDrift {
0% { transform: translate(0, 0) scale(1); }
50% { transform: translate(-3%, 2%) scale(1.04); }
100% { transform: translate(2%, -1%) scale(0.98); }
}
.particles { position: fixed; inset: 0; z-index: 1; pointer-events: none; }
.particle { position: absolute; border-radius: 50%; opacity: 0; animation: particleFloat linear infinite; }
@keyframes particleFloat {
0% { transform: translateY(100vh) scale(0); opacity: 0; }
10% { opacity: 0.6; }
90% { opacity: 0.6; }
100% { transform: translateY(-10vh) scale(1); opacity: 0; }
}
.grid-overlay {
position: fixed; inset: 0; z-index: 1;
background-image: linear-gradient(rgba(139, 111, 192, 0.04) 1px, transparent 1px), linear-gradient(90deg, rgba(139, 111, 192, 0.04) 1px, transparent 1px);
background-size: 60px 60px; animation: gridPulse 8s ease-in-out infinite alternate;
}
@keyframes gridPulse { 0% { opacity: 0.3; } 100% { opacity: 0.7; } }
.center-stage { position: relative; z-index: 10; display: flex; flex-direction: column; align-items: center; gap: 36px; padding: 40px 40px 48px; max-height: 100vh; overflow-y: auto; overflow-x: hidden; scrollbar-width: none; }
.center-stage::-webkit-scrollbar { display: none; }
.orbit-system { position: relative; width: 220px; height: 220px; display: flex; align-items: center; justify-content: center; }
.core-shape { position: absolute; width: 120px; height: 120px; z-index: 5; background: url('/power-pages-icon.png') center/contain no-repeat; animation: coreFloat 4s ease-in-out infinite; }
@keyframes coreFloat { 0%, 100% { transform: translateY(0) scale(1); } 50% { transform: translateY(-3px) scale(1.025); } }
.core-glow { position: absolute; width: 130px; height: 130px; border-radius: 50%; background: radial-gradient(circle, rgba(167, 139, 219, 0.18) 0%, transparent 70%); z-index: 3; animation: coreGlow 3s ease-in-out infinite alternate; filter: blur(25px); }
@keyframes coreGlow { 0% { transform: scale(1); opacity: 0.5; } 100% { transform: scale(1.5); opacity: 0.9; } }
.orbit-ring { position: absolute; border: 1px solid rgba(139, 111, 192, 0.15); border-radius: 50%; animation: ringRotate linear infinite; }
.orbit-ring:nth-child(1) { width: 160px; height: 160px; animation-duration: 12s; }
.orbit-ring:nth-child(2) { width: 200px; height: 200px; animation-duration: 18s; animation-direction: reverse; border-color: rgba(90, 155, 213, 0.12); }
.orbit-ring:nth-child(3) { width: 240px; height: 240px; animation-duration: 25s; border-style: dashed; border-color: rgba(139, 111, 192, 0.08); }
@keyframes ringRotate { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
.orbit-dot { position: absolute; width: 8px; height: 8px; border-radius: 50%; z-index: 6; }
.orbit-dot::after { content: ''; position: absolute; inset: -4px; border-radius: 50%; filter: blur(6px); }
.orbit-dot:nth-child(4) { background: var(--pp-lavender); box-shadow: 0 0 10px rgba(167, 139, 219, 0.5); animation: orbitA 12s linear infinite; }
.orbit-dot:nth-child(4)::after { background: rgba(167, 139, 219, 0.4); }
.orbit-dot:nth-child(5) { background: var(--pp-sky); box-shadow: 0 0 10px rgba(90, 155, 213, 0.5); width: 6px; height: 6px; animation: orbitB 18s linear infinite reverse; }
.orbit-dot:nth-child(5)::after { background: rgba(90, 155, 213, 0.4); }
.orbit-dot:nth-child(6) { background: var(--pp-periwinkle); box-shadow: 0 0 8px rgba(128, 136, 217, 0.5); width: 5px; height: 5px; animation: orbitC 25s linear infinite; }
.orbit-dot:nth-child(6)::after { background: rgba(128, 136, 217, 0.4); }
@keyframes orbitA { from { transform: rotate(0deg) translateX(80px) rotate(0deg); } to { transform: rotate(360deg) translateX(80px) rotate(-360deg); } }
@keyframes orbitB { from { transform: rotate(0deg) translateX(100px) rotate(0deg); } to { transform: rotate(360deg) translateX(100px) rotate(-360deg); } }
@keyframes orbitC { from { transform: rotate(0deg) translateX(120px) rotate(0deg); } to { transform: rotate(360deg) translateX(120px) rotate(-360deg); } }
.text-content { text-align: center; display: flex; flex-direction: column; align-items: center; gap: 20px; }
.brand-title { font-family: 'Outfit', sans-serif; font-weight: 300; font-size: 14px; letter-spacing: 4px; text-transform: uppercase; color: var(--pp-violet); opacity: 0; animation: fadeSlideUp 1s ease-out 0.5s forwards; }
.main-heading { font-family: 'Outfit', sans-serif; font-weight: 600; font-size: 32px; line-height: 1.3; background: linear-gradient(135deg, var(--pp-deep-violet) 0%, var(--pp-violet) 40%, var(--pp-sky) 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; opacity: 0; animation: fadeSlideUp 1s ease-out 0.8s forwards; }
.status-area { height: 52px; display: flex; flex-direction: column; align-items: center; justify-content: center; position: relative; overflow: hidden; opacity: 0; animation: fadeSlideUp 1s ease-out 1.2s forwards; }
.status-message { position: absolute; white-space: nowrap; font-size: 15px; font-weight: 400; color: var(--pp-text-secondary); display: flex; align-items: center; gap: 10px; opacity: 0; transform: translateY(20px); transition: all 0.7s cubic-bezier(0.22, 1, 0.36, 1); }
.status-message.active { opacity: 1; transform: translateY(0); }
.status-message.exiting { opacity: 0; transform: translateY(-20px); }
.status-icon { width: 20px; height: 20px; border-radius: 50%; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.status-icon.working { border: 2px solid rgba(139, 111, 192, 0.2); border-top-color: var(--pp-violet); animation: spin 1s linear infinite; }
.status-icon.done { background: rgba(34, 158, 92, 0.1); border: none; }
.status-icon.done::after { content: '\\2713'; font-size: 11px; color: #229E5C; font-weight: 600; }
@keyframes spin { to { transform: rotate(360deg); } }
.progress-container { width: 320px; opacity: 0; animation: fadeSlideUp 1s ease-out 1.5s forwards; display: flex; flex-direction: column; align-items: center; gap: 14px; }
.progress-track { width: 100%; height: 3px; background: rgba(139, 111, 192, 0.1); border-radius: 4px; overflow: hidden; position: relative; }
.progress-fill { height: 100%; width: 40%; border-radius: 4px; background: linear-gradient(90deg, transparent, var(--pp-violet), var(--pp-lavender), var(--pp-sky), transparent); position: absolute; top: 0; left: -40%; animation: infiniteSlide 2s cubic-bezier(0.4, 0, 0.2, 1) infinite; }
.progress-fill::after { content: ''; position: absolute; right: 10%; top: -3px; width: 8px; height: 8px; border-radius: 50%; background: var(--pp-sky); box-shadow: 0 0 8px rgba(90, 155, 213, 0.5), 0 0 16px rgba(90, 155, 213, 0.2); opacity: 0.9; }
@keyframes infiniteSlide { 0% { left: -40%; } 100% { left: 100%; } }
.progress-track.complete .progress-fill { width: 100%; left: 0; animation: none; background: linear-gradient(90deg, var(--pp-violet), var(--pp-lavender), var(--pp-sky)); transition: all 0.8s ease; }
.progress-track.complete .progress-fill::after { display: none; }
.progress-label-text { font-size: 12px; color: var(--pp-text-muted); font-family: 'DM Sans', sans-serif; letter-spacing: 0.3px; }
.feature-cards { display: flex; gap: 16px; margin-top: 16px; opacity: 0; animation: fadeSlideUp 1s ease-out 2s forwards; }
.feature-card { background: var(--pp-surface); border: 1px solid rgba(139, 111, 192, 0.1); border-radius: 16px; padding: 20px 22px; width: 150px; box-shadow: 0 2px 12px rgba(139, 111, 192, 0.06), 0 1px 3px rgba(0, 0, 0, 0.04); opacity: 0; transform: translateY(20px) scale(0.95); transition: all 0.8s cubic-bezier(0.22, 1, 0.36, 1); }
.feature-card.visible { opacity: 1; transform: translateY(0) scale(1); }
.feature-card:hover { border-color: rgba(139, 111, 192, 0.2); box-shadow: 0 4px 24px rgba(139, 111, 192, 0.12), 0 2px 6px rgba(0, 0, 0, 0.04); transform: translateY(-2px) scale(1.02); }
.feature-card-icon { font-size: 24px; margin-bottom: 10px; display: block; }
.feature-card-label { font-family: 'Outfit', sans-serif; font-size: 13px; font-weight: 500; color: var(--pp-text-secondary); line-height: 1.4; }
.connector-lines { position: fixed; inset: 0; z-index: 2; pointer-events: none; overflow: hidden; }
.connector { position: absolute; height: 1px; background: linear-gradient(90deg, transparent, rgba(139, 111, 192, 0.12), transparent); animation: connectorSweep 6s ease-in-out infinite; }
@keyframes connectorSweep { 0%, 100% { transform: translateX(-100%); opacity: 0; } 20% { opacity: 1; } 80% { opacity: 1; } 100% { transform: translateX(100vw); opacity: 0; } }
.scanline { position: fixed; left: 0; right: 0; height: 1px; background: linear-gradient(90deg, transparent 0%, var(--pp-lavender) 50%, transparent 100%); opacity: 0.06; z-index: 2; animation: scanDrop 8s linear infinite; pointer-events: none; }
@keyframes scanDrop { 0% { top: -2px; } 100% { top: 100%; } }
.tip-area { text-align: center; margin-top: 12px; opacity: 0; animation: fadeSlideUp 1s ease-out 3s forwards; }
.tip-label { font-size: 10px; letter-spacing: 2px; text-transform: uppercase; color: var(--pp-text-muted); margin-bottom: 8px; font-family: 'Outfit', sans-serif; }
.tip-text { font-size: 13px; color: var(--pp-text-muted); max-width: 380px; line-height: 1.6; margin: 0 auto; min-height: 42px; transition: opacity 0.6s ease, transform 0.6s ease; }
@keyframes fadeSlideUp { from { opacity: 0; transform: translateY(16px); } to { opacity: 1; transform: translateY(0); } }
.center-stage.complete .main-heading { animation: completePulse 2s ease-in-out infinite alternate; }
@keyframes completePulse { 0% { filter: brightness(1); } 100% { filter: brightness(1.15); } }
.input-banner { position: fixed; top: 24px; right: 24px; z-index: 100; display: flex; align-items: center; gap: 12px; background: linear-gradient(135deg, #FFF4D6 0%, #FFE9B3 100%); border: 1px solid #F5B800; color: #5C3D00; padding: 12px 14px 12px 22px; border-radius: 999px; font-family: 'Outfit', sans-serif; font-size: 14px; font-weight: 500; box-shadow: 0 4px 16px rgba(245, 184, 0, 0.25); max-width: min(360px, calc(100vw - 48px)); animation: bannerEnter 0.4s cubic-bezier(0.22, 1, 0.36, 1); }
.input-banner[hidden] { display: none; }
.input-banner-icon { font-size: 18px; animation: bannerPulse 1.5s ease-in-out infinite; flex-shrink: 0; }
.input-banner-text { line-height: 1.4; flex: 1; min-width: 0; }
.input-banner-text b { font-weight: 600; display: block; letter-spacing: 0.3px; }
.input-banner-text small { display: block; font-size: 12px; font-weight: 400; opacity: 0.8; margin-top: 2px; }
.input-banner-close { width: 24px; height: 24px; border: none; border-radius: 50%; background: rgba(92, 61, 0, 0.12); color: #5C3D00; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 18px; line-height: 1; flex-shrink: 0; transition: background 0.2s ease, transform 0.2s ease; }
.input-banner-close:hover { background: rgba(92, 61, 0, 0.2); transform: scale(1.05); }
@keyframes bannerPulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.2); } }
@keyframes bannerEnter { from { opacity: 0; transform: translateY(-12px); } to { opacity: 1; transform: translateY(0); } }
@media (max-width: 600px) { .main-heading { font-size: 24px; } .progress-container { width: 260px; } .feature-cards { flex-direction: column; align-items: center; } .orbit-system { width: 180px; height: 180px; } .input-banner { top: 12px; right: 12px; padding: 10px 12px 10px 16px; font-size: 13px; max-width: calc(100vw - 24px); } }
`],
template: `
<div class="loading-wrapper">
<div class="ambient"></div>
<div class="grid-overlay"></div>
<div class="scanline"></div>
<div class="particles" id="particles"></div>
<div class="connector-lines" id="connectors"></div>
<div class="input-banner" id="inputBanner" hidden>
<span class="input-banner-icon">⚠️</span>
<div class="input-banner-text">
<b>Waiting for your input</b>
<small id="inputBannerPrompt">Please check your terminal to respond.</small>
</div>
<button type="button" class="input-banner-close" id="inputBannerClose" aria-label="Dismiss notification">×</button>
</div>
<div class="center-stage" id="centerStage">
<div class="orbit-system">
<div class="core-glow"></div>
<div class="core-shape">
</div>
<div class="orbit-ring"></div>
<div class="orbit-ring"></div>
<div class="orbit-ring"></div>
<div class="orbit-dot"></div>
<div class="orbit-dot"></div>
<div class="orbit-dot"></div>
</div>
<div class="text-content">
<div class="brand-title">Power Pages</div>
<div class="main-heading" id="mainHeading">Building __SITE_NAME__</div>
<div class="status-area" id="statusArea"></div>
<div class="progress-container">
<div class="progress-track" id="progressTrack">
<div class="progress-fill"></div>
</div>
<span class="progress-label-text" id="progressLabel">Initializing...</span>
</div>
</div>
<div class="feature-cards">
<div class="feature-card" data-delay="0">
<span class="feature-card-icon">🔒</span>
<span class="feature-card-label">Enterprise-grade security</span>
</div>
<div class="feature-card" data-delay="400">
<span class="feature-card-icon">⚡</span>
<span class="feature-card-label">Lightning-fast performance</span>
</div>
<div class="feature-card" data-delay="800">
<span class="feature-card-icon">🌐</span>
<span class="feature-card-label">Ready to scale globally</span>
</div>
</div>
<div class="tip-area">
<div class="tip-label">Did you know</div>
<div class="tip-text" id="tipText"></div>
</div>
</div>
</div>
`,
})
export class HomeComponent implements AfterViewInit, OnDestroy {
private intervals: number[] = []
private timeouts: number[] = []
private inputBannerCloseCleanup: (() => void) | null = null
ngAfterViewInit() {
const particlesEl = document.getElementById('particles')
const connectorsEl = document.getElementById('connectors')
const statusArea = document.getElementById('statusArea')
const progressLabel = document.getElementById('progressLabel')
const tipTextEl = document.getElementById('tipText')
const inputBannerClose = document.getElementById('inputBannerClose')
// Particles
const createParticle = () => {
if (!particlesEl) return
const p = document.createElement('div')
p.classList.add('particle')
const size = Math.random() * 3 + 1
const colors = ['rgba(167,139,219,0.5)', 'rgba(128,136,217,0.5)', 'rgba(90,155,213,0.5)', 'rgba(139,111,192,0.5)']
const color = colors[Math.floor(Math.random() * colors.length)]
p.style.cssText = `width:${size}px;height:${size}px;left:${Math.random() * 100}%;background:${color};box-shadow:0 0 ${size * 2}px ${color};animation-duration:${Math.random() * 15 + 12}s;animation-delay:${Math.random() * 10}s;`
particlesEl.appendChild(p)
this.timeouts.push(window.setTimeout(() => p.remove(), 30000))
}
for (let i = 0; i < 30; i++) this.timeouts.push(window.setTimeout(() => createParticle(), i * 200))
this.intervals.push(window.setInterval(createParticle, 800))
// Connectors
const createConnector = () => {
if (!connectorsEl) return
const c = document.createElement('div')
c.classList.add('connector')
c.style.cssText = `top:${Math.random() * 100}%;width:${Math.random() * 300 + 200}px;animation-duration:${Math.random() * 4 + 4}s;animation-delay:${Math.random() * 6}s;`
connectorsEl.appendChild(c)
this.timeouts.push(window.setTimeout(() => c.remove(), 12000))
}
for (let i = 0; i < 8; i++) this.timeouts.push(window.setTimeout(() => createConnector(), i * 1500))
this.intervals.push(window.setInterval(createConnector, 2000))
// Status messages
const statuses = [
{ text: 'Setting up your workspace', duration: 8000 },
{ text: 'Installing dependencies', duration: 10000 },
{ text: 'Configuring build tools', duration: 12000 },
{ text: 'Loading design system', duration: 14000 },
{ text: 'Preparing components', duration: 10000 },
{ text: 'Building page templates', duration: 8000 },
{ text: 'Setting up routing', duration: 12000 },
{ text: 'Applying design tokens', duration: 10000 },
{ text: 'Optimizing assets', duration: 10000 },
{ text: 'Running final checks', duration: 12000 },
{ text: 'Polishing details', duration: 8000 },
{ text: 'Almost there...', duration: 6000 },
]
const phaseLabels = [
'Getting started...', 'Setting up infrastructure...', 'Building your pages...',
'Configuring features...', 'Optimizing & securing...', 'Final steps...',
]
let currentStatusIndex = 0
let liveOverride = false
let lastLiveMessage: string | null = null
let lastAwaiting = false
let lastPrompt: string | null = null
let dismissedPrompt: string | null = null
const dismissInputBanner = () => {
const banner = document.getElementById('inputBanner')
dismissedPrompt = lastPrompt || 'Please check your terminal to respond.'
if (banner) banner.hidden = true
}
inputBannerClose?.addEventListener('click', dismissInputBanner)
this.inputBannerCloseCleanup = () => inputBannerClose?.removeEventListener('click', dismissInputBanner)
const renderStatusMessage = (text: string) => {
if (!statusArea) return
const existing = statusArea.querySelector('.status-message')
if (existing) {
existing.classList.remove('active')
existing.classList.add('exiting')
this.timeouts.push(window.setTimeout(() => existing.remove(), 700))
}
const msg = document.createElement('div')
msg.classList.add('status-message')
const icon = document.createElement('div')
icon.classList.add('status-icon', 'working')
msg.appendChild(icon)
const textEl = document.createElement('span')
textEl.textContent = text
msg.appendChild(textEl)
statusArea.appendChild(msg)
requestAnimationFrame(() => requestAnimationFrame(() => msg.classList.add('active')))
}
const updatePhaseLabel = (text: string) => {
if (progressLabel) progressLabel.textContent = text
}
const showStatus = (index: number) => {
renderStatusMessage(statuses[index % statuses.length].text)
if (liveOverride) return
const phaseIndex = Math.min(Math.floor((index / statuses.length) * phaseLabels.length), phaseLabels.length - 1)
updatePhaseLabel(phaseLabels[phaseIndex])
}
const advanceStatus = () => {
showStatus(currentStatusIndex % statuses.length)
const duration = statuses[currentStatusIndex % statuses.length].duration
this.timeouts.push(window.setTimeout(() => {
currentStatusIndex++
advanceStatus()
}, duration))
}
this.timeouts.push(window.setTimeout(() => advanceStatus(), 2000))
const pollStatus = async () => {
const banner = document.getElementById('inputBanner') as HTMLElement | null
const promptEl = document.getElementById('inputBannerPrompt')
try {
const res = await fetch('/scaffold-status.json?t=' + Date.now(), { cache: 'no-store' })
if (!res.ok) throw new Error('no status')
const data = await res.json() as { message?: string; awaitingInput?: boolean; inputPrompt?: string }
if (data.message) {
liveOverride = true
if (data.message !== lastLiveMessage) {
lastLiveMessage = data.message
updatePhaseLabel(data.message)
}
} else {
if (liveOverride) {
const phaseIndex = Math.min(Math.floor((currentStatusIndex / statuses.length) * phaseLabels.length), phaseLabels.length - 1)
updatePhaseLabel(phaseLabels[phaseIndex])
}
liveOverride = false
lastLiveMessage = null
}
const awaiting = !!data.awaitingInput
const prompt = data.inputPrompt || 'Please check your terminal to respond.'
const awaitingChanged = awaiting !== lastAwaiting
const promptChanged = prompt !== lastPrompt
if (!awaiting) dismissedPrompt = null
if (awaitingChanged || promptChanged) {
lastAwaiting = awaiting
lastPrompt = prompt
if (promptEl) promptEl.textContent = prompt
}
if (banner) banner.hidden = !awaiting || dismissedPrompt === prompt
} catch {
if (liveOverride) {
const phaseIndex = Math.min(Math.floor((currentStatusIndex / statuses.length) * phaseLabels.length), phaseLabels.length - 1)
updatePhaseLabel(phaseLabels[phaseIndex])
}
liveOverride = false
lastLiveMessage = null
if (!lastAwaiting) {
lastPrompt = null
if (banner) banner.hidden = true
}
}
}
pollStatus()
this.intervals.push(window.setInterval(pollStatus, 1500))
// Feature cards
this.timeouts.push(window.setTimeout(() => {
document.querySelectorAll('.feature-card').forEach(card => {
const delay = parseInt((card as HTMLElement).dataset['delay'] || '0')
this.timeouts.push(window.setTimeout(() => card.classList.add('visible'), delay))
})
}, 2500))
// Tips
const tips = [
'Power Pages sites are built on Microsoft Dataverse, giving you enterprise-level data capabilities from day one.',
'Your site automatically includes responsive design \u2014 it looks great on phones, tablets, and desktops.',
'With role-based security, you can control exactly who sees what on your site.',
'Power Pages integrates seamlessly with Power Automate, Power BI, and the entire Microsoft ecosystem.',
'You can extend your site with custom code, and JavaScript for unlimited flexibility.',
'Built-in content delivery networks ensure your pages load fast for users worldwide.',
'Multi-language support lets you reach audiences across the globe with localized content.',
]
let tipIndex = 0
const showTip = () => {
if (!tipTextEl) return
tipTextEl.style.opacity = '0'
tipTextEl.style.transform = 'translateY(8px)'
this.timeouts.push(window.setTimeout(() => {
tipTextEl.textContent = tips[tipIndex % tips.length]
tipTextEl.style.opacity = '1'
tipTextEl.style.transform = 'translateY(0)'
tipIndex++
}, 500))
}
this.timeouts.push(window.setTimeout(() => showTip(), 3500))
this.intervals.push(window.setInterval(showTip, 12000))
}
ngOnDestroy() {
this.inputBannerCloseCleanup?.()
this.intervals.forEach(id => clearInterval(id))
this.timeouts.forEach(id => clearTimeout(id))
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>__SITE_NAME__</title>
<meta name="description" content="__SITE_DESCRIPTION__" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;0,9..40,600;1,9..40,300&family=Outfit:wght@200;300;400;500;600;700&display=swap" rel="stylesheet" />
</head>
<body>
<app-root></app-root>
</body>
</html>
import { bootstrapApplication } from '@angular/platform-browser'
import { AppComponent } from './app/app.component'
import { appConfig } from './app/app.config'
bootstrapApplication(AppComponent, appConfig)
/* Power Pages — Site Theme
This file is populated during site implementation with your chosen design tokens,
typography, and color palette. */
*,*::before,*::after { margin: 0; padding: 0; box-sizing: border-box; }
html, body {
width: 100%; height: 100%;
margin: 0; padding: 0;
font-family: 'DM Sans', sans-serif;
}
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app"
},
"files": ["src/main.ts"],
"include": ["src/**/*.d.ts"]
}
{
"compileOnSave": false,
"compilerOptions": {
"outDir": "./dist/out-tsc",
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"sourceMap": true,
"declaration": false,
"downlevelIteration": true,
"experimentalDecorators": true,
"moduleResolution": "bundler",
"importHelpers": true,
"target": "ES2022",
"module": "ES2022",
"lib": ["ES2022", "dom"],
"skipLibCheck": true
}
}
import { defineConfig } from 'astro/config'
export default defineConfig({
output: 'static',
build: {
format: 'directory',
},
})
# Dependencies
node_modules
# Build output
dist
# Astro
.astro
# TypeScript
*.tsbuildinfo
# Environment variables
.env
.env.local
.env*.local
# Logs
*.log
npm-debug.log*
yarn-debug.log*
pnpm-debug.log*
# Testing
coverage
# Editor
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.playwright-mcp/
# OS
.DS_Store
Thumbs.db
{
"name": "__SITE_SLUG__",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview"
},
"dependencies": {
"astro": "^6.1.6"
}
}
{
"$schema": "https://www.schemastore.org/powerpages.config.json",
"siteName": "__SITE_NAME__",
"compiledPath": "dist",
"defaultLandingPage": "index.html"
}
<!-- Scaffold placeholder — replaced during site implementation -->
<!-- Scaffold placeholder — replaced during site implementation -->
---
import { ClientRouter } from 'astro:transitions'
import '../styles/theme.css'
interface Props {
title: string
}
const { title } = Astro.props
const pageTitle = title === 'Home' ? '__SITE_NAME__' : `${title} | __SITE_NAME__`
---
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{pageTitle}</title>
<meta name="description" content="__SITE_DESCRIPTION__" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;0,9..40,600;1,9..40,300&family=Outfit:wght@200;300;400;500;600;700&display=swap" rel="stylesheet" />
<ClientRouter />
</head>
<body>
<slot />
</body>
</html>
---
import Layout from '../layouts/Layout.astro'
---
<Layout title="About">
</Layout>
---
import Layout from '../layouts/Layout.astro'
---
<Layout title="Home">
<div class="loading-wrapper">
<div class="ambient"></div>
<div class="grid-overlay"></div>
<div class="scanline"></div>
<div class="particles" id="particles"></div>
<div class="connector-lines" id="connectors"></div>
<div class="input-banner" id="inputBanner" hidden>
<span class="input-banner-icon">⚠️</span>
<div class="input-banner-text">
<b>Waiting for your input</b>
<small id="inputBannerPrompt">Please check your terminal to respond.</small>
</div>
<button type="button" class="input-banner-close" id="inputBannerClose" aria-label="Dismiss notification">×</button>
</div>
<div class="center-stage" id="centerStage">
<div class="orbit-system">
<div class="core-glow"></div>
<div class="core-shape">
</div>
<div class="orbit-ring"></div>
<div class="orbit-ring"></div>
<div class="orbit-ring"></div>
<div class="orbit-dot"></div>
<div class="orbit-dot"></div>
<div class="orbit-dot"></div>
</div>
<div class="text-content">
<div class="brand-title">Power Pages</div>
<div class="main-heading" id="mainHeading">Building __SITE_NAME__</div>
<div class="status-area" id="statusArea"></div>
<div class="progress-container">
<div class="progress-track" id="progressTrack">
<div class="progress-fill"></div>
</div>
<span class="progress-label-text" id="progressLabel">Initializing...</span>
</div>
</div>
<div class="feature-cards">
<div class="feature-card" data-delay="0">
<span class="feature-card-icon">🔒</span>
<span class="feature-card-label">Enterprise-grade security</span>
</div>
<div class="feature-card" data-delay="400">
<span class="feature-card-icon">⚡</span>
<span class="feature-card-label">Lightning-fast performance</span>
</div>
<div class="feature-card" data-delay="800">
<span class="feature-card-icon">🌐</span>
<span class="feature-card-label">Ready to scale globally</span>
</div>
</div>
<div class="tip-area">
<div class="tip-label">Did you know</div>
<div class="tip-text" id="tipText"></div>
</div>
</div>
</div>
</Layout>
<style is:global>
:root {
--pp-violet: #7B5EA7;
--pp-purple: #8B6FC0;
--pp-lavender: #A78BDB;
--pp-periwinkle: #8088D9;
--pp-sky: #5A9BD5;
--pp-deep-violet: #5C3D8F;
--pp-bg: #F7F5FA;
--pp-surface: #FFFFFF;
--pp-text: #2D1B4E;
--pp-text-secondary: #6B5A82;
--pp-text-muted: #9B8FB5;
--pp-border: rgba(139, 111, 192, 0.12);
--pp-glow: rgba(139, 111, 192, 0.2);
}
html, body { overflow: hidden; background: var(--pp-bg); color: var(--pp-text); }
.loading-wrapper { position: fixed; inset: 0; display: flex; align-items: center; justify-content: center; background: var(--pp-bg); }
.ambient { position: fixed; inset: 0; z-index: 0; overflow: hidden; }
.ambient::before {
content: ''; position: absolute; width: 160%; height: 160%; top: -30%; left: -30%;
background:
radial-gradient(ellipse 600px 600px at 25% 30%, rgba(167, 139, 219, 0.12) 0%, transparent 70%),
radial-gradient(ellipse 500px 500px at 75% 70%, rgba(90, 155, 213, 0.08) 0%, transparent 70%),
radial-gradient(ellipse 800px 400px at 50% 50%, rgba(139, 111, 192, 0.06) 0%, transparent 60%);
animation: ambientDrift 25s ease-in-out infinite alternate;
}
@keyframes ambientDrift {
0% { transform: translate(0, 0) scale(1); }
50% { transform: translate(-3%, 2%) scale(1.04); }
100% { transform: translate(2%, -1%) scale(0.98); }
}
.particles { position: fixed; inset: 0; z-index: 1; pointer-events: none; }
.particle { position: absolute; border-radius: 50%; opacity: 0; animation: particleFloat linear infinite; }
@keyframes particleFloat {
0% { transform: translateY(100vh) scale(0); opacity: 0; }
10% { opacity: 0.6; }
90% { opacity: 0.6; }
100% { transform: translateY(-10vh) scale(1); opacity: 0; }
}
.grid-overlay {
position: fixed; inset: 0; z-index: 1;
background-image: linear-gradient(rgba(139, 111, 192, 0.04) 1px, transparent 1px), linear-gradient(90deg, rgba(139, 111, 192, 0.04) 1px, transparent 1px);
background-size: 60px 60px; animation: gridPulse 8s ease-in-out infinite alternate;
}
@keyframes gridPulse { 0% { opacity: 0.3; } 100% { opacity: 0.7; } }
.center-stage { position: relative; z-index: 10; display: flex; flex-direction: column; align-items: center; gap: 36px; padding: 40px 40px 48px; max-height: 100vh; overflow-y: auto; overflow-x: hidden; scrollbar-width: none; }
.center-stage::-webkit-scrollbar { display: none; }
.orbit-system { position: relative; width: 220px; height: 220px; display: flex; align-items: center; justify-content: center; }
.core-shape { position: absolute; width: 120px; height: 120px; z-index: 5; background: url('/power-pages-icon.png') center/contain no-repeat; animation: coreFloat 4s ease-in-out infinite; }
@keyframes coreFloat { 0%, 100% { transform: translateY(0) scale(1); } 50% { transform: translateY(-3px) scale(1.025); } }
.core-glow { position: absolute; width: 130px; height: 130px; border-radius: 50%; background: radial-gradient(circle, rgba(167, 139, 219, 0.18) 0%, transparent 70%); z-index: 3; animation: coreGlow 3s ease-in-out infinite alternate; filter: blur(25px); }
@keyframes coreGlow { 0% { transform: scale(1); opacity: 0.5; } 100% { transform: scale(1.5); opacity: 0.9; } }
.orbit-ring { position: absolute; border: 1px solid rgba(139, 111, 192, 0.15); border-radius: 50%; animation: ringRotate linear infinite; }
.orbit-ring:nth-child(1) { width: 160px; height: 160px; animation-duration: 12s; }
.orbit-ring:nth-child(2) { width: 200px; height: 200px; animation-duration: 18s; animation-direction: reverse; border-color: rgba(90, 155, 213, 0.12); }
.orbit-ring:nth-child(3) { width: 240px; height: 240px; animation-duration: 25s; border-style: dashed; border-color: rgba(139, 111, 192, 0.08); }
@keyframes ringRotate { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
.orbit-dot { position: absolute; width: 8px; height: 8px; border-radius: 50%; z-index: 6; }
.orbit-dot::after { content: ''; position: absolute; inset: -4px; border-radius: 50%; filter: blur(6px); }
.orbit-dot:nth-child(4) { background: var(--pp-lavender); box-shadow: 0 0 10px rgba(167, 139, 219, 0.5); animation: orbitA 12s linear infinite; }
.orbit-dot:nth-child(4)::after { background: rgba(167, 139, 219, 0.4); }
.orbit-dot:nth-child(5) { background: var(--pp-sky); box-shadow: 0 0 10px rgba(90, 155, 213, 0.5); width: 6px; height: 6px; animation: orbitB 18s linear infinite reverse; }
.orbit-dot:nth-child(5)::after { background: rgba(90, 155, 213, 0.4); }
.orbit-dot:nth-child(6) { background: var(--pp-periwinkle); box-shadow: 0 0 8px rgba(128, 136, 217, 0.5); width: 5px; height: 5px; animation: orbitC 25s linear infinite; }
.orbit-dot:nth-child(6)::after { background: rgba(128, 136, 217, 0.4); }
@keyframes orbitA { from { transform: rotate(0deg) translateX(80px) rotate(0deg); } to { transform: rotate(360deg) translateX(80px) rotate(-360deg); } }
@keyframes orbitB { from { transform: rotate(0deg) translateX(100px) rotate(0deg); } to { transform: rotate(360deg) translateX(100px) rotate(-360deg); } }
@keyframes orbitC { from { transform: rotate(0deg) translateX(120px) rotate(0deg); } to { transform: rotate(360deg) translateX(120px) rotate(-360deg); } }
.text-content { text-align: center; display: flex; flex-direction: column; align-items: center; gap: 20px; }
.brand-title { font-family: 'Outfit', sans-serif; font-weight: 300; font-size: 14px; letter-spacing: 4px; text-transform: uppercase; color: var(--pp-violet); opacity: 0; animation: fadeSlideUp 1s ease-out 0.5s forwards; }
.main-heading { font-family: 'Outfit', sans-serif; font-weight: 600; font-size: 32px; line-height: 1.3; background: linear-gradient(135deg, var(--pp-deep-violet) 0%, var(--pp-violet) 40%, var(--pp-sky) 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; opacity: 0; animation: fadeSlideUp 1s ease-out 0.8s forwards; }
.status-area { height: 52px; display: flex; flex-direction: column; align-items: center; justify-content: center; position: relative; overflow: hidden; opacity: 0; animation: fadeSlideUp 1s ease-out 1.2s forwards; }
.status-message { position: absolute; white-space: nowrap; font-size: 15px; font-weight: 400; color: var(--pp-text-secondary); display: flex; align-items: center; gap: 10px; opacity: 0; transform: translateY(20px); transition: all 0.7s cubic-bezier(0.22, 1, 0.36, 1); }
.status-message.active { opacity: 1; transform: translateY(0); }
.status-message.exiting { opacity: 0; transform: translateY(-20px); }
.status-icon { width: 20px; height: 20px; border-radius: 50%; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.status-icon.working { border: 2px solid rgba(139, 111, 192, 0.2); border-top-color: var(--pp-violet); animation: spin 1s linear infinite; }
.status-icon.done { background: rgba(34, 158, 92, 0.1); border: none; }
.status-icon.done::after { content: '\2713'; font-size: 11px; color: #229E5C; font-weight: 600; }
@keyframes spin { to { transform: rotate(360deg); } }
.progress-container { width: 320px; opacity: 0; animation: fadeSlideUp 1s ease-out 1.5s forwards; display: flex; flex-direction: column; align-items: center; gap: 14px; }
.progress-track { width: 100%; height: 3px; background: rgba(139, 111, 192, 0.1); border-radius: 4px; overflow: hidden; position: relative; }
.progress-fill { height: 100%; width: 40%; border-radius: 4px; background: linear-gradient(90deg, transparent, var(--pp-violet), var(--pp-lavender), var(--pp-sky), transparent); position: absolute; top: 0; left: -40%; animation: infiniteSlide 2s cubic-bezier(0.4, 0, 0.2, 1) infinite; }
.progress-fill::after { content: ''; position: absolute; right: 10%; top: -3px; width: 8px; height: 8px; border-radius: 50%; background: var(--pp-sky); box-shadow: 0 0 8px rgba(90, 155, 213, 0.5), 0 0 16px rgba(90, 155, 213, 0.2); opacity: 0.9; }
@keyframes infiniteSlide { 0% { left: -40%; } 100% { left: 100%; } }
.progress-track.complete .progress-fill { width: 100%; left: 0; animation: none; background: linear-gradient(90deg, var(--pp-violet), var(--pp-lavender), var(--pp-sky)); transition: all 0.8s ease; }
.progress-track.complete .progress-fill::after { display: none; }
.progress-label-text { font-size: 12px; color: var(--pp-text-muted); font-family: 'DM Sans', sans-serif; letter-spacing: 0.3px; }
.feature-cards { display: flex; gap: 16px; margin-top: 16px; opacity: 0; animation: fadeSlideUp 1s ease-out 2s forwards; }
.feature-card { background: var(--pp-surface); border: 1px solid rgba(139, 111, 192, 0.1); border-radius: 16px; padding: 20px 22px; width: 150px; box-shadow: 0 2px 12px rgba(139, 111, 192, 0.06), 0 1px 3px rgba(0, 0, 0, 0.04); opacity: 0; transform: translateY(20px) scale(0.95); transition: all 0.8s cubic-bezier(0.22, 1, 0.36, 1); }
.feature-card.visible { opacity: 1; transform: translateY(0) scale(1); }
.feature-card:hover { border-color: rgba(139, 111, 192, 0.2); box-shadow: 0 4px 24px rgba(139, 111, 192, 0.12), 0 2px 6px rgba(0, 0, 0, 0.04); transform: translateY(-2px) scale(1.02); }
.feature-card-icon { font-size: 24px; margin-bottom: 10px; display: block; }
.feature-card-label { font-family: 'Outfit', sans-serif; font-size: 13px; font-weight: 500; color: var(--pp-text-secondary); line-height: 1.4; }
.connector-lines { position: fixed; inset: 0; z-index: 2; pointer-events: none; overflow: hidden; }
.connector { position: absolute; height: 1px; background: linear-gradient(90deg, transparent, rgba(139, 111, 192, 0.12), transparent); animation: connectorSweep 6s ease-in-out infinite; }
@keyframes connectorSweep { 0%, 100% { transform: translateX(-100%); opacity: 0; } 20% { opacity: 1; } 80% { opacity: 1; } 100% { transform: translateX(100vw); opacity: 0; } }
.scanline { position: fixed; left: 0; right: 0; height: 1px; background: linear-gradient(90deg, transparent 0%, var(--pp-lavender) 50%, transparent 100%); opacity: 0.06; z-index: 2; animation: scanDrop 8s linear infinite; pointer-events: none; }
@keyframes scanDrop { 0% { top: -2px; } 100% { top: 100%; } }
.tip-area { text-align: center; margin-top: 12px; opacity: 0; animation: fadeSlideUp 1s ease-out 3s forwards; }
.tip-label { font-size: 10px; letter-spacing: 2px; text-transform: uppercase; color: var(--pp-text-muted); margin-bottom: 8px; font-family: 'Outfit', sans-serif; }
.tip-text { font-size: 13px; color: var(--pp-text-muted); max-width: 380px; line-height: 1.6; margin: 0 auto; min-height: 42px; transition: opacity 0.6s ease, transform 0.6s ease; }
@keyframes fadeSlideUp { from { opacity: 0; transform: translateY(16px); } to { opacity: 1; transform: translateY(0); } }
.center-stage.complete .main-heading { animation: completePulse 2s ease-in-out infinite alternate; }
@keyframes completePulse { 0% { filter: brightness(1); } 100% { filter: brightness(1.15); } }
.input-banner { position: fixed; top: 24px; right: 24px; z-index: 100; display: flex; align-items: center; gap: 12px; background: linear-gradient(135deg, #FFF4D6 0%, #FFE9B3 100%); border: 1px solid #F5B800; color: #5C3D00; padding: 12px 14px 12px 22px; border-radius: 999px; font-family: 'Outfit', sans-serif; font-size: 14px; font-weight: 500; box-shadow: 0 4px 16px rgba(245, 184, 0, 0.25); max-width: min(360px, calc(100vw - 48px)); animation: bannerEnter 0.4s cubic-bezier(0.22, 1, 0.36, 1); }
.input-banner[hidden] { display: none; }
.input-banner-icon { font-size: 18px; animation: bannerPulse 1.5s ease-in-out infinite; flex-shrink: 0; }
.input-banner-text { line-height: 1.4; flex: 1; min-width: 0; }
.input-banner-text b { font-weight: 600; display: block; letter-spacing: 0.3px; }
.input-banner-text small { display: block; font-size: 12px; font-weight: 400; opacity: 0.8; margin-top: 2px; }
.input-banner-close { width: 24px; height: 24px; border: none; border-radius: 50%; background: rgba(92, 61, 0, 0.12); color: #5C3D00; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 18px; line-height: 1; flex-shrink: 0; transition: background 0.2s ease, transform 0.2s ease; }
.input-banner-close:hover { background: rgba(92, 61, 0, 0.2); transform: scale(1.05); }
@keyframes bannerPulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.2); } }
@keyframes bannerEnter { from { opacity: 0; transform: translateY(-12px); } to { opacity: 1; transform: translateY(0); } }
@media (max-width: 600px) { .main-heading { font-size: 24px; } .progress-container { width: 260px; } .feature-cards { flex-direction: column; align-items: center; } .orbit-system { width: 180px; height: 180px; } .input-banner { top: 12px; right: 12px; padding: 10px 12px 10px 16px; font-size: 13px; max-width: calc(100vw - 24px); } }
</style>
<script>
const particlesEl = document.getElementById('particles')
const connectorsEl = document.getElementById('connectors')
const statusArea = document.getElementById('statusArea')
const progressLabel = document.getElementById('progressLabel')
const tipTextEl = document.getElementById('tipText')
const inputBannerClose = document.getElementById('inputBannerClose')
// Particles
function createParticle() {
if (!particlesEl) return
const p = document.createElement('div')
p.classList.add('particle')
const size = Math.random() * 3 + 1
const colors = ['rgba(167,139,219,0.5)', 'rgba(128,136,217,0.5)', 'rgba(90,155,213,0.5)', 'rgba(139,111,192,0.5)']
const color = colors[Math.floor(Math.random() * colors.length)]
p.style.cssText = `width:${size}px;height:${size}px;left:${Math.random() * 100}%;background:${color};box-shadow:0 0 ${size * 2}px ${color};animation-duration:${Math.random() * 15 + 12}s;animation-delay:${Math.random() * 10}s;`
particlesEl.appendChild(p)
setTimeout(() => p.remove(), 30000)
}
for (let i = 0; i < 30; i++) setTimeout(() => createParticle(), i * 200)
setInterval(createParticle, 800)
// Connectors
function createConnector() {
if (!connectorsEl) return
const c = document.createElement('div')
c.classList.add('connector')
c.style.cssText = `top:${Math.random() * 100}%;width:${Math.random() * 300 + 200}px;animation-duration:${Math.random() * 4 + 4}s;animation-delay:${Math.random() * 6}s;`
connectorsEl.appendChild(c)
setTimeout(() => c.remove(), 12000)
}
for (let i = 0; i < 8; i++) setTimeout(() => createConnector(), i * 1500)
setInterval(createConnector, 2000)
// Status messages
const statuses = [
{ text: 'Setting up your workspace', duration: 8000 },
{ text: 'Installing dependencies', duration: 10000 },
{ text: 'Configuring build tools', duration: 12000 },
{ text: 'Loading design system', duration: 14000 },
{ text: 'Preparing components', duration: 10000 },
{ text: 'Building page templates', duration: 8000 },
{ text: 'Setting up routing', duration: 12000 },
{ text: 'Applying design tokens', duration: 10000 },
{ text: 'Optimizing assets', duration: 10000 },
{ text: 'Running final checks', duration: 12000 },
{ text: 'Polishing details', duration: 8000 },
{ text: 'Almost there...', duration: 6000 },
]
const phaseLabels = [
'Getting started...', 'Setting up infrastructure...', 'Building your pages...',
'Configuring features...', 'Optimizing & securing...', 'Final steps...',
]
let currentStatusIndex = 0
let liveOverride = false
let lastLiveMessage: string | null = null
let lastAwaiting = false
let lastPrompt: string | null = null
let dismissedPrompt: string | null = null
const dismissInputBanner = () => {
const banner = document.getElementById('inputBanner')
dismissedPrompt = lastPrompt || 'Please check your terminal to respond.'
if (banner) banner.hidden = true
}
inputBannerClose?.addEventListener('click', dismissInputBanner)
function renderStatusMessage(text: string) {
if (!statusArea) return
const existing = statusArea.querySelector('.status-message')
if (existing) {
existing.classList.remove('active')
existing.classList.add('exiting')
setTimeout(() => existing.remove(), 700)
}
const msg = document.createElement('div')
msg.classList.add('status-message')
const icon = document.createElement('div')
icon.classList.add('status-icon', 'working')
msg.appendChild(icon)
const textEl = document.createElement('span')
textEl.textContent = text
msg.appendChild(textEl)
statusArea.appendChild(msg)
requestAnimationFrame(() => requestAnimationFrame(() => msg.classList.add('active')))
}
function updatePhaseLabel(text: string) {
if (progressLabel) progressLabel.textContent = text
}
function showStatus(index: number) {
renderStatusMessage(statuses[index].text)
if (liveOverride) return
const phaseIndex = Math.min(Math.floor((index / statuses.length) * phaseLabels.length), phaseLabels.length - 1)
updatePhaseLabel(phaseLabels[phaseIndex])
}
function advanceStatus() {
showStatus(currentStatusIndex % statuses.length)
const duration = statuses[currentStatusIndex % statuses.length].duration
setTimeout(() => {
currentStatusIndex++
advanceStatus()
}, duration)
}
setTimeout(() => advanceStatus(), 2000)
async function pollStatus() {
const banner = document.getElementById('inputBanner') as HTMLElement | null
const promptEl = document.getElementById('inputBannerPrompt')
try {
const res = await fetch('/scaffold-status.json?t=' + Date.now(), { cache: 'no-store' })
if (!res.ok) throw new Error('no status')
const data = await res.json() as { message?: string; awaitingInput?: boolean; inputPrompt?: string }
if (data.message) {
liveOverride = true
if (data.message !== lastLiveMessage) {
lastLiveMessage = data.message
updatePhaseLabel(data.message)
}
} else {
if (liveOverride) {
const phaseIndex = Math.min(Math.floor((currentStatusIndex / statuses.length) * phaseLabels.length), phaseLabels.length - 1)
updatePhaseLabel(phaseLabels[phaseIndex])
}
liveOverride = false
lastLiveMessage = null
}
const awaiting = !!data.awaitingInput
const prompt = data.inputPrompt || 'Please check your terminal to respond.'
const awaitingChanged = awaiting !== lastAwaiting
const promptChanged = prompt !== lastPrompt
if (!awaiting) dismissedPrompt = null
if (awaitingChanged || promptChanged) {
lastAwaiting = awaiting
lastPrompt = prompt
if (promptEl) promptEl.textContent = prompt
}
if (banner) banner.hidden = !awaiting || dismissedPrompt === prompt
} catch {
if (liveOverride) {
const phaseIndex = Math.min(Math.floor((currentStatusIndex / statuses.length) * phaseLabels.length), phaseLabels.length - 1)
updatePhaseLabel(phaseLabels[phaseIndex])
}
liveOverride = false
lastLiveMessage = null
if (!lastAwaiting) {
lastPrompt = null
if (banner) banner.hidden = true
}
}
}
pollStatus()
setInterval(pollStatus, 1500)
// Feature cards
setTimeout(() => {
document.querySelectorAll('.feature-card').forEach(card => {
const delay = parseInt((card as HTMLElement).dataset.delay || '0')
setTimeout(() => card.classList.add('visible'), delay)
})
}, 2500)
// Tips
const tips = [
'Power Pages sites are built on Microsoft Dataverse, giving you enterprise-level data capabilities from day one.',
'Your site automatically includes responsive design \u2014 it looks great on phones, tablets, and desktops.',
'With role-based security, you can control exactly who sees what on your site.',
'Power Pages integrates seamlessly with Power Automate, Power BI, and the entire Microsoft ecosystem.',
'You can extend your site with custom code, and JavaScript for unlimited flexibility.',
'Built-in content delivery networks ensure your pages load fast for users worldwide.',
'Multi-language support lets you reach audiences across the globe with localized content.',
]
let tipIndex = 0
function showTip() {
if (!tipTextEl) return
tipTextEl.style.opacity = '0'
tipTextEl.style.transform = 'translateY(8px)'
setTimeout(() => {
tipTextEl.textContent = tips[tipIndex % tips.length]
tipTextEl.style.opacity = '1'
tipTextEl.style.transform = 'translateY(0)'
tipIndex++
}, 500)
}
setTimeout(() => showTip(), 3500)
setInterval(showTip, 12000)
</script>
/* Power Pages — Site Theme
This file is populated during site implementation with your chosen design tokens,
typography, and color palette. */
*,*::before,*::after { margin: 0; padding: 0; box-sizing: border-box; }
html, body {
width: 100%; height: 100%;
margin: 0; padding: 0;
font-family: 'DM Sans', sans-serif;
}
{
"extends": "astro/tsconfigs/strict"
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
<title>__PLAN_TITLE__ - __SITE_NAME__</title>
<style>
:root{
--bg:#faf9f8; --surface:#ffffff; --surface2:#f3f2f1; --border:#e1dfdd; --border-light:#c8c6c4;
--text:#323130; --text-dim:#605e5c; --text-bright:#201f1e; --accent:#0078d4; --accent-bg:#0078d40a; --accent-border:#0078d425;
--pass:#107c10; --pass-bg:#107c100a; --pass-border:#107c1020;
--purple:#8764b8; --purple-bg:#8764b80a; --purple-border:#8764b820;
--warn:#ca5010; --warn-bg:#ca50100a; --warn-border:#ca501020;
--mono:'Cascadia Code','Consolas',monospace; --sans:'Segoe UI','Segoe UI Web (West European)',-apple-system,system-ui,sans-serif;
--radius:8px; --radius-sm:4px; --shadow-4:0 1.6px 3.6px 0 rgba(0,0,0,0.132),0 0.3px 0.9px 0 rgba(0,0,0,0.108);
}
*{margin:0;padding:0;box-sizing:border-box;}
html{scroll-behavior:smooth;}
body{font-family:var(--sans);background:var(--bg);color:var(--text);font-size:14px;line-height:1.6;}
.topbar{z-index:100;background:var(--surface);box-shadow:var(--shadow-4);padding:14px 28px;display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:10px;position:sticky;top:0;}
.topbar-left{display:flex;align-items:center;gap:14px;}
.logo{width:36px;height:36px;object-fit:contain;display:block;flex-shrink:0;}
.topbar-title{font-size:16px;font-weight:700;color:var(--text-bright);}
.topbar-sub{font-size:11px;color:var(--text-dim);margin-top:1px;display:flex;align-items:center;gap:6px;flex-wrap:wrap;}
.topbar-sep{opacity:0.5;}
.topbar-right{display:flex;gap:6px;flex-wrap:wrap;}
.pill{font-size:11px;padding:4px 10px;border-radius:999px;font-weight:600;letter-spacing:0.2px;border:1px solid transparent;}
.pill-aesthetic{background:var(--accent-bg);color:var(--accent);border-color:var(--accent-border);}
.pill-mood{background:var(--purple-bg);color:var(--purple);border-color:var(--purple-border);}
.pill-framework{background:var(--pass-bg);color:var(--pass);border-color:var(--pass-border);}
.layout{display:flex;min-height:calc(100vh - 65px);}
.sidebar{width:240px;background:var(--surface);border-right:1px solid var(--border);padding:20px 0;flex-shrink:0;}
.nav-group-label{font-size:10px;font-weight:700;color:var(--text-dim);text-transform:uppercase;letter-spacing:1.2px;padding:16px 22px 6px;}
.nav-btn{display:flex;align-items:center;gap:10px;width:100%;padding:11px 22px;background:none;border:none;border-left:2px solid transparent;color:var(--text-dim);font-size:13px;font-weight:500;cursor:pointer;font-family:var(--sans);text-align:left;transition:all 0.15s;}
.nav-btn:hover{color:var(--text);background:var(--surface2);}
.nav-btn.active{color:var(--accent);font-weight:600;border-left-color:var(--accent);background:var(--accent-bg);}
.nav-btn .nav-icon{font-size:15px;opacity:0.5;width:18px;text-align:center;}
.nav-btn.active .nav-icon{opacity:0.9;}
.content{flex:1;padding:32px 40px 72px;max-width:1060px;}
.section{display:none;}
.section.active{display:block;animation:fadeIn 0.3s ease;}
@keyframes fadeIn{from{opacity:0;transform:translateY(8px);}to{opacity:1;transform:translateY(0);}}
h2{font-size:21px;font-weight:800;color:var(--text-bright);letter-spacing:-0.3px;margin-bottom:5px;}
.section-desc{font-size:13px;color:var(--text-dim);margin-bottom:20px;}
.section-desc code{color:var(--accent);background:var(--accent-bg);padding:1px 6px;border-radius:3px;font-family:var(--mono);font-size:12px;}
h3{font-size:15px;font-weight:700;color:var(--text-bright);margin-top:24px;margin-bottom:12px;}
.summary-box{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:22px;margin-bottom:22px;font-size:14px;color:var(--text);line-height:1.75;}
.stats-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-bottom:24px;}
.stat-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:18px 16px;text-align:center;position:relative;overflow:hidden;box-shadow:var(--shadow-4);}
.stat-card::after{content:'';position:absolute;top:0;left:50%;transform:translateX(-50%);width:40px;height:2px;border-radius:0 0 2px 2px;background:var(--accent);}
.stat-card:nth-child(2)::after{background:var(--purple);}
.stat-card:nth-child(3)::after{background:var(--warn);}
.stat-num{font-size:30px;font-weight:800;font-family:var(--mono);line-height:1;color:var(--text-bright);}
.stat-label{font-size:10px;color:var(--text-dim);text-transform:uppercase;letter-spacing:1px;margin-top:6px;}
.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:18px 20px;margin-bottom:10px;transition:box-shadow 0.2s,border-color 0.2s;}
.card:hover{box-shadow:var(--shadow-4);}
.field-label{font-size:11px;font-weight:600;color:var(--text-dim);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:4px;}
.chip{display:inline-flex;align-items:center;gap:5px;font-size:11px;padding:3px 10px;border-radius:4px;font-weight:600;background:var(--surface2);border:1px solid var(--border);color:var(--text);margin:0 4px 4px 0;}
.chip-accent{background:var(--accent-bg);border-color:var(--accent-border);color:var(--accent);}
/* Design tab */
.type-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px;}
.type-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:20px 22px;box-shadow:var(--shadow-4);}
.type-role{font-size:10px;color:var(--text-dim);text-transform:uppercase;letter-spacing:1px;margin-bottom:8px;}
.type-sample{font-size:32px;font-weight:700;color:var(--text-bright);line-height:1.1;margin-bottom:6px;}
.type-name{font-size:13px;color:var(--text);font-family:var(--mono);margin-bottom:10px;}
.type-reason{font-size:12px;color:var(--text-dim);line-height:1.55;}
.palette-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(170px,1fr));gap:12px;}
.swatch-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);overflow:hidden;box-shadow:var(--shadow-4);}
.swatch{height:80px;width:100%;display:block;}
.swatch-body{padding:10px 14px 14px;}
.swatch-var{font-size:11px;font-family:var(--mono);color:var(--text);}
.swatch-hex{font-size:13px;font-weight:700;color:var(--text-bright);font-family:var(--mono);margin-top:2px;}
.swatch-desc{font-size:11px;color:var(--text-dim);margin-top:4px;}
.list-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:4px 4px;}
.list-item{display:flex;gap:14px;padding:14px 18px;border-bottom:1px solid var(--border);}
.list-item:last-child{border-bottom:none;}
.list-item-icon{flex-shrink:0;width:28px;height:28px;border-radius:50%;background:var(--accent-bg);color:var(--accent);display:flex;align-items:center;justify-content:center;font-size:14px;}
.list-item-label{font-size:13px;font-weight:700;color:var(--text-bright);margin-bottom:2px;}
.list-item-desc{font-size:12px;color:var(--text-dim);line-height:1.55;}
/* Pages tab */
.page-card{background:var(--surface);border:1px solid var(--border);border-left:3px solid var(--accent);border-radius:var(--radius);padding:18px 22px;margin-bottom:10px;box-shadow:var(--shadow-4);}
.page-head{display:flex;justify-content:space-between;align-items:baseline;gap:12px;flex-wrap:wrap;margin-bottom:8px;}
.page-name{font-size:15px;font-weight:700;color:var(--text-bright);}
.page-route{font-size:12px;font-family:var(--mono);color:var(--accent);background:var(--accent-bg);padding:2px 8px;border-radius:4px;}
.page-desc{font-size:13px;color:var(--text-dim);margin-bottom:12px;}
.page-subsection{margin-top:10px;}
.bullet-list{list-style:none;padding:0;margin:6px 0 0;}
.bullet-list li{position:relative;padding-left:16px;font-size:12px;color:var(--text);margin-bottom:4px;}
.bullet-list li::before{content:'';position:absolute;left:4px;top:8px;width:5px;height:5px;border-radius:50%;background:var(--accent);}
.component-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:16px 20px;margin-bottom:8px;}
.component-head{display:flex;justify-content:space-between;gap:12px;margin-bottom:6px;flex-wrap:wrap;}
.component-name{font-size:14px;font-weight:700;color:var(--text-bright);}
.component-purpose{font-size:12px;color:var(--text-dim);margin-bottom:8px;}
.routes-table{width:100%;border-collapse:separate;border-spacing:0;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);overflow:hidden;}
.routes-table th{text-align:left;font-size:11px;font-weight:700;color:var(--text-dim);text-transform:uppercase;letter-spacing:0.5px;padding:10px 16px;background:var(--surface2);border-bottom:1px solid var(--border);}
.routes-table td{font-size:13px;padding:10px 16px;border-bottom:1px solid var(--border);}
.routes-table tr:last-child td{border-bottom:none;}
.routes-table td.path{font-family:var(--mono);color:var(--accent);}
/* Deployment tab */
.checklist{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:4px 4px;}
.checklist-item{display:flex;align-items:flex-start;gap:12px;padding:12px 18px;border-bottom:1px solid var(--border);font-size:13px;color:var(--text);}
.checklist-item:last-child{border-bottom:none;}
.checklist-check{flex-shrink:0;width:18px;height:18px;border:1.5px solid var(--border-light);border-radius:4px;margin-top:1px;}
.deploy-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:12px;}
.deploy-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:18px 20px;box-shadow:var(--shadow-4);position:relative;}
.deploy-card.recommended{border-color:var(--accent-border);background:linear-gradient(135deg,var(--accent-bg),transparent);}
.deploy-card.recommended::before{content:'Recommended';position:absolute;top:-9px;right:14px;font-size:10px;font-weight:700;color:#fff;background:var(--accent);padding:3px 10px;border-radius:999px;letter-spacing:0.5px;text-transform:uppercase;}
.deploy-title{font-size:14px;font-weight:700;color:var(--text-bright);margin-bottom:4px;}
.deploy-desc{font-size:12px;color:var(--text-dim);line-height:1.55;}
.empty-note{font-size:12px;color:var(--text-dim);font-style:italic;padding:12px 0;}
@media (max-width:900px){
.layout{flex-direction:column;}
.sidebar{width:100%;border-right:none;border-bottom:1px solid var(--border);display:flex;flex-wrap:wrap;padding:8px;}
.nav-group-label{width:100%;}
.nav-btn{width:auto;border-left:none;border-bottom:2px solid transparent;padding:10px 14px;}
.nav-btn.active{border-left:none;border-bottom-color:var(--accent);}
.content{padding:20px;}
.type-grid{grid-template-columns:1fr;}
.stats-grid{grid-template-columns:repeat(3,1fr);}
}
</style>
</head>
<body>
<div class="topbar">
<div class="topbar-left">
<img class="logo" src="./power-pages-icon.png" alt="Power Pages" />
<div>
<div class="topbar-title">__PLAN_TITLE__</div>
<div class="topbar-sub">
<span>__SITE_NAME__</span>
<span class="topbar-sep">·</span>
<span class="pill pill-framework">__FRAMEWORK__</span>
</div>
</div>
</div>
<div class="topbar-right">
<span class="pill pill-aesthetic">__AESTHETIC__</span>
<span class="pill pill-mood">__MOOD__</span>
</div>
</div>
<div class="layout">
<div class="sidebar">
<div class="nav-group-label">Plan</div>
<button class="nav-btn active" data-tab="overview"><span class="nav-icon">◉</span> Overview</button>
<button class="nav-btn" data-tab="design"><span class="nav-icon">☀</span> Design</button>
<button class="nav-btn" data-tab="pages"><span class="nav-icon">▦</span> Pages & Components</button>
<button class="nav-btn" data-tab="deploy"><span class="nav-icon">✓</span> Deployment & Review</button>
</div>
<div class="content">
<!-- Overview Tab -->
<div class="section active" id="tab-overview">
<h2>Overview</h2>
<p class="section-desc">Implementation plan for <code>__SITE_NAME__</code></p>
<div class="summary-box" id="summaryBox"></div>
<script id="summaryData" type="application/json">__SUMMARY_DATA__</script>
<div class="stats-grid">
<div class="stat-card"><div class="stat-num" id="statPages">0</div><div class="stat-label">Pages</div></div>
<div class="stat-card"><div class="stat-num" id="statComponents">0</div><div class="stat-label">Shared Components</div></div>
<div class="stat-card"><div class="stat-num" id="statRoutes">0</div><div class="stat-label">Routes</div></div>
</div>
<h3>What happens next</h3>
<div class="list-card" id="nextStepsList"></div>
</div>
<!-- Design Tab -->
<div class="section" id="tab-design">
<h2>Design</h2>
<p class="section-desc">Typography, palette, motion, and background treatments</p>
<h3>Typography</h3>
<div class="type-grid" id="typographyContainer"></div>
<h3>Color palette</h3>
<div class="palette-grid" id="paletteContainer"></div>
<h3>Motion & animation</h3>
<div class="list-card" id="motionContainer"></div>
<h3>Background treatment</h3>
<div class="list-card" id="backgroundsContainer"></div>
</div>
<!-- Pages & Components Tab -->
<div class="section" id="tab-pages">
<h2>Pages & Components</h2>
<p class="section-desc">Pages to build, their content outline, and the shared components they rely on</p>
<h3>Pages</h3>
<div id="pagesContainer"></div>
<h3>Shared components</h3>
<div id="componentsContainer"></div>
<h3>Routing</h3>
<table class="routes-table" id="routesTable">
<thead><tr><th>Path</th><th>Page</th></tr></thead>
<tbody id="routesBody"></tbody>
</table>
</div>
<!-- Deployment & Review Tab -->
<div class="section" id="tab-deploy">
<h2>Deployment & Review</h2>
<p class="section-desc">Verification checklist and deployment options</p>
<h3>Before handoff — verify</h3>
<div class="checklist" id="reviewContainer"></div>
<h3>Deployment options</h3>
<div class="deploy-grid" id="deploymentContainer"></div>
</div>
</div>
</div>
<script id="typographyData" type="application/json">__TYPOGRAPHY_DATA__</script>
<script id="paletteData" type="application/json">__PALETTE_DATA__</script>
<script id="motionData" type="application/json">__MOTION_DATA__</script>
<script id="backgroundsData" type="application/json">__BACKGROUNDS_DATA__</script>
<script id="pagesData" type="application/json">__PAGES_DATA__</script>
<script id="componentsData" type="application/json">__COMPONENTS_DATA__</script>
<script id="routesData" type="application/json">__ROUTES_DATA__</script>
<script id="reviewData" type="application/json">__REVIEW_DATA__</script>
<script id="deploymentData" type="application/json">__DEPLOYMENT_DATA__</script>
<script>
function readJsonData(id){
const node = document.getElementById(id);
return node ? JSON.parse(node.textContent) : null;
}
const TYPOGRAPHY = readJsonData('typographyData');
const PALETTE = readJsonData('paletteData');
const MOTION = readJsonData('motionData');
const BACKGROUNDS = readJsonData('backgroundsData');
const PAGES = readJsonData('pagesData');
const COMPONENTS = readJsonData('componentsData');
const ROUTES = readJsonData('routesData');
const REVIEW = readJsonData('reviewData');
const DEPLOYMENT = readJsonData('deploymentData');
function esc(s){const d=document.createElement('div');d.textContent=s==null?'':String(s);return d.innerHTML;}
// --- Load chosen Google Fonts for the typography preview ---
(function loadFonts(){
const names=[TYPOGRAPHY && TYPOGRAPHY.primary && TYPOGRAPHY.primary.name, TYPOGRAPHY && TYPOGRAPHY.secondary && TYPOGRAPHY.secondary.name].filter(Boolean);
if(!names.length)return;
const families=names.map(n=>'family='+encodeURIComponent(n).replace(/%20/g,'+')+':wght@400;600;700').join('&');
const link=document.createElement('link');
link.rel='stylesheet';
link.href='https://fonts.googleapis.com/css2?'+families+'&display=swap';
document.head.appendChild(link);
})();
// --- Tab navigation ---
document.querySelectorAll('.nav-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
document.querySelectorAll('.section').forEach(s => s.classList.remove('active'));
document.getElementById('tab-' + btn.dataset.tab).classList.add('active');
});
});
// --- Summary ---
document.getElementById('summaryBox').textContent = JSON.parse(document.getElementById('summaryData').textContent).text || '';
// --- Overview stats ---
document.getElementById('statPages').textContent = PAGES.length;
document.getElementById('statComponents').textContent = COMPONENTS.length;
document.getElementById('statRoutes').textContent = ROUTES.length;
const nextSteps = [
{ icon:'✎', label:'Apply design tokens', desc:'Fonts, palette, motion, and backgrounds written into your theme' },
{ icon:'▦', label:'Build shared components', desc:COMPONENTS.length+' reusable component'+(COMPONENTS.length===1?'':'s')+' used across pages' },
{ icon:'📄', label:'Create pages', desc:PAGES.length+' page'+(PAGES.length===1?'':'s')+' with routing and navigation' },
{ icon:'✓', label:'Verify & accessibility', desc:'axe-core audit, Playwright checks, and user review' },
];
document.getElementById('nextStepsList').innerHTML = nextSteps.map(s =>
'<div class="list-item"><div class="list-item-icon">'+s.icon+'</div><div><div class="list-item-label">'+s.label+'</div><div class="list-item-desc">'+s.desc+'</div></div></div>'
).join('');
// --- Design: typography ---
const typeContainer = document.getElementById('typographyContainer');
const typeSlots = [
{ slot:'primary', role:'Primary — body & UI' },
{ slot:'secondary', role:'Secondary — headings' },
];
typeContainer.innerHTML = typeSlots.map(s => {
const t = TYPOGRAPHY && TYPOGRAPHY[s.slot];
if(!t) return '';
const fam = t.name ? "'"+t.name.replace(/'/g, "\\'")+"', var(--sans)" : 'var(--sans)';
return '<div class="type-card">'
+ '<div class="type-role">'+s.role+'</div>'
+ '<div class="type-sample" style="font-family:'+fam+';">'+esc(t.sample || 'Aa Bb Cc')+'</div>'
+ '<div class="type-name">'+esc(t.name || '')+'</div>'
+ '<div class="type-reason">'+esc(t.reason || '')+'</div>'
+ '</div>';
}).join('');
// --- Design: palette ---
document.getElementById('paletteContainer').innerHTML = PALETTE.map(c =>
'<div class="swatch-card">'
+ '<div class="swatch" style="background:'+esc(c.hex)+';"></div>'
+ '<div class="swatch-body">'
+ '<div class="swatch-var">'+esc(c.var || '')+'</div>'
+ '<div class="swatch-hex">'+esc(c.hex)+'</div>'
+ (c.description ? '<div class="swatch-desc">'+esc(c.description)+'</div>' : '')
+ '</div></div>'
).join('');
// --- Design: motion & backgrounds ---
function renderList(arr) {
if(!arr || !arr.length) return '<div class="empty-note" style="padding:12px 18px;">None specified.</div>';
return arr.map(item =>
'<div class="list-item"><div class="list-item-icon">◈</div>'
+ '<div><div class="list-item-label">'+esc(item.label || '')+'</div>'
+ '<div class="list-item-desc">'+esc(item.description || '')+'</div></div></div>'
).join('');
}
document.getElementById('motionContainer').innerHTML = renderList(MOTION);
document.getElementById('backgroundsContainer').innerHTML = renderList(BACKGROUNDS);
// --- Pages ---
document.getElementById('pagesContainer').innerHTML = PAGES.map(p => {
const content = (p.content && p.content.length)
? '<div class="page-subsection"><div class="field-label">Content</div><ul class="bullet-list">'
+ p.content.map(c => '<li>'+esc(c)+'</li>').join('')
+ '</ul></div>'
: '';
const comps = (p.components && p.components.length)
? '<div class="page-subsection"><div class="field-label">Components used</div>'
+ p.components.map(c => '<span class="chip">'+esc(c)+'</span>').join('')
+ '</div>'
: '';
return '<div class="page-card">'
+ '<div class="page-head">'
+ '<div class="page-name">'+esc(p.name)+'</div>'
+ '<div class="page-route">'+esc(p.route || '')+'</div>'
+ '</div>'
+ (p.description ? '<div class="page-desc">'+esc(p.description)+'</div>' : '')
+ content + comps
+ '</div>';
}).join('');
// --- Components ---
const compsContainer = document.getElementById('componentsContainer');
if (!COMPONENTS.length) {
compsContainer.innerHTML = '<div class="empty-note">No shared components planned yet.</div>';
} else {
compsContainer.innerHTML = COMPONENTS.map(c =>
'<div class="component-card">'
+ '<div class="component-head"><div class="component-name">'+esc(c.name)+'</div></div>'
+ (c.purpose ? '<div class="component-purpose">'+esc(c.purpose)+'</div>' : '')
+ (c.usedBy && c.usedBy.length
? '<div><div class="field-label">Used by</div>'+c.usedBy.map(u => '<span class="chip chip-accent">'+esc(u)+'</span>').join('')+'</div>'
: '')
+ '</div>'
).join('');
}
// --- Routes ---
document.getElementById('routesBody').innerHTML = ROUTES.map(r =>
'<tr><td class="path">'+esc(r.path)+'</td><td>'+esc(r.page)+'</td></tr>'
).join('');
// --- Review checklist ---
document.getElementById('reviewContainer').innerHTML = REVIEW.map(item =>
'<div class="checklist-item"><div class="checklist-check"></div><div>'+esc(item)+'</div></div>'
).join('');
// --- Deployment options ---
document.getElementById('deploymentContainer').innerHTML = DEPLOYMENT.map(d =>
'<div class="deploy-card'+(d.recommended ? ' recommended' : '')+'">'
+ '<div class="deploy-title">'+esc(d.title)+'</div>'
+ '<div class="deploy-desc">'+esc(d.description || '')+'</div>'
+ '</div>'
).join('');
</script>
<footer style="position:fixed;bottom:0;left:0;right:0;text-align:center;padding:10px;font-size:12px;color:#605e5c;border-top:1px solid #c8c6c4;background:#ffffff;z-index:100;">AI-generated content may be incorrect</footer>
</body>
</html>
# Dependencies
node_modules
# Build output
dist
dist-ssr
# Vite
.vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
# TypeScript
*.tsbuildinfo
# Environment variables
.env
.env.local
.env*.local
# Logs
*.log
npm-debug.log*
yarn-debug.log*
pnpm-debug.log*
# Testing
coverage
# Editor
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.playwright-mcp/
# OS
.DS_Store
Thumbs.db
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>__SITE_NAME__</title>
<meta name="description" content="__SITE_DESCRIPTION__" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;0,9..40,600;1,9..40,300&family=Outfit:wght@200;300;400;500;600;700&display=swap" rel="stylesheet" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
export default function About() {
return null
}
Related skills
FAQ
What does create-site do?
>-
When should I use create-site?
Invoke when >-.
Is create-site safe to install?
Review the Security Audits panel on this page before installing in production.