feat(onboarding): add fluent/legacy mode selection flow

Expand onboarding wizard with new interactive steps to guide users
through UI architecture selection:

- Add animated typing intro component with AI-style streaming effect
- Add UI mode selection step (Fluent vs Legacy interface styles)
- Add Fluent placement configuration (Sideleaves vs Inline layout)
- Refactor onboarding flow from 5 to 7 steps with conditional branching
- Apply selected architecture preferences to plugin settings
- Add visual mode classes and enhanced styling support

The new flow allows users to customize their Task Genius experience
during initial setup while maintaining backward compatibility with
existing configurations.
This commit is contained in:
Quorafind 2025-09-30 16:09:25 +08:00
parent 68247f03be
commit 3fcf2f9705
8 changed files with 1746 additions and 800 deletions

View file

@ -0,0 +1,112 @@
import { t } from "@/translations/helper";
export class IntroTyping {
private timers: number[] = [];
cleanup() {
this.timers.forEach((id) => window.clearTimeout(id));
this.timers = [];
}
render(container: HTMLElement) {
container.empty();
const wrap = container.createDiv({cls: "intro-typing"});
const line1 = wrap.createEl("h1", {cls: "intro-line intro-line-1"});
const line2 = wrap.createEl("h2", {cls: "intro-line intro-line-2"});
const line3 = wrap.createEl("p", {cls: "intro-line intro-line-3"});
const line4 = wrap.createEl("p", {cls: "intro-line intro-line-4"});
const seq = [
{
el: line1,
text: t("Hi,"),
speed: 35
},
{
el: line2,
text: t("Thank you for using Task Genius"),
speed: 25
},
{
el: line3,
text: t("In the following steps, you will gradually set up Task Genius to get a more suitable environment for you"),
speed: 20
},
{
el: line4,
text: t("In the current version, Task Genius provides a brand new visual and interactive experience: Fluent; while also providing the option to return to the previous interface. Which one do you prefer?"),
speed: 20
},
];
this.aiStreamSequence(seq);
}
/**
* AI-style streaming text effect with smooth character fade-in
*/
private aiStreamSequence(seq: { el: HTMLElement; text: string; speed: number }[]) {
const streamLine = (idx: number) => {
if (idx >= seq.length) return;
const {el, text, speed} = seq[idx];
// Clear and prepare element
el.empty();
el.addClass("is-streaming");
// Create individual character spans for smooth animation
const chars: HTMLElement[] = [];
for (let i = 0; i < text.length; i++) {
const char = text[i];
const span = el.createSpan({
cls: "intro-char",
text: char,
});
// Special handling for spaces to preserve layout
if (char === " ") {
span.addClass("intro-char-space");
}
chars.push(span);
}
// Create cursor element that will follow the characters
const cursor = el.createSpan({
cls: "intro-cursor",
text: "▊",
});
// Animate characters with AI-style streaming effect
let charIndex = 0;
const animateChar = () => {
if (charIndex < chars.length) {
const char = chars[charIndex];
// Add small random variation for more natural feel
const jitter = Math.random() * speed * 0.3;
char.addClass("intro-char-visible");
// Move cursor after the just-revealed character
char.after(cursor);
charIndex++;
const id = window.setTimeout(animateChar, speed + jitter);
this.timers.push(id);
} else {
// Line complete, remove cursor and move to next
cursor.remove();
el.removeClass("is-streaming");
el.addClass("stream-complete");
// Small delay before next line
const id = window.setTimeout(() => streamLine(idx + 1), 300);
this.timers.push(id);
}
};
// Start animation
animateChar();
};
streamLine(0);
}
}

View file

@ -0,0 +1,66 @@
import { t } from "@/translations/helper";
export type UIMode = 'fluent' | 'legacy';
export class ModeSelection {
render(
container: HTMLElement,
current: UIMode | null,
onSelect: (mode: UIMode) => void
) {
container.empty();
const section = container.createDiv({ cls: "mode-selection" });
section.createEl("h2", { text: t("选择界面风格"), cls: "section-title" });
section.createEl("p", {
text: t("在下方选择你更喜欢的视觉与交互风格:全新的 Fluent 或 传统的 Legacy"),
cls: "section-desc",
});
const options = section.createDiv({ cls: "mode-options" });
const card = (
mode: UIMode,
title: string,
desc: string,
icon: string
) => {
const el = options.createDiv({ cls: `mode-card tg-v2-card mode-${mode}` });
const header = el.createDiv({ cls: "tg-v2-card-header" });
const titleEl = header.createDiv({ cls: "tg-v2-card-title" });
titleEl.setText(title);
const body = el.createDiv({ cls: "mode-card-body" });
const preview = body.createDiv({ cls: "mode-card-preview" });
preview.setText(" "); // 预览区域占位,后续可替换为图片
const description = body.createDiv({ cls: "mode-card-desc" });
description.setText(desc);
if (current === mode) el.addClass("is-selected");
el.addEventListener("click", () => onSelect(mode));
el.setAttr("tabindex", "0");
el.addClass("clickable-icon");
return el;
};
card(
"fluent",
t("Fluent现代与灵动"),
t("全新的信息架构、导航与交互,配合更优雅的样式与动画。"),
"sparkles"
);
card(
"legacy",
t("Legacy经典与稳定"),
t("延续过往界面与交互风格,保持熟悉的使用体验。"),
"layout"
);
// 小提示
const tips = section.createDiv({ cls: "mode-tips" });
tips.createEl("p", {
text: t("你可以在设置中随时切换这些选项。我们会尽量保持迁移的平滑与安全。"),
cls: "text-muted",
});
}
}

View file

@ -10,13 +10,19 @@ import { UserLevelSelector } from "./UserLevelSelector";
import { ConfigPreview } from "./ConfigPreview";
import { TaskCreationGuide } from "./TaskCreationGuide";
import { OnboardingComplete } from "./OnboardingComplete";
import { IntroTyping } from "./IntroTyping";
import { ModeSelection } from "./ModeSelection";
import { FluentPlacement } from "./FluentPlacement";
import "@/experimental/v2/styles/v2-enhanced.css";
export enum OnboardingStep {
WELCOME = 0,
USER_LEVEL_SELECT = 1,
CONFIG_PREVIEW = 2,
TASK_CREATION_GUIDE = 3,
COMPLETE = 4,
INTRO = 0,
MODE_SELECT = 1,
FLUENT_PLACEMENT = 2,
USER_LEVEL_SELECT = 3,
CONFIG_PREVIEW = 4,
TASK_CREATION_GUIDE = 5,
COMPLETE = 6,
}
export interface OnboardingState {
@ -24,6 +30,8 @@ export interface OnboardingState {
selectedConfig?: OnboardingConfig;
skipTaskGuide: boolean;
isCompleting: boolean;
uiMode: 'fluent' | 'legacy';
useSideLeaves: boolean; // Applies when uiMode === 'fluent'
}
export class OnboardingModal extends Modal {
@ -33,6 +41,9 @@ export class OnboardingModal extends Modal {
private state: OnboardingState;
// Step components
private introTyping: IntroTyping;
private modeSelection: ModeSelection;
private fluentPlacement: FluentPlacement;
private userLevelSelector: UserLevelSelector;
private configPreview: ConfigPreview;
private taskCreationGuide: TaskCreationGuide;
@ -58,12 +69,17 @@ export class OnboardingModal extends Modal {
// Initialize state
this.state = {
currentStep: OnboardingStep.WELCOME,
currentStep: OnboardingStep.INTRO,
skipTaskGuide: false,
isCompleting: false,
uiMode: 'fluent',
useSideLeaves: true,
};
// Initialize components
this.introTyping = new IntroTyping();
this.modeSelection = new ModeSelection();
this.fluentPlacement = new FluentPlacement();
this.userLevelSelector = new UserLevelSelector(this.configManager);
this.configPreview = new ConfigPreview(this.configManager);
this.taskCreationGuide = new TaskCreationGuide(this.plugin);
@ -74,7 +90,7 @@ export class OnboardingModal extends Modal {
}
onOpen() {
const { contentEl } = this;
const {contentEl} = this;
contentEl.empty();
this.createModalStructure();
@ -82,7 +98,7 @@ export class OnboardingModal extends Modal {
}
onClose() {
const { contentEl } = this;
const {contentEl} = this;
contentEl.empty();
}
@ -90,8 +106,8 @@ export class OnboardingModal extends Modal {
* Create the basic modal structure
*/
private createModalStructure() {
const { contentEl } = this;
const {contentEl} = this;
// Header section
this.headerEl = contentEl.createDiv("onboarding-header");
@ -101,6 +117,30 @@ export class OnboardingModal extends Modal {
// Footer with navigation buttons
this.footerEl = contentEl.createDiv("onboarding-footer");
this.createFooterButtons();
// Apply initial UI mode class
this.applyUIModeClass();
}
/**
* Render top header bar (minimal title-only; choices are in content)
*/
private renderHeaderBar() {
this.applyUIModeClass();
// const bar = this.headerEl.createDiv({cls: "onboarding-topbar"});
// const left = bar.createDiv({cls: "onboarding-topbar-left"});
// left.createEl("span", {text: t("Task Genius"), cls: "onboarding-topbar-title"});
// bar.createDiv({cls: "onboarding-topbar-right"});
}
/**
* Toggle UI mode classes on the modal element
*/
private applyUIModeClass() {
const root = this.modalEl;
root.toggleClass("mod-fluent", this.state.uiMode === 'fluent');
root.toggleClass("mod-legacy", this.state.uiMode === 'legacy');
}
/**
@ -113,17 +153,20 @@ export class OnboardingModal extends Modal {
this.skipButton = new ButtonComponent(buttonContainer)
.setButtonText(t("Skip onboarding"))
.onClick(() => this.handleSkip());
this.skipButton.buttonEl.addClass("clickable-icon");
// Back button
this.backButton = new ButtonComponent(buttonContainer)
.setButtonText(t("Back"))
.onClick(() => this.handleBack());
this.backButton.buttonEl.addClass("clickable-icon");
// Next button
this.nextButton = new ButtonComponent(buttonContainer)
.setButtonText(t("Next"))
.setCta()
.onClick(() => this.handleNext());
this.nextButton.buttonEl.addClass("clickable-icon");
}
/**
@ -134,12 +177,21 @@ export class OnboardingModal extends Modal {
this.headerEl.empty();
this.onboardingContentEl.empty();
// Render topbar
this.renderHeaderBar();
// Update button visibility
this.updateButtonStates();
switch (this.state.currentStep) {
case OnboardingStep.WELCOME:
this.displayWelcomeStep();
case OnboardingStep.INTRO:
this.displayIntroTypingStep();
break;
case OnboardingStep.MODE_SELECT:
this.displayModeSelectionStep();
break;
case OnboardingStep.FLUENT_PLACEMENT:
this.displayFluentPlacementStep();
break;
case OnboardingStep.USER_LEVEL_SELECT:
this.displayUserLevelSelectStep();
@ -161,7 +213,7 @@ export class OnboardingModal extends Modal {
*/
private displayWelcomeStep() {
// Header
this.headerEl.createEl("h1", { text: t("Welcome to Task Genius") });
this.headerEl.createEl("h1", {text: t("Welcome to Task Genius")});
this.headerEl.createEl("p", {
text: t(
"Transform your task management with advanced progress tracking and workflow automation"
@ -214,8 +266,8 @@ export class OnboardingModal extends Modal {
const iconEl = featureEl.createDiv("feature-icon");
setIcon(iconEl, feature.icon);
const featureContent = featureEl.createDiv("feature-content");
featureContent.createEl("h3", { text: feature.title });
featureContent.createEl("p", { text: feature.description });
featureContent.createEl("h3", {text: feature.title});
featureContent.createEl("p", {text: feature.description});
});
// Setup note
@ -228,12 +280,50 @@ export class OnboardingModal extends Modal {
});
}
/**
* New: Intro typing step
*/
private displayIntroTypingStep() {
// this.headerEl.createEl("h1", {text: t("Welcome")});
this.introTyping.render(this.onboardingContentEl);
}
/**
* New: Mode selection (Fluent vs Legacy)
*/
private displayModeSelectionStep() {
this.headerEl.createEl("h1", {text: t("Choose Your Interface Style")});
this.headerEl.createEl("p", {
text: t("Select your preferred visual and interaction style: modern Fluent or traditional Legacy"),
cls: "onboarding-subtitle"
});
this.modeSelection.render(this.onboardingContentEl, this.state.uiMode as any, (mode) => {
this.state.uiMode = mode;
this.updateButtonStates();
});
}
/**
* New: Fluent placement selection
*/
private displayFluentPlacementStep() {
this.headerEl.createEl("h1", {text: t("Fluent Layout")});
this.headerEl.createEl("p", {
text: t("Choose how to display Fluent: use Sideleaves for enhanced multi-column collaboration, or Inline for an immersive single-page experience."),
cls: "onboarding-subtitle"
});
this.fluentPlacement.render(this.onboardingContentEl, this.state.useSideLeaves ? "sideleaves" : "inline", (p) => {
this.state.useSideLeaves = p === "sideleaves";
this.updateButtonStates();
});
}
/**
* Display user level selection step
*/
private displayUserLevelSelectStep() {
// Header
this.headerEl.createEl("h1", { text: t("Choose Your Usage Mode") });
this.headerEl.createEl("h1", {text: t("Choose Your Usage Mode")});
this.headerEl.createEl("p", {
text: t(
"Select the configuration that best matches your task management experience"
@ -259,7 +349,7 @@ export class OnboardingModal extends Modal {
}
// Header
this.headerEl.createEl("h1", { text: t("Configuration Preview") });
this.headerEl.createEl("h1", {text: t("Configuration Preview")});
this.headerEl.createEl("p", {
text: t(
"Review the settings that will be applied for your selected mode"
@ -292,7 +382,7 @@ export class OnboardingModal extends Modal {
*/
private displayTaskCreationGuideStep() {
// Header
this.headerEl.createEl("h1", { text: t("Create Your First Task") });
this.headerEl.createEl("h1", {text: t("Create Your First Task")});
this.headerEl.createEl("p", {
text: t("Learn how to create and format tasks in Task Genius"),
cls: "onboarding-subtitle",
@ -309,7 +399,7 @@ export class OnboardingModal extends Modal {
if (!this.state.selectedConfig) return;
// Header
this.headerEl.createEl("h1", { text: t("Setup Complete!") });
this.headerEl.createEl("h1", {text: t("Setup Complete!")});
this.headerEl.createEl("p", {
text: t("Task Genius is now configured and ready to use"),
cls: "onboarding-subtitle",
@ -328,13 +418,13 @@ export class OnboardingModal extends Modal {
private updateButtonStates() {
const step = this.state.currentStep;
// Skip button - only show on welcome
// Skip button - only show on intro
this.skipButton.buttonEl.style.display =
step === OnboardingStep.WELCOME ? "inline-block" : "none";
step === OnboardingStep.INTRO ? "inline-block" : "none";
// Back button - hide on first step
this.backButton.buttonEl.style.display =
step === OnboardingStep.WELCOME ? "none" : "inline-block";
step === OnboardingStep.INTRO ? "none" : "inline-block";
// Next button
const isLastStep = step === OnboardingStep.COMPLETE;
@ -362,7 +452,7 @@ export class OnboardingModal extends Modal {
* Handle back navigation
*/
private handleBack() {
if (this.state.currentStep > OnboardingStep.WELCOME) {
if (this.state.currentStep > OnboardingStep.INTRO) {
this.state.currentStep--;
// Skip task guide if it was skipped
@ -394,15 +484,26 @@ export class OnboardingModal extends Modal {
return;
}
// Move to next step
this.state.currentStep++;
// Branching for intro/mode/placement
if (step === OnboardingStep.INTRO) {
this.state.currentStep = OnboardingStep.MODE_SELECT;
} else if (step === OnboardingStep.MODE_SELECT) {
this.state.currentStep = this.state.uiMode === 'fluent'
? OnboardingStep.FLUENT_PLACEMENT
: OnboardingStep.USER_LEVEL_SELECT;
} else if (step === OnboardingStep.FLUENT_PLACEMENT) {
this.state.currentStep = OnboardingStep.USER_LEVEL_SELECT;
} else {
this.state.currentStep++;
}
// Apply configuration when moving to preview
// Apply architecture selection and configuration when moving to preview
if (
this.state.currentStep === OnboardingStep.CONFIG_PREVIEW &&
this.state.selectedConfig
) {
try {
await this.applyArchitectureSelections();
await this.configManager.applyConfiguration(
this.state.selectedConfig.mode
);
@ -435,6 +536,34 @@ export class OnboardingModal extends Modal {
}
}
/**
* Apply Legacy vs Fluent selection and sideleaves preference to settings
*/
private async applyArchitectureSelections() {
const isFluent = this.state.uiMode === 'fluent';
if (!this.plugin.settings.experimental) {
(this.plugin.settings as any).experimental = {enableV2: false, showV2Ribbon: false};
}
this.plugin.settings.experimental!.enableV2 = isFluent;
// Prepare v2 config and set placement option when Fluent is chosen
if (!this.plugin.settings.experimental!.v2Config) {
(this.plugin.settings.experimental as any).v2Config = {
enableWorkspaces: true,
defaultWorkspace: 'default',
showTopNavigation: true,
showNewSidebar: true,
allowViewSwitching: true,
persistViewMode: true,
maxOtherViewsBeforeOverflow: 5,
};
}
if (isFluent) {
(this.plugin.settings.experimental as any).v2Config.useWorkspaceSideLeaves = !!this.state.useSideLeaves;
}
await this.plugin.saveSettings();
}
/**
* Complete onboarding process
*/

View file

@ -1,4 +1,4 @@
import { ItemView, WorkspaceLeaf, setIcon, ButtonComponent } from "obsidian";
import { ItemView, WorkspaceLeaf, setIcon, ButtonComponent, Setting } from "obsidian";
import type TaskProgressBarPlugin from "@/index";
import { t } from "@/translations/helper";
import {
@ -11,16 +11,22 @@ import { UserLevelSelector } from "./UserLevelSelector";
import { ConfigPreview } from "./ConfigPreview";
import { TaskCreationGuide } from "./TaskCreationGuide";
import { OnboardingComplete } from "./OnboardingComplete";
import { IntroTyping } from "./IntroTyping";
import { ModeSelection } from "./ModeSelection";
import { FluentPlacement } from "./FluentPlacement";
import "@/experimental/v2/styles/v2-enhanced.css";
export const ONBOARDING_VIEW_TYPE = "task-genius-onboarding";
export enum OnboardingStep {
SETTINGS_CHECK = 0, // New: Check if user wants onboarding
WELCOME = 1,
USER_LEVEL_SELECT = 2,
CONFIG_PREVIEW = 3,
TASK_CREATION_GUIDE = 4,
COMPLETE = 5,
SETTINGS_CHECK = 0,
INTRO = 1,
MODE_SELECT = 2,
FLUENT_PLACEMENT = 3,
USER_LEVEL_SELECT = 4,
CONFIG_PREVIEW = 5,
TASK_CREATION_GUIDE = 6,
COMPLETE = 7,
}
export interface OnboardingState {
@ -30,6 +36,8 @@ export interface OnboardingState {
isCompleting: boolean;
userHasChanges: boolean;
changesSummary: string[];
uiMode: 'fluent' | 'legacy';
useSideLeaves: boolean; // Applies when uiMode === 'fluent'
}
export class OnboardingView extends ItemView {
@ -40,6 +48,9 @@ export class OnboardingView extends ItemView {
private state: OnboardingState;
// Step components
private introTyping: IntroTyping;
private modeSelection: ModeSelection;
private fluentPlacement: FluentPlacement;
private userLevelSelector: UserLevelSelector;
private configPreview: ConfigPreview;
private taskCreationGuide: TaskCreationGuide;
@ -60,16 +71,21 @@ export class OnboardingView extends ItemView {
this.settingsDetector = new SettingsChangeDetector(plugin);
this.onComplete = onComplete;
// Initialize state
// Initialize state - always start with INTRO
this.state = {
currentStep: OnboardingStep.SETTINGS_CHECK,
currentStep: OnboardingStep.INTRO,
skipTaskGuide: false,
isCompleting: false,
userHasChanges: this.settingsDetector.hasUserMadeChanges(),
changesSummary: this.settingsDetector.getChangesSummary(),
uiMode: 'fluent',
useSideLeaves: true,
};
// Initialize components
this.introTyping = new IntroTyping();
this.modeSelection = new ModeSelection();
this.fluentPlacement = new FluentPlacement();
this.userLevelSelector = new UserLevelSelector(this.configManager);
this.configPreview = new ConfigPreview(this.configManager);
this.taskCreationGuide = new TaskCreationGuide(this.plugin);
@ -81,7 +97,7 @@ export class OnboardingView extends ItemView {
}
getDisplayText(): string {
return t("Task Genius Setup");
return t("Task Genius Onboarding");
}
getIcon(): string {
@ -102,7 +118,7 @@ export class OnboardingView extends ItemView {
* Create the basic view structure
*/
private createViewStructure() {
const container = this.contentEl;
const container = this.containerEl;
container.empty();
container.addClass("onboarding-view");
@ -112,9 +128,38 @@ export class OnboardingView extends ItemView {
// Main content section
this.onboardingContentEl = container.createDiv("onboarding-content");
// Footer with navigation buttons
this.footerEl = container.createDiv("onboarding-footer");
this.createFooterButtons();
// Apply initial UI mode class
this.applyUIModeClass();
container.createEl("div", {
cls: "onboarding-shadow"
});
}
/**
* Render top header bar (minimal title-only; choices are in content)
*/
private renderHeaderBar() {
this.applyUIModeClass();
// const bar = this.onboardingHeaderEl.createDiv({cls: "onboarding-topbar"});
// const left = bar.createDiv({cls: "onboarding-topbar-left"});
// left.createEl("span", {text: t("Task Genius"), cls: "onboarding-topbar-title"});
// bar.createDiv({cls: "onboarding-topbar-right"});
}
/**
* Toggle UI mode classes on the root container
*/
private applyUIModeClass() {
const root = this.contentEl;
root.toggleClass("mod-fluent", this.state.uiMode === 'fluent');
root.toggleClass("mod-legacy", this.state.uiMode === 'legacy');
}
/**
@ -127,17 +172,20 @@ export class OnboardingView extends ItemView {
this.skipButton = new ButtonComponent(buttonContainer)
.setButtonText(t("Skip setup"))
.onClick(() => this.handleSkip());
this.skipButton.buttonEl.addClass("clickable-icon");
// Back button
this.backButton = new ButtonComponent(buttonContainer)
.setButtonText(t("Back"))
.onClick(() => this.handleBack());
this.backButton.buttonEl.addClass("clickable-icon");
// Next button
this.nextButton = new ButtonComponent(buttonContainer)
.setButtonText(t("Next"))
.setCta()
.onClick(() => this.handleNext());
this.nextButton.buttonEl.addClass("clickable-icon");
}
/**
@ -148,6 +196,9 @@ export class OnboardingView extends ItemView {
this.onboardingHeaderEl.empty();
this.onboardingContentEl.empty();
// Render topbar
this.renderHeaderBar();
// Update button visibility
this.updateButtonStates();
@ -155,8 +206,14 @@ export class OnboardingView extends ItemView {
case OnboardingStep.SETTINGS_CHECK:
this.displaySettingsCheckStep();
break;
case OnboardingStep.WELCOME:
this.displayWelcomeStep();
case OnboardingStep.INTRO:
this.displayIntroTypingStep();
break;
case OnboardingStep.MODE_SELECT:
this.displayModeSelectionStep();
break;
case OnboardingStep.FLUENT_PLACEMENT:
this.displayFluentPlacementStep();
break;
case OnboardingStep.USER_LEVEL_SELECT:
this.displayUserLevelSelectStep();
@ -174,61 +231,51 @@ export class OnboardingView extends ItemView {
}
/**
* Display settings check step (new async approach)
* Display settings check step - ask if user wants to continue wizard
*/
private displaySettingsCheckStep() {
// Header
this.onboardingHeaderEl.createEl("h1", { text: t("Task Genius Setup") });
this.onboardingHeaderEl.createEl("h1", {text: t("Task Genius Setup")});
this.onboardingHeaderEl.createEl("p", {
text: t("We noticed you've already configured Task Genius"),
cls: "onboarding-subtitle",
});
// Content
const content = this.onboardingContentEl;
const checkSection = content.createDiv("settings-check-section");
if (this.state.userHasChanges) {
// User has made changes - ask if they want onboarding
this.onboardingHeaderEl.createEl("p", {
text: t("We noticed you've already configured Task Genius"),
cls: "onboarding-subtitle",
});
// Show detected changes
checkSection.createEl("h3", {text: t("Your current configuration includes:")});
const changesList = checkSection.createEl("ul", {cls: "changes-summary-list"});
const checkSection = content.createDiv("settings-check-section");
this.state.changesSummary.forEach(change => {
const item = changesList.createEl("li");
const checkIcon = item.createSpan("change-check");
setIcon(checkIcon, "check");
item.createSpan("change-text").setText(change);
});
// Show detected changes
checkSection.createEl("h3", { text: t("Your current configuration includes:") });
const changesList = checkSection.createEl("ul", { cls: "changes-summary-list" });
this.state.changesSummary.forEach(change => {
const item = changesList.createEl("li");
const checkIcon = item.createSpan("change-check");
setIcon(checkIcon, "check");
item.createSpan("change-text").setText(change);
});
// Ask if they want onboarding
const questionSection = content.createDiv("onboarding-question");
questionSection.createEl("h3", {text: t("Would you like to continue with the setup wizard?")});
// Ask if they want onboarding
const questionSection = content.createDiv("onboarding-question");
questionSection.createEl("h3", { text: t("Would you like to run the setup wizard anyway?") });
const optionsContainer = questionSection.createDiv("question-options");
const yesButton = optionsContainer.createEl("button", {
text: t("Yes, show me the setup wizard"),
cls: "mod-cta question-button",
});
yesButton.addEventListener("click", () => {
this.state.currentStep = OnboardingStep.WELCOME;
this.displayCurrentStep();
});
const optionsContainer = questionSection.createDiv("question-options");
const noButton = optionsContainer.createEl("button", {
text: t("No, I'm happy with my current setup"),
cls: "question-button",
});
noButton.addEventListener("click", () => this.handleSkip());
} else {
// User hasn't made changes - proceed with normal onboarding
this.state.currentStep = OnboardingStep.WELCOME;
const yesButton = optionsContainer.createEl("button", {
text: t("Yes, show me the setup wizard"),
cls: "mod-cta question-button clickable-icon",
});
yesButton.addEventListener("click", () => {
this.state.currentStep = OnboardingStep.MODE_SELECT;
this.displayCurrentStep();
}
});
const noButton = optionsContainer.createEl("button", {
text: t("No, I'm happy with my current setup"),
cls: "question-button clickable-icon",
});
noButton.addEventListener("click", () => this.handleSkip());
}
/**
@ -236,7 +283,7 @@ export class OnboardingView extends ItemView {
*/
private displayWelcomeStep() {
// Header
this.onboardingHeaderEl.createEl("h1", { text: t("Welcome to Task Genius") });
this.onboardingHeaderEl.createEl("h1", {text: t("Welcome to Task Genius")});
this.onboardingHeaderEl.createEl("p", {
text: t(
"Transform your task management with advanced progress tracking and workflow automation"
@ -287,8 +334,8 @@ export class OnboardingView extends ItemView {
const iconEl = featureEl.createDiv("feature-icon");
setIcon(iconEl, feature.icon);
const featureContent = featureEl.createDiv("feature-content");
featureContent.createEl("h3", { text: feature.title });
featureContent.createEl("p", { text: feature.description });
featureContent.createEl("h3", {text: feature.title});
featureContent.createEl("p", {text: feature.description});
});
// Setup note
@ -301,12 +348,56 @@ export class OnboardingView extends ItemView {
});
}
/**
* New: Intro typing step
*/
private displayIntroTypingStep() {
// Header minimal
// this.onboardingHeaderEl.createEl("h1", {text: t("Welcome")});
// Content typing animation
this.introTyping.render(this.onboardingContentEl);
}
/**
* New: Mode selection (Fluent vs Legacy) with preview cards
*/
private displayModeSelectionStep() {
// Header
this.onboardingHeaderEl.createEl("h1", {text: t("Choose Your Interface Style")});
this.onboardingHeaderEl.createEl("p", {
text: t("Select your preferred visual and interaction style: modern Fluent or traditional Legacy"),
cls: "onboarding-subtitle"
});
// Content
this.modeSelection.render(this.onboardingContentEl, this.state.uiMode as any, (mode) => {
this.state.uiMode = mode;
this.updateButtonStates();
});
}
/**
* New: Fluent placement selection (Sideleaves vs Inline)
*/
private displayFluentPlacementStep() {
// Header
this.onboardingHeaderEl.createEl("h1", {text: t("Fluent Layout")});
this.onboardingHeaderEl.createEl("p", {
text: t("Choose how to display Fluent: use Sideleaves for enhanced multi-column collaboration, or Inline for an immersive single-page experience."),
cls: "onboarding-subtitle",
});
// Content
this.fluentPlacement.render(this.onboardingContentEl, this.state.useSideLeaves ? "sideleaves" : "inline", (p) => {
this.state.useSideLeaves = p === "sideleaves";
this.updateButtonStates();
});
}
/**
* Display user level selection step
*/
private displayUserLevelSelectStep() {
// Header
this.onboardingHeaderEl.createEl("h1", { text: t("Choose Your Usage Mode") });
this.onboardingHeaderEl.createEl("h1", {text: t("Choose Your Usage Mode")});
this.onboardingHeaderEl.createEl("p", {
text: t(
"Select the configuration that best matches your task management experience"
@ -332,7 +423,7 @@ export class OnboardingView extends ItemView {
}
// Header
this.onboardingHeaderEl.createEl("h1", { text: t("Configuration Preview") });
this.onboardingHeaderEl.createEl("h1", {text: t("Configuration Preview")});
this.onboardingHeaderEl.createEl("p", {
text: t(
"Review the settings that will be applied for your selected mode"
@ -345,6 +436,19 @@ export class OnboardingView extends ItemView {
this.onboardingContentEl,
this.state.selectedConfig
);
// Task guide option
const optionsSection =
this.onboardingContentEl.createDiv("config-options");
new Setting(optionsSection)
.setName(t("Include task creation guide"))
.setDesc(t("Show a quick tutorial on creating your first task"))
.addToggle((toggle) => {
toggle.setValue(!this.state.skipTaskGuide).onChange((value) => {
this.state.skipTaskGuide = !value;
});
});
}
/**
@ -352,7 +456,7 @@ export class OnboardingView extends ItemView {
*/
private displayTaskCreationGuideStep() {
// Header
this.onboardingHeaderEl.createEl("h1", { text: t("Create Your First Task") });
this.onboardingHeaderEl.createEl("h1", {text: t("Create Your First Task")});
this.onboardingHeaderEl.createEl("p", {
text: t("Learn how to create and format tasks in Task Genius"),
cls: "onboarding-subtitle",
@ -369,7 +473,7 @@ export class OnboardingView extends ItemView {
if (!this.state.selectedConfig) return;
// Header
this.onboardingHeaderEl.createEl("h1", { text: t("Setup Complete!") });
this.onboardingHeaderEl.createEl("h1", {text: t("Setup Complete!")});
this.onboardingHeaderEl.createEl("p", {
text: t("Task Genius is now configured and ready to use"),
cls: "onboarding-subtitle",
@ -388,19 +492,19 @@ export class OnboardingView extends ItemView {
private updateButtonStates() {
const step = this.state.currentStep;
// Skip button - show on settings check and welcome
// Skip button - show on settings check and intro
this.skipButton.buttonEl.style.display =
step === OnboardingStep.SETTINGS_CHECK || step === OnboardingStep.WELCOME
step === OnboardingStep.SETTINGS_CHECK || step === OnboardingStep.INTRO
? "inline-block" : "none";
// Back button - hide on first two steps
// Back button - hide on intro, show on settings check and after
this.backButton.buttonEl.style.display =
step <= OnboardingStep.WELCOME ? "none" : "inline-block";
step === OnboardingStep.INTRO ? "none" : "inline-block";
// Next button text and state
const isLastStep = step === OnboardingStep.COMPLETE;
const isSettingsCheck = step === OnboardingStep.SETTINGS_CHECK;
if (isSettingsCheck) {
this.nextButton.buttonEl.style.display = "none"; // Hide on settings check
} else {
@ -431,15 +535,27 @@ export class OnboardingView extends ItemView {
* Handle back navigation
*/
private handleBack() {
if (this.state.currentStep > OnboardingStep.SETTINGS_CHECK) {
this.state.currentStep--;
// Skip task guide if it was skipped
if (
this.state.currentStep === OnboardingStep.TASK_CREATION_GUIDE &&
this.state.skipTaskGuide
) {
if (this.state.currentStep > OnboardingStep.INTRO) {
// Handle back from settings check - go to intro
if (this.state.currentStep === OnboardingStep.SETTINGS_CHECK) {
this.state.currentStep = OnboardingStep.INTRO;
}
// Handle back from mode select - check if came from settings check
else if (this.state.currentStep === OnboardingStep.MODE_SELECT) {
// If user has changes, go back to settings check, otherwise to intro
this.state.currentStep = this.state.userHasChanges
? OnboardingStep.SETTINGS_CHECK
: OnboardingStep.INTRO;
} else {
this.state.currentStep--;
// Skip task guide if it was skipped
if (
this.state.currentStep === OnboardingStep.TASK_CREATION_GUIDE &&
this.state.skipTaskGuide
) {
this.state.currentStep--;
}
}
this.displayCurrentStep();
@ -463,15 +579,30 @@ export class OnboardingView extends ItemView {
return;
}
// Move to next step
this.state.currentStep++;
// Custom flow for new intro/mode/placement steps
if (step === OnboardingStep.INTRO) {
// After intro, check if user has changes to decide next step
this.state.currentStep = this.state.userHasChanges
? OnboardingStep.SETTINGS_CHECK
: OnboardingStep.MODE_SELECT;
} else if (step === OnboardingStep.MODE_SELECT) {
this.state.currentStep = this.state.uiMode === 'fluent'
? OnboardingStep.FLUENT_PLACEMENT
: OnboardingStep.USER_LEVEL_SELECT;
} else if (step === OnboardingStep.FLUENT_PLACEMENT) {
this.state.currentStep = OnboardingStep.USER_LEVEL_SELECT;
} else {
// Default increment for the remaining steps
this.state.currentStep++;
}
// Apply configuration when moving to preview
// Apply architecture selection and configuration when moving to preview
if (
this.state.currentStep === OnboardingStep.CONFIG_PREVIEW &&
this.state.selectedConfig
) {
try {
await this.applyArchitectureSelections();
await this.configManager.applyConfiguration(
this.state.selectedConfig.mode
);
@ -504,6 +635,34 @@ export class OnboardingView extends ItemView {
}
}
/**
* Apply Legacy vs Fluent selection and sideleaves preference to settings
*/
private async applyArchitectureSelections() {
const isFluent = this.state.uiMode === 'fluent';
if (!this.plugin.settings.experimental) {
(this.plugin.settings as any).experimental = {enableV2: false, showV2Ribbon: false};
}
this.plugin.settings.experimental!.enableV2 = isFluent;
// Prepare v2 config and set placement option when Fluent is chosen
if (!this.plugin.settings.experimental!.v2Config) {
(this.plugin.settings.experimental as any).v2Config = {
enableWorkspaces: true,
defaultWorkspace: 'default',
showTopNavigation: true,
showNewSidebar: true,
allowViewSwitching: true,
persistViewMode: true,
maxOtherViewsBeforeOverflow: 5,
};
}
if (isFluent) {
(this.plugin.settings.experimental as any).v2Config.useWorkspaceSideLeaves = !!this.state.useSideLeaves;
}
await this.plugin.saveSettings();
}
/**
* Complete onboarding process
*/
@ -535,4 +694,4 @@ export class OnboardingView extends ItemView {
private close() {
this.leaf.detach();
}
}
}

View file

@ -8,6 +8,7 @@ import { t } from "@/translations/helper";
export class V2Integration {
private plugin: TaskProgressBarPlugin;
private revealingSideLeaves = false;
constructor(plugin: TaskProgressBarPlugin) {
this.plugin = plugin;
@ -64,25 +65,35 @@ export class V2Integration {
// When any of the V2 views becomes active, reveal the other side leaves without focusing them
this.plugin.registerEvent(
this.plugin.app.workspace.on("active-leaf-change", async (leaf) => {
if (this.revealingSideLeaves) return;
const useSideLeaves = !!(this.plugin.settings.experimental as any)?.v2Config?.useWorkspaceSideLeaves;
if (!useSideLeaves || !leaf?.view?.getViewType) return;
const vt = leaf.view.getViewType();
const watched = new Set<string>([
TASK_VIEW_V2_TYPE,
TG_LEFT_SIDEBAR_VIEW_TYPE,
TG_RIGHT_DETAIL_VIEW_TYPE,
]);
if (!watched.has(vt)) return;
const ws = this.plugin.app.workspace as Workspace & any;
this.revealingSideLeaves = true;
try {
const useSideLeaves = !!(this.plugin.settings.experimental as any)?.v2Config?.useWorkspaceSideLeaves;
if (!useSideLeaves) return;
if (!leaf?.view?.getViewType) return;
const vt = leaf.view.getViewType();
const watched = new Set<string>([
TASK_VIEW_V2_TYPE,
TG_LEFT_SIDEBAR_VIEW_TYPE,
TG_RIGHT_DETAIL_VIEW_TYPE,
]);
if (!watched.has(vt)) return;
const ws = this.plugin.app.workspace as Workspace & any;
// Ensure side leaves exist and are visible, but do not focus
await ws.ensureSideLeaf(TG_LEFT_SIDEBAR_VIEW_TYPE, "left", { active: false });
await ws.ensureSideLeaf(TG_RIGHT_DETAIL_VIEW_TYPE, "right", { active: false });
// Expand sidebars if they are collapsed, without changing focus
// Ensure side leaves exist
const leftLeaf = await ws.ensureSideLeaf(TG_LEFT_SIDEBAR_VIEW_TYPE, "left", { active: false });
const rightLeaf = await ws.ensureSideLeaf(TG_RIGHT_DETAIL_VIEW_TYPE, "right", { active: false });
// Bring them to front within their splits (without keeping focus)
if (leftLeaf) ws.revealLeaf(leftLeaf);
if (rightLeaf) ws.revealLeaf(rightLeaf);
// Expand sidebars if they are collapsed
if (ws.leftSplit?.collapsed && typeof ws.leftSplit.expand === "function") ws.leftSplit.expand();
if (ws.rightSplit?.collapsed && typeof ws.rightSplit.expand === "function") ws.rightSplit.expand();
} catch (e) {}
// Restore focus to the currently active (incoming) leaf
if (ws.setActiveLeaf && leaf) ws.setActiveLeaf(leaf, { focus: true });
} catch (_) {
// noop
} finally {
this.revealingSideLeaves = false;
}
})
);

View file

@ -863,3 +863,27 @@
opacity: 1;
}
}
.onboarding-shadow {
position: absolute;
background: hsl(var(--color-accent-hsl), 0);
border-radius: 24px;
rotate: 35deg;
width: 260px;
top: 200px;
height: 400px;
filter: blur(150px);
animation: shadow-slide infinite 4s linear alternate;
}
@keyframes shadow-slide {
0% {
background: hsl(var(--color-accent-hsl), 0.2);
right: 360px
}
to {
background: hsl(var(--color-accent-hsl), 0.8);
right: 160px
}
}

View file

@ -1093,3 +1093,133 @@
transform: scale(1);
}
}
/* ============================== */
/* AI-Style Intro Typing Animation */
/* ============================== */
.intro-typing {
padding: var(--size-4-6) var(--size-4-4);
text-align: left;
max-width: 800px;
margin: 0 auto;
}
.intro-line {
margin-bottom: var(--size-4-4);
line-height: 1.6;
}
.intro-line-1 {
/* "Hi," - Large responsive greeting */
font-size: clamp(3rem, 6vw, 5rem);
font-weight: 700;
color: var(--text-normal);
margin-bottom: var(--size-4-2);
line-height: 1.2;
}
.intro-line-2 {
/* "Thank you for using Task Genius" - Secondary greeting */
font-size: clamp(1.5rem, 3vw, 2.5rem);
font-weight: 600;
color: var(--text-normal);
margin-bottom: var(--size-4-6);
line-height: 1.4;
}
.intro-line-3,
.intro-line-4 {
/* Description text - Responsive body text */
font-size: clamp(1rem, 2vw, 1.4rem);
color: var(--text-muted);
line-height: 1.8;
margin-bottom: var(--size-4-5);
}
/* AI-style streaming characters */
.intro-char {
display: inline;
opacity: 0;
filter: blur(4px);
transform: translateY(-2px);
transition: opacity 0.4s cubic-bezier(0.4, 0, 0.2, 1),
filter 0.4s cubic-bezier(0.4, 0, 0.2, 1),
transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
}
.intro-char-visible {
opacity: 1;
filter: blur(0px);
transform: translateY(0);
}
/* Space character handling */
.intro-char-space {
width: 0.25em;
}
/* Streaming cursor that follows characters */
.intro-cursor {
display: inline-block;
margin-left: 1px;
opacity: 0.7;
animation: cursorBlink 1s infinite;
color: var(--interactive-accent);
font-weight: normal;
vertical-align: baseline;
}
@keyframes cursorBlink {
0%, 49% {
opacity: 0.7;
}
50%, 100% {
opacity: 0;
}
}
/* Completed stream line */
.stream-complete {
animation: streamComplete 0.3s ease-out;
}
@keyframes streamComplete {
0% {
opacity: 1;
}
50% {
opacity: 0.95;
}
100% {
opacity: 1;
}
}
/* Enhanced readability on different themes */
.theme-dark .intro-line-1 {
color: var(--text-normal);
text-shadow: 0 0 20px rgba(var(--interactive-accent-rgb), 0.1);
}
.theme-dark .intro-char-visible {
/* Subtle glow effect for dark theme */
text-shadow: 0 0 8px rgba(var(--interactive-accent-rgb), 0.05);
}
/* Smooth appearance for whole typing container */
.intro-typing {
animation: fadeInTyping 0.5s ease-out;
}
@keyframes fadeInTyping {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}

1671
styles.css

File diff suppressed because one or more lines are too long