diff --git a/.planning/revert-onboarding-plan.md b/.planning/revert-onboarding-plan.md
new file mode 100644
index 0000000000..169aee1837
--- /dev/null
+++ b/.planning/revert-onboarding-plan.md
@@ -0,0 +1,289 @@
+# Plan: Revert New Onboarding (PR #714) and Preserve Work
+
+## Overview
+
+This plan describes how to revert PR #714 (New User Onboarding) on the `develop` branch while preserving all the new onboarding work on a separate branch.
+
+**PR #714 Details:**
+- Merged: December 16, 2025
+- Merge commit: `e7323c4f21c9b71eb4b2ee3f96ae294fd53ca516`
+- Title: "New user onboarding"
+
+## Current State Analysis
+
+### Commits Since PR #714 (53 total)
+The following PRs were merged after #714:
+- #746: Site editor sidebar (not onboarding related)
+- #744: Fix interactive task resubmission (not onboarding related)
+- #743: Safeguards for plugin update (not onboarding related)
+- #742: Update for #740 (not onboarding related)
+- #741: Onboarding tweaks (**onboarding related**)
+- #740: Remove popover positioning (not onboarding related)
+- #739: Exclude direct_file_access check (not onboarding related)
+- #738: Fix year-week boundary bug (not onboarding related)
+- #736: Delay onboarding init (**onboarding related**)
+- #733: Reverse filter logic (**onboarding related**)
+- #730: New onboarding tweaks (**onboarding related**)
+
+### PRs with Onboarding Changes (need special handling)
+
+| PR | Files Changed | Non-Onboarding Changes |
+|----|---------------|------------------------|
+| #730 | `assets/css/onboarding/onboarding.css`, `assets/js/onboarding/steps/SettingsStep.js`, `views/onboarding/settings.php` | `classes/class-plugin-upgrade-tasks.php` (upgrade task condition) |
+| #733 | `classes/class-onboard-wizard.php`, `tests/phpunit/test-class-onboard-wizard.php` | None |
+| #736 | `assets/js/onboarding/onboarding.js`, `classes/class-onboard-wizard.php` | `classes/utils/class-debug-tools.php`, `classes/utils/class-playground.php` |
+| #741 | Multiple onboarding views and CSS | None |
+
+### Files Created by PR #714 (New Onboarding)
+These files/directories didn't exist before and will need to be **deleted**:
+```
+assets/css/onboarding/
+assets/images/onboarding/
+assets/js/onboarding/
+classes/class-onboard-wizard.php
+tests/phpunit/test-class-onboard-wizard.php
+views/onboarding/
+```
+
+### Files Deleted by PR #714 (Old Onboarding)
+These files need to be **restored** from before the merge:
+```
+assets/css/onboard.css
+assets/css/welcome.css
+assets/js/onboard.js
+views/welcome.php
+```
+
+### Files Modified by PR #714 (Need Selective Revert)
+These existing files were modified and need **careful handling**:
+```
+assets/css/admin.css
+assets/js/settings.js
+classes/admin/class-page-settings.php
+classes/admin/class-page.php
+classes/class-base.php
+classes/class-suggested-tasks.php
+classes/suggested-tasks/class-tasks-interface.php
+classes/suggested-tasks/providers/class-blog-description.php
+classes/suggested-tasks/providers/class-select-locale.php
+classes/suggested-tasks/providers/class-select-timezone.php
+classes/suggested-tasks/providers/class-site-icon.php
+classes/suggested-tasks/providers/class-tasks.php
+classes/ui/class-chart.php
+classes/utils/class-date.php
+tests/e2e/sequential/onboarding.spec.js
+views/admin-page.php
+```
+
+---
+
+## Execution Plan
+
+### Phase 1: Preserve New Onboarding Work
+
+1. **Create a preservation branch from current develop**
+ ```bash
+ git checkout develop
+ git pull origin develop
+ git checkout -b filip/new-onboarding-preserved
+ git push -u origin filip/new-onboarding-preserved
+ ```
+
+2. **Document the branch purpose**
+ Add a note to the branch or create an issue tracking it.
+
+---
+
+### Phase 2: Create Revert Branch
+
+1. **Create revert branch**
+ ```bash
+ git checkout develop
+ git checkout -b revert/old-onboarding
+ ```
+
+---
+
+### Phase 3: Restore Old Onboarding Files
+
+1. **Restore deleted files from commit before PR #714**
+ The parent commit (before merge) is: `e7323c4f21c9b71eb4b2ee3f96ae294fd53ca516^`
+
+ ```bash
+ # Restore old onboarding files
+ git checkout e7323c4f21c9b71eb4b2ee3f96ae294fd53ca516^ -- assets/css/onboard.css
+ git checkout e7323c4f21c9b71eb4b2ee3f96ae294fd53ca516^ -- assets/css/welcome.css
+ git checkout e7323c4f21c9b71eb4b2ee3f96ae294fd53ca516^ -- assets/js/onboard.js
+ git checkout e7323c4f21c9b71eb4b2ee3f96ae294fd53ca516^ -- views/welcome.php
+ ```
+
+---
+
+### Phase 4: Delete New Onboarding Files
+
+1. **Remove new onboarding directories and files**
+ ```bash
+ rm -rf assets/css/onboarding/
+ rm -rf assets/images/onboarding/
+ rm -rf assets/js/onboarding/
+ rm -f classes/class-onboard-wizard.php
+ rm -f tests/phpunit/test-class-onboard-wizard.php
+ rm -rf views/onboarding/
+ ```
+
+---
+
+### Phase 5: Revert Modified Files (Manual Review Required)
+
+For each file below, compare the old version with the current version and selectively revert onboarding-related changes while keeping other improvements:
+
+#### 5.1 Core Files (Critical - Review Carefully)
+
+| File | Action |
+|------|--------|
+| `classes/class-base.php` | Revert `get_license_generator_content` changes |
+| `classes/admin/class-page.php` | Restore `welcome_redirect` method |
+| `classes/admin/class-page-settings.php` | Review licensing changes |
+| `views/admin-page.php` | Revert onboarding container changes |
+
+**Commands to view diffs:**
+```bash
+# View what changed in each file
+git diff e7323c4f21c9b71eb4b2ee3f96ae294fd53ca516^..e7323c4f21c9b71eb4b2ee3f96ae294fd53ca516 -- classes/class-base.php
+git diff e7323c4f21c9b71eb4b2ee3f96ae294fd53ca516^..e7323c4f21c9b71eb4b2ee3f96ae294fd53ca516 -- classes/admin/class-page.php
+git diff e7323c4f21c9b71eb4b2ee3f96ae294fd53ca516^..e7323c4f21c9b71eb4b2ee3f96ae294fd53ca516 -- views/admin-page.php
+```
+
+#### 5.2 Suggested Tasks Providers
+These files had changes for onboarding task providers. Review and revert onboarding-specific code:
+- `classes/class-suggested-tasks.php`
+- `classes/suggested-tasks/class-tasks-interface.php`
+- `classes/suggested-tasks/providers/class-blog-description.php`
+- `classes/suggested-tasks/providers/class-select-locale.php`
+- `classes/suggested-tasks/providers/class-select-timezone.php`
+- `classes/suggested-tasks/providers/class-site-icon.php`
+- `classes/suggested-tasks/providers/class-tasks.php`
+
+#### 5.3 CSS/JS Files
+- `assets/css/admin.css` - Revert onboarding-related styles
+- `assets/js/settings.js` - Review changes
+
+#### 5.4 Utility Files
+- `classes/ui/class-chart.php` - Review `get_week_badge_gauge_html` method
+- `classes/utils/class-date.php` - Review `format_date_for_display` method
+
+---
+
+### Phase 6: Handle Non-Onboarding Changes from Onboarding PRs
+
+**IMPORTANT:** These changes should be kept even though they came from onboarding-related PRs:
+
+1. **From PR #730** - `classes/class-plugin-upgrade-tasks.php`
+ - Changed condition for upgrade tasks display
+ - **Keep this change** (related to privacy policy acceptance)
+
+2. **From PR #736** - Playground functionality
+ - `classes/utils/class-debug-tools.php` - Debug tools for playground
+ - `classes/utils/class-playground.php` - Playground hooks and license key insertion
+ - **Keep these changes** (but review if they reference new onboarding)
+
+---
+
+### Phase 7: Update E2E Tests
+
+1. **Restore old onboarding E2E tests**
+ ```bash
+ git checkout e7323c4f21c9b71eb4b2ee3f96ae294fd53ca516^ -- tests/e2e/sequential/onboarding.spec.js
+ ```
+
+2. **Review if tests need updates for any other changes made since**
+
+---
+
+### Phase 8: Final Steps
+
+1. **Run tests**
+ ```bash
+ composer test
+ npm run test:e2e
+ ```
+
+2. **Run code style checks**
+ ```bash
+ composer check-cs
+ composer phpstan
+ ```
+
+3. **Manual testing checklist**
+ - [ ] Fresh plugin activation shows old onboarding
+ - [ ] Welcome page displays correctly
+ - [ ] Onboarding steps work as expected
+ - [ ] Dashboard loads without errors
+ - [ ] Settings page works correctly
+ - [ ] Playground functionality still works
+
+4. **Create PR**
+ ```bash
+ git add -A
+ git commit -m "Revert to old onboarding system
+
+ This reverts the new onboarding introduced in PR #714 and subsequent
+ onboarding-related PRs (#730, #733, #736, #741).
+
+ The new onboarding work is preserved in branch: filip/new-onboarding-preserved"
+
+ git push -u origin revert/old-onboarding
+ gh pr create --draft --title "Revert to old onboarding system" --body "..."
+ ```
+
+---
+
+## Risk Assessment
+
+### High Risk Areas
+1. **`classes/class-base.php`** - Core plugin functionality, changes affect initialization
+2. **`views/admin-page.php`** - Main dashboard view
+3. **Suggested tasks providers** - May have cross-dependencies
+
+### Medium Risk Areas
+1. **CSS files** - May affect overall admin styling
+2. **E2E tests** - May need updates for CI to pass
+
+### Low Risk Areas
+1. **Deleting new onboarding files** - Clean removal
+2. **Restoring old files** - Straightforward git checkout
+
+---
+
+## Alternative Approach: Git Revert
+
+Instead of manual file manipulation, you could try:
+```bash
+git revert --no-commit e7323c4f21c9b71eb4b2ee3f96ae294fd53ca516
+```
+
+However, this may cause conflicts with subsequent commits. The manual approach gives more control.
+
+---
+
+## Rollback Plan
+
+If the revert causes issues:
+1. The new onboarding is preserved in `filip/new-onboarding-preserved`
+2. Can simply close the revert PR without merging
+3. Current `develop` remains unchanged until PR is merged
+
+---
+
+## Timeline Estimate
+
+- Phase 1-2: Branch setup - Quick
+- Phase 3-4: File restoration/deletion - Quick
+- Phase 5: Manual review of modified files - Requires careful review
+- Phase 6: Non-onboarding changes - Quick review
+- Phase 7-8: Testing and PR - Depends on test results
+
+---
+
+*Plan created: January 21, 2026*
+*Last updated: January 21, 2026*
diff --git a/assets/css/admin.css b/assets/css/admin.css
index 58938bef6c..d0c7981551 100644
--- a/assets/css/admin.css
+++ b/assets/css/admin.css
@@ -80,6 +80,24 @@ body.toplevel_page_progress-planner {
margin-top: var(--prpl-padding);
}
+/*------------------------------------*\
+ Styles for the container of the page when the privacy policy is not accepted.
+\*------------------------------------*/
+.prpl-pp-not-accepted {
+
+ .prpl-start-onboarding-container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: var(--prpl-padding);
+ }
+
+ .prpl-start-onboarding-graphic {
+ width: 250px;
+ }
+}
+
/*------------------------------------*\
Generic styles.
\*------------------------------------*/
diff --git a/assets/css/onboard.css b/assets/css/onboard.css
deleted file mode 100644
index bac24ea7dd..0000000000
--- a/assets/css/onboard.css
+++ /dev/null
@@ -1,23 +0,0 @@
-#prpl-onboarding-form .prpl-form-fields label {
- display: grid;
- grid-template-columns: 1fr 3fr;
- margin-bottom: 0.5em;
- gap: var(--prpl-padding);
-}
-
-#prpl-onboarding-form label > span:has(input[type="checkbox"]) {
- display: flex;
- align-items: baseline;
-}
-
-.prpl-onboard-form-radio-select {
-
- label {
- display: block !important;
- }
-}
-
-#prpl-onboarding-submit-wrapper {
- display: flex;
- align-items: center;
-}
diff --git a/assets/css/onboarding/onboarding.css b/assets/css/onboarding/onboarding.css
new file mode 100644
index 0000000000..ae11f3057c
--- /dev/null
+++ b/assets/css/onboarding/onboarding.css
@@ -0,0 +1,1359 @@
+.prpl-popover-onboarding {
+
+ --prpl-color-text: #4b5563;
+
+ /* Paper */
+ --prpl-onboarding-popover-background: var(--prpl-background-paper, #fff);
+
+ /* Steps navigation */
+ --prpl-background-steps: var(--prpl-background, #f6f7f9);
+ --prpl-background-info: var(--prpl-background-content, #f6f5fb);
+ --prpl-background-step-active: var(--prpl-graph-color-4, #534786);
+ --prpl-color-number-step-active: var(--prpl-background-paper, #fff);
+ --prpl-color-ui-icon: #6b7280; /* already exists in variables-color.css */
+ --prpl-color-text-step-active: var(--prpl-color-headings, #38296d);
+ --prpl-color-border: #d1d5db; /* already exists in variables-color.css */
+
+ /* Button secondary */
+ --prpl-color-button-secondary: var(--prpl-background-point, #f9b23c );
+ --prpl-color-button-secondary-hover: var(--prpl-color-monthly, #faa310);
+ --prpl-color-button-secondary-text: #374151;
+ --prpl-color-button-secondary-icon: var(--prpl-color-button-secondary-text);
+
+ /* Button disabled */
+ --prpl-color-button-inactive: var(--prpl-color-gauge-remain, #e1e3e7 );
+ --prpl-color-button-inactive-text: var(--prpl-color-text, #4b5563);
+
+ --prpl-background-step-label: var(--prpl-background-step-active);
+ --prpl-color-step-label: var(--prpl-background-paper, #fff);
+
+ /* Required text */
+ --prpl-color-text-error: var(--prpl-color-button-primary, #dd324f);
+ --prpl-color-alert-error: #e73136; /* already exists in variables-color.css */
+
+ /* General error */
+ --prpl-background-alert-error: #fdeded;/* already exists in variables-color.css */
+ --prpl-color-alert-error-text: #7f1d1d; /* already exists in variables-color.css */
+
+ /* Custom contros (inputs, radio, checkbox) */
+ --prpl-color-selection-controls-inactive: #9ca3af;
+ --prpl-color-selection-controls: var(--prpl-graph-color-4, #534786);
+
+ --prpl-color-field-border: var(--prpl-color-border);
+ --prpl-color-field-border-active: var(--prpl-color-alert-info, #2563eb);
+ --prpl-background-field-active: var(--prpl-background-alert-info, #eff6ff);
+
+
+ font-family: system-ui, Arial, sans-serif;
+ font-size: 16px;
+
+ padding: 0;
+ box-sizing: border-box;
+
+ border-radius: 8px;
+ font-weight: 400;
+ max-height: 82vh;
+ width: 1200px;
+ max-width: 80vw;
+ color: var(--prpl-color-text);
+ background-color: var(--prpl-onboarding-popover-background);
+ border: 1px solid var(--prpl-color-border);
+ box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.07),-2px 0 6px rgba(0, 0, 0, 0.07);
+
+ /* Popover backdrop. */
+ &::backdrop {
+ background: rgba(0, 0, 0, 0.5);
+ }
+
+ /* Popover close button. */
+ .prpl-popover-close {
+ position: absolute;
+ top: 5px;
+ right: 5px;
+ padding: 8px;
+ cursor: pointer;
+ background: none;
+ border: none;
+ color: var(--prpl-color-text);
+ z-index: 10;
+ }
+
+ /* General styles. */
+ & * {
+ box-sizing: border-box;
+ }
+
+ p {
+ font-size: 16px;
+ margin-top: 0;
+ margin-bottom: 8px;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+
+ h2 {
+ font-family: system-ui, Arial, sans-serif;
+ margin-top: 0;
+ margin-bottom: 12px;
+ font-size: 20px;
+ }
+
+ h3 {
+ font-family: system-ui, Arial, sans-serif;
+ margin-top: 0;
+ margin-bottom: 8px;
+ font-size: 16px;
+ font-weight: 600;
+ color: var(--prpl-color-text);
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+
+ /* Copied from WP Core CSS. */
+ select {
+ font-size: 14px;
+ line-height: 2;
+ color: #2c3338;
+ border-color: var(--prpl-color-field-border);
+ box-shadow: none;
+ border-radius: 3px;
+ padding: 0 24px 0 8px;
+ min-height: 30px;
+
+ /* max-width: 400px; */
+ width: 100%;
+ -webkit-appearance: none;
+ background: #fff url(data:image/svg+xml;charset=US-ASCII,%3Csvg%20width%3D%2220%22%20height%3D%2220%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M5%206l5%205%205-5%202%201-7%207-7-7%202-1z%22%20fill%3D%22%23555%22%2F%3E%3C%2Fsvg%3E) no-repeat right 5px top 55%;
+ background-size: 16px 16px;
+ cursor: pointer;
+ vertical-align: middle;
+ }
+
+ input[type="text"],
+ input[type="email"] {
+ width: 100%;
+ padding: 8px;
+ border: 1px solid var(--prpl-color-field-border);
+ border-radius: 4px;
+ font-size: 14px;
+ line-height: 1.4;
+ color: var(--prpl-color-text);
+ }
+
+ /* Used for radio and checkbox inputs. */
+ .prpl-custom-inputs-wrapper{
+ padding-left: 3px;
+
+ /* To prevent custom radio and checkbox from being cut off. */
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+
+ .prpl-checkbox-wrapper,
+ .prpl-radio-wrapper {
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ }
+
+ /* Hide the default input, because WP has it's own styles (which include pseudo-elements). */
+ .prpl-custom-checkbox input[type="checkbox"],
+ .prpl-custom-radio input[type="radio"] {
+ position: absolute;
+ opacity: 0;
+ width: 0;
+ height: 0;
+ }
+
+ /* Shared styles for the custom radios and checkboxes. */
+ .prpl-custom-control {
+ display: inline-block;
+ vertical-align: middle;
+ margin-right: 12px;
+ width: 20px;
+ height: 20px;
+ box-sizing: border-box;
+ position: relative;
+ transition: border-color 0.2s, background 0.2s;
+ flex-shrink: 0;
+ }
+
+ /* Label text styling */
+ .prpl-custom-checkbox,
+ .prpl-custom-radio {
+ display: flex;
+ align-items: center;
+ margin-bottom: 8px;
+ cursor: pointer;
+ user-select: none;
+ }
+
+ /* Checkbox styles */
+ .prpl-custom-checkbox {
+
+ .prpl-custom-control {
+ border: 1px solid var(--prpl-color-selection-controls-inactive);
+
+ /* border-radius: 6px; */
+ background-color: var(--prpl-onboarding-popover-background);
+ }
+
+ input[type="checkbox"] {
+
+ /* Checkbox checked (on) */
+ &:checked+.prpl-custom-control {
+ background: var(--prpl-color-selection-controls);
+ border-color: var(--prpl-color-selection-controls);
+ }
+ }
+
+ /* Checkmark */
+ .prpl-custom-control::after {
+ content: "";
+ position: absolute;
+ left: 6px;
+ top: 2px;
+ width: 4px;
+ height: 9px;
+ border: solid var(--prpl-onboarding-popover-background);
+ border-width: 0 2px 2px 0;
+ opacity: 0;
+ transform: scale(0.8) rotate(45deg);
+ transition: opacity 0.2s, transform 0.2s;
+ }
+
+ input[type="checkbox"]:checked+.prpl-custom-control::after {
+ opacity: 1;
+ transform: scale(1) rotate(45deg);
+ }
+ }
+
+ /* Radio styles */
+ .prpl-custom-radio {
+
+ .prpl-custom-control {
+ border: 1px solid var(--prpl-color-selection-controls-inactive);
+ border-radius: 50%;
+ background-color: var(--prpl-onboarding-popover-background);
+ }
+
+ /* Radio hover (off) */
+ input[type="radio"] {
+
+ /* Radio checked (on) */
+ &:checked+.prpl-custom-control {
+ background: var(--prpl-color-selection-controls);
+ border-color: var(--prpl-color-selection-controls);
+ }
+ }
+
+ /* Radio dot */
+ .prpl-custom-control::after {
+ content: "";
+ position: absolute;
+ top: 5px;
+ left: 5px;
+ width: 8px;
+ height: 8px;
+ background-color: var(--prpl-onboarding-popover-background);
+ border-radius: 50%;
+ opacity: 0;
+ transition: opacity 0.2s;
+ }
+
+ input[type="radio"]:checked+.prpl-custom-control::after {
+ opacity: 1;
+ background-color: var(--prpl-onboarding-popover-background);
+ }
+ }
+ }
+
+ /* Main layout container */
+ .prpl-onboarding-layout {
+ display: flex;
+
+ /* gap: 24px; */
+ min-height: 350px;
+ }
+
+ /* Left column: Step navigation */
+ .prpl-onboarding-navigation {
+ width: 340px;
+ background: var(--prpl-background-steps);
+ border-right: none;
+ padding: 32px 24px 24px 24px;
+ flex-shrink: 0;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ gap: 24px;
+ }
+
+ .prpl-onboarding-logo {
+ height: 60px;
+ max-height: 100%;
+
+ img, svg {
+ height: 100%;
+ width: auto;
+ max-width: 100%;
+ }
+ }
+
+ .prpl-step-list {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ }
+
+ .prpl-nav-step-item {
+ margin-top: 10px;
+ display: flex;
+ align-items: flex-start;
+ gap: 14px;
+ padding: 0;
+ margin-bottom: 4px;
+ cursor: default;
+
+ &:first-child {
+ margin-top: 0;
+ }
+ }
+
+ .prpl-step-icon {
+ width: 20px;
+ height: 20px;
+ border-radius: 50%;
+ border: 1px solid var(--prpl-color-border);
+ color: var(--prpl-color-text);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ margin-top: 2px;
+ font-size: 14px;
+ }
+
+ .prpl-nav-step-item.prpl-active .prpl-step-icon {
+ background: var(--prpl-background-step-active);
+ border-color: var(--prpl-background-step-active);
+ color: var(--prpl-color-number-step-active);
+ }
+
+ .prpl-nav-step-item.prpl-completed .prpl-step-icon {
+ background: var(--prpl-color-ui-icon);
+ border-color: var(--prpl-color-ui-icon);
+ color: var(--prpl-background-paper);
+ }
+
+ .prpl-step-label {
+ font-size: 15px;
+ color: var(--prpl-color-ui-icon);
+ line-height: 1.5;
+ }
+
+ .prpl-nav-step-item.prpl-active .prpl-step-label {
+ color: var(--prpl-color-text-step-active);
+ font-weight: 700;
+ }
+
+ .prpl-nav-step-item.prpl-completed .prpl-step-label {
+ color: var(--prpl-color-text-step-active);
+ }
+
+ #prpl-onboarding-mobile-step-label {
+ display: none;
+ margin-bottom: 4px;
+ font-size: 14px;
+ color: var(--prpl-background-step-active);
+ font-weight: 600;
+ }
+
+ /* Right section: Content area */
+ .prpl-onboarding-content {
+ padding: 32px 24px;
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+
+ /* gap: 24px; */
+ }
+
+ .tour-content-wrapper {
+ flex: 1;
+
+ /* overflow-y: auto; */
+ }
+
+ .tour-header {
+
+ .tour-title {
+ color: var(--prpl-background-step-active);
+ font-weight: 600;
+ }
+ }
+
+ .tour-footer {
+ margin-top: 16px;
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ gap: 10px;
+ width: 100%;
+
+ .prpl-tour-next-wrapper {
+ margin-left: auto;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ }
+ }
+
+ .tour-content {
+ font-size: 16px;
+ line-height: 1.5;
+
+ img {
+ max-width: 100%;
+ height: auto;
+ }
+
+ }
+
+ .prpl-columns-wrapper-flex {
+ display: flex;
+ gap: 24px;
+
+ /* overflow: hidden; */
+
+ /* padding-bottom: 10px; */
+
+ .prpl-background-content {
+ padding: 20px;
+ border-radius: 12px;
+ background-color: var(--prpl-background-info);
+ }
+
+ .prpl-column {
+ flex-grow: 1;
+ flex-basis: 50%;
+ flex-direction: column;
+ }
+
+ &.prpl-columns-2-1 {
+
+ .prpl-column:first-child {
+ flex: 2 1 0;
+ min-width: 0;
+ }
+
+ .prpl-column:last-child {
+ flex: 1 1 0;
+ min-width: 0;
+ }
+ }
+
+ &.prpl-columns-1-2 {
+
+ .prpl-column:first-child {
+ flex: 1 1 0;
+ min-width: 0;
+ }
+
+ .prpl-column:last-child {
+ flex: 2 1 0;
+ min-width: 0;
+ }
+ }
+ }
+
+ .prpl-btn {
+ display: inline-block;
+ margin: 0;
+ padding: 12px 20px;
+
+ text-decoration: none;
+ cursor: pointer;
+ font-size: 16px;
+
+ /* color: var(--prpl-color-button-primary-text); */
+
+ /* background: var(--prpl-color-button-primary); */
+ background-color: var(--prpl-color-button-secondary);
+ color: var(--prpl-color-button-secondary-text);
+ line-height: 1.25;
+ box-shadow: none;
+ border: none;
+ border-radius: 6px;
+ transition: all 0.25s ease-in-out;
+ font-weight: 600;
+ text-align: center;
+ box-sizing: border-box;
+ position: relative;
+ z-index: 1;
+
+ flex-shrink: 0;
+
+ /* &:not([disabled]):hover,
+ &:not([disabled]):focus {
+ background: var(--prpl-color-button-primary-hover);
+ } */
+
+ &.prpl-btn-secondary {
+ background-color: var(--prpl-color-button-secondary);
+ color: var(--prpl-color-button-secondary-text);
+
+ &:not([disabled]):not(.prpl-btn-disabled):hover,
+ &:not([disabled]):not(.prpl-btn-disabled):focus {
+ background-color: var(--prpl-color-button-secondary-hover);
+ color: var(--prpl-color-button-secondary-text);
+ }
+ }
+
+ &.prpl-btn-disabled,
+ &:disabled {
+
+ &,
+ &:hover{
+ color: var(--prpl-color-button-inactive-text);
+ color: rgb(from var(--prpl-color-button-inactive-text) r g b / 0.88);
+ background-color: var(--prpl-color-button-inactive);
+ cursor: not-allowed;
+ }
+ }
+ }
+
+ .prpl-complete-task-btn:not(.prpl-btn) {
+ border: none;
+ background: none;
+ cursor: pointer;
+ padding: 0;
+ margin: 0;
+ font-size: 16px;
+ color: var(--prpl-color-link);
+ }
+
+/*
+ .prpl-complete-task-btn-completed:not(.prpl-btn) {
+ color: #059669;
+ pointer-events: none;
+ opacity: 0.5;
+ } */
+
+ .prpl-complete-task-btn-error {
+ color: #9f0712;
+ }
+
+ /* Generic error message (reusable for all steps) */
+ .prpl-error-message {
+ animation: prpl-slide-down 0.3s ease-out;
+ }
+
+ @keyframes prpl-slide-down {
+
+ from {
+ opacity: 0;
+ transform: translateY(-10px);
+ }
+
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+ }
+
+ .prpl-error-box {
+ display: flex;
+ align-items: flex-start;
+ gap: 15px;
+ background: var(--prpl-background-alert-error);
+ padding: 20px;
+ border-radius: 6px;
+
+ .prpl-error-icon {
+ color: var(--prpl-color-alert-error);
+ width: 20px;
+ height: 20px;
+ flex-shrink: 0;
+
+ img, svg {
+ width: 100%;
+ height: 100%;
+ }
+ }
+
+ h3 {
+ margin: 0 0 8px 0;
+ font-size: 16px;
+ font-weight: 700;
+ color: var(--prpl-color-alert-error-text);
+ }
+
+ p {
+ margin: 0;
+ font-size: 16px;
+ line-height: 1.4;
+ color: var(--prpl-color-alert-error-text);
+ }
+ }
+
+ /* Popover: Quit confirmation */
+ &:has(.prpl-quit-confirmation) {
+
+ #prpl-tour-close-btn,
+ .prpl-onboarding-navigation {
+ display: none;
+ }
+ }
+
+ .prpl-column:has(.prpl-quit-confirmation) {
+ justify-content: flex-start !important;
+ }
+
+ .prpl-quit-confirmation {
+ display: flex;
+ flex-direction: column;
+ gap: 30px;
+ }
+
+ .prpl-quit-message {
+ display: flex;
+ align-items: flex-start;
+ gap: 15px;
+ background: var(--prpl-background-alert-error);
+ padding: 20px;
+ border-radius: 6px;
+ border-left: 4px solid var(--prpl-color-alert-error);
+
+ h3 {
+ font-size: 18px;
+ font-weight: 700;
+ }
+
+ p {
+ font-size: 15px;
+ line-height: 1.6;
+ }
+ }
+
+ .prpl-quit-actions {
+ display: flex;
+ gap: 40px;
+ justify-content: space-between;
+ align-items: center;
+ }
+
+ .prpl-quit-link {
+ font-size: 15px;
+ color: var(--prpl-color-link);
+ text-decoration: underline;
+ cursor: pointer;
+
+ &.prpl-quit-link-primary {
+ font-weight: 600;
+ font-size: 16px;
+ }
+ }
+
+ #prpl-quit-confirmation-graphic {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ img,
+ svg {
+ height: 250px;
+
+ /* Set height since it is what we do in the PP dashboard. */
+ max-width: 100%;
+ }
+ }
+
+ /* Form for onboarding tasks. */
+ .prpl-onboarding-task-form {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ border: none;
+ padding: 0;
+ margin: 0;
+ background: none;
+
+ .prpl-complete-task-btn {
+ flex-shrink: 0;
+ margin-top: 16px;
+ }
+ }
+
+ /* Welcome Step */
+ &[data-prpl-step="0"] {
+
+ .prpl-column:not(.prpl-column-content) {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ }
+
+ #prpl-welcome-graphic {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ img, svg {
+ height: 250px; /* Set height since it is what we do in the PP dashboard. */
+ max-width: 100%;
+ }
+ }
+
+ .prpl-welcome-note{
+ font-size: 14px;
+ font-style: italic;
+ }
+ }
+
+ /* Privacy checkbox */
+ .prpl-privacy-checkbox-wrapper {
+ margin-top: 20px;
+
+ label {
+ gap: 10px;
+ cursor: pointer;
+ font-size: 15px;
+ color: var(--prpl-color-text);
+ }
+
+ input[type="checkbox"] {
+ width: 18px;
+ height: 18px;
+ cursor: pointer;
+ }
+
+ a {
+ color: var(--prpl-color-link);
+ text-decoration: underline;
+ }
+
+ .prpl-required-indicator {
+ font-size: 14px;
+ font-style: italic;
+
+ &::before {
+ content: '(';
+ }
+
+ &::after {
+ content: ')';
+ }
+
+ &.prpl-required-indicator-active {
+ color: var(--prpl-color-text-error);
+ font-size: 16px;
+ font-style: normal;
+
+ &::before {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ content: '!';
+ width: 16px;
+ height: 16px;
+ border-radius: 50%;
+ background-color: var(--prpl-color-alert-error);
+ color: #fff;
+ font-size: 12px;
+ margin-right: 5px;
+ }
+
+ &::after {
+ content: '!';
+ }
+ }
+ }
+ }
+
+ /* What's Next Step */
+ .prpl-suggested-task-points {
+ font-size: var(--prpl-font-size-xs, 12px);
+ font-weight: 700;
+ color: var(--prpl-text-point);
+ background-color: var(--prpl-background-point);
+ width: 24px;
+ height: 24px;
+ border-radius: 50%;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ /* First Task Step */
+ .prpl-onboarding-task {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 10px;
+
+ /* .prpl-onboarding-task-title {
+ margin: 0 0 16px 0;
+ font-size: 18px;
+ font-weight: 600;
+ } */
+
+ .prpl-onboarding-task-form {
+ margin-top: 16px;
+ width: 100%;
+ flex-direction: column;
+
+ .prpl-complete-task-btn {
+ align-self: flex-end;
+
+ &.prpl-complete-task-btn-completed {
+ opacity: 0.5;
+ pointer-events: none;
+ }
+ }
+ }
+ }
+
+ /* Badges Step */
+ .prpl-gauge-wrapper {
+ max-width: 100%;
+ text-align: center;
+ }
+
+ /* Email Frequency Step */
+
+ /* .prpl-email-frequency-options {
+
+ .
+ display: flex;
+ gap: 10px;
+ flex-direction: column;
+ } */
+
+ #prpl-email-form {
+ margin-top: 16px;
+
+ .prpl-form-field {
+ margin-top: 16px;
+
+ &:first-child {
+ margin-top: 0;
+ }
+
+ label {
+ display: inline-block;
+ margin-bottom: 8px;
+ }
+ }
+ }
+
+ /* Settings Step */
+ .tour-content-wrapper:has(.prpl-setting-item) {
+ display: flex;
+ flex-direction: column;
+ flex-grow: 1;
+
+ .tour-content,
+ .prpl-setting-item,
+ .prpl-setting-content {
+ display: flex;
+ flex-direction: column;
+ flex-grow: 1;
+ gap: 24px;
+ }
+
+ .prpl-setting-title {
+ margin: 0 0 16px 0;
+ }
+
+ .prpl-settings-progress {
+ font-size: 14px;
+ color: var(--prpl-color-step-label);
+ padding: 4px 8px;
+ border-radius: 20px;
+ background-color: var(--prpl-background-step-active);
+ }
+
+ .prpl-setting-footer {
+ display: flex;
+ justify-content: flex-end;
+ align-items: flex-start;
+ gap: 10px;
+ margin-top: 16px;
+
+ .prpl-save-setting-btn {
+ flex-shrink: 0;
+ }
+ }
+
+ .prpl-select-page {
+
+ &.prpl-disabled {
+ opacity: 0.5;
+ pointer-events: none;
+ }
+ }
+
+ .prpl-setting-note {
+ display: none;
+ gap: 10px;
+ border-radius: 12px;
+ padding: 16px;
+ color: var(--prpl-color-field-border-active);
+ font-size: 14px;
+ background-color: var(--prpl-background-field-active);
+
+ .prpl-setting-note-icon {
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 20px;
+ height: 20px;
+ border-radius: 50%;
+ color: var(--prpl-color-field-border-active);
+ }
+ }
+
+ /* Post types sub-step */
+ #prpl-post-types-include-wrapper {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ }
+
+ /* Toggle checkbox for post types. */
+ .prpl-post-type-toggle-wrapper {
+ display: flex;
+ align-items: center;
+ }
+
+ .prpl-post-type-toggle-label {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ cursor: pointer;
+ position: relative;
+ }
+
+ .prpl-post-type-toggle-input {
+ position: absolute;
+ opacity: 0;
+ width: 0;
+ height: 0;
+ margin: 0;
+ padding: 0;
+ }
+
+ .prpl-post-type-toggle-switch {
+ position: relative;
+ width: 44px;
+ height: 24px;
+ border-radius: 12px;
+ background-color: var(--prpl-color-selection-controls-inactive);
+ transition: background-color 0.2s;
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ &::after {
+ content: '';
+ position: absolute;
+ top: 2px;
+ left: 2px;
+ width: 20px;
+ height: 20px;
+ border-radius: 50%;
+ background-color: var(--prpl-onboarding-popover-background);
+ transition: transform 0.2s;
+ z-index: 1;
+ }
+
+ svg {
+ position: absolute;
+ width: 12px;
+ height: 12px;
+ top: 50%;
+ left: 6px;
+ transform: translateY(-50%);
+ z-index: 2;
+ transition: opacity 0.2s, left 0.2s, color 0.2s;
+ color: var(--prpl-color-ui-icon);
+ }
+
+ .prpl-toggle-icon-check {
+ display: none;
+ }
+
+ .prpl-toggle-icon-x {
+ display: block;
+ }
+ }
+
+ .prpl-post-type-toggle-label:hover .prpl-post-type-toggle-switch svg {
+ opacity: 0.6;
+ }
+
+ .prpl-post-type-toggle-input:checked ~ .prpl-post-type-toggle-switch {
+ background-color: var(--prpl-background-step-active);
+
+ &::after {
+ transform: translateX(20px);
+ }
+
+ svg {
+ left: 26px;
+ color: var(--prpl-background-step-active);
+ transform: translateY(-50%);
+ }
+
+ .prpl-toggle-icon-check {
+ display: block;
+ }
+
+ .prpl-toggle-icon-x {
+ display: none;
+ }
+ }
+
+ .prpl-post-type-toggle-text {
+ font-size: 16px;
+ line-height: 1.5;
+ transition: opacity 0.2s;
+ }
+
+ .prpl-post-type-toggle-input:not(:checked) ~ .prpl-post-type-toggle-text {
+ opacity: 0.78;
+ }
+
+ .prpl-post-type-toggle-input:checked ~ .prpl-post-type-toggle-text {
+ opacity: 1;
+ }
+ }
+
+ /* More Tasks Step */
+
+ #prpl-success-graphic {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ img, svg {
+ height: 250px;
+ max-width: 100%;
+ }
+ }
+
+ /* Intro substep */
+ .prpl-more-tasks-substep[data-substep="more-tasks-intro"] {
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+ }
+
+ .prpl-more-tasks-intro-buttons {
+ margin-top: 16px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 24px;
+ flex-wrap: wrap;
+ }
+
+ .prpl-finish-onboarding {
+ background: none;
+ border: none;
+ color: var(--prpl-color-link, #534786);
+ text-decoration: underline;
+ font-size: 16px;
+ cursor: pointer;
+ padding: 12px 0;
+ }
+
+ .prpl-finish-onboarding:hover,
+ .prpl-finish-onboarding:focus {
+ color: var(--prpl-color-link-hover, #38296d);
+ }
+
+ /* Tasks substep */
+ .prpl-more-tasks-substep[data-substep="more-tasks-tasks"] .prpl-task-list {
+ margin: 0;
+ }
+
+ .prpl-column:has(.prpl-task-list) {
+ display: flex;
+ align-items: center;
+ }
+
+ .prpl-task-list {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ width: 100%;
+ border: 1px solid #ccc;
+
+ li {
+ padding: 7px 5px;
+ margin: 0;
+
+ &:nth-child(odd) {
+ background-color: var(--prpl-background-steps);
+ }
+
+ .task-title {
+ color: var(--prpl-color-text);
+ font-weight: 500;
+ }
+ }
+ }
+
+ .prpl-complete-task-item {
+ display: flex;
+ gap: 30px;
+ justify-content: space-between;
+ }
+
+ .prpl-task-arrow {
+ padding-right: 8px;
+ }
+
+ .prpl-task-item-button-wrapper {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex-shrink: 0;
+ }
+
+ .prpl-task-completed-icon {
+ display: none;
+ width: 20px;
+ height: 20px;
+ border-radius: 50%;
+ align-items: center;
+ justify-content: center;
+ color: #fff;
+ background-color: #059669;
+ }
+
+ .prpl-task-completed {
+
+ /* Show the completed icon. */
+ .prpl-task-completed-icon {
+ display: inline-flex;
+ }
+
+ /* Hide the trigger button and +1. */
+ .prpl-task-item-button-wrapper {
+ display: none;
+ }
+ }
+
+ .tour-content-wrapper:has(.prpl-task-content-active) {
+ display: flex;
+ flex-direction: column;
+ flex-grow: 1;
+
+ /* Task content active, TOOD: change markup so this is simpler. */
+ .prpl-task-content-active {
+ display: flex;
+ flex-direction: column;
+ flex-grow: 1;
+ gap: 24px;
+ width: 100%;
+
+ .prpl-onboarding-task {
+ display: flex;
+ flex-direction: column;
+ flex-grow: 1;
+
+ .prpl-onboarding-task-form,
+ .tour-content {
+ display: flex;
+ flex-direction: column;
+ flex-grow: 1;
+ }
+
+ .prpl-onboarding-task-form {
+ justify-content: space-between;
+ gap: 48px;
+ }
+ }
+ }
+
+ /* Task buttons. */
+ .prpl-task-buttons {
+ display: flex;
+ justify-content: flex-end;
+ gap: 16px;
+ width: 100%;
+
+ .prpl-btn {
+ margin-top: 0;
+ margin-bottom: 0;
+
+ &.prpl-task-close-btn {
+ background-color: var(--prpl-background-banner);
+ color: var(--prpl-color-text);
+ }
+ }
+ }
+ }
+
+ /* File drop zone. */
+ .prpl-file-drop-zone {
+ width: 100%;
+ border-radius: 12px;
+ padding: 40px;
+ text-align: center;
+ color: var(--prpl-color-text);
+ transition: background 0.2s, border-color 0.2s;
+ cursor: pointer;
+ border: 2px dashed var(--prpl-color-field-border);
+
+ svg {
+
+ path {
+ stroke: var(--prpl-color-ui-icon);
+ }
+ }
+ }
+
+ .prpl-file-drop-zone.dragover {
+ background-color: var(--prpl-background-field-active);
+ border-color: var(--prpl-color-field-border-active);
+ }
+
+ /* When an image has been uploaded. */
+ .prpl-file-drop-zone.has-image {
+ border-color: var(--prpl-color-field-border-active);
+ background-color: var(--prpl-background-field-active);
+
+ & > .prpl-icon-image,
+ & > p,
+ & > .prpl-file-upload-hints {
+ display: none;
+ }
+
+ .prpl-file-preview img {
+ border: 2px solid var(--prpl-color-field-border);
+ border-radius: 8px;
+ padding: 8px;
+ background: var(--prpl-color-background-white);
+ }
+ }
+
+ .prpl-file-browse-link {
+ color: var(--prpl-color-link);
+ text-decoration: underline;
+ cursor: pointer;
+ }
+
+ .prpl-file-remove-btn {
+ border: none;
+ background: none;
+ cursor: pointer;
+ padding: 0;
+ margin: 12px 0 0;
+ font-size: 16px;
+ color: var(--prpl-color-link);
+ text-decoration: underline;
+ }
+
+ .prpl-file-upload-hints {
+ display: flex;
+ flex-direction: column;
+ font-size: 14px;
+ color: var(--prpl-color-ui-icon);
+ }
+
+ /* WIP */
+ #prpl-upload-status {
+ margin-top: 10px;
+ font-family: monospace;
+ }
+
+ .prpl-file-preview {
+ display: none; /* Hidden by default. */
+ margin-top: 10px;
+ margin-left: auto;
+ margin-right: auto;
+ max-width: 200px;
+ height: auto;
+ }
+}
+
+@media (max-width: 1023px) {
+
+ .prpl-popover-onboarding {
+ --prpl-mobile-nav-height: 60px;
+ max-width: 90vw;
+ inset: 0 0 var(--prpl-mobile-nav-height) 0; /* TODO: Adjust this for smallest screen sizes. */
+ margin: auto auto var(--prpl-mobile-nav-height) auto; /* Center in available space above nav */
+
+ /* Hide graphics */
+ .prpl-hide-on-mobile {
+ display: none !important;
+ }
+
+ /* Quit confirmation */
+ .prpl-quit-actions {
+ flex-direction: column;
+ gap: 16px;
+ }
+
+ /* Naviation section */
+ .prpl-onboarding-navigation {
+ position: fixed;
+ left: 0;
+ bottom: 0;
+ width: 100%;
+ height: var(--prpl-mobile-nav-height);
+ z-index: 1000;
+
+ flex-direction: row;
+ gap: 16px;
+ justify-content: center;
+ align-items: center;
+ padding: 8px;
+
+ .prpl-step-list {
+ display: flex;
+ gap: 8px;
+
+ .prpl-nav-step-item {
+ margin: 0;
+ }
+
+ .prpl-step-label {
+ display: none;
+ }
+ }
+
+ #prpl-onboarding-mobile-step-label {
+ display: block;
+ }
+ }
+
+ /* Content section */
+ .prpl-columns-wrapper-flex {
+ flex-direction: column;
+ }
+
+ .prpl-setting-footer, /* On the settings steps */
+ .tour-footer {
+ flex-direction: column; /* So the info / error message is on top of the button. */
+ }
+
+ /* Badges step */
+ .prpl-gauge-wrapper {
+ max-width: 400px;
+ margin-left: auto;
+ margin-right: auto;
+ }
+ }
+}
diff --git a/assets/css/welcome.css b/assets/css/welcome.css
deleted file mode 100644
index 20004600b4..0000000000
--- a/assets/css/welcome.css
+++ /dev/null
@@ -1,77 +0,0 @@
-.prpl-wrap.prpl-pp-not-accepted {
- padding: 0;
- background-color: var(--prpl-background-paper);
- border: 1px solid var(--prpl-color-border);
- border-radius: var(--prpl-border-radius);
-}
-
-.prpl-welcome {
-
- .inner-content {
- padding: calc(var(--prpl-gap) * 1.5);
- padding-bottom: 0;
- margin-bottom: calc(var(--prpl-gap) * 1.5);
- display: flex;
- grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
- gap: calc(var(--prpl-gap) * 2);
-
- .left {
- flex-grow: 1;
- }
-
- img {
- max-width: 100%;
- width: 550px;
- height: auto;
- }
- }
-
- .welcome-header {
- background: var(--prpl-background-banner);
- display: flex;
- justify-content: space-between;
- align-items: center;
- border-top-left-radius: var(--prpl-border-radius);
- border-top-right-radius: var(--prpl-border-radius);
- overflow: hidden;
-
- h1 {
- font-size: var(--prpl-font-size-3xl);
- padding: var(--prpl-padding) calc(var(--prpl-gap) * 1.5);
- font-weight: 600;
- }
-
- .welcome-header-icon {
- background: var(--prpl-background-banner);
- background: linear-gradient(105deg, var(--prpl-background-banner) 25%, var(--prpl-background-monthly) 25%);
- padding: var(--prpl-padding);
- padding-left: 100px;
- padding-right: calc(var(--prpl-gap) * 1.5);
-
- svg {
- height: 100px;
- }
- }
- }
-
- .prpl-form-notice-title {
- font-size: var(--prpl-font-size-lg);
- }
-
- ul {
- list-style: disc;
- margin-left: 1rem;
- }
-
- .prpl-onboard-form-radio-select {
- margin-top: 0.75rem;
-
- label {
- margin-top: 0.5rem;
-
- &:first-child {
- margin-top: 0;
- }
- }
- }
-}
diff --git a/assets/images/onboarding/icon_image.svg b/assets/images/onboarding/icon_image.svg
new file mode 100644
index 0000000000..24f79c262a
--- /dev/null
+++ b/assets/images/onboarding/icon_image.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/images/onboarding/icon_info_solid.svg b/assets/images/onboarding/icon_info_solid.svg
new file mode 100644
index 0000000000..9b6e2d98f2
--- /dev/null
+++ b/assets/images/onboarding/icon_info_solid.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/images/onboarding/neglected_site_ravi.svg b/assets/images/onboarding/neglected_site_ravi.svg
new file mode 100644
index 0000000000..14acee56f5
--- /dev/null
+++ b/assets/images/onboarding/neglected_site_ravi.svg
@@ -0,0 +1 @@
+
diff --git a/assets/images/onboarding/success_ravi.svg b/assets/images/onboarding/success_ravi.svg
new file mode 100644
index 0000000000..45e8535d4e
--- /dev/null
+++ b/assets/images/onboarding/success_ravi.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/images/onboarding/thumbs_up_ravi_rtl.svg b/assets/images/onboarding/thumbs_up_ravi_rtl.svg
new file mode 100644
index 0000000000..27234bc510
--- /dev/null
+++ b/assets/images/onboarding/thumbs_up_ravi_rtl.svg
@@ -0,0 +1 @@
+
diff --git a/assets/js/license-generator.js b/assets/js/license-generator.js
new file mode 100644
index 0000000000..5dfc2bb109
--- /dev/null
+++ b/assets/js/license-generator.js
@@ -0,0 +1,140 @@
+/**
+ * License Generator - Handles license key generation during onboarding
+ * Adapted from onboard.js
+ */
+/* global progressPlanner */
+
+// eslint-disable-next-line no-unused-vars
+class LicenseGenerator {
+ /**
+ * Store config for use in other methods.
+ *
+ * @type {Object}
+ */
+ static config = null;
+
+ /**
+ * Get the default config from progressPlanner global.
+ *
+ * @return {Object} Default configuration object.
+ */
+ static getDefaultConfig() {
+ // eslint-disable-next-line no-undef
+ if ( typeof progressPlanner !== 'undefined' ) {
+ return {
+ // eslint-disable-next-line no-undef
+ onboardNonceURL: progressPlanner.onboardNonceURL,
+ // eslint-disable-next-line no-undef
+ onboardAPIUrl: progressPlanner.onboardAPIUrl,
+ // eslint-disable-next-line no-undef
+ adminAjaxUrl: progressPlanner.ajaxUrl,
+ // eslint-disable-next-line no-undef
+ nonce: progressPlanner.nonce,
+ };
+ }
+ return {};
+ }
+
+ /**
+ * Make a request to save the license key.
+ *
+ * @param {string} licenseKey The license key.
+ * @return {Promise} Promise that resolves when license is saved
+ */
+ static saveLicenseKey( licenseKey ) {
+ console.log( 'License key: ' + licenseKey );
+ return LicenseGenerator.ajaxRequest( {
+ url: LicenseGenerator.config.adminAjaxUrl,
+ data: {
+ action: 'progress_planner_save_onboard_data',
+ _ajax_nonce: LicenseGenerator.config.nonce,
+ key: licenseKey,
+ },
+ } );
+ }
+
+ /**
+ * Make the AJAX request to the API.
+ *
+ * @param {Object} data The data to send with the request.
+ * @return {Promise} Promise that resolves when request completes
+ */
+ static ajaxAPIRequest( data ) {
+ return LicenseGenerator.ajaxRequest( {
+ url: LicenseGenerator.config.onboardAPIUrl,
+ data,
+ } )
+ .then( ( response ) => {
+ // Make a local request to save the response data.
+ return LicenseGenerator.saveLicenseKey( response.license_key );
+ } )
+ .catch( ( error ) => {
+ console.warn( error );
+ throw error;
+ } );
+ }
+
+ /**
+ * Make the AJAX request.
+ *
+ * Make a request to get the nonce.
+ * Once the nonce is received, make a request to the API.
+ *
+ * @param {Object} data The data to send with the request.
+ * @param {Object} config Optional configuration object. Falls back to progressPlanner global.
+ * @return {Promise} Promise that resolves when license is generated
+ */
+ static generateLicense( data = {}, config = null ) {
+ // Store config for use in other methods, fall back to default if not provided.
+ LicenseGenerator.config = config || LicenseGenerator.getDefaultConfig();
+
+ return LicenseGenerator.ajaxRequest( {
+ url: LicenseGenerator.config.onboardNonceURL,
+ data,
+ } ).then( ( response ) => {
+ if ( 'ok' === response.status ) {
+ // Add the nonce to our data object.
+ data.nonce = response.nonce;
+
+ // Make the request to the API.
+ return LicenseGenerator.ajaxAPIRequest( data );
+ }
+ // Handle error response
+ const errorMessage =
+ response.message ||
+ 'Failed to get nonce for license generation';
+ throw new Error( errorMessage );
+ } );
+ }
+
+ /**
+ * Helper function to make AJAX requests
+ *
+ * @param {Object} options Request options
+ * @param {string} options.url The URL to send the request to
+ * @param {Object} options.data The data to send with the request
+ * @return {Promise} Promise that resolves with response data
+ */
+ static ajaxRequest( options ) {
+ const { url, data } = options;
+
+ return fetch( url, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ },
+ body: new URLSearchParams( data ),
+ credentials: 'same-origin',
+ } )
+ .then( ( response ) => {
+ if ( ! response.ok ) {
+ throw new Error( 'Request failed: ' + response.status );
+ }
+ return response.json();
+ } )
+ .catch( ( error ) => {
+ console.error( 'AJAX request error:', error );
+ throw error;
+ } );
+ }
+}
diff --git a/assets/js/onboard.js b/assets/js/onboard.js
deleted file mode 100644
index 529b05831b..0000000000
--- a/assets/js/onboard.js
+++ /dev/null
@@ -1,168 +0,0 @@
-/* global progressPlanner, progressPlannerAjaxRequest */
-/*
- * Onboard
- *
- * A script to handle the onboarding process.
- *
- * Dependencies: progress-planner/ajax-request, progress-planner/upgrade-tasks
- */
-
-/**
- * Make a request to save the license key.
- *
- * @param {string} licenseKey The license key.
- */
-const progressPlannerSaveLicenseKey = ( licenseKey ) => {
- console.log( 'License key: ' + licenseKey );
- return progressPlannerAjaxRequest( {
- url: progressPlanner.ajaxUrl,
- data: {
- action: 'progress_planner_save_onboard_data',
- _ajax_nonce: progressPlanner.nonce,
- key: licenseKey,
- },
- } );
-};
-
-/**
- * Make the AJAX request.
- *
- * @param {Object} data The data to send with the request.
- */
-const progressPlannerAjaxAPIRequest = ( data ) => {
- progressPlannerAjaxRequest( {
- url: progressPlanner.onboardAPIUrl,
- data,
- } )
- .then( ( response ) => {
- // Make a local request to save the response data.
- progressPlannerSaveLicenseKey( response.license_key ).then( () => {
- // Refresh the page.
- window.location.reload();
- } );
- } )
- .catch( ( error ) => {
- console.warn( error );
- } );
-};
-
-/**
- * Make the AJAX request.
- *
- * Make a request to get the nonce.
- * Once the nonce is received, make a request to the API.
- *
- * @param {Object} data The data to send with the request.
- */
-const progressPlannerOnboardCall = ( data ) => {
- progressPlannerAjaxRequest( {
- url: progressPlanner.onboardNonceURL,
- data,
- } ).then( ( response ) => {
- if ( 'ok' === response.status ) {
- // Add the nonce to our data object.
- data.nonce = response.nonce;
-
- // Make the request to the API.
- progressPlannerAjaxAPIRequest( data );
- }
- } );
-};
-
-if ( document.getElementById( 'prpl-onboarding-form' ) ) {
- document
- .querySelectorAll( 'input[name="with-email"]' )
- .forEach( ( input ) => {
- input.addEventListener( 'change', function () {
- if ( 'no' === this.value ) {
- document
- .getElementById( 'prpl-onboarding-form' )
- .querySelectorAll( 'input' )
- .forEach( ( inputField ) => {
- inputField.required = false;
- } );
- } else {
- document
- .getElementById( 'prpl-onboarding-form' )
- .querySelectorAll( 'input' )
- .forEach( ( inputField ) => {
- if (
- 'name' === inputField.name ||
- 'email' === inputField.name
- ) {
- inputField.required = true;
- }
- } );
- }
- document
- .getElementById( 'prpl-onboarding-form' )
- .querySelectorAll(
- '.prpl-form-fields, .prpl-form-fields, .prpl-button-primary, .prpl-button-secondary--no-email'
- )
- .forEach( ( el ) => el.classList.toggle( 'prpl-hidden' ) );
- } );
- } );
-
- document
- .querySelector( '#prpl-onboarding-form input[name="privacy-policy"]' )
- .addEventListener( 'change', function () {
- const privacyPolicyAccepted = !! this.checked;
-
- if ( privacyPolicyAccepted ) {
- document
- .getElementById( 'prpl-onboarding-submit-wrapper' )
- .classList.remove( 'prpl-disabled' );
- } else {
- document
- .getElementById( 'prpl-onboarding-submit-wrapper' )
- .classList.add( 'prpl-disabled' );
- }
- } );
-
- document
- .getElementById( 'prpl-onboarding-form' )
- .addEventListener( 'submit', function ( event ) {
- event.preventDefault();
-
- const privacyPolicyAccepted = !! document.querySelector(
- '#prpl-onboarding-form input[name="privacy-policy"]'
- ).checked;
-
- // Make sure the user accepted the privacy policy.
- if ( ! privacyPolicyAccepted ) {
- return;
- }
-
- // Disable all (both buttons) submit buttons.
- document
- .querySelectorAll(
- '#prpl-onboarding-form input[type="submit"]'
- )
- .forEach( ( input ) => {
- input.disabled = true;
- } );
-
- // Show the spinner.
- const spinner = document.createElement( 'span' );
- spinner.classList.add( 'prpl-spinner' );
- spinner.innerHTML =
- ''; // WP spinner.
-
- // Append spinner after submit button.
-
- document
- .getElementById( 'prpl-onboarding-submit-wrapper' )
- .appendChild( spinner );
-
- // Get all form data.
- const data = Object.fromEntries( new FormData( event.target ) );
-
- // If the user doesn't want to use email, remove the email and name.
- if ( 'no' === data.with_email ) {
- data.email = '';
- data.name = '';
- }
-
- progressPlannerOnboardCall( data );
- } );
-}
diff --git a/assets/js/onboarding/OnboardTask.js b/assets/js/onboarding/OnboardTask.js
new file mode 100644
index 0000000000..2db23b7816
--- /dev/null
+++ b/assets/js/onboarding/OnboardTask.js
@@ -0,0 +1,470 @@
+/**
+ * OnboardTask - Handles individual tasks that open within the column
+ * Used by the MoreTasksStep for tasks that require user input
+ * Toggles visibility of task list and shows task content in the same column
+ */
+/* global ProgressPlannerOnboardData, ProgressPlannerTourUtils */
+
+// eslint-disable-next-line no-unused-vars
+class PrplOnboardTask {
+ constructor( el, wizard ) {
+ this.el = el;
+ this.id = el.dataset.taskId;
+ this.wizard = wizard;
+ this.taskContent = null;
+ this.formValues = {};
+ this.openTaskBtn = el.querySelector( '[prpl-open-task]' );
+
+ // Register task open event
+ this.openTaskBtn?.addEventListener( 'click', () => this.open() );
+ }
+
+ /**
+ * Get the tour footer element via the current step
+ * @return {HTMLElement|null} The tour footer element or null if not found
+ */
+ getTourFooter() {
+ // Get the current step and use its getTourFooter method
+ const currentStep =
+ this.wizard?.tourSteps?.[ this.wizard.state.currentStep ];
+ if ( currentStep && typeof currentStep.getTourFooter === 'function' ) {
+ return currentStep.getTourFooter();
+ }
+
+ // Fallback in case step doesn't have the method
+ return this.wizard?.contentWrapper?.querySelector( '.tour-footer' );
+ }
+
+ registerEvents() {
+ this.taskContent.addEventListener( 'click', ( e ) => {
+ if ( e.target.classList.contains( 'prpl-complete-task-btn' ) ) {
+ const formData = new FormData(
+ this.taskContent.querySelector( 'form' )
+ );
+ this.formValues = Object.fromEntries( formData.entries() );
+ this.complete();
+ }
+ } );
+
+ // Close button handler
+ const closeBtn = this.taskContent.querySelector(
+ '.prpl-task-close-btn'
+ );
+ closeBtn?.addEventListener( 'click', () => this.close() );
+
+ this.setupFormValidation();
+
+ // Initialize upload handling (only if upload field exists)
+ this.setupFileUpload();
+
+ this.el.addEventListener( 'prplFileUploaded', ( e ) => {
+ // Handle file upload for the 'set site icon' task.
+ if ( 'core-siteicon' === e.detail.fileInput.dataset.taskId ) {
+ // Element which will be used to store the file post ID.
+ const nextElementSibling =
+ e.detail.fileInput.nextElementSibling;
+
+ nextElementSibling.value = e.detail.filePost.id;
+
+ // Trigger change so validation is triggered and "Complete" button is enabled.
+ nextElementSibling.dispatchEvent(
+ new CustomEvent( 'change', {
+ bubbles: true,
+ } )
+ );
+ }
+ } );
+ }
+
+ open() {
+ if ( this.taskContent ) {
+ return; // Already open
+ }
+
+ // Find the column containing the task list
+ const taskList = this.wizard.popover.querySelector( '.prpl-task-list' );
+ if ( ! taskList ) {
+ return;
+ }
+
+ const column = taskList.closest( '.prpl-column' );
+ if ( ! column ) {
+ return;
+ }
+
+ // Hide the task list
+ taskList.style.display = 'none';
+
+ // Hide the tour footer (it's part of the step content)
+ const tourFooter = this.getTourFooter();
+ if ( tourFooter ) {
+ tourFooter.style.display = 'none';
+ }
+
+ // Get task content from template
+ const content = this.el
+ .querySelector( 'template' )
+ .content.cloneNode( true );
+
+ // Create task content wrapper
+ this.taskContent = document.createElement( 'div' );
+ this.taskContent.className = 'prpl-task-content-active';
+ this.taskContent.appendChild( content );
+
+ // Find the complete button in the form
+ const completeBtn = this.taskContent.querySelector(
+ '.prpl-complete-task-btn'
+ );
+
+ if ( completeBtn ) {
+ // Create close button
+ const closeBtn = document.createElement( 'button' );
+ closeBtn.type = 'button';
+ closeBtn.className = 'prpl-btn prpl-task-close-btn';
+ closeBtn.innerHTML =
+ ' ' +
+ ProgressPlannerOnboardData.l10n.backToRecommendations;
+
+ // Create button wrapper
+ const buttonWrapper = document.createElement( 'div' );
+ buttonWrapper.className = 'prpl-task-buttons';
+
+ // Move complete button into wrapper
+ completeBtn.parentNode.insertBefore( buttonWrapper, completeBtn );
+ buttonWrapper.appendChild( closeBtn );
+ buttonWrapper.appendChild( completeBtn );
+ }
+
+ // Add task content to the column
+ column.appendChild( this.taskContent );
+
+ // Hide the popover close button
+ const popoverCloseBtn = this.wizard.popover.querySelector(
+ '#prpl-tour-close-btn'
+ );
+ if ( popoverCloseBtn ) {
+ popoverCloseBtn.style.display = 'none';
+ }
+
+ // Register events
+ this.registerEvents();
+ }
+
+ close() {
+ if ( ! this.taskContent ) {
+ return;
+ }
+
+ // Remove task content
+ this.taskContent.remove();
+
+ // Show the task list
+ const taskList = this.wizard.popover.querySelector( '.prpl-task-list' );
+ if ( taskList ) {
+ taskList.style.display = '';
+ }
+
+ // Show the tour footer (it's part of the step content)
+ const tourFooter = this.getTourFooter();
+ if ( tourFooter ) {
+ tourFooter.style.display = '';
+ }
+
+ // Show the popover close button
+ const popoverCloseBtn = this.wizard.popover.querySelector(
+ '#prpl-tour-close-btn'
+ );
+ if ( popoverCloseBtn ) {
+ popoverCloseBtn.style.display = '';
+ }
+
+ // Clean up
+ this.taskContent = null;
+ }
+
+ complete() {
+ ProgressPlannerTourUtils.completeTask( this.id, this.formValues )
+ .then( () => {
+ this.el.classList.add( 'prpl-task-completed' );
+ const taskBtn = this.el.querySelector(
+ '.prpl-complete-task-btn'
+ );
+ if ( taskBtn ) {
+ taskBtn.disabled = true;
+ }
+
+ this.close();
+ this.notifyParent();
+ } )
+ .catch( ( error ) => {
+ console.error( error );
+ // TODO: Handle error.
+ } );
+ }
+
+ notifyParent() {
+ const event = new CustomEvent( 'taskCompleted', {
+ bubbles: true,
+ detail: { id: this.id, formValues: this.formValues },
+ } );
+ this.el.dispatchEvent( event );
+ }
+
+ setupFormValidation() {
+ const form = this.taskContent.querySelector( 'form' );
+ const submitButton = this.taskContent.querySelector(
+ '.prpl-complete-task-btn'
+ );
+
+ if ( ! form || ! submitButton ) {
+ return;
+ }
+
+ const validateElements = form.querySelectorAll( '[data-validate]' );
+ if ( validateElements.length === 0 ) {
+ return;
+ }
+
+ const checkValidation = () => {
+ let isValid = true;
+
+ validateElements.forEach( ( element ) => {
+ const validationType = element.getAttribute( 'data-validate' );
+ let elementValid = false;
+
+ switch ( validationType ) {
+ case 'required':
+ elementValid =
+ element.value !== null &&
+ element.value !== undefined &&
+ element.value !== '';
+ break;
+ case 'not-empty':
+ elementValid = element.value.trim() !== '';
+ break;
+ default:
+ elementValid = true;
+ }
+
+ if ( ! elementValid ) {
+ isValid = false;
+ }
+ } );
+
+ submitButton.disabled = ! isValid;
+ };
+
+ checkValidation();
+ validateElements.forEach( ( element ) => {
+ element.addEventListener( 'change', checkValidation );
+ element.addEventListener( 'input', checkValidation );
+ } );
+ }
+
+ /**
+ * Handles drag-and-drop or manual file upload for specific tasks.
+ * Only runs if the form contains an upload field.
+ */
+ setupFileUpload() {
+ const uploadContainer = this.taskContent.querySelector(
+ '[data-upload-field]'
+ );
+ if ( ! uploadContainer ) {
+ return;
+ } // no upload for this task
+
+ const fileInput = uploadContainer.querySelector( 'input[type="file"]' );
+ const statusDiv = uploadContainer.querySelector(
+ '.prpl-upload-status'
+ );
+
+ // Visual drag behavior
+ [ 'dragenter', 'dragover' ].forEach( ( event ) => {
+ uploadContainer.addEventListener( event, ( e ) => {
+ e.preventDefault();
+ uploadContainer.classList.add( 'dragover' );
+ } );
+ } );
+
+ [ 'dragleave', 'drop' ].forEach( ( event ) => {
+ uploadContainer.addEventListener( event, ( e ) => {
+ e.preventDefault();
+ uploadContainer.classList.remove( 'dragover' );
+ } );
+ } );
+
+ uploadContainer.addEventListener( 'drop', ( e ) => {
+ const file = e.dataTransfer.files[ 0 ];
+ if ( file ) {
+ this.uploadFile( file, statusDiv ).then( ( response ) => {
+ this.el.dispatchEvent(
+ new CustomEvent( 'prplFileUploaded', {
+ detail: { file, filePost: response, fileInput },
+ bubbles: true,
+ } )
+ );
+ } );
+ }
+ } );
+
+ fileInput?.addEventListener( 'change', ( e ) => {
+ const file = e.target.files[ 0 ];
+ if ( file ) {
+ this.uploadFile( file, statusDiv, fileInput ).then(
+ ( response ) => {
+ this.el.dispatchEvent(
+ new CustomEvent( 'prplFileUploaded', {
+ detail: { file, filePost: response, fileInput },
+ bubbles: true,
+ } )
+ );
+ }
+ );
+ }
+ } );
+
+ // Remove button handler.
+ const removeBtn = uploadContainer.querySelector( '.prpl-file-remove-btn' );
+ const previewDiv = uploadContainer.querySelector( '.prpl-file-preview' );
+ removeBtn?.addEventListener( 'click', () => {
+ this.removeUploadedFile( uploadContainer, previewDiv );
+ } );
+ }
+
+ async uploadFile( file, statusDiv ) {
+ // Validate file extension
+ if ( ! this.isValidFaviconFile( file ) ) {
+ const fileInput =
+ this.taskContent.querySelector( 'input[type="file"]' );
+ const acceptedTypes = fileInput?.accept || 'supported file types';
+ statusDiv.textContent = `Invalid file type. Please upload a file with one of these formats: ${ acceptedTypes }`;
+ return;
+ }
+
+ statusDiv.textContent = `Uploading ${ file.name }...`;
+
+ const formData = new FormData();
+ formData.append( 'file', file );
+ formData.append( 'prplFileUpload', '1' );
+
+ return fetch( '/wp-json/wp/v2/media', {
+ method: 'POST',
+ headers: {
+ 'X-WP-Nonce': ProgressPlannerOnboardData.nonceWPAPI, // usually wp_localize_script adds this
+ },
+ body: formData,
+ credentials: 'same-origin',
+ } )
+ .then( ( res ) => {
+ if ( 201 !== res.status ) {
+ throw new Error( 'Failed to upload file' );
+ }
+ return res.json();
+ } )
+ .then( ( response ) => {
+ // Testing only, no need to display file name in production.
+ // statusDiv.textContent = `${ file.name } uploaded.`;
+ statusDiv.style.display = 'none';
+
+ // Update the file preview.
+ const previewDiv =
+ this.taskContent.querySelector( '.prpl-file-preview' );
+ if ( previewDiv ) {
+ previewDiv.innerHTML = ``;
+ previewDiv.style.display = 'block';
+
+ // Add has-image class to drop zone to update styling.
+ const dropZone = this.taskContent.querySelector(
+ '.prpl-file-drop-zone'
+ );
+ if ( dropZone ) {
+ dropZone.classList.add( 'has-image' );
+
+ // Show the remove button.
+ const removeBtn = dropZone.querySelector(
+ '.prpl-file-remove-btn'
+ );
+ if ( removeBtn ) {
+ removeBtn.hidden = false;
+ }
+ }
+ }
+ return response;
+ } )
+ .catch( ( error ) => {
+ console.error( error );
+ statusDiv.textContent = `Error: ${ error.message }`;
+ } );
+ }
+
+ /**
+ * Validate if file matches the accepted file types from the input
+ * @param {File} file The file to validate
+ * @return {boolean} True if file extension is supported
+ */
+ isValidFaviconFile( file ) {
+ const fileInput =
+ this.taskContent.querySelector( 'input[type="file"]' );
+ if ( ! fileInput || ! fileInput.accept ) {
+ return true; // No restrictions if no accept attribute
+ }
+
+ const acceptedTypes = fileInput.accept
+ .split( ',' )
+ .map( ( type ) => type.trim() );
+ const fileName = file.name.toLowerCase();
+
+ return acceptedTypes.some( ( type ) => {
+ if ( type.startsWith( '.' ) ) {
+ // Extension-based validation
+ return fileName.endsWith( type );
+ } else if ( type.includes( '/' ) ) {
+ // MIME type-based validation
+ return file.type === type;
+ }
+ return false;
+ } );
+ }
+
+ /**
+ * Remove uploaded file and reset the drop zone state.
+ * @param {HTMLElement} dropZone The drop zone element.
+ * @param {HTMLElement} previewDiv The preview container element.
+ */
+ removeUploadedFile( dropZone, previewDiv ) {
+ // Clear the preview.
+ previewDiv.innerHTML = '';
+ previewDiv.style.display = 'none';
+
+ // Remove has-image class.
+ dropZone.classList.remove( 'has-image' );
+
+ // Hide the remove button.
+ const removeBtn = dropZone.querySelector( '.prpl-file-remove-btn' );
+ if ( removeBtn ) {
+ removeBtn.hidden = true;
+ }
+
+ // Clear the file input.
+ const fileInput = dropZone.querySelector( 'input[type="file"]' );
+ if ( fileInput ) {
+ fileInput.value = '';
+ }
+
+ // Clear the hidden post_id input and trigger validation.
+ const postIdInput = dropZone.querySelector( 'input[name="post_id"]' );
+ if ( postIdInput ) {
+ postIdInput.value = '';
+ postIdInput.dispatchEvent(
+ new CustomEvent( 'change', { bubbles: true } )
+ );
+ }
+
+ // Show status div again.
+ const statusDiv = dropZone.querySelector( '.prpl-upload-status' );
+ if ( statusDiv ) {
+ statusDiv.style.display = '';
+ statusDiv.textContent = '';
+ }
+ }
+}
diff --git a/assets/js/onboarding/onboarding.js b/assets/js/onboarding/onboarding.js
new file mode 100644
index 0000000000..3bda0afeb6
--- /dev/null
+++ b/assets/js/onboarding/onboarding.js
@@ -0,0 +1,501 @@
+/**
+ * Progress Planner Onboarding Wizard
+ * Handles the onboarding wizard functionality
+ *
+ * Dependencies: progress-planner/license-generator
+ */
+/* global ProgressPlannerOnboardData */
+
+// eslint-disable-next-line no-unused-vars
+class ProgressPlannerOnboardWizard {
+ constructor( config ) {
+ this.config = config;
+ this.state = {
+ currentStep: 0,
+ data: {
+ moreTasksCompleted: {},
+ firstTaskCompleted: false,
+ finished: false,
+ },
+ cleanup: null,
+ };
+
+ // Store previously focused element for accessibility
+ this.previouslyFocusedElement = null;
+
+ // Restore saved progress if available
+ this.restoreSavedProgress();
+
+ // Make state work with reactive updates.
+ this.setupStateProxy();
+
+ // Set DOM related properties FIRST.
+ this.popover = document.getElementById( this.config.popoverId );
+ this.contentWrapper = this.popover.querySelector(
+ '.tour-content-wrapper'
+ );
+
+ // Popover buttons.
+ this.closeBtn = this.popover.querySelector( '#prpl-tour-close-btn' );
+
+ // Initialize tour steps AFTER popover is set
+ this.tourSteps = this.initializeTourSteps();
+
+ // Setup event listeners after DOM is ready
+ this.setupEventListeners();
+ }
+
+ /**
+ * Restore saved progress from server
+ */
+ restoreSavedProgress() {
+ if (
+ ! this.config.savedProgress ||
+ typeof this.config.savedProgress !== 'object'
+ ) {
+ return;
+ }
+
+ const savedState = this.config.savedProgress;
+
+ // Restore currentStep if valid
+ if (
+ typeof savedState.currentStep === 'number' &&
+ savedState.currentStep >= 0
+ ) {
+ this.state.currentStep = savedState.currentStep;
+ console.log(
+ 'Restored onboarding progress to step:',
+ this.state.currentStep
+ );
+ }
+
+ // Restore data object if present
+ if ( savedState.data && typeof savedState.data === 'object' ) {
+ // Merge saved data with default state
+ this.state.data = {
+ ...this.state.data,
+ ...savedState.data,
+ };
+
+ // Ensure moreTasksCompleted is an object
+ if (
+ ! this.state.data.moreTasksCompleted ||
+ typeof this.state.data.moreTasksCompleted !== 'object'
+ ) {
+ this.state.data.moreTasksCompleted = {};
+ }
+
+ console.log( 'Restored onboarding data:', this.state.data );
+ }
+ }
+
+ /**
+ * Initialize tour steps configuration
+ * Creates instances of step components
+ */
+ initializeTourSteps() {
+ // Create instances of step components.
+ const steps = this.config.steps.map( ( stepName ) => {
+ if (
+ window[ `Prpl${ stepName }` ] &&
+ typeof window[ `Prpl${ stepName }` ] === 'object'
+ ) {
+ return window[ `Prpl${ stepName }` ];
+ }
+
+ console.error(
+ `Step class "${ stepName }" not found. Available on window:`,
+ Object.keys( window ).filter( ( key ) =>
+ key.includes( 'Step' )
+ )
+ );
+
+ return null;
+ } );
+
+ // Set wizard reference for each step
+ steps.forEach( ( step ) => step.setWizard( this ) );
+
+ return steps;
+ }
+
+ /**
+ * Render current step
+ */
+ renderStep() {
+ const step = this.tourSteps[ this.state.currentStep ];
+
+ // Render step content
+ this.contentWrapper.innerHTML = step.render();
+
+ // Cleanup previous step
+ if ( this.state.cleanup ) {
+ this.state.cleanup();
+ this.state.cleanup = null;
+ }
+
+ // Mount current step and store cleanup function
+ this.state.cleanup = step.onMount( this.state );
+
+ // Setup next button (handled by step now)
+ step.setupNextButton();
+
+ // Update step indicator
+ this.popover.dataset.prplStep = this.state.currentStep;
+ this.updateStepNavigation();
+ }
+
+ /**
+ * Update step navigation in left column
+ */
+ updateStepNavigation() {
+ const stepItems = this.popover.querySelectorAll(
+ '.prpl-nav-step-item'
+ );
+ let activeStepTitle = '';
+
+ stepItems.forEach( ( item, index ) => {
+ const icon = item.querySelector( '.prpl-step-icon' );
+ const stepNumber = index + 1;
+
+ // Remove all state classes
+ item.classList.remove( 'prpl-active', 'prpl-completed' );
+
+ // Add appropriate class and update icon
+ if ( index < this.state.currentStep ) {
+ // Completed step: show checkmark
+ item.classList.add( 'prpl-completed' );
+ icon.textContent = 'âś“';
+ } else if ( index === this.state.currentStep ) {
+ // Active step: show number
+ item.classList.add( 'prpl-active' );
+ icon.textContent = stepNumber;
+ activeStepTitle =
+ item.querySelector( '.prpl-step-label' ).textContent;
+ } else {
+ // Future step: show number
+ icon.textContent = stepNumber;
+ }
+ } );
+
+ // Update mobile step label
+ const mobileStepLabel = this.popover.querySelector(
+ '#prpl-onboarding-mobile-step-label'
+ );
+ if ( mobileStepLabel ) {
+ mobileStepLabel.textContent = activeStepTitle;
+ }
+ }
+
+ /**
+ * Move to next step
+ */
+ async nextStep() {
+ console.log(
+ 'nextStep() called, current step:',
+ this.state.currentStep
+ );
+ const step = this.tourSteps[ this.state.currentStep ];
+
+ // Check if user can proceed from current step
+ if ( ! step.canProceed( this.state ) ) {
+ console.log( 'Cannot proceed - step requirements not met' );
+ return;
+ }
+
+ // Call beforeNextStep if step has it (for async operations like license generation)
+ if ( step.beforeNextStep ) {
+ try {
+ await step.beforeNextStep();
+ } catch ( error ) {
+ console.error( 'Error in beforeNextStep:', error );
+ return; // Don't proceed if beforeNextStep fails
+ }
+ }
+
+ if ( this.state.currentStep < this.tourSteps.length - 1 ) {
+ this.state.currentStep++;
+ console.log( 'Moving to step:', this.state.currentStep );
+ this.saveProgressToServer();
+ this.renderStep();
+ } else {
+ console.log( 'Finishing tour - reached last step' );
+ this.state.data.finished = true;
+ this.closeTour();
+
+ // Redirect to the Progress Planner dashboard
+ if (
+ this.config.lastStepRedirectUrl &&
+ this.config.lastStepRedirectUrl.length > 0
+ ) {
+ window.location.href = this.config.lastStepRedirectUrl;
+ }
+ }
+ }
+
+ /**
+ * Move to previous step, currently not used.
+ */
+ prevStep() {
+ if ( this.state.currentStep > 0 ) {
+ this.state.currentStep--;
+ this.renderStep();
+ }
+ }
+
+ /**
+ * Close the tour
+ */
+ closeTour() {
+ if ( this.popover ) {
+ this.popover.hidePopover();
+ }
+ this.saveProgressToServer();
+
+ // Cleanup active step
+ if ( this.state.cleanup ) {
+ this.state.cleanup();
+ }
+
+ // Reset cleanup
+ this.state.cleanup = null;
+
+ // Restore focus to previously focused element for accessibility
+ if (
+ this.previouslyFocusedElement &&
+ typeof this.previouslyFocusedElement.focus === 'function'
+ ) {
+ this.previouslyFocusedElement.focus();
+ this.previouslyFocusedElement = null;
+ }
+ }
+
+ /**
+ * Start the onboarding
+ */
+ startOnboarding() {
+ if ( this.popover ) {
+ // Store currently focused element for accessibility
+ this.previouslyFocusedElement =
+ this.popover.ownerDocument.activeElement;
+
+ this.popover.showPopover();
+ this.updateStepNavigation();
+ this.renderStep();
+
+ // Move focus to popover for keyboard accessibility
+ // Use setTimeout to ensure popover is visible before focusing
+ setTimeout( () => {
+ this.popover.focus();
+ }, 0 );
+ }
+ }
+
+ /**
+ * Save progress to server
+ */
+ async saveProgressToServer() {
+ try {
+ const response = await fetch( this.config.adminAjaxUrl, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ },
+ body: new URLSearchParams( {
+ state: JSON.stringify( this.state ),
+ nonce: this.config.nonceProgressPlanner,
+ action: 'progress_planner_onboarding_save_progress',
+ } ),
+ credentials: 'same-origin',
+ } );
+
+ if ( ! response.ok ) {
+ throw new Error( 'Request failed: ' + response.status );
+ }
+
+ return response.json();
+ } catch ( error ) {
+ console.error( 'Failed to save tour progress:', error );
+ }
+ }
+
+ /**
+ * Update next button state
+ * Delegates to current step's updateNextButton method
+ */
+ updateNextButton() {
+ const step = this.tourSteps[ this.state.currentStep ];
+ if ( step && typeof step.updateNextButton === 'function' ) {
+ step.updateNextButton();
+ }
+ }
+
+ /**
+ * Update DOM, used for reactive updates.
+ * All changes which should happen when the state changes should be done here.
+ */
+ updateDOM() {
+ this.updateNextButton();
+ }
+
+ /**
+ * Setup event listeners
+ */
+ setupEventListeners() {
+ console.log( 'Setting up event listeners...' );
+ if ( this.popover ) {
+ console.log( 'Popover found:', this.popover );
+
+ this.popover.addEventListener( 'beforetoggle', ( event ) => {
+ if ( event.newState === 'open' ) {
+ console.log( 'Tour opened' );
+ }
+ if ( event.newState === 'closed' ) {
+ console.log( 'Tour closed' );
+ }
+ } );
+
+ // Note: nextBtn click handler is now set up in renderStep()
+ // since the button is part of the step content
+
+ if ( this.closeBtn ) {
+ this.closeBtn.addEventListener( 'click', ( e ) => {
+ console.log( 'Close button clicked!' );
+
+ // Display quit confirmation if on welcome step (since privacy policy is accepted there)
+ if ( this.state.currentStep === 0 ) {
+ e.preventDefault();
+ this.showQuitConfirmation();
+ return;
+ }
+
+ this.state.data.finished =
+ this.state.currentStep === this.tourSteps.length - 1;
+ this.closeTour();
+
+ // If on PP Dashboard page and privacy was accepted during onboarding,
+ // refresh the page to properly initialize dashboard components.
+ if (
+ this.state.data.privacyAccepted &&
+ window.location.href.includes(
+ 'admin.php?page=progress-planner'
+ )
+ ) {
+ window.location.reload();
+ }
+ } );
+ }
+ } else {
+ console.error( 'Popover not found!' );
+ }
+ }
+
+ /**
+ * Show quit confirmation when trying to close without accepting privacy
+ */
+ showQuitConfirmation() {
+ // Replace content with confirmation message
+ const originalContent = this.contentWrapper.innerHTML;
+
+ // Get template from DOM
+ const template = document.getElementById(
+ 'prpl-onboarding-quit-confirmation'
+ );
+ if ( ! template ) {
+ console.error( 'Quit confirmation template not found' );
+ return;
+ }
+
+ this.contentWrapper.innerHTML = template.innerHTML;
+
+ // Add event listeners
+ const quitYes = this.contentWrapper.querySelector( '#prpl-quit-yes' );
+ const quitNo = this.contentWrapper.querySelector( '#prpl-quit-no' );
+
+ if ( quitYes ) {
+ quitYes.addEventListener( 'click', ( e ) => {
+ e.preventDefault();
+ this.closeTour();
+ } );
+ }
+
+ if ( quitNo ) {
+ quitNo.addEventListener( 'click', ( e ) => {
+ e.preventDefault();
+ // Restore original content
+ this.contentWrapper.innerHTML = originalContent;
+
+ // Re-mount the step
+ this.renderStep();
+ } );
+ }
+ }
+
+ /**
+ * Setup state proxy for reactive updates
+ */
+ setupStateProxy() {
+ this.state.data = this.createDeepProxy( this.state.data, () =>
+ this.updateDOM()
+ );
+ }
+
+ /**
+ * Create deep proxy for nested object changes
+ * @param {Object} target
+ * @param {Function} callback
+ */
+ createDeepProxy( target, callback ) {
+ // Recursively wrap existing nested objects first
+ for ( const key of Object.keys( target ) ) {
+ if (
+ target[ key ] &&
+ typeof target[ key ] === 'object' &&
+ ! Array.isArray( target[ key ] )
+ ) {
+ target[ key ] = this.createDeepProxy( target[ key ], callback );
+ }
+ }
+
+ return new Proxy( target, {
+ set: ( obj, prop, value ) => {
+ if (
+ value &&
+ typeof value === 'object' &&
+ ! Array.isArray( value )
+ ) {
+ value = this.createDeepProxy( value, callback );
+ }
+ obj[ prop ] = value;
+ callback();
+ return true;
+ },
+ } );
+ }
+}
+
+class ProgressPlannerTourUtils {
+ /**
+ * Complete a task via AJAX
+ * @param {string} taskId
+ * @param {Object} formValues
+ */
+ static async completeTask( taskId, formValues = {} ) {
+ const response = await fetch( ProgressPlannerOnboardData.adminAjaxUrl, {
+ method: 'POST',
+ body: new URLSearchParams( {
+ form_values: JSON.stringify( formValues ),
+ task_id: taskId,
+ nonce: ProgressPlannerOnboardData.nonceProgressPlanner,
+ action: 'progress_planner_onboarding_complete_task',
+ } ),
+ } );
+
+ if ( ! response.ok ) {
+ throw new Error( 'Request failed: ' + response.status );
+ }
+
+ return response.json();
+ }
+}
diff --git a/assets/js/onboarding/steps/BadgesStep.js b/assets/js/onboarding/steps/BadgesStep.js
new file mode 100644
index 0000000000..516636fadb
--- /dev/null
+++ b/assets/js/onboarding/steps/BadgesStep.js
@@ -0,0 +1,62 @@
+/**
+ * Badges step - Explains the badge system to users
+ * Simple informational step with no user interaction required
+ */
+/* global OnboardingStep */
+
+class PrplBadgesStep extends OnboardingStep {
+ constructor() {
+ super( {
+ templateId: 'onboarding-step-badges',
+ } );
+ }
+
+ /**
+ * Mount badges step and lazy-load badge graphic
+ * Badge is only loaded after privacy policy is accepted
+ * @return {Function} Cleanup function
+ */
+ onMount() {
+ const gaugeElement = document.getElementById( 'prpl-gauge-onboarding' );
+
+ if ( ! gaugeElement ) {
+ return () => {};
+ }
+
+ // Create badge element using innerHTML to properly instantiate the custom element
+ const badgeId = gaugeElement.getAttribute( 'data-badge-id' );
+ const badgeName = gaugeElement.getAttribute( 'data-badge-name' );
+ const brandingId = gaugeElement.getAttribute( 'data-branding-id' );
+
+ gaugeElement.innerHTML = `
+
${ this.escapeHtml( message ) }
+