feat(onboarding): enhance setup with modal-driven file filter editing

Refactor onboarding system with improved component lifecycle and enhanced
file filter management. Replace prompt-based interactions with modal dialogs
and add comprehensive autocomplete support.

Key changes:
- Add OnboardingStepComponent base class for consistent lifecycle management
- Introduce FileFilterRuleEditorModal for interactive rule editing
- Replace prompts with modal-based file/folder selection with autocomplete
- Add per-rule scope configuration and inline editing capabilities
- Update view title from "Onboarding" to "Setup" with custom icon
- Extend OnboardingLayout to Component class for better resource management
- Improve UI polish with toggleVisibility and enhanced styling
This commit is contained in:
Quorafind 2025-10-07 20:39:46 +08:00
parent 22865d135c
commit e17caaff8f
18 changed files with 1089 additions and 523 deletions

View file

@ -1,4 +1,8 @@
import { OnboardingConfig, OnboardingConfigMode } from "@/managers/onboarding-manager";
import {
OnboardingConfig,
OnboardingConfigMode,
} from "@/managers/onboarding-manager";
import { Component } from "obsidian";
export enum OnboardingStep {
INTRO = 0,
@ -26,16 +30,16 @@ export interface OnboardingState {
isCompleting: boolean;
userHasChanges: boolean;
changesSummary: string[];
uiMode: 'fluent' | 'legacy';
uiMode: "fluent" | "legacy";
useSideLeaves: boolean;
settingsCheckAction?: 'wizard' | 'keep';
settingsCheckAction?: "wizard" | "keep";
}
export type OnboardingEventType =
| 'step-changed'
| 'state-updated'
| 'navigation-blocked'
| 'completed';
| "step-changed"
| "state-updated"
| "navigation-blocked"
| "completed";
export interface OnboardingEvent {
type: OnboardingEventType;
@ -53,7 +57,10 @@ export interface OnboardingEvent {
*/
export class OnboardingController {
private state: OnboardingState;
private listeners: Map<OnboardingEventType, ((event: OnboardingEvent) => void)[]> = new Map();
private listeners: Map<
OnboardingEventType,
((event: OnboardingEvent) => void)[]
> = new Map();
constructor(initialState?: Partial<OnboardingState>) {
this.state = {
@ -62,7 +69,7 @@ export class OnboardingController {
isCompleting: false,
userHasChanges: false,
changesSummary: [],
uiMode: 'fluent',
uiMode: "fluent",
useSideLeaves: true,
...initialState,
};
@ -89,7 +96,7 @@ export class OnboardingController {
*/
updateState(updates: Partial<OnboardingState>) {
this.state = { ...this.state, ...updates };
this.emit('state-updated', { state: updates });
this.emit("state-updated", { state: updates });
}
/**
@ -102,7 +109,7 @@ export class OnboardingController {
/**
* Set UI mode
*/
setUIMode(mode: 'fluent' | 'legacy') {
setUIMode(mode: "fluent" | "legacy") {
this.updateState({ uiMode: mode });
}
@ -128,7 +135,7 @@ export class OnboardingController {
async next(): Promise<boolean> {
// Validate current step
if (!this.validateCurrentStep()) {
this.emit('navigation-blocked', { step: this.state.currentStep });
this.emit("navigation-blocked", { step: this.state.currentStep });
return false;
}
@ -143,7 +150,7 @@ export class OnboardingController {
break;
case OnboardingStep.MODE_SELECT:
if (this.state.uiMode === 'fluent') {
if (this.state.uiMode === "fluent") {
nextStep = OnboardingStep.FLUENT_OVERVIEW;
} else {
nextStep = OnboardingStep.SETTINGS_CHECK;
@ -171,11 +178,11 @@ export class OnboardingController {
case OnboardingStep.SETTINGS_CHECK:
// User decided to continue wizard
if (this.state.settingsCheckAction === 'wizard') {
if (this.state.settingsCheckAction === "wizard") {
nextStep = OnboardingStep.USER_LEVEL_SELECT;
} else {
// User chose to keep settings, exit onboarding
this.emit('completed', { step: currentStep });
this.emit("completed", { step: currentStep });
return true;
}
break;
@ -203,7 +210,7 @@ export class OnboardingController {
case OnboardingStep.COMPLETE:
// Trigger completion
this.emit('completed', { step: currentStep });
this.emit("completed", { step: currentStep });
return true;
default:
@ -255,7 +262,7 @@ export class OnboardingController {
case OnboardingStep.SETTINGS_CHECK:
// Go back to last fluent step or mode select based on UI mode
if (this.state.uiMode === 'fluent') {
if (this.state.uiMode === "fluent") {
prevStep = OnboardingStep.FLUENT_TOPNAV;
} else {
prevStep = OnboardingStep.MODE_SELECT;
@ -264,9 +271,12 @@ export class OnboardingController {
case OnboardingStep.USER_LEVEL_SELECT:
// Go back based on whether we went through settings check
if (this.state.userHasChanges && this.state.settingsCheckAction === 'wizard') {
if (
this.state.userHasChanges &&
this.state.settingsCheckAction === "wizard"
) {
prevStep = OnboardingStep.SETTINGS_CHECK;
} else if (this.state.uiMode === 'fluent') {
} else if (this.state.uiMode === "fluent") {
prevStep = OnboardingStep.FLUENT_TOPNAV;
} else {
prevStep = OnboardingStep.MODE_SELECT;
@ -307,14 +317,14 @@ export class OnboardingController {
*/
setStep(step: OnboardingStep) {
this.state.currentStep = step;
this.emit('step-changed', { step });
this.emit("step-changed", { step });
}
/**
* Skip onboarding
*/
skip() {
this.emit('completed', { step: this.state.currentStep });
this.emit("completed", { step: this.state.currentStep });
}
// ==================== Validation ====================
@ -396,7 +406,10 @@ export class OnboardingController {
/**
* Unregister event listener
*/
off(event: OnboardingEventType, callback: (event: OnboardingEvent) => void) {
off(
event: OnboardingEventType,
callback: (event: OnboardingEvent) => void
) {
const callbacks = this.listeners.get(event);
if (callbacks) {
const index = callbacks.indexOf(callback);
@ -409,7 +422,10 @@ export class OnboardingController {
/**
* Emit event to all listeners
*/
private emit(type: OnboardingEventType, data: Partial<OnboardingEvent> = {}) {
private emit(
type: OnboardingEventType,
data: Partial<OnboardingEvent> = {}
) {
const event: OnboardingEvent = { type, ...data };
const callbacks = this.listeners.get(type);
if (callbacks) {
@ -417,3 +433,17 @@ export class OnboardingController {
}
}
}
export abstract class OnboardingStepComponent extends Component {
constructor() {
super();
}
abstract render(
headerEl: HTMLElement,
contentEl: HTMLElement,
controller: OnboardingController
): void;
abstract clear(headerEl: HTMLElement, contentEl: HTMLElement): void;
abstract getStep(): OnboardingStep;
}

View file

@ -1,4 +1,4 @@
import { ButtonComponent } from "obsidian";
import { ButtonComponent, Component } from "obsidian";
import { t } from "@/translations/helper";
import { OnboardingController, OnboardingStep } from "./OnboardingController";
@ -15,7 +15,7 @@ export interface OnboardingLayoutCallbacks {
* - Content section (for steps to render into)
* - Footer with navigation buttons
*/
export class OnboardingLayout {
export class OnboardingLayout extends Component {
private container: HTMLElement;
private controller: OnboardingController;
private callbacks: OnboardingLayoutCallbacks;
@ -35,6 +35,7 @@ export class OnboardingLayout {
controller: OnboardingController,
callbacks: OnboardingLayoutCallbacks
) {
super();
this.container = container;
this.controller = controller;
this.callbacks = callbacks;
@ -63,7 +64,7 @@ export class OnboardingLayout {
// Shadow element
this.container.createEl("div", {
cls: "onboarding-shadow"
cls: "onboarding-shadow",
});
}
@ -102,11 +103,11 @@ export class OnboardingLayout {
* Setup listeners for controller events
*/
private setupListeners() {
this.controller.on('step-changed', () => {
this.controller.on("step-changed", () => {
this.updateButtons();
});
this.controller.on('state-updated', () => {
this.controller.on("state-updated", () => {
this.updateButtons();
});
}
@ -119,21 +120,15 @@ export class OnboardingLayout {
const state = this.controller.getState();
// Skip button visibility
this.skipButton.buttonEl.style.display = this.controller.canSkip()
? "inline-block"
: "none";
this.skipButton.buttonEl.toggleVisibility(this.controller.canSkip());
// Back button visibility
this.backButton.buttonEl.style.display = this.controller.canGoBack()
? "inline-block"
: "none";
this.backButton.buttonEl.toggleVisibility(this.controller.canGoBack());
// Next button
const isLastStep = step === OnboardingStep.COMPLETE;
const isSettingsCheck = step === OnboardingStep.SETTINGS_CHECK;
// Show next button for all steps
this.nextButton.buttonEl.style.display = "inline-block";
this.nextButton.buttonEl.toggleVisibility(true);
// Update button text based on step and selection
if (isSettingsCheck) {
@ -181,29 +176,21 @@ export class OnboardingLayout {
* Clear header content
*/
clearHeader() {
// Remove all children and event listeners
while (this.headerEl.firstChild) {
this.headerEl.removeChild(this.headerEl.firstChild);
}
this.headerEl.empty();
this.headerEl?.empty();
}
/**
* Clear content
*/
clearContent() {
// Remove all children and event listeners
while (this.contentEl.firstChild) {
this.contentEl.removeChild(this.contentEl.firstChild);
}
this.contentEl.empty();
this.contentEl?.empty();
}
/**
* Show/hide footer
*/
setFooterVisible(visible: boolean) {
this.footerEl.style.display = visible ? '' : 'none';
this.footerEl.toggleVisibility(visible);
}
/**

View file

@ -77,24 +77,20 @@ export class OnboardingView extends ItemView {
}
getDisplayText(): string {
return t("Task Genius Onboarding");
return t("Task Genius Setup");
}
getIcon(): string {
return "zap";
return "task-genius";
}
async onOpen() {
// Create layout
this.layout = new OnboardingLayout(
this.containerEl,
this.controller,
{
onNext: async () => this.handleNext(),
onBack: async () => this.handleBack(),
onSkip: async () => this.handleSkip(),
}
);
this.layout = new OnboardingLayout(this.containerEl, this.controller, {
onNext: async () => this.handleNext(),
onBack: async () => this.handleBack(),
onSkip: async () => this.handleSkip(),
});
// Render initial step
this.renderCurrentStep();
@ -148,26 +144,54 @@ export class OnboardingView extends ItemView {
break;
case OnboardingStep.MODE_SELECT:
ModeSelectionStep.render(headerEl, contentEl, this.controller);
ModeSelectionStep.render(
headerEl,
contentEl,
this.controller
);
break;
case OnboardingStep.FLUENT_OVERVIEW:
FluentOverviewStep.render(headerEl, contentEl, this.controller);
FluentOverviewStep.render(
headerEl,
contentEl,
this.controller
);
break;
case OnboardingStep.FLUENT_WS_SELECTOR:
FluentWorkspaceSelectorStep.render(headerEl, contentEl, this.controller);
FluentWorkspaceSelectorStep.render(
headerEl,
contentEl,
this.controller
);
break;
case OnboardingStep.FLUENT_MAIN_NAV:
FluentMainNavigationStep.render(headerEl, contentEl, this.controller);
FluentMainNavigationStep.render(
headerEl,
contentEl,
this.controller
);
break;
case OnboardingStep.FLUENT_PROJECTS:
FluentProjectSectionStep.render(headerEl, contentEl, this.controller);
FluentProjectSectionStep.render(
headerEl,
contentEl,
this.controller
);
break;
case OnboardingStep.FLUENT_OTHER_VIEWS:
FluentOtherViewsStep.render(headerEl, contentEl, this.controller);
FluentOtherViewsStep.render(
headerEl,
contentEl,
this.controller
);
break;
case OnboardingStep.FLUENT_TOPNAV:
FluentTopNavigationStep.render(headerEl, contentEl, this.controller);
FluentTopNavigationStep.render(
headerEl,
contentEl,
this.controller
);
break;
case OnboardingStep.SETTINGS_CHECK:
@ -282,7 +306,10 @@ export class OnboardingView extends ItemView {
// Navigate to next step
const success = await this.controller.next();
console.log("handleNext - Navigation result:", success);
console.log("handleNext - New step:", OnboardingStep[this.controller.getCurrentStep()]);
console.log(
"handleNext - New step:",
OnboardingStep[this.controller.getCurrentStep()]
);
}
/**
@ -357,8 +384,9 @@ export class OnboardingView extends ItemView {
}
if (isFluent) {
(this.plugin.settings.experimental as any).v2Config.useWorkspaceSideLeaves =
!!state.useSideLeaves;
(
this.plugin.settings.experimental as any
).v2Config.useWorkspaceSideLeaves = !!state.useSideLeaves;
}
await this.plugin.saveSettings();

View file

@ -0,0 +1,308 @@
import { App, DropdownComponent, Modal, Setting, setIcon } from "obsidian";
import { t } from "@/translations/helper";
import TaskProgressBarPlugin from "@/index";
import { FilterMode, FileFilterRule } from "@/common/setting-definition";
import {
FolderSuggest,
SimpleFileSuggest as FileSuggest,
} from "@/components/ui/inputs/AutoComplete";
import "@/styles/file-filter-settings.css";
interface FileFilterRuleEditorModalOptions {
autoAddRuleType?: FileFilterRule["type"];
onClose?: () => void;
}
export class FileFilterRuleEditorModal extends Modal {
private rulesContainer: HTMLElement | null = null;
private statsContainer: HTMLElement | null = null;
private pendingRule?: FileFilterRule;
private readonly plugin: TaskProgressBarPlugin;
private readonly options: FileFilterRuleEditorModalOptions;
constructor(
app: App,
plugin: TaskProgressBarPlugin,
options: FileFilterRuleEditorModalOptions = {}
) {
super(app);
this.plugin = plugin;
this.options = options;
}
onOpen(): void {
this.modalEl.addClass("file-filter-rule-editor-modal");
const { contentEl } = this;
contentEl.empty();
contentEl.createEl("h2", { text: t("Edit File Filter Rules") });
contentEl.createEl("p", {
text: t(
"Configure which files, folders, or patterns are included during task indexing."
),
cls: "setting-item-description",
});
new Setting(contentEl)
.setName(t("Filter Mode"))
.setDesc(
t(
"Whitelist: include only specified paths. Blacklist: exclude specified paths."
)
)
.addDropdown((dropdown) =>
dropdown
.addOption(
FilterMode.WHITELIST,
t("Whitelist (Include only)")
)
.addOption(FilterMode.BLACKLIST, t("Blacklist (Exclude)"))
.setValue(this.plugin.settings.fileFilter.mode)
.onChange(async (value: FilterMode) => {
this.plugin.settings.fileFilter.mode = value;
await this.plugin.saveSettings();
this.updateStats();
})
);
const actionSetting = new Setting(contentEl);
actionSetting.settingEl.addClass("file-filter-rule-actions");
actionSetting
.addButton((button) =>
button
.setButtonText(t("Add File Rule"))
.setCta()
.onClick(() => {
void this.addRule("file");
})
)
.addButton((button) =>
button.setButtonText(t("Add Folder Rule")).onClick(() => {
void this.addRule("folder");
})
)
.addButton((button) =>
button.setButtonText(t("Add Pattern Rule")).onClick(() => {
void this.addRule("pattern");
})
);
this.rulesContainer = contentEl.createDiv({
cls: "file-filter-rules-container",
});
this.statsContainer = contentEl.createDiv({
cls: "file-filter-stats-preview",
});
new Setting(contentEl).addButton((button) =>
button
.setButtonText(t("Done"))
.setCta()
.onClick(() => this.close())
);
this.renderRules();
this.updateStats();
if (this.options.autoAddRuleType) {
void this.addRule(this.options.autoAddRuleType).then((rule) => {
this.pendingRule = rule;
});
}
}
onClose() {
if (this.pendingRule && !this.pendingRule.path.trim()) {
const index = this.plugin.settings.fileFilter.rules.indexOf(
this.pendingRule
);
if (index !== -1) {
this.plugin.settings.fileFilter.rules.splice(index, 1);
void this.plugin.saveSettings();
}
}
this.pendingRule = undefined;
this.contentEl.empty();
this.options.onClose?.();
}
private async addRule(
type: FileFilterRule["type"]
): Promise<FileFilterRule> {
const newRule: FileFilterRule = {
type,
path: "",
enabled: true,
};
this.plugin.settings.fileFilter.rules.push(newRule);
await this.plugin.saveSettings();
this.renderRules();
this.updateStats();
return newRule;
}
private renderRules() {
if (!this.rulesContainer) return;
const container = this.rulesContainer;
container.empty();
if (this.plugin.settings.fileFilter.rules.length === 0) {
container.createEl("p", {
text: t("No filter rules configured yet"),
cls: "setting-item-description",
});
return;
}
this.plugin.settings.fileFilter.rules.forEach((rule, index) => {
const ruleContainer = container.createDiv({
cls: "file-filter-rule",
});
const typeContainer = ruleContainer.createDiv({
cls: "file-filter-rule-type",
});
typeContainer.createEl("label", { text: t("Type:") });
new DropdownComponent(typeContainer)
.addOption("file", t("File"))
.addOption("folder", t("Folder"))
.addOption("pattern", t("Pattern"))
.setValue(rule.type)
.onChange(async (value: FileFilterRule["type"]) => {
rule.type = value;
await this.plugin.saveSettings();
this.renderRules();
this.updateStats();
});
const scopeContainer = ruleContainer.createDiv({
cls: "file-filter-rule-scope",
});
scopeContainer.createEl("label", { text: t("Scope:") });
new DropdownComponent(scopeContainer)
.addOption("both", t("Both"))
.addOption("inline", t("Inline"))
.addOption("file", t("File"))
.setValue((rule as any).scope || "both")
.onChange(async (value: "both" | "inline" | "file") => {
(rule as any).scope = value;
await this.plugin.saveSettings();
});
const pathContainer = ruleContainer.createDiv({
cls: "file-filter-rule-path",
});
pathContainer.createEl("label", { text: t("Path:") });
const pathInput = pathContainer.createEl("input", {
type: "text",
value: rule.path,
placeholder:
rule.type === "pattern"
? "*.tmp, temp/*"
: rule.type === "folder"
? "path/to/folder"
: "path/to/file.md",
});
if (rule.type === "folder") {
new FolderSuggest(
this.plugin.app,
pathInput,
this.plugin,
"single"
);
} else if (rule.type === "file") {
new FileSuggest(pathInput, this.plugin, (file) => {
rule.path = file.path;
pathInput.value = file.path;
this.plugin.saveSettings();
});
}
pathInput.addEventListener("input", async () => {
rule.path = pathInput.value;
await this.plugin.saveSettings();
});
const enabledContainer = ruleContainer.createDiv({
cls: "file-filter-rule-enabled",
});
enabledContainer.createEl("label", { text: t("Enabled:") });
const enabledCheckbox = enabledContainer.createEl("input", {
type: "checkbox",
});
enabledCheckbox.checked = rule.enabled;
enabledCheckbox.addEventListener("change", async () => {
rule.enabled = enabledCheckbox.checked;
await this.plugin.saveSettings();
this.updateStats();
});
const deleteButton = ruleContainer.createEl("button", {
cls: "file-filter-rule-delete mod-destructive",
});
setIcon(deleteButton, "trash");
deleteButton.title = t("Delete rule");
deleteButton.addEventListener("click", async () => {
this.plugin.settings.fileFilter.rules.splice(index, 1);
await this.plugin.saveSettings();
this.renderRules();
this.updateStats();
});
});
}
private updateStats() {
if (!this.statsContainer) return;
const container = this.statsContainer;
container.empty();
const activeRules = this.plugin.settings.fileFilter.rules.filter(
(rule) => rule.enabled
).length;
const stats = [
{
label: t("Active Rules"),
value: activeRules.toString(),
},
{
label: t("Filter Mode"),
value:
this.plugin.settings.fileFilter.mode ===
FilterMode.WHITELIST
? t("Whitelist")
: t("Blacklist"),
},
{
label: t("Status"),
value: this.plugin.settings.fileFilter.enabled
? t("Enabled")
: t("Disabled"),
},
];
stats.forEach((stat) => {
const statItem = container.createDiv({ cls: "filter-stat-item" });
statItem.createSpan({
text: stat.value,
cls: "filter-stat-value",
});
statItem.createSpan({
text: stat.label,
cls: "filter-stat-label",
});
});
}
}

View file

@ -3,6 +3,11 @@ import { t } from "@/translations/helper";
import { OnboardingController } from "../OnboardingController";
import { FilterMode, FileFilterRule } from "@/common/setting-definition";
import TaskProgressBarPlugin from "@/index";
import {
FolderSuggest,
SimpleFileSuggest as FileSuggest,
} from "@/components/ui/inputs/AutoComplete";
import { FileFilterRuleEditorModal } from "@/components/features/onboarding/modals/FileFilterRuleEditorModal";
import "@/styles/onboarding-components.css";
import "@/styles/file-filter-settings.css";
@ -117,23 +122,55 @@ export class FileFilterStep {
cls: "setting-item-control",
});
// Add file rule button
const addFileBtn = buttonsContainer.createEl("button", {
text: t("Add File"),
cls: "mod-cta",
});
addFileBtn.addEventListener("click", () => {
const modal = new FileFilterRuleEditorModal(plugin.app, plugin, {
autoAddRuleType: "file",
onClose: () => {
const rulesEl = container.querySelector(
".file-filter-rules-container"
) as HTMLElement | null;
const statsEl = container.querySelector(
".file-filter-stats-preview"
) as HTMLElement | null;
if (rulesEl) {
this.renderRules(rulesEl, plugin);
}
if (statsEl) {
this.updateStats(statsEl, plugin);
}
},
});
modal.open();
});
// Add folder rule button
const addFolderBtn = buttonsContainer.createEl("button", {
text: t("Add Folder"),
cls: "mod-cta",
});
addFolderBtn.addEventListener("click", () => {
const folderPath = prompt(t("Enter folder path:"), "");
if (folderPath) {
plugin.settings.fileFilter.rules.push({
type: "folder",
path: folderPath,
enabled: true,
});
plugin.saveSettings();
this.updateStats(container, plugin);
new Notice(t("Folder rule added"));
}
const modal = new FileFilterRuleEditorModal(plugin.app, plugin, {
autoAddRuleType: "folder",
onClose: () => {
const rulesEl = container.querySelector(
".file-filter-rules-container"
) as HTMLElement | null;
const statsEl = container.querySelector(
".file-filter-stats-preview"
) as HTMLElement | null;
if (rulesEl) {
this.renderRules(rulesEl, plugin);
}
if (statsEl) {
this.updateStats(statsEl, plugin);
}
},
});
modal.open();
});
// Add pattern rule button
@ -141,17 +178,24 @@ export class FileFilterStep {
text: t("Add Pattern"),
});
addPatternBtn.addEventListener("click", () => {
const pattern = prompt(t("Enter file pattern (e.g., *.tmp):"), "");
if (pattern) {
plugin.settings.fileFilter.rules.push({
type: "pattern",
path: pattern,
enabled: true,
});
plugin.saveSettings();
this.updateStats(container, plugin);
new Notice(t("Pattern rule added"));
}
const modal = new FileFilterRuleEditorModal(plugin.app, plugin, {
autoAddRuleType: "pattern",
onClose: () => {
const rulesEl = container.querySelector(
".file-filter-rules-container"
) as HTMLElement | null;
const statsEl = container.querySelector(
".file-filter-stats-preview"
) as HTMLElement | null;
if (rulesEl) {
this.renderRules(rulesEl, plugin);
}
if (statsEl) {
this.updateStats(statsEl, plugin);
}
},
});
modal.open();
});
// Current rules list
@ -168,7 +212,7 @@ export class FileFilterStep {
}
/**
* Render current rules
* Render current rules with inline editing support
*/
private static renderRules(
container: HTMLElement,
@ -185,29 +229,117 @@ export class FileFilterStep {
}
plugin.settings.fileFilter.rules.forEach((rule, index) => {
const ruleEl = container.createDiv({ cls: "file-filter-rule" });
const ruleContainer = container.createDiv({
cls: "file-filter-rule",
});
// Rule type icon
const typeIcon = ruleEl.createSpan({ cls: "file-filter-rule-type" });
setIcon(
typeIcon,
rule.type === "folder"
? "folder"
: rule.type === "file"
? "file"
: "regex"
);
// Rule type dropdown
const typeContainer = ruleContainer.createDiv({
cls: "file-filter-rule-type",
});
typeContainer.createEl("label", { text: t("Type:") });
// Rule path
ruleEl.createSpan({
text: rule.path,
new DropdownComponent(typeContainer)
.addOption("file", t("File"))
.addOption("folder", t("Folder"))
.addOption("pattern", t("Pattern"))
.setValue(rule.type)
.onChange(async (value: "file" | "folder" | "pattern") => {
rule.type = value;
await plugin.saveSettings();
// Only re-render rules container, not the whole component
this.renderRules(container, plugin);
this.updateStats(
container.parentElement?.querySelector(
".file-filter-stats-preview"
) as HTMLElement,
plugin
);
});
// Rule scope dropdown (per-rule)
const scopeContainer = ruleContainer.createDiv({
cls: "file-filter-rule-scope",
});
scopeContainer.createEl("label", { text: t("Scope:") });
new DropdownComponent(scopeContainer)
.addOption("both", t("Both"))
.addOption("inline", t("Inline"))
.addOption("file", t("File"))
.setValue((rule as any).scope || "both")
.onChange(async (value: "both" | "inline" | "file") => {
(rule as any).scope = value;
await plugin.saveSettings();
});
// Path input with autocomplete
const pathContainer = ruleContainer.createDiv({
cls: "file-filter-rule-path",
});
pathContainer.createEl("label", { text: t("Path:") });
const pathInput = pathContainer.createEl("input", {
type: "text",
value: rule.path,
placeholder:
rule.type === "pattern"
? "*.tmp, temp/*"
: rule.type === "folder"
? "path/to/folder"
: "path/to/file.md",
});
// Add appropriate autocomplete based on rule type
if (rule.type === "folder") {
new FolderSuggest(
plugin.app,
pathInput,
plugin,
"single"
);
} else if (rule.type === "file") {
new FileSuggest(pathInput, plugin, (file) => {
rule.path = file.path;
pathInput.value = file.path;
plugin.saveSettings();
});
}
pathInput.addEventListener("input", async () => {
rule.path = pathInput.value;
await plugin.saveSettings();
});
// Enabled toggle
const enabledContainer = ruleContainer.createDiv({
cls: "file-filter-rule-enabled",
});
enabledContainer.createEl("label", { text: t("Enabled:") });
const enabledCheckbox = enabledContainer.createEl("input", {
type: "checkbox",
});
enabledCheckbox.checked = rule.enabled;
enabledCheckbox.addEventListener("change", async () => {
rule.enabled = enabledCheckbox.checked;
await plugin.saveSettings();
this.updateStats(
container.parentElement?.querySelector(
".file-filter-stats-preview"
) as HTMLElement,
plugin
);
});
// Delete button
const deleteBtn = ruleEl.createSpan({ cls: "clickable-icon" });
setIcon(deleteBtn, "trash");
deleteBtn.addEventListener("click", async () => {
const deleteButton = ruleContainer.createEl("button", {
cls: "file-filter-rule-delete mod-destructive",
});
setIcon(deleteButton, "trash");
deleteButton.title = t("Delete rule");
deleteButton.addEventListener("click", async () => {
plugin.settings.fileFilter.rules.splice(index, 1);
await plugin.saveSettings();
this.renderRules(container, plugin);

View file

@ -4,43 +4,52 @@ import { ComponentPreviewFactory } from "../previews/ComponentPreviewFactory";
import "@/styles/onboarding-components.css";
export class FluentMainNavigationStep {
static render(
headerEl: HTMLElement,
contentEl: HTMLElement,
controller: OnboardingController
) {
headerEl.empty();
contentEl.empty();
static render(
headerEl: HTMLElement,
contentEl: HTMLElement,
controller: OnboardingController
) {
headerEl.empty();
contentEl.empty();
headerEl.createEl("h1", { text: t("Main Navigation") });
headerEl.createEl("p", {
text: t("Access Inbox, Today, Upcoming and more from the primary section"),
cls: "onboarding-subtitle",
});
headerEl.createEl("h1", { text: t("Main Navigation") });
headerEl.createEl("p", {
text: t(
"Access Inbox, Today, Upcoming and more from the primary section"
),
cls: "onboarding-subtitle",
});
const showcase = contentEl.createDiv({ cls: "component-showcase" });
const preview = showcase.createDiv({ cls: "component-showcase-preview focus-mode" });
const desc = showcase.createDiv({ cls: "component-showcase-description" });
const showcase = contentEl.createDiv({ cls: "component-showcase" });
const preview = showcase.createDiv({
cls: "component-showcase-preview focus-mode",
});
const desc = showcase.createDiv({
cls: "component-showcase-description",
});
ComponentPreviewFactory.createSidebarPreview(preview);
ComponentPreviewFactory.createSidebarPreview(preview);
const primary = preview.querySelector<HTMLElement>(".v2-sidebar-section-primary");
primary?.classList.add("is-focused");
const primary = preview.querySelector<HTMLElement>(
".v2-sidebar-section-primary"
);
primary?.classList.add("is-focused");
const dimTargets = preview.querySelectorAll<HTMLElement>(
".v2-sidebar-section-projects, .v2-sidebar-section-other"
);
dimTargets.forEach((el) => el.classList.add("is-dimmed"));
const dimTargets = preview.querySelectorAll<HTMLElement>(
".v2-sidebar-section-projects, .v2-sidebar-section-other"
);
dimTargets.forEach((el) => el.classList.add("is-dimmed"));
desc.createEl("h3", { text: t("Navigate core views") });
desc.createEl("p", {
text: t("Quickly jump to core views like Inbox, Today, Upcoming and Flagged."),
});
const ul = desc.createEl("ul", { cls: "component-feature-list" });
[
t("Unread counts and indicators keep you informed"),
t("Keyboard-ready with clear selection states"),
].forEach((txt) => ul.createEl("li", { text: txt }));
}
desc.createEl("h3", { text: t("Navigate core views") });
desc.createEl("p", {
text: t(
"Quickly jump to core views like Inbox, Today, Upcoming and Flagged."
),
});
const ul = desc.createEl("ul", { cls: "component-feature-list" });
[
t("Unread counts and indicators keep you informed"),
t("Keyboard-ready with clear selection states"),
].forEach((txt) => ul.createEl("li", { text: txt }));
}
}

View file

@ -4,45 +4,52 @@ import { ComponentPreviewFactory } from "../previews/ComponentPreviewFactory";
import "@/styles/onboarding-components.css";
export class FluentOtherViewsStep {
static render(
headerEl: HTMLElement,
contentEl: HTMLElement,
controller: OnboardingController
) {
headerEl.empty();
contentEl.empty();
static render(
headerEl: HTMLElement,
contentEl: HTMLElement,
controller: OnboardingController
) {
headerEl.empty();
contentEl.empty();
headerEl.createEl("h1", { text: t("Other Views") });
headerEl.createEl("p", {
text: t("Access Calendar, Gantt and Tags from the other views section"),
cls: "onboarding-subtitle",
});
headerEl.createEl("h1", { text: t("Other Views") });
headerEl.createEl("p", {
text: t(
"Access Calendar, Gantt and Tags from the other views section"
),
cls: "onboarding-subtitle",
});
const showcase = contentEl.createDiv({ cls: "component-showcase" });
const preview = showcase.createDiv({ cls: "component-showcase-preview focus-mode" });
const desc = showcase.createDiv({ cls: "component-showcase-description" });
const showcase = contentEl.createDiv({ cls: "component-showcase" });
const preview = showcase.createDiv({
cls: "component-showcase-preview focus-mode",
});
const desc = showcase.createDiv({
cls: "component-showcase-description",
});
ComponentPreviewFactory.createSidebarPreview(preview);
ComponentPreviewFactory.createSidebarPreview(preview);
const other = preview.querySelector<HTMLElement>(".v2-sidebar-section-other");
other?.classList.add("is-focused");
const other = preview.querySelector<HTMLElement>(
".v2-sidebar-section-other"
);
other?.classList.add("is-focused");
const dimTargets = preview.querySelectorAll<HTMLElement>(
".v2-sidebar-section-primary, .v2-sidebar-section-projects"
);
dimTargets.forEach((el) => el.classList.add("is-dimmed"));
const dimTargets = preview.querySelectorAll<HTMLElement>(
".v2-sidebar-section-primary, .v2-sidebar-section-projects"
);
dimTargets.forEach((el) => el.classList.add("is-dimmed"));
desc.createEl("h3", { text: t("Specialized views") });
desc.createEl("p", {
text: t(
"Quickly reach Calendar scheduling, Gantt timelines and tag-based filtering."
),
});
const ul = desc.createEl("ul", { cls: "component-feature-list" });
[
t("Compact list with clear icons"),
t("Consistent selection feedback"),
].forEach((txt) => ul.createEl("li", { text: txt }));
}
desc.createEl("h3", { text: t("Specialized views") });
desc.createEl("p", {
text: t(
"Quickly reach views like Calendar, Gantt, Flagged and Tags, etc."
),
});
const ul = desc.createEl("ul", { cls: "component-feature-list" });
[
t("Compact list with clear icons"),
t("Right click for more options"),
].forEach((txt) => ul.createEl("li", { text: txt }));
}
}

View file

@ -4,46 +4,54 @@ import { ComponentPreviewFactory } from "../previews/ComponentPreviewFactory";
import "@/styles/onboarding-components.css";
export class FluentProjectSectionStep {
static render(
headerEl: HTMLElement,
contentEl: HTMLElement,
controller: OnboardingController
) {
headerEl.empty();
contentEl.empty();
static render(
headerEl: HTMLElement,
contentEl: HTMLElement,
controller: OnboardingController
) {
headerEl.empty();
contentEl.empty();
headerEl.createEl("h1", { text: t("Projects Section") });
headerEl.createEl("p", {
text: t("Organize your work with projects and hierarchies"),
cls: "onboarding-subtitle",
});
headerEl.createEl("h1", { text: t("Projects Section") });
headerEl.createEl("p", {
text: t("Organize your work with projects and hierarchies"),
cls: "onboarding-subtitle",
});
const showcase = contentEl.createDiv({ cls: "component-showcase" });
const preview = showcase.createDiv({ cls: "component-showcase-preview focus-mode" });
const desc = showcase.createDiv({ cls: "component-showcase-description" });
const showcase = contentEl.createDiv({ cls: "component-showcase" });
const preview = showcase.createDiv({
cls: "component-showcase-preview focus-mode",
});
const desc = showcase.createDiv({
cls: "component-showcase-description",
});
ComponentPreviewFactory.createSidebarPreview(preview);
ComponentPreviewFactory.createSidebarPreview(preview);
const projects = preview.querySelector<HTMLElement>(".v2-sidebar-section-projects");
projects?.classList.add("is-focused");
const projects = preview.querySelector<HTMLElement>(
".v2-sidebar-section-projects"
);
projects?.classList.add("is-focused");
const dimTargets = preview.querySelectorAll<HTMLElement>(
".v2-sidebar-section-primary, .v2-sidebar-section-other"
);
dimTargets.forEach((el) => el.classList.add("is-dimmed"));
const dimTargets = preview.querySelectorAll<HTMLElement>(
".v2-sidebar-section-primary, .v2-sidebar-section-other"
);
dimTargets.forEach((el) => el.classList.add("is-dimmed"));
desc.createEl("h3", { text: t("Project organization") });
desc.createEl("p", {
text: t(
"Group related tasks under projects. Build nested hierarchies and get quick stats."
),
});
const ul = desc.createEl("ul", { cls: "component-feature-list" });
[
t("Color-coded projects with counts"),
t("Supports nested structures for complex work"),
t("Quick actions via project popover (preview)"),
].forEach((txt) => ul.createEl("li", { text: txt }));
}
desc.createEl("h3", { text: t("Project organization") });
desc.createEl("p", {
text: t(
"Group related tasks under projects. Build nested hierarchies and get quick stats."
),
});
const ul = desc.createEl("ul", { cls: "component-feature-list" });
[
t(
"Color-coded projects with counts (You can change the color in the settings)"
),
t("Supports nested structures for complex work"),
t("Right click for more options"),
t("Sort projects by name or tasks count, etc."),
].forEach((txt) => ul.createEl("li", { text: txt }));
}
}

View file

@ -24,7 +24,7 @@ export class IntroStep {
contentEl.empty();
// Hide footer during intro animation
footerEl.style.display = "none";
footerEl.hide();
// Create wrapper for typing animation
const introWrapper = contentEl.createDiv({
@ -55,9 +55,9 @@ export class IntroStep {
className: "intro-line-3",
speed: 20,
fadeOut: true,
pauseAfter: 3000, // Wait 3s for user to read
pauseAfter: 2000, // Wait 3s for user to read
fadeOutDelay: 0, // Start fading out immediately after pause
fadeOutDuration: 2000, // 2s fade out animation
fadeOutDuration: 1000, // 2s fade out animation
delayNext: 0, // No extra delay before next message
},
{
@ -73,7 +73,7 @@ export class IntroStep {
// Start typing animation
new TypingAnimation(typingContainer, messages, () => {
// Typing completed: show footer and move to Mode Selection step
footerEl.style.display = "";
footerEl.show();
controller.setStep(OnboardingStep.MODE_SELECT);
});
}

View file

@ -1,9 +1,12 @@
import { t } from "@/translations/helper";
import { SelectableCard, SelectableCardConfig } from "@/components/features/onboarding/ui/SelectableCard";
import {
SelectableCard,
SelectableCardConfig,
} from "@/components/features/onboarding/ui/SelectableCard";
import { OnboardingController } from "@/components/features/onboarding/OnboardingController";
import { Alert } from "@/components/features/onboarding/ui/Alert";
export type UIMode = 'fluent' | 'legacy';
export type UIMode = "fluent" | "legacy";
/**
* Mode Selection Step - Choose between Fluent and Legacy UI
@ -21,14 +24,16 @@ export class ModeSelectionStep {
headerEl.empty();
contentEl.empty();
headerEl.toggleClass("intro-typing-wrapper", true);
contentEl.toggleClass("intro-typing-wrapper", true);
// Intro guidance text (same as intro-line-4)
headerEl.createEl("p", {
cls: "intro-line intro-line-4",
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?"
),
});
// Intro guidance text (same as intro-line-4)
headerEl.createEl("p", {
cls: "intro-line intro-line-4",
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?"
),
});
// Get current state
const currentMode = controller.getState().uiMode;
@ -85,83 +90,29 @@ export class ModeSelectionStep {
);
}
/**
* Render mode selection inline (for intro step)
* This version doesn't clear the container and calls a custom callback
*/
static renderInline(
containerEl: HTMLElement,
controller: OnboardingController,
onSelect: (mode: UIMode) => void
) {
// Get current state
const currentMode = controller.getState().uiMode;
// Create cards configuration
const cardConfigs: SelectableCardConfig<UIMode>[] = [
{
id: "fluent",
title: t("Fluent"),
subtitle: t("Modern & Sleek"),
description: t(
"New visual design with elegant animations and modern interactions"
),
preview: this.createFluentPreview(),
},
{
id: "legacy",
title: t("Legacy"),
subtitle: t("Classic & Familiar"),
description: t(
"Keep the familiar interface and interaction style you know"
),
preview: this.createLegacyPreview(),
},
];
// Render selectable cards
const card = new SelectableCard<UIMode>(
containerEl,
cardConfigs,
{
containerClass: "selectable-cards-container",
cardClass: "selectable-card",
showPreview: true,
},
(mode) => {
onSelect(mode);
}
);
// Set initial selection
if (currentMode) {
card.setSelected(currentMode);
}
// Add info alert
Alert.create(
containerEl,
t("You can change this option later in settings"),
{
variant: "info",
className: "mode-selection-tip",
}
);
}
/**
* Create Fluent mode preview
*/
private static createFluentPreview(): HTMLElement {
const preview = document.createElement("div");
preview.addClass("mode-preview", "mode-preview-fluent");
const preview = createDiv({
cls: ["mode-preview", "mode-preview-fluent"],
});
// Check theme
const isDark = document.body.classList.contains("theme-dark");
const theme = isDark ? "" : "-light";
const imageUrl = `https://raw.githubusercontent.com/Quorafind/Obsidian-Task-Progress-Bar/master/media/fluent${theme}.png`;
preview.innerHTML = `<img src="${imageUrl}" alt="Fluent mode preview" style="max-width: 100%; max-height: 100%; object-fit: contain; border-radius: 4px;">`;
const img = preview.createEl("img", {
attr: {
src: imageUrl,
alt: "Fluent mode preview",
},
});
img.style.maxWidth = "100%";
img.style.maxHeight = "100%";
img.style.objectFit = "contain";
img.style.borderRadius = "4px";
return preview;
}
@ -170,15 +121,25 @@ export class ModeSelectionStep {
* Create Legacy mode preview
*/
private static createLegacyPreview(): HTMLElement {
const preview = document.createElement("div");
preview.addClass("mode-preview", "mode-preview-legacy");
const preview = createDiv({
cls: ["mode-preview", "mode-preview-legacy"],
});
// Check theme
const isDark = document.body.classList.contains("theme-dark");
const theme = isDark ? "" : "-light";
const imageUrl = `https://raw.githubusercontent.com/Quorafind/Obsidian-Task-Progress-Bar/master/media/legacy${theme}.png`;
preview.innerHTML = `<img src="${imageUrl}" alt="Legacy mode preview" style="max-width: 100%; max-height: 100%; object-fit: contain; border-radius: 4px;">`;
const img = preview.createEl("img", {
attr: {
src: imageUrl,
alt: "Legacy mode preview",
},
});
img.style.maxWidth = "100%";
img.style.maxHeight = "100%";
img.style.objectFit = "contain";
img.style.borderRadius = "4px";
return preview;
}

View file

@ -77,17 +77,17 @@ export class SettingsCheckStep {
"settings-check-actions"
);
// Action 2: Keep current settings (secondary)
const keepCard = actionsContainer.createDiv(
"settings-check-action-card settings-check-action-keep"
);
// Action 1: Continue with wizard (prominent)
const wizardCard = actionsContainer.createDiv(
"settings-check-action-card settings-check-action-wizard"
);
wizardCard.addEventListener("click", () => {
// Select this option
this.selectedAction = "wizard";
this.updateCardSelection(wizardCard, keepCard);
controller.updateState({ settingsCheckAction: "wizard" });
});
// 先渲染卡片内容,再绑定事件,避免 keepCard/wizardCard 未定义
const wizardHeader = wizardCard.createDiv("action-card-header");
const wizardIcon = wizardHeader.createDiv("action-card-icon");
setIcon(wizardIcon, "wand-2");
@ -114,17 +114,6 @@ export class SettingsCheckStep {
item.createSpan({ text: feature });
});
// Action 2: Keep current settings (secondary)
const keepCard = actionsContainer.createDiv(
"settings-check-action-card settings-check-action-keep"
);
keepCard.addEventListener("click", () => {
// Select this option
this.selectedAction = "keep";
this.updateCardSelection(keepCard, wizardCard);
controller.updateState({ settingsCheckAction: "keep" });
});
const keepHeader = keepCard.createDiv("action-card-header");
const keepIcon = keepHeader.createDiv("action-card-icon");
setIcon(keepIcon, "shield-check");
@ -143,6 +132,21 @@ export class SettingsCheckStep {
keepNote.createSpan({
text: t("Your customizations will be preserved"),
});
// 事件绑定前,确保两个卡片都已渲染
wizardCard.addEventListener("click", () => {
if (this.selectedAction === "wizard") return;
this.selectedAction = "wizard";
this.updateCardSelection(wizardCard, keepCard);
controller.updateState({ settingsCheckAction: "wizard" });
});
keepCard.addEventListener("click", () => {
if (this.selectedAction === "keep") return;
this.selectedAction = "keep";
this.updateCardSelection(keepCard, wizardCard);
controller.updateState({ settingsCheckAction: "keep" });
});
}
/**

View file

@ -62,25 +62,22 @@ export class UserLevelStep {
contentEl,
cardConfigs,
{
containerClass: "selectable-cards-container",
containerClass: [
"selectable-cards-container",
"user-level-cards",
],
cardClass: "selectable-card",
showIcon: true,
showFeatures: true,
showPreview: false,
},
(mode) => {
// Find the selected config
const selectedConfig = configs.find((c) => c.mode === mode);
controller.setSelectedConfig(selectedConfig);
controller.setSelectedConfig(
configs.find((c) => c.mode === mode)
);
}
);
// Set initial selection
if (currentConfig) {
card.setSelected(currentConfig.mode);
}
}
/**
* Get icon for configuration mode
*/

View file

@ -12,7 +12,7 @@ export interface SelectableCardConfig<T> {
}
export interface SelectableCardOptions {
containerClass?: string;
containerClass?: string | string[];
cardClass?: string;
showIcon?: boolean;
showPreview?: boolean;
@ -66,11 +66,11 @@ export class SelectableCard<T = string> {
});
// Header
const header = card.createDiv({cls: `${cardClass}-header`});
const header = card.createDiv({ cls: `${cardClass}-header` });
// Icon
if (showIcon && config.icon) {
const iconEl = header.createDiv({cls: `${cardClass}-icon`});
const iconEl = header.createDiv({ cls: `${cardClass}-icon` });
setIcon(iconEl, config.icon);
}
@ -100,7 +100,7 @@ export class SelectableCard<T = string> {
}
// Body
const body = card.createDiv({cls: `${cardClass}-body`});
const body = card.createDiv({ cls: `${cardClass}-body` });
// Preview (optional)
if (showPreview && config.preview) {
@ -128,7 +128,7 @@ export class SelectableCard<T = string> {
});
const featuresList = featuresContainer.createEl("ul");
config.features.forEach((feature) => {
featuresList.createEl("li", {text: feature});
featuresList.createEl("li", { text: feature });
});
}

View file

@ -119,9 +119,15 @@ import { createDataflow, isDataflowEnabled } from "./dataflow/createDataflow";
import type { DataflowOrchestrator } from "./dataflow/Orchestrator";
import { WriteAPI } from "./dataflow/api/WriteAPI";
import { Events } from "./dataflow/events/Events";
import { installWorkspaceDragMonitor, registerRestrictedDnDViewTypes } from "./patches/workspace-dnd-patch";
import {
installWorkspaceDragMonitor,
registerRestrictedDnDViewTypes,
} from "./patches/workspace-dnd-patch";
import { TASK_VIEW_V2_TYPE } from "./experimental/v2/TaskViewV2";
import { setPriorityAtCursor, removePriorityAtCursor } from "./utils/task/curosr-priority-utils";
import {
setPriorityAtCursor,
removePriorityAtCursor,
} from "./utils/task/curosr-priority-utils";
class TaskProgressBarPopover extends HoverPopover {
plugin: TaskProgressBarPlugin;
@ -146,7 +152,7 @@ class TaskProgressBarPopover extends HoverPopover {
},
parent: HoverParent,
targetEl: HTMLElement,
waitTime: number = 1000,
waitTime: number = 1000
) {
super(parent, targetEl, waitTime);
@ -170,7 +176,7 @@ class TaskProgressBarPopover extends HoverPopover {
`,
this.hoverEl,
"",
this.plugin,
this.plugin
);
}
}
@ -192,7 +198,7 @@ export const showPopoverWithProgressBar = (
planned: string;
};
view: EditorView;
},
}
) => {
const editor = view.state.field(editorInfoField);
if (!editor) return;
@ -265,7 +271,6 @@ export default class TaskProgressBarPlugin extends Plugin {
// V2 Integration instance
v2Integration?: V2Integration;
// Uninstaller for workspace drag-and-drop monkey patch
private uninstallWorkspaceDnD?: () => void;
@ -296,7 +301,9 @@ export default class TaskProgressBarPlugin extends Plugin {
this.globalSuggestManager = new SuggestManager(this.app, this);
// Initialize workspace manager
const {WorkspaceManager} = await import("./experimental/v2/managers/WorkspaceManager");
const { WorkspaceManager } = await import(
"./experimental/v2/managers/WorkspaceManager"
);
this.workspaceManager = new WorkspaceManager(this);
await this.workspaceManager.migrateToV2();
this.workspaceManager.ensureDefaultWorkspaceInvariant();
@ -308,11 +315,6 @@ export default class TaskProgressBarPlugin extends Plugin {
// Initialize rebuild progress manager
this.rebuildProgressManager = new RebuildProgressManager();
// Initialize V2 Integration
this.v2Integration = new V2Integration(this);
await this.v2Integration.migrateSettings();
this.v2Integration.register();
// Initialize task management systems
if (this.settings.enableIndexer) {
// Initialize indexer-dependent features
@ -326,28 +328,35 @@ export default class TaskProgressBarPlugin extends Plugin {
this.initializeDataflowWithVersionCheck().catch((error) => {
console.error(
"Failed to initialize dataflow with version check:",
error,
error
);
});
}
console.time("[TPB] registerViewsAndCommands");
// Register the TaskView
this.registerView(
TASK_VIEW_TYPE,
(leaf) => new TaskView(leaf, this),
);
if (!this.settings.onboarding?.completed) {
// Initialize V2 Integration
this.v2Integration = new V2Integration(this);
await this.v2Integration.migrateSettings();
this.v2Integration.register();
this.registerView(
TASK_SPECIFIC_VIEW_TYPE,
(leaf) => new TaskSpecificView(leaf, this),
);
this.registerView(
TASK_VIEW_TYPE,
(leaf) => new TaskView(leaf, this)
);
// Register the Timeline Sidebar View
this.registerView(
TIMELINE_SIDEBAR_VIEW_TYPE,
(leaf) => new TimelineSidebarView(leaf, this),
);
this.registerView(
TASK_SPECIFIC_VIEW_TYPE,
(leaf) => new TaskSpecificView(leaf, this)
);
// Register the Timeline Sidebar View
this.registerView(
TIMELINE_SIDEBAR_VIEW_TYPE,
(leaf) => new TimelineSidebarView(leaf, this)
);
}
// Register the Onboarding View
this.registerView(
@ -357,7 +366,7 @@ export default class TaskProgressBarPlugin extends Plugin {
console.log("Onboarding completed successfully");
// Close the onboarding view and refresh views
leaf.detach();
}),
})
);
// Add a command to open the TaskView
@ -400,7 +409,7 @@ export default class TaskProgressBarPlugin extends Plugin {
t("Open Task Genius view"),
() => {
this.activateTaskView();
},
}
);
console.timeEnd("[TPB] registerViewsAndCommands");
@ -446,22 +455,22 @@ export default class TaskProgressBarPlugin extends Plugin {
detectionMethods:
this.settings.projectConfig?.metadataConfig
?.detectionMethods || [],
},
}
);
console.timeEnd("[TPB] createDataflow");
console.log(
"[Plugin] Dataflow orchestrator initialized successfully",
"[Plugin] Dataflow orchestrator initialized successfully"
);
} catch (error) {
console.error(
"[Plugin] Failed to initialize dataflow orchestrator:",
error,
error
);
// Fatal error - cannot continue without dataflow
new Notice(
t(
"Failed to initialize task system. Please restart Obsidian.",
),
"Failed to initialize task system. Please restart Obsidian."
)
);
}
@ -486,7 +495,7 @@ export default class TaskProgressBarPlugin extends Plugin {
this.app.vault,
this.app.metadataCache,
this,
getTaskById,
getTaskById
);
// Initialize OnCompletionManager
@ -511,11 +520,12 @@ export default class TaskProgressBarPlugin extends Plugin {
this.registerEditorExt();
console.timeEnd("[TPB] registerEditorExt");
// Install workspace DnD monkey patch (blocks dragging restricted views to center)
installWorkspaceDragMonitor(this);
// Also restrict V2 main view from being dropped to center, as it is sidebar-managed
try { registerRestrictedDnDViewTypes(TASK_VIEW_V2_TYPE); } catch {}
try {
registerRestrictedDnDViewTypes(TASK_VIEW_V2_TYPE);
} catch {}
this.settingTab = new TaskProgressBarSettingTab(this.app, this);
this.addSettingTab(this.settingTab);
@ -536,18 +546,18 @@ export default class TaskProgressBarPlugin extends Plugin {
item.setTitle(
`${t("Set priority")}: ${
priority.text
}`,
}`
);
item.setIcon("arrow-big-up-dash");
item.onClick(() => {
setPriorityAtCursor(
editor,
priority.emoji,
priority.emoji
);
});
});
}
},
}
);
submenu.addSeparator();
@ -557,17 +567,17 @@ export default class TaskProgressBarPlugin extends Plugin {
([key, priority]) => {
submenu.addItem((item) => {
item.setTitle(
`${t("Set priority")}: ${key}`,
`${t("Set priority")}: ${key}`
);
item.setIcon("a-arrow-up");
item.onClick(() => {
setPriorityAtCursor(
editor,
`[#${key}]`,
`[#${key}]`
);
});
});
},
}
);
// Remove priority command
@ -587,18 +597,23 @@ export default class TaskProgressBarPlugin extends Plugin {
if (this.settings.workflow.enableWorkflow) {
updateWorkflowContextMenu(menu, editor, this);
}
}),
})
);
this.app.workspace.onLayoutReady(() => {
console.time("[TPB] onLayoutReady");
// Update workspace leaves when layout is ready
const deferWorkspaceLeaves = this.app.workspace.getLeavesOfType(TASK_VIEW_TYPE);
const deferSpecificLeaves = this.app.workspace.getLeavesOfType(TASK_SPECIFIC_VIEW_TYPE);
[...deferWorkspaceLeaves, ...deferSpecificLeaves].forEach(leaf => {
leaf.loadIfDeferred();
});
const deferWorkspaceLeaves =
this.app.workspace.getLeavesOfType(TASK_VIEW_TYPE);
const deferSpecificLeaves = this.app.workspace.getLeavesOfType(
TASK_SPECIFIC_VIEW_TYPE
);
[...deferWorkspaceLeaves, ...deferSpecificLeaves].forEach(
(leaf) => {
leaf.loadIfDeferred();
}
);
// Initialize Task Genius Icon Manager
this.taskGeniusIconManager = new TaskGeniusIconManager(this);
this.addChild(this.taskGeniusIconManager);
@ -616,8 +631,8 @@ export default class TaskProgressBarPlugin extends Plugin {
this.registerEvent(
this.app.workspace.on(
Events.TASK_CACHE_UPDATED as any,
() => this.notificationManager?.onTaskCacheUpdated(),
),
() => this.notificationManager?.onTaskCacheUpdated()
)
);
}
@ -669,7 +684,7 @@ export default class TaskProgressBarPlugin extends Plugin {
this.icsManager = new IcsManager(
this.settings.icsIntegration,
this.settings,
this,
this
);
this.addChild(this.icsManager);
@ -689,7 +704,7 @@ export default class TaskProgressBarPlugin extends Plugin {
this.activateTimelineSidebarView().catch((error) => {
console.error(
"Failed to auto-open timeline sidebar:",
error,
error
);
});
}, 1000);
@ -708,11 +723,11 @@ export default class TaskProgressBarPlugin extends Plugin {
(preset: any) => {
if (preset.options) {
preset.options = migrateOldFilterOptions(
preset.options,
preset.options
);
}
return preset;
},
}
);
await this.saveSettings();
console.timeEnd("[TPB] migratePresetTaskFilters");
@ -763,7 +778,7 @@ export default class TaskProgressBarPlugin extends Plugin {
if (view) {
view.dispatch({
effects: toggleTaskFilter.of(
!view.state.field(taskFilterState),
!view.state.field(taskFilterState)
),
});
}
@ -785,14 +800,14 @@ export default class TaskProgressBarPlugin extends Plugin {
const changes = sortTasksInDocument(
editorView,
this,
false,
false
);
if (changes) {
new Notice(
t(
"Tasks sorted (using settings). Change application needs refinement.",
),
"Tasks sorted (using settings). Change application needs refinement."
)
);
} else {
// Notice is already handled within sortTasksInDocument if no changes or sorting disabled
@ -816,11 +831,11 @@ export default class TaskProgressBarPlugin extends Plugin {
return changes;
});
new Notice(
t("Entire document sorted (using settings)."),
t("Entire document sorted (using settings).")
);
} else {
new Notice(
t("Tasks already sorted or no tasks found."),
t("Tasks already sorted or no tasks found.")
);
}
},
@ -861,7 +876,7 @@ export default class TaskProgressBarPlugin extends Plugin {
) {
// Use dataflow orchestrator for refresh
console.log(
"[Command] Refreshing task index via dataflow",
"[Command] Refreshing task index via dataflow"
);
// Re-scan all files to refresh the index
@ -883,8 +898,8 @@ export default class TaskProgressBarPlugin extends Plugin {
batch.map((file) =>
(
this.dataflowOrchestrator as any
).processFileImmediate(file),
),
).processFileImmediate(file)
)
);
}
@ -921,12 +936,10 @@ export default class TaskProgressBarPlugin extends Plugin {
) {
// Use dataflow orchestrator for force reindex
console.log(
"[Command] Force reindexing via dataflow",
"[Command] Force reindexing via dataflow"
);
new Notice(
t(
"Clearing task cache and rebuilding index...",
),
t("Clearing task cache and rebuilding index...")
);
// Clear all caches and rebuild from scratch
@ -1026,7 +1039,7 @@ export default class TaskProgressBarPlugin extends Plugin {
editor,
ctx,
this,
"allCompleted",
"allCompleted"
);
},
});
@ -1041,7 +1054,7 @@ export default class TaskProgressBarPlugin extends Plugin {
editor,
ctx,
this,
"directChildren",
"directChildren"
);
},
});
@ -1056,7 +1069,7 @@ export default class TaskProgressBarPlugin extends Plugin {
editor,
ctx,
this,
"all",
"all"
);
},
});
@ -1072,7 +1085,7 @@ export default class TaskProgressBarPlugin extends Plugin {
editor,
ctx,
this,
"allCompleted",
"allCompleted"
);
},
});
@ -1080,7 +1093,7 @@ export default class TaskProgressBarPlugin extends Plugin {
this.addCommand({
id: "auto-move-direct-completed-subtasks",
name: t(
"Auto-move direct completed subtasks to default file",
"Auto-move direct completed subtasks to default file"
),
editorCheckCallback: (checking, editor, ctx) => {
return autoMoveCompletedTasksCommand(
@ -1088,7 +1101,7 @@ export default class TaskProgressBarPlugin extends Plugin {
editor,
ctx,
this,
"directChildren",
"directChildren"
);
},
});
@ -1102,7 +1115,7 @@ export default class TaskProgressBarPlugin extends Plugin {
editor,
ctx,
this,
"all",
"all"
);
},
});
@ -1121,7 +1134,7 @@ export default class TaskProgressBarPlugin extends Plugin {
editor,
ctx,
this,
"allIncompleted",
"allIncompleted"
);
},
});
@ -1136,7 +1149,7 @@ export default class TaskProgressBarPlugin extends Plugin {
editor,
ctx,
this,
"directIncompletedChildren",
"directIncompletedChildren"
);
},
});
@ -1152,7 +1165,7 @@ export default class TaskProgressBarPlugin extends Plugin {
editor,
ctx,
this,
"allIncompleted",
"allIncompleted"
);
},
});
@ -1160,7 +1173,7 @@ export default class TaskProgressBarPlugin extends Plugin {
this.addCommand({
id: "auto-move-direct-incomplete-subtasks",
name: t(
"Auto-move direct incomplete subtasks to default file",
"Auto-move direct incomplete subtasks to default file"
),
editorCheckCallback: (checking, editor, ctx) => {
return autoMoveCompletedTasksCommand(
@ -1168,7 +1181,7 @@ export default class TaskProgressBarPlugin extends Plugin {
editor,
ctx,
this,
"directIncompletedChildren",
"directIncompletedChildren"
);
},
});
@ -1234,8 +1247,8 @@ export default class TaskProgressBarPlugin extends Plugin {
} catch (e) {
new Notice(
t(
"Could not open quick capture panel in the current editor",
),
"Could not open quick capture panel in the current editor"
)
);
}
}, 100);
@ -1254,7 +1267,7 @@ export default class TaskProgressBarPlugin extends Plugin {
checking,
editor,
ctx,
this,
this
);
},
});
@ -1267,7 +1280,7 @@ export default class TaskProgressBarPlugin extends Plugin {
checking,
editor,
ctx,
this,
this
);
},
});
@ -1280,7 +1293,7 @@ export default class TaskProgressBarPlugin extends Plugin {
checking,
editor,
ctx,
this,
this
);
},
});
@ -1293,7 +1306,7 @@ export default class TaskProgressBarPlugin extends Plugin {
checking,
editor,
ctx,
this,
this
);
},
});
@ -1306,7 +1319,7 @@ export default class TaskProgressBarPlugin extends Plugin {
checking,
editor,
ctx,
this,
this
);
},
});
@ -1319,7 +1332,7 @@ export default class TaskProgressBarPlugin extends Plugin {
checking,
editor,
ctx,
this,
this
);
},
});
@ -1357,7 +1370,7 @@ export default class TaskProgressBarPlugin extends Plugin {
URL.revokeObjectURL(url);
new Notice(
`Exported ${stats.activeTimers} timer records`,
`Exported ${stats.activeTimers} timer records`
);
} catch (error) {
console.error("Error exporting timer data:", error);
@ -1388,17 +1401,17 @@ export default class TaskProgressBarPlugin extends Plugin {
if (success) {
new Notice(
"Timer data imported successfully",
"Timer data imported successfully"
);
} else {
new Notice(
"Failed to import timer data - invalid format",
"Failed to import timer data - invalid format"
);
}
} catch (error) {
console.error(
"Error importing timer data:",
error,
error
);
new Notice("Failed to import timer data");
}
@ -1442,12 +1455,12 @@ export default class TaskProgressBarPlugin extends Plugin {
URL.revokeObjectURL(url);
new Notice(
`Exported ${stats.activeTimers} timer records to YAML`,
`Exported ${stats.activeTimers} timer records to YAML`
);
} catch (error) {
console.error(
"Error exporting timer data to YAML:",
error,
error
);
new Notice("Failed to export timer data to YAML");
}
@ -1495,7 +1508,7 @@ export default class TaskProgressBarPlugin extends Plugin {
let message = `Task Timer Statistics:\n`;
message += `Active timers: ${stats.activeTimers}\n`;
message += `Total duration: ${Math.round(
stats.totalDuration / 60000,
stats.totalDuration / 60000
)} minutes\n`;
if (stats.oldestTimer) {
@ -1525,12 +1538,12 @@ export default class TaskProgressBarPlugin extends Plugin {
// Initialize task timer manager and exporter
if (!this.taskTimerManager) {
this.taskTimerManager = new TaskTimerManager(
this.settings.taskTimer,
this.settings.taskTimer
);
}
if (!this.taskTimerExporter) {
this.taskTimerExporter = new TaskTimerExporter(
this.taskTimerManager,
this.taskTimerManager
);
}
@ -1538,12 +1551,12 @@ export default class TaskProgressBarPlugin extends Plugin {
}
this.settings.taskGutter.enableTaskGutter &&
this.registerEditorExtension([taskGutterExtension(this.app, this)]);
this.registerEditorExtension([taskGutterExtension(this.app, this)]);
this.settings.enableTaskStatusSwitcher &&
this.settings.enableCustomTaskMarks &&
this.registerEditorExtension([
taskStatusSwitcherExtension(this.app, this),
]);
this.settings.enableCustomTaskMarks &&
this.registerEditorExtension([
taskStatusSwitcherExtension(this.app, this),
]);
// Add priority picker extension
if (this.settings.enablePriorityPicker) {
@ -1579,7 +1592,7 @@ export default class TaskProgressBarPlugin extends Plugin {
if (this.settings.quickCapture.enableMinimalMode) {
this.minimalQuickCaptureSuggest = new MinimalQuickCaptureSuggest(
this.app,
this,
this
);
this.registerEditorSuggest(this.minimalQuickCaptureSuggest);
}
@ -1611,7 +1624,7 @@ export default class TaskProgressBarPlugin extends Plugin {
this.dataflowOrchestrator.cleanup().catch((error) => {
console.error(
"Error cleaning up dataflow orchestrator:",
error,
error
);
});
// Set to undefined to prevent any further access
@ -1657,7 +1670,7 @@ export default class TaskProgressBarPlugin extends Plugin {
* Open the onboarding view in a new leaf
*/
async openOnboardingView(): Promise<void> {
const {workspace} = this.app;
const { workspace } = this.app;
// Check if onboarding view is already open
const existingLeaf = workspace.getLeavesOfType(ONBOARDING_VIEW_TYPE)[0];
@ -1669,7 +1682,7 @@ export default class TaskProgressBarPlugin extends Plugin {
// Create a new leaf in the main area and open the onboarding view
const leaf = workspace.getLeaf("tab");
await leaf.setViewState({type: ONBOARDING_VIEW_TYPE});
await leaf.setViewState({ type: ONBOARDING_VIEW_TYPE });
workspace.revealLeaf(leaf);
}
@ -1679,14 +1692,13 @@ export default class TaskProgressBarPlugin extends Plugin {
try {
console.debug(
"[Plugin][loadSettings] fileMetadataInheritance (raw):",
savedData?.fileMetadataInheritance,
savedData?.fileMetadataInheritance
);
console.debug(
"[Plugin][loadSettings] fileMetadataInheritance (effective):",
this.settings.fileMetadataInheritance,
this.settings.fileMetadataInheritance
);
} catch {
}
} catch {}
// Migrate old inheritance settings to new structure
this.migrateInheritanceSettings(savedData);
@ -1726,10 +1738,9 @@ export default class TaskProgressBarPlugin extends Plugin {
try {
console.debug(
"[Plugin][saveSettings] fileMetadataInheritance:",
this.settings?.fileMetadataInheritance,
this.settings?.fileMetadataInheritance
);
} catch {
}
} catch {}
await this.saveData(this.settings);
}
@ -1744,10 +1755,10 @@ export default class TaskProgressBarPlugin extends Plugin {
// Add any missing default views to user settings
defaultViews.forEach((defaultView) => {
const existingView = this.settings.viewConfiguration.find(
(v) => v.id === defaultView.id,
(v) => v.id === defaultView.id
);
if (!existingView) {
this.settings.viewConfiguration.push({...defaultView});
this.settings.viewConfiguration.push({ ...defaultView });
}
});
@ -1766,7 +1777,7 @@ export default class TaskProgressBarPlugin extends Plugin {
this.isActivatingView = true;
try {
const {workspace} = this.app;
const { workspace } = this.app;
// Check if view is already open
const existingLeaves = workspace.getLeavesOfType(TASK_VIEW_TYPE);
@ -1784,7 +1795,7 @@ export default class TaskProgressBarPlugin extends Plugin {
// Otherwise, create a new leaf and open the view
const leaf = workspace.getLeaf("tab");
await leaf.setViewState({type: TASK_VIEW_TYPE});
await leaf.setViewState({ type: TASK_VIEW_TYPE });
workspace.revealLeaf(leaf);
} finally {
this.isActivatingView = false;
@ -1801,11 +1812,11 @@ export default class TaskProgressBarPlugin extends Plugin {
this.isActivatingSidebar = true;
try {
const {workspace} = this.app;
const { workspace } = this.app;
// Check if view is already open
const existingLeaves = workspace.getLeavesOfType(
TIMELINE_SIDEBAR_VIEW_TYPE,
TIMELINE_SIDEBAR_VIEW_TYPE
);
if (existingLeaves.length > 0) {
@ -1822,7 +1833,7 @@ export default class TaskProgressBarPlugin extends Plugin {
// Open in the right sidebar
const leaf = workspace.getRightLeaf(false);
if (leaf) {
await leaf.setViewState({type: TIMELINE_SIDEBAR_VIEW_TYPE});
await leaf.setViewState({ type: TIMELINE_SIDEBAR_VIEW_TYPE });
workspace.revealLeaf(leaf);
}
} finally {
@ -1851,7 +1862,7 @@ export default class TaskProgressBarPlugin extends Plugin {
// Update Timeline Sidebar Views
const timelineViewLeaves = this.app.workspace.getLeavesOfType(
TIMELINE_SIDEBAR_VIEW_TYPE,
TIMELINE_SIDEBAR_VIEW_TYPE
);
if (timelineViewLeaves.length > 0) {
for (const leaf of timelineViewLeaves) {
@ -1885,7 +1896,7 @@ export default class TaskProgressBarPlugin extends Plugin {
if (!diagnosticInfo.canWrite) {
throw new Error(
"Cannot write to version storage - storage may be corrupted",
"Cannot write to version storage - storage may be corrupted"
);
}
@ -1894,7 +1905,7 @@ export default class TaskProgressBarPlugin extends Plugin {
diagnosticInfo.previousVersion
) {
console.warn(
"Invalid version data detected, attempting recovery",
"Invalid version data detected, attempting recovery"
);
await this.versionManager.recoverFromCorruptedVersion();
}
@ -1905,7 +1916,7 @@ export default class TaskProgressBarPlugin extends Plugin {
if (versionResult.requiresRebuild) {
console.log(
`Task Genius (Dataflow): ${versionResult.rebuildReason}`,
`Task Genius (Dataflow): ${versionResult.rebuildReason}`
);
// Get all supported files for progress tracking
@ -1914,13 +1925,13 @@ export default class TaskProgressBarPlugin extends Plugin {
.filter(
(file) =>
file.extension === "md" ||
file.extension === "canvas",
file.extension === "canvas"
);
// Start rebuild progress tracking
this.rebuildProgressManager.startRebuild(
allFiles.length,
versionResult.rebuildReason,
versionResult.rebuildReason
);
// After dataflow rebuild, refresh habits to keep in sync
@ -1946,20 +1957,20 @@ export default class TaskProgressBarPlugin extends Plugin {
} else {
// No rebuild needed, dataflow already initialized during creation
console.log(
"Task Genius (Dataflow): No rebuild needed, using existing cache",
"Task Genius (Dataflow): No rebuild needed, using existing cache"
);
}
} catch (error) {
console.error(
"Error during dataflow initialization with version check:",
error,
error
);
// Trigger emergency rebuild for dataflow
try {
const emergencyResult =
await this.versionManager.handleEmergencyRebuild(
`Dataflow initialization failed: ${error.message}`,
`Dataflow initialization failed: ${error.message}`
);
// Get all supported files for progress tracking
@ -1968,13 +1979,13 @@ export default class TaskProgressBarPlugin extends Plugin {
.filter(
(file) =>
file.extension === "md" ||
file.extension === "canvas",
file.extension === "canvas"
);
// Start emergency rebuild
this.rebuildProgressManager.startRebuild(
allFiles.length,
emergencyResult.rebuildReason,
emergencyResult.rebuildReason
);
// Force rebuild dataflow
@ -1992,12 +2003,12 @@ export default class TaskProgressBarPlugin extends Plugin {
await this.versionManager.markVersionProcessed();
console.log(
"Emergency dataflow rebuild completed successfully",
"Emergency dataflow rebuild completed successfully"
);
} catch (emergencyError) {
console.error(
"Emergency dataflow rebuild failed:",
emergencyError,
emergencyError
);
throw emergencyError;
}
@ -2012,7 +2023,7 @@ export default class TaskProgressBarPlugin extends Plugin {
private async initializeTaskManagerWithVersionCheck(): Promise<void> {
// This method is deprecated and should not be called
console.warn(
"initializeTaskManagerWithVersionCheck is deprecated and should not be used",
"initializeTaskManagerWithVersionCheck is deprecated and should not be used"
);
return Promise.resolve();
}

View file

@ -17,6 +17,7 @@
border: 1px solid var(--background-modifier-border);
border-radius: 4px;
background: var(--background-primary);
overflow-x: auto;
}
.file-filter-rule:last-child {
@ -247,3 +248,8 @@
background: var(--color-green-rgb);
opacity: 0.8;
}
.file-filter-rule-editor-modal {
max-width: 800px;
width: 90vw;
}

View file

@ -4,45 +4,67 @@
overflow-y: hidden;
}
.component-preview .v2-nav-left {
display: none;
}
.onboarding-view:has(.component-showcase) .onboarding-header {
padding: calc(var(--onboarding-spacing) * 2) var(--onboarding-spacing) var(--size-4-2) var(--onboarding-spacing);
padding: calc(var(--onboarding-spacing) * 2) var(--onboarding-spacing)
var(--size-4-2) var(--onboarding-spacing);
}
.v2-top-navigation.component-preview {
overflow-x: auto;
padding: 0;
}
.v2-sidebar.component-preview {
width: 100%;
}
.component-showcase:has(
.tg-v2-container.component-preview-sidebar
+ .tg-v2-container.component-preview-topnav
)
.tg-v2-container {
background: var(--background-primary);
}
.tg-v2-container.component-preview-sidebar {
width: 200px;
min-width: 200px;
border-right: 1px solid var(--background-modifier-border);
width: 200px;
min-width: 200px;
border-right: 1px solid var(--background-modifier-border);
border-top: unset;
}
.tg-v2-container.component-preview-topnav {
background: var(--background-primary);
}
.component-showcase:has(
.tg-v2-container.component-preview-sidebar
+ .tg-v2-container.component-preview-topnav
)
.tg-v2-container.component-preview-topnav {
border-top: unset;
background: var(--background-primary);
}
/* Component showcase container */
.component-showcase {
display: flex;
gap: var(--size-4-6);
margin: var(--size-4-4) 0;
min-height: 400px;
min-width: 90%;
}
.component-showcase-preview {
flex: 1;
background: var(--background-secondary);
border-radius: var(--radius-m);
padding: var(--size-4-4);
overflow: hidden;
position: relative;
border: 1px solid var(--background-modifier-border);
display: flex;
flex-direction: row;
overflow-x: auto;
flex: 1;
background: var(--background-secondary);
border-radius: var(--radius-m);
padding: var(--size-4-4);
overflow: hidden;
position: relative;
border: 1px solid var(--background-modifier-border);
display: flex;
flex-direction: row;
overflow-x: auto;
}
.component-showcase-description {
@ -231,41 +253,50 @@
font-size: var(--font-ui-small);
}
.component-showcase-preview.tg-v2-container.component-preview-sidebar .v2-sidebar {
width: 100%;
.component-showcase-preview.tg-v2-container.component-preview-sidebar
.v2-sidebar {
width: 100%;
}
/* Preview interactivity (scoped to preview only) */
.component-showcase-preview .component-preview-sidebar .v2-navigation-item,
.component-showcase-preview .component-preview-sidebar .v2-project-item {
pointer-events: auto;
cursor: pointer;
outline: none;
pointer-events: auto;
cursor: pointer;
outline: none;
}
.component-showcase-preview .component-preview-sidebar .v2-navigation-item.is-active,
.component-showcase-preview .component-preview-sidebar .v2-project-item.is-active {
background: var(--background-modifier-hover);
border-radius: var(--radius-s);
.component-showcase-preview
.component-preview-sidebar
.v2-navigation-item.is-active,
.component-showcase-preview
.component-preview-sidebar
.v2-project-item.is-active {
background: var(--background-modifier-hover);
border-radius: var(--radius-s);
}
.component-showcase-preview .component-preview-sidebar .v2-navigation-item:focus-visible,
.component-showcase-preview .component-preview-sidebar .v2-project-item:focus-visible {
box-shadow: 0 0 0 2px var(--interactive-accent);
border-radius: var(--radius-s);
.component-showcase-preview
.component-preview-sidebar
.v2-navigation-item:focus-visible,
.component-showcase-preview
.component-preview-sidebar
.v2-project-item:focus-visible {
box-shadow: 0 0 0 2px var(--interactive-accent);
border-radius: var(--radius-s);
}
/* Focus/Dim mode for step highlights */
.component-showcase-preview.focus-mode .is-dimmed {
opacity: 0.5;
filter: grayscale(15%);
pointer-events: none;
opacity: 0.5;
filter: grayscale(15%);
pointer-events: none;
}
.component-showcase-preview.focus-mode .is-focused {
position: relative;
outline: 1px solid var(--color-accent);
outline-offset: -1px;
sborder-radius: var(--radius-s);
z-index: 1;
position: relative;
outline: 1px solid var(--color-accent);
outline-offset: -1px;
sborder-radius: var(--radius-s);
z-index: 1;
}

View file

@ -15,8 +15,10 @@
/* shadcn design tokens */
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--shadow-md: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
--shadow-lg: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
--shadow-xl: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
--shadow-lg: 0 4px 6px -1px rgb(0 0 0 / 0.1),
0 2px 4px -2px rgb(0 0 0 / 0.1);
--shadow-xl: 0 10px 15px -3px rgb(0 0 0 / 0.1),
0 4px 6px -4px rgb(0 0 0 / 0.1);
}
/* Modal specific styles */
@ -49,7 +51,8 @@
/* Header section */
.onboarding-view .onboarding-header {
padding: calc(var(--onboarding-spacing) * 4) var(--onboarding-spacing) var(--size-4-2) var(--onboarding-spacing);
padding: calc(var(--onboarding-spacing) * 4) var(--onboarding-spacing)
var(--size-4-2) var(--onboarding-spacing);
text-align: center;
}
@ -72,7 +75,7 @@
min-height: 0;
display: flex;
flex-direction: column;
align-items: center
align-items: center;
}
/* Footer section - Modern design */
@ -88,7 +91,7 @@
/* Glass morphism effect for modern look */
.onboarding-view .onboarding-footer::before {
content: '';
content: "";
position: absolute;
top: 0;
left: 0;
@ -139,7 +142,7 @@
/* Ripple effect on button click */
.onboarding-view .onboarding-buttons button::after {
content: '';
content: "";
position: absolute;
top: 50%;
left: 50%;
@ -262,7 +265,8 @@
rgba(var(--interactive-accent-rgb), 0.05) 0%,
var(--background-primary) 100%
);
box-shadow: var(--shadow-md), 0 0 0 3px rgba(var(--interactive-accent-rgb), 0.1);
box-shadow: var(--shadow-md),
0 0 0 3px rgba(var(--interactive-accent-rgb), 0.1);
}
.onboarding-view .settings-check-action-card:hover {
@ -1303,7 +1307,11 @@
}
.mode-fluent .mode-card-preview {
background: linear-gradient(135deg, var(--background-secondary) 0%, var(--background-modifier-hover) 100%);
background: linear-gradient(
135deg,
var(--background-secondary) 0%,
var(--background-modifier-hover) 100%
);
}
.mode-legacy .mode-card-preview {
@ -1497,9 +1505,16 @@
/* 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);
text-align: left;s
text-align: left;
}
.intro-line-4 {
padding-top: calc(var(--onboarding-spacing) * 4);
}
.onboarding-header > .intro-line-4 {
padding-top: 0;
}
/* AI-style streaming characters */
@ -1509,8 +1524,8 @@
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);
filter 0.4s cubic-bezier(0.4, 0, 0.2, 1),
transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
}
@ -1537,10 +1552,12 @@
}
@keyframes cursorBlink {
0%, 49% {
0%,
49% {
opacity: 0.7;
}
50%, 100% {
50%,
100% {
opacity: 0;
}
}
@ -1734,6 +1751,12 @@
margin: var(--size-4-4) 0;
}
.selectable-cards-container.user-level-cards {
display: flex;
flex-direction: row;
gap: var(--size-4-4);
}
.selectable-card {
border: 1px solid var(--background-modifier-border);
border-radius: var(--onboarding-border-radius);

File diff suppressed because one or more lines are too long