mirror of
https://github.com/taskgenius/taskgenius-plugin.git
synced 2026-07-22 06:40:25 +00:00
refactor(onboarding): integrate mode selection into intro step
- Merge mode selection UI directly into intro step after typing animation - Update navigation flow to skip standalone MODE_SELECT step - Improve component previews with accurate V2 UI structure - Fix transition logic for fluent vs legacy mode paths - Add documentation for onboarding flow improvements
This commit is contained in:
parent
6307b018ca
commit
055ba03772
9 changed files with 515 additions and 94 deletions
276
docs/onboarding-intro-fix.md
Normal file
276
docs/onboarding-intro-fix.md
Normal file
|
|
@ -0,0 +1,276 @@
|
|||
# Onboarding Intro Step 修复文档
|
||||
|
||||
## 问题描述
|
||||
|
||||
在引导流程(Onboarding)的 Intro 步骤中,第四条消息(intro-line-4)在显示后立即被清除,用户无法阅读完整内容。
|
||||
|
||||
**问题消息内容:**
|
||||
> "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?"
|
||||
|
||||
## 根本原因
|
||||
|
||||
### 错误的实现逻辑
|
||||
|
||||
在重构后的代码中(commit 8f5daebd),`IntroStep.ts` 的实现逻辑是:
|
||||
|
||||
```typescript
|
||||
// 错误的实现
|
||||
new TypingAnimation(typingContainer, messages, () => {
|
||||
// 打字完成后立即切换步骤
|
||||
footerEl.style.display = "";
|
||||
controller.setStep(OnboardingStep.MODE_SELECT); // ❌ 这里切换步骤
|
||||
});
|
||||
```
|
||||
|
||||
当 `controller.setStep(OnboardingStep.MODE_SELECT)` 被调用时:
|
||||
1. `ModeSelectionStep.render()` 被触发
|
||||
2. 该方法首先调用 `contentEl.empty()` 清空内容
|
||||
3. `intro-line-4` 消息被立即清除
|
||||
4. 用户几乎看不到消息内容
|
||||
|
||||
### 原始的正确逻辑
|
||||
|
||||
在旧版本中(commit 491e2a6),`IntroTyping` 组件的实现是:
|
||||
|
||||
```typescript
|
||||
// 正确的实现
|
||||
this.introTyping.render(this.onboardingContentEl, () => {
|
||||
// 打字完成后,在同一容器中添加模式选择
|
||||
const modeContainer = this.onboardingContentEl.createDiv({
|
||||
cls: "intro-mode-selection-container"
|
||||
});
|
||||
|
||||
this.modeSelection.render(modeContainer, this.state.uiMode, (mode) => {
|
||||
this.state.uiMode = mode;
|
||||
// 用户选择后才显示 footer
|
||||
this.footerEl.style.display = '';
|
||||
this.updateButtonStates();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**关键区别:**
|
||||
- ✅ 不切换步骤,保持在 INTRO 步骤
|
||||
- ✅ 在同一个 `contentEl` 中追加模式选择 UI
|
||||
- ✅ `intro-line-4` 消息保留在页面上
|
||||
- ✅ 用户可以完整阅读消息后再选择模式
|
||||
|
||||
## 解决方案
|
||||
|
||||
### 修改的文件
|
||||
|
||||
1. **`src/components/features/onboarding/steps/IntroStep.ts`**
|
||||
- 修改 `TypingAnimation` 的 `onComplete` 回调
|
||||
- 不再切换到 MODE_SELECT 步骤
|
||||
- 而是在同一容器中渲染模式选择 UI
|
||||
|
||||
2. **`src/components/features/onboarding/steps/ModeSelectionStep.ts`**
|
||||
- 添加新方法 `renderInline()`
|
||||
- 该方法不清空容器,直接在给定容器中渲染
|
||||
- 接受自定义的 `onSelect` 回调
|
||||
|
||||
### 具体实现
|
||||
|
||||
#### IntroStep.ts 修改
|
||||
|
||||
```typescript
|
||||
// Start typing animation
|
||||
new TypingAnimation(typingContainer, messages, () => {
|
||||
// After typing completes, show mode selection in same container
|
||||
const modeContainer = introWrapper.createDiv({
|
||||
cls: "intro-mode-selection-container"
|
||||
});
|
||||
|
||||
// Render mode selection inline (without clearing intro-line-4)
|
||||
ModeSelectionStep.renderInline(
|
||||
modeContainer,
|
||||
controller,
|
||||
(mode: UIMode) => {
|
||||
// User selected a mode, show footer with Next button
|
||||
controller.setUIMode(mode);
|
||||
footerEl.style.display = "";
|
||||
}
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
#### ModeSelectionStep.ts 新增方法
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* 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>[] = [
|
||||
// ... 卡片配置
|
||||
];
|
||||
|
||||
// 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",
|
||||
}
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## 用户体验流程
|
||||
|
||||
修复后的用户体验流程:
|
||||
|
||||
1. **打字动画阶段**
|
||||
- 显示 "Hi,"
|
||||
- 显示 "Thank you for using Task Genius"
|
||||
- 显示长文本,然后淡出前三条消息
|
||||
- 显示 `intro-line-4` 消息(关键消息)
|
||||
|
||||
2. **模式选择阶段**
|
||||
- `intro-line-4` 消息**保留在页面上**
|
||||
- 在消息下方显示模式选择卡片(Fluent vs Legacy)
|
||||
- 用户可以完整阅读消息内容
|
||||
- 用户选择模式后,显示 Next 按钮
|
||||
|
||||
3. **下一步**
|
||||
- 用户点击 Next 按钮
|
||||
- 才清除内容并进入下一个步骤
|
||||
|
||||
## CSS 样式支持
|
||||
|
||||
相关的 CSS 样式已经存在于 `src/styles/onboarding.css`:
|
||||
|
||||
```css
|
||||
/* Mode selection container that appears after intro typing */
|
||||
.intro-mode-selection-container {
|
||||
animation: fadeInFromBottom 0.6s ease-out;
|
||||
animation-fill-mode: both;
|
||||
width: 100%;
|
||||
margin-top: var(--size-4-10);
|
||||
}
|
||||
|
||||
.intro-line-4 {
|
||||
font-size: clamp(1rem, 2vw, 1.4rem);
|
||||
color: var(--text-muted);
|
||||
line-height: 1.8;
|
||||
margin-bottom: var(--size-4-5);
|
||||
}
|
||||
```
|
||||
|
||||
## 测试验证
|
||||
|
||||
构建成功,无错误:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
# ✅ Build successful
|
||||
# dist\main.js 3.7mb
|
||||
# dist\main.css 345.1kb
|
||||
```
|
||||
|
||||
## 导航逻辑修复
|
||||
|
||||
### 问题
|
||||
|
||||
修复 intro 消息显示后,发现了新的导航问题:
|
||||
|
||||
当用户在 INTRO 步骤中选择模式后点击 Next,会经过以下流程:
|
||||
1. INTRO → MODE_SELECT(因为 handleNext 中 INTRO 的下一步是 MODE_SELECT)
|
||||
2. MODE_SELECT 被渲染(但用户已经选择过了)
|
||||
3. 用户再次点击 Next
|
||||
4. MODE_SELECT → FLUENT_COMPONENTS(**跳过了 FLUENT_PLACEMENT**)
|
||||
|
||||
### 解决方案
|
||||
|
||||
修改 `OnboardingController.ts` 中的 `handleNext()` 逻辑:
|
||||
|
||||
```typescript
|
||||
case OnboardingStep.INTRO:
|
||||
// Mode selection is now inline in INTRO step
|
||||
// So we skip MODE_SELECT and go directly based on selected mode
|
||||
if (this.state.uiMode === 'fluent') {
|
||||
nextStep = OnboardingStep.FLUENT_PLACEMENT; // ✅ 正确跳转
|
||||
} else {
|
||||
// Legacy mode: check for existing changes
|
||||
if (this.state.userHasChanges) {
|
||||
nextStep = OnboardingStep.SETTINGS_CHECK;
|
||||
} else {
|
||||
nextStep = OnboardingStep.USER_LEVEL_SELECT;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case OnboardingStep.MODE_SELECT:
|
||||
// This step is now integrated into INTRO, but keep for backward compatibility
|
||||
if (this.state.uiMode === 'fluent') {
|
||||
nextStep = OnboardingStep.FLUENT_PLACEMENT; // ✅ 修复:不再跳过
|
||||
} else {
|
||||
// Legacy mode: check for existing changes
|
||||
if (this.state.userHasChanges) {
|
||||
nextStep = OnboardingStep.SETTINGS_CHECK;
|
||||
} else {
|
||||
nextStep = OnboardingStep.USER_LEVEL_SELECT;
|
||||
}
|
||||
}
|
||||
break;
|
||||
```
|
||||
|
||||
### 修复后的流程
|
||||
|
||||
**Fluent 模式:**
|
||||
1. INTRO(包含模式选择)
|
||||
2. FLUENT_PLACEMENT(选择 Sideleaves 或 Inline)
|
||||
3. FLUENT_COMPONENTS(组件预览)
|
||||
4. SETTINGS_CHECK 或 USER_LEVEL_SELECT(根据是否有更改)
|
||||
|
||||
**Legacy 模式:**
|
||||
1. INTRO(包含模式选择)
|
||||
2. SETTINGS_CHECK(如果有更改)或 USER_LEVEL_SELECT(如果没有更改)
|
||||
|
||||
## 总结
|
||||
|
||||
通过恢复原始的实现逻辑并修复导航逻辑,现在:
|
||||
|
||||
**消息显示:**
|
||||
- ✅ `intro-line-4` 消息完整显示在页面上
|
||||
- ✅ 在模式选择卡片上方保留
|
||||
- ✅ 用户有充足时间阅读
|
||||
- ✅ 只有在点击 Next 后才被清除
|
||||
|
||||
**导航流程:**
|
||||
- ✅ INTRO 步骤包含模式选择(内联)
|
||||
- ✅ 选择 Fluent 后正确进入 FLUENT_PLACEMENT
|
||||
- ✅ 选择 Legacy 后根据配置进入相应步骤
|
||||
- ✅ 不会跳过任何必要的步骤
|
||||
|
||||
这符合原始设计意图,提供了更好的用户体验。
|
||||
|
||||
|
|
@ -132,13 +132,24 @@ export class OnboardingController {
|
|||
// Determine next step based on current step and state
|
||||
switch (currentStep) {
|
||||
case OnboardingStep.INTRO:
|
||||
nextStep = OnboardingStep.MODE_SELECT;
|
||||
// Mode selection is now inline in INTRO step
|
||||
// So we skip MODE_SELECT and go directly based on selected mode
|
||||
if (this.state.uiMode === 'fluent') {
|
||||
nextStep = OnboardingStep.FLUENT_PLACEMENT;
|
||||
} else {
|
||||
// Legacy mode: check for existing changes
|
||||
if (this.state.userHasChanges) {
|
||||
nextStep = OnboardingStep.SETTINGS_CHECK;
|
||||
} else {
|
||||
nextStep = OnboardingStep.USER_LEVEL_SELECT;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case OnboardingStep.MODE_SELECT:
|
||||
// This step is now integrated into INTRO, but keep for backward compatibility
|
||||
if (this.state.uiMode === 'fluent') {
|
||||
// Go directly to Fluent components when Fluent mode is selected
|
||||
nextStep = OnboardingStep.FLUENT_COMPONENTS;
|
||||
nextStep = OnboardingStep.FLUENT_PLACEMENT;
|
||||
} else {
|
||||
// Legacy mode: check for existing changes
|
||||
if (this.state.userHasChanges) {
|
||||
|
|
|
|||
|
|
@ -218,7 +218,7 @@ export class OnboardingView extends ItemView {
|
|||
console.log("handleNext - User has changes:", state.userHasChanges);
|
||||
|
||||
// Special handling for INTRO step - show config check transition
|
||||
if (step === OnboardingStep.INTRO) {
|
||||
if (step === OnboardingStep.INTRO && state.uiMode !== "fluent") {
|
||||
// If user has changes, show checking animation before settings check
|
||||
if (state.userHasChanges) {
|
||||
console.log("Showing config check transition from INTRO");
|
||||
|
|
|
|||
|
|
@ -15,81 +15,119 @@ export class ComponentPreviewFactory {
|
|||
|
||||
// Header with workspace selector
|
||||
const header = sidebar.createDiv({ cls: "v2-sidebar-header" });
|
||||
const workspaceSelector = header.createDiv({ cls: "workspace-selector" });
|
||||
|
||||
const workspaceButton = workspaceSelector.createDiv({ cls: "workspace-button" });
|
||||
const workspaceIcon = workspaceButton.createSpan({ cls: "workspace-icon" });
|
||||
setIcon(workspaceIcon, "layout-dashboard");
|
||||
workspaceButton.createSpan({ text: t("Personal"), cls: "workspace-name" });
|
||||
const chevron = workspaceButton.createSpan({ cls: "workspace-chevron" });
|
||||
setIcon(chevron, "chevron-down");
|
||||
// Workspace selector with correct structure
|
||||
const workspaceSelectorEl = header.createDiv();
|
||||
const workspaceSelector = workspaceSelectorEl.createDiv({ cls: "workspace-selector" });
|
||||
const workspaceButton = workspaceSelector.createDiv({ cls: "workspace-selector-button" });
|
||||
|
||||
const workspaceInfo = workspaceButton.createDiv({ cls: "workspace-info" });
|
||||
const workspaceIcon = workspaceInfo.createDiv({ cls: "workspace-icon" });
|
||||
workspaceIcon.style.backgroundColor = "#3498db";
|
||||
setIcon(workspaceIcon, "layers");
|
||||
|
||||
const workspaceDetails = workspaceInfo.createDiv({ cls: "workspace-details" });
|
||||
const nameContainer = workspaceDetails.createDiv({ cls: "workspace-name-container" });
|
||||
nameContainer.createSpan({ text: t("Personal"), cls: "workspace-name" });
|
||||
workspaceDetails.createDiv({ text: t("Workspace"), cls: "workspace-label" });
|
||||
|
||||
const dropdownIcon = workspaceButton.createDiv({ cls: "workspace-dropdown-icon" });
|
||||
setIcon(dropdownIcon, "chevron-down");
|
||||
|
||||
// New task button
|
||||
const newTaskBtn = header.createDiv({ cls: "new-task-button" });
|
||||
setIcon(newTaskBtn, "plus");
|
||||
newTaskBtn.createSpan({ text: t("New Task") });
|
||||
const newTaskBtn = header.createEl("button", { cls: "v2-new-task-btn", text: t("New Task") });
|
||||
setIcon(newTaskBtn.createDiv({ cls: "v2-new-task-icon" }), "plus");
|
||||
|
||||
// Main navigation area
|
||||
const content = sidebar.createDiv({ cls: "v2-sidebar-content" });
|
||||
|
||||
// Primary navigation section
|
||||
const primarySection = content.createDiv({ cls: "v2-sidebar-section" });
|
||||
const primaryList = primarySection.createDiv({ cls: "v2-navigation-list" });
|
||||
|
||||
// Primary navigation
|
||||
const primaryNav = sidebar.createDiv({ cls: "v2-sidebar-section" });
|
||||
const primaryItems = [
|
||||
{ id: "inbox", label: t("Inbox"), icon: "inbox", count: 5 },
|
||||
{ id: "today", label: t("Today"), icon: "calendar-days", count: 3 },
|
||||
{ id: "upcoming", label: t("Upcoming"), icon: "calendar", count: 8 },
|
||||
{ id: "flagged", label: t("Flagged"), icon: "flag", count: 2 },
|
||||
{ id: "inbox", label: t("Inbox"), icon: "inbox", badge: 5 },
|
||||
{ id: "today", label: t("Today"), icon: "calendar-days", badge: 3 },
|
||||
{ id: "upcoming", label: t("Upcoming"), icon: "calendar", badge: 8 },
|
||||
{ id: "flagged", label: t("Flagged"), icon: "flag", badge: 2 },
|
||||
];
|
||||
|
||||
primaryItems.forEach((item, index) => {
|
||||
const navItem = primaryNav.createDiv({ cls: "v2-nav-item" });
|
||||
const navItem = primaryList.createDiv({
|
||||
cls: "v2-navigation-item",
|
||||
attr: { "data-view-id": item.id }
|
||||
});
|
||||
if (index === 0) navItem.addClass("is-active");
|
||||
|
||||
const icon = navItem.createSpan({ cls: "v2-nav-icon" });
|
||||
const icon = navItem.createDiv({ cls: "v2-navigation-icon" });
|
||||
setIcon(icon, item.icon);
|
||||
navItem.createSpan({ text: item.label, cls: "v2-nav-label" });
|
||||
if (item.count > 0) {
|
||||
navItem.createSpan({ text: item.count.toString(), cls: "v2-nav-count" });
|
||||
navItem.createSpan({ text: item.label, cls: "v2-navigation-label" });
|
||||
if (item.badge && item.badge > 0) {
|
||||
navItem.createDiv({ text: item.badge.toString(), cls: "v2-navigation-badge" });
|
||||
}
|
||||
});
|
||||
|
||||
// Projects section
|
||||
const projectsSection = sidebar.createDiv({ cls: "v2-sidebar-section" });
|
||||
const projectsSection = content.createDiv({ cls: "v2-sidebar-section" });
|
||||
const projectsHeader = projectsSection.createDiv({ cls: "v2-section-header" });
|
||||
projectsHeader.createSpan({ text: t("Projects") });
|
||||
|
||||
const projectsHeaderActions = projectsHeader.createDiv({ cls: "v2-section-actions" });
|
||||
const addIcon = projectsHeaderActions.createSpan({ cls: "clickable-icon" });
|
||||
setIcon(addIcon, "plus");
|
||||
const buttonContainer = projectsHeader.createDiv({ cls: "v2-project-header-buttons" });
|
||||
const treeToggleBtn = buttonContainer.createDiv({
|
||||
cls: "v2-tree-toggle-btn",
|
||||
attr: { "aria-label": t("Toggle tree/list view") }
|
||||
});
|
||||
setIcon(treeToggleBtn, "list");
|
||||
|
||||
const sortProjectBtn = buttonContainer.createDiv({
|
||||
cls: "v2-sort-project-btn",
|
||||
attr: { "aria-label": t("Sort projects") }
|
||||
});
|
||||
setIcon(sortProjectBtn, "arrow-up-down");
|
||||
|
||||
// Project list
|
||||
const projectListEl = projectsSection.createDiv();
|
||||
const projectList = projectListEl.createDiv({ cls: "v2-project-list" });
|
||||
const scrollArea = projectList.createDiv({ cls: "v2-project-scroll" });
|
||||
|
||||
// Mock projects
|
||||
const projects = [
|
||||
{ name: t("Work"), color: "#3b82f6", count: 12 },
|
||||
{ name: t("Personal"), color: "#10b981", count: 5 },
|
||||
{ name: t("Learning"), color: "#f59e0b", count: 3 },
|
||||
{ id: "work", name: t("Work"), color: "#3b82f6", count: 12 },
|
||||
{ id: "personal", name: t("Personal"), color: "#10b981", count: 5 },
|
||||
{ id: "learning", name: t("Learning"), color: "#f59e0b", count: 3 },
|
||||
];
|
||||
|
||||
projects.forEach(project => {
|
||||
const projectItem = projectsSection.createDiv({ cls: "project-item" });
|
||||
const colorDot = projectItem.createSpan({ cls: "project-color" });
|
||||
const projectItem = scrollArea.createDiv({
|
||||
cls: "v2-project-item",
|
||||
attr: { "data-project-id": project.id, "data-level": "0" }
|
||||
});
|
||||
const colorDot = projectItem.createDiv({ cls: "v2-project-color" });
|
||||
colorDot.style.backgroundColor = project.color;
|
||||
projectItem.createSpan({ text: project.name, cls: "project-name" });
|
||||
projectItem.createSpan({ text: project.count.toString(), cls: "project-count" });
|
||||
projectItem.createSpan({ text: project.name, cls: "v2-project-name" });
|
||||
projectItem.createSpan({ text: project.count.toString(), cls: "v2-project-count" });
|
||||
});
|
||||
|
||||
// Other views section
|
||||
const otherSection = sidebar.createDiv({ cls: "v2-sidebar-section" });
|
||||
const otherSection = content.createDiv({ cls: "v2-sidebar-section" });
|
||||
const otherHeader = otherSection.createDiv({ cls: "v2-section-header" });
|
||||
otherHeader.createSpan({ text: t("Other Views") });
|
||||
|
||||
const otherList = otherSection.createDiv({ cls: "v2-navigation-list" });
|
||||
const otherItems = [
|
||||
{ label: t("Calendar"), icon: "calendar" },
|
||||
{ label: t("Gantt"), icon: "git-branch" },
|
||||
{ label: t("Tags"), icon: "tag" },
|
||||
{ id: "calendar", label: t("Calendar"), icon: "calendar" },
|
||||
{ id: "gantt", label: t("Gantt"), icon: "git-branch" },
|
||||
{ id: "tags", label: t("Tags"), icon: "tag" },
|
||||
];
|
||||
|
||||
otherItems.forEach(item => {
|
||||
const navItem = otherSection.createDiv({ cls: "v2-nav-item" });
|
||||
const icon = navItem.createSpan({ cls: "v2-nav-icon" });
|
||||
const navItem = otherList.createDiv({
|
||||
cls: "v2-navigation-item",
|
||||
attr: { "data-view-id": item.id }
|
||||
});
|
||||
const icon = navItem.createDiv({ cls: "v2-navigation-icon" });
|
||||
setIcon(icon, item.icon);
|
||||
navItem.createSpan({ text: item.label, cls: "v2-nav-label" });
|
||||
navItem.createSpan({ text: item.label, cls: "v2-navigation-label" });
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -104,12 +142,15 @@ export class ComponentPreviewFactory {
|
|||
// Left section - Search
|
||||
const leftSection = topNav.createDiv({ cls: "v2-nav-left" });
|
||||
const searchContainer = leftSection.createDiv({ cls: "v2-search-container" });
|
||||
const searchInput = searchContainer.createEl("input", {
|
||||
|
||||
// Match SearchComponent structure
|
||||
const searchInputContainer = searchContainer.createDiv({ cls: "search-input-container" });
|
||||
const searchInput = searchInputContainer.createEl("input", {
|
||||
type: "text",
|
||||
placeholder: t("Search tasks, projects ..."),
|
||||
cls: "search-input",
|
||||
attr: { disabled: "true" },
|
||||
});
|
||||
searchInput.addClass("search-input");
|
||||
|
||||
// Center section - View mode tabs
|
||||
const centerSection = topNav.createDiv({ cls: "v2-nav-center" });
|
||||
|
|
@ -117,31 +158,33 @@ export class ComponentPreviewFactory {
|
|||
|
||||
const modes = [
|
||||
{ id: "list", label: t("List"), icon: "list" },
|
||||
{ id: "kanban", label: t("Kanban"), icon: "columns" },
|
||||
{ id: "tree", label: t("Tree"), icon: "network" },
|
||||
{ id: "kanban", label: t("Kanban"), icon: "layout-grid" },
|
||||
{ id: "tree", label: t("Tree"), icon: "git-branch" },
|
||||
{ id: "calendar", label: t("Calendar"), icon: "calendar" },
|
||||
];
|
||||
|
||||
modes.forEach((mode, index) => {
|
||||
const tab = viewTabs.createDiv({ cls: "v2-view-tab" });
|
||||
const tab = viewTabs.createEl("button", {
|
||||
cls: ["v2-view-tab", "clickable-icon"],
|
||||
attr: { "data-mode": mode.id }
|
||||
});
|
||||
if (index === 0) tab.addClass("is-active");
|
||||
|
||||
const icon = tab.createSpan({ cls: "v2-tab-icon" });
|
||||
const icon = tab.createDiv({ cls: "v2-view-tab-icon" });
|
||||
setIcon(icon, mode.icon);
|
||||
tab.createSpan({ text: mode.label, cls: "v2-tab-label" });
|
||||
tab.createSpan({ text: mode.label });
|
||||
});
|
||||
|
||||
// Right section - Actions
|
||||
// Right section - Notifications and Settings
|
||||
const rightSection = topNav.createDiv({ cls: "v2-nav-right" });
|
||||
|
||||
const filterBtn = rightSection.createDiv({ cls: "v2-nav-action" });
|
||||
setIcon(filterBtn, "filter");
|
||||
filterBtn.createSpan({ text: t("Filter") });
|
||||
// Notification button
|
||||
const notificationBtn = rightSection.createDiv({ cls: "v2-nav-icon-button" });
|
||||
setIcon(notificationBtn, "bell");
|
||||
const badge = notificationBtn.createDiv({ cls: "v2-notification-badge", text: "3" });
|
||||
|
||||
const sortBtn = rightSection.createDiv({ cls: "v2-nav-action" });
|
||||
setIcon(sortBtn, "arrow-up-down");
|
||||
|
||||
const settingsBtn = rightSection.createDiv({ cls: "v2-nav-action" });
|
||||
// Settings button
|
||||
const settingsBtn = rightSection.createDiv({ cls: "v2-nav-icon-button" });
|
||||
setIcon(settingsBtn, "settings");
|
||||
}
|
||||
|
||||
|
|
@ -151,16 +194,31 @@ export class ComponentPreviewFactory {
|
|||
static createContentAreaPreview(container: HTMLElement): void {
|
||||
container.addClass("tg-v2-container", "component-preview-content");
|
||||
|
||||
const content = container.createDiv({ cls: "v2-content-area component-preview" });
|
||||
const content = container.createDiv({ cls: "task-content component-preview" });
|
||||
|
||||
// Content header
|
||||
const header = content.createDiv({ cls: "v2-content-header" });
|
||||
header.createEl("h2", { text: t("Inbox") });
|
||||
const headerStats = header.createDiv({ cls: "v2-content-stats" });
|
||||
headerStats.createSpan({ text: `5 ${t("tasks")}` });
|
||||
const header = content.createDiv({ cls: "content-header" });
|
||||
|
||||
// View title
|
||||
const titleEl = header.createDiv({ cls: "content-title", text: t("Inbox") });
|
||||
|
||||
// Task count
|
||||
const countEl = header.createDiv({ cls: "task-count", text: `5 ${t("tasks")}` });
|
||||
|
||||
// Filter controls
|
||||
const filterEl = header.createDiv({ cls: "content-filter" });
|
||||
const filterInput = filterEl.createEl("input", {
|
||||
cls: "filter-input",
|
||||
attr: { type: "text", placeholder: t("Filter tasks..."), disabled: "true" }
|
||||
});
|
||||
|
||||
// View toggle button
|
||||
const viewToggleBtn = header.createDiv({ cls: "view-toggle-btn" });
|
||||
setIcon(viewToggleBtn, "list");
|
||||
viewToggleBtn.setAttribute("aria-label", t("Toggle list/tree view"));
|
||||
|
||||
// Task list
|
||||
const taskList = content.createDiv({ cls: "v2-task-list" });
|
||||
const taskList = content.createDiv({ cls: "task-list" });
|
||||
|
||||
const mockTasks = [
|
||||
{ title: t("Review project proposal"), priority: "high", project: "Work" },
|
||||
|
|
@ -171,7 +229,7 @@ export class ComponentPreviewFactory {
|
|||
];
|
||||
|
||||
mockTasks.forEach(task => {
|
||||
const taskItem = taskList.createDiv({ cls: "v2-task-item" });
|
||||
const taskItem = taskList.createDiv({ cls: "task-list-item" });
|
||||
|
||||
const checkbox = taskItem.createDiv({ cls: "task-checkbox" });
|
||||
setIcon(checkbox, "circle");
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { t } from "@/translations/helper";
|
||||
import { OnboardingController, OnboardingStep } from "../OnboardingController";
|
||||
import { OnboardingController } from "../OnboardingController";
|
||||
import { TypingAnimation } from "./intro/TypingAnimation";
|
||||
import { TransitionMessage } from "./intro/TransitionMessage";
|
||||
import { ModeSelectionStep, UIMode } from "./ModeSelectionStep";
|
||||
|
||||
/**
|
||||
* Intro Step - Welcome message with typing animation + mode selection
|
||||
|
|
@ -36,11 +37,6 @@ export class IntroStep {
|
|||
cls: "intro-typing",
|
||||
});
|
||||
|
||||
// Create transition message container (initially hidden)
|
||||
const transitionContainer = introWrapper.createDiv({
|
||||
cls: "intro-transition-container",
|
||||
});
|
||||
|
||||
// Define welcome messages with timing from original implementation
|
||||
const messages = [
|
||||
{
|
||||
|
|
@ -76,10 +72,22 @@ export class IntroStep {
|
|||
];
|
||||
|
||||
// Start typing animation
|
||||
const typing = new TypingAnimation(typingContainer, messages, () => {
|
||||
// Reveal footer and transition to proper MODE_SELECT step instead of rendering inline
|
||||
footerEl.style.display = "";
|
||||
controller.setStep(OnboardingStep.MODE_SELECT);
|
||||
new TypingAnimation(typingContainer, messages, () => {
|
||||
// After typing completes, show mode selection in same container
|
||||
const modeContainer = introWrapper.createDiv({
|
||||
cls: "intro-mode-selection-container"
|
||||
});
|
||||
|
||||
// Render mode selection inline (without clearing intro-line-4)
|
||||
ModeSelectionStep.renderInline(
|
||||
modeContainer,
|
||||
controller,
|
||||
(mode: UIMode) => {
|
||||
// User selected a mode, show footer with Next button
|
||||
controller.setUIMode(mode);
|
||||
footerEl.style.display = "";
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -76,6 +76,70 @@ 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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
/* Onboarding Component Previews Styles */
|
||||
|
||||
.onboarding-view:has(.component-showcase) .onboarding-content {
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
/* Component showcase container */
|
||||
.component-showcase {
|
||||
display: flex;
|
||||
|
|
@ -70,7 +74,6 @@
|
|||
/* Scale down previews to fit */
|
||||
.component-preview-sidebar {
|
||||
max-width: 300px;
|
||||
transform: scale(0.85);
|
||||
}
|
||||
|
||||
.component-preview-topnav {
|
||||
|
|
@ -127,7 +130,6 @@
|
|||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: var(--size-4-8);
|
||||
margin-top: var(--size-4-6);
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
|
|
@ -219,3 +221,7 @@
|
|||
color: var(--text-muted);
|
||||
font-size: var(--font-ui-small);
|
||||
}
|
||||
|
||||
.component-showcase-preview.tg-v2-container.component-preview-sidebar .v2-sidebar {
|
||||
width: 100%;
|
||||
}
|
||||
|
|
@ -53,6 +53,11 @@
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
/* Hide onboarding-header if no header content */
|
||||
.onboarding-view .onboarding-header:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.onboarding-view .onboarding-subtitle {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.95em;
|
||||
|
|
@ -1105,13 +1110,6 @@
|
|||
/* Responsive Design */
|
||||
/* ============================== */
|
||||
|
||||
/* Tablet adjustments */
|
||||
@media (max-width: 1024px) {
|
||||
.intro-mode-selection-container {
|
||||
max-width: 700px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.onboarding-view {
|
||||
--dialog-width: 95vw;
|
||||
|
|
@ -1576,7 +1574,6 @@
|
|||
animation: fadeInFromBottom 0.6s ease-out;
|
||||
animation-fill-mode: both;
|
||||
width: 100%;
|
||||
margin-top: var(--size-4-10);
|
||||
}
|
||||
|
||||
@keyframes fadeInFromBottom {
|
||||
|
|
|
|||
19
styles.css
19
styles.css
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue