关于 recovery-app-onboarding
面向康复与健康应用设计者的入门体验技能,强调尊重用户处境、先展示价值并采用非评判性语言。内容涉及分阶段引导、危机资源、隐私说明、无障碍和测试。
主要功能
- 01以同理心设计入门流程
- 02渐进式功能引导
- 03情境化权限请求
- 04危机资源与安全计划入口
- 05无障碍与体验测试清单
适用场景
- 01设计首次使用与功能导览
- 02安排权限请求和隐私说明
- 03加入康复项目选择与安全计划
- 04检查引导流程的无障碍性
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ curiositech/ some_claude_skills --skill recovery-app-onboarding安装后在 Claude Code 中输入/recovery-app-onboarding确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
assets/example_asset.txtreferences/api_reference.mdscripts/example.py详细介绍
康复应用引导体验优化
为康复和健康应用打造富有同理心、有效的引导体验,以尊严和实用价值服务于脆弱人群。
适用场景
✅ 以下场景请使用此技能:
- 首次用户引导流程
- 功能发现和应用导览
- 渐进式信息披露设计
- 权限请求的时机和说明方式
- 欢迎屏幕和价值主张
- 康复计划选择流程
- 危机资源集成
- 隐私和匿名性说明
❌ 以下场景请勿使用:
- 通用移动端响应式设计 → 使用
mobile-ux-optimizer - 营销/转化优化 → 使用
seo-visibility-expert - 原生 iOS/Android 开发 → 使用平台专属技能
- 数据库架构设计 → 使用
supabase-admin
核心原则
1. 同理心优先,功能其次
康复用户往往处于脆弱状态。每项引导决策都必须考虑:
❌ ANTI-PATTERN: "Sign up to track your progress!"
✅ CORRECT: "You're taking a brave step. Let's set up a private space for your journey."
❌ ANTI-PATTERN: Requiring account creation before showing any value
✅ CORRECT: Let users explore core features anonymously, then offer accounts for persistence
2. 先展示价值,再要求承诺
在索取个人信息前,先展示切实的价值:
WRONG ORDER:
1. Create account
2. Enter personal details
3. Grant permissions
4. Finally see the app
RIGHT ORDER:
1. Show immediate value (meeting finder, crisis resources)
2. Demonstrate app benefits
3. Offer optional account for saved data
4. Request permissions contextually
3. 使用不带评判的语言
避免使用引发羞耻感或造成刺激的语言:
❌ "How many days since your last relapse?"
✅ "What's your current sobriety date?"
❌ "You failed to complete..."
✅ "No worries—pick up where you left off"
❌ "Are you an alcoholic or drug addict?"
✅ "Which recovery programs interest you?"
移动端引导 UX(2025 年最佳实践)
渐进式信息披露
将复杂的引导拆分为易于理解的阶段:
// ✅ GOOD: Staged onboarding with clear progress
const ONBOARDING_STAGES = [
{ id: 'welcome', required: false }, // Emotional connection
{ id: 'programs', required: false }, // Personalization
{ id: 'features', required: false }, // Value discovery
{ id: 'preferences', required: false }, // Customization
{ id: 'safety', required: false }, // Crisis setup
];
// Each stage should be:
// - Skippable (never trap users)
// - Under 60 seconds to complete
// - Visually distinct with progress indicators
// - Reversible (can go back)
权限预先说明(授权率提高 28%)
切勿脱离具体情境请求权限:
// ❌ BAD: Immediate system permission dialog
useEffect(() => {
requestLocationPermission();
}, []);
// ✅ GOOD: Contextual priming before system dialog
function MeetingSearchPrimer() {
return (
<div className="p-4 bg-blue-50 rounded-lg mb-4">
<MapPin className="text-blue-500 mb-2" />
<h3>Find Meetings Near You</h3>
<p className="text-sm text-gray-600 mb-3">
To show meetings within walking distance, we need your location.
Your location stays on your device.
</p>
<button onClick={handleEnableLocation} className="btn-primary">
Enable Location
</button>
<button onClick={handleSkip} className="btn-text">
Search by ZIP instead
</button>
</div>
);
}
“持续引导”——不止于首次启动
引导并非一次性事件:
// Feature discovery on first use of each feature
function useFeatureOnboarding(featureId: string) {
const [hasSeenTooltip, setHasSeen] = useLocalStorage(`onboard:${featureId}`, false);
return {
showTooltip: !hasSeenTooltip,
dismissTooltip: () => setHasSeen(true),
};
}
// Example: First time using gratitude journal
function GratitudeJournal() {
const { showTooltip, dismissTooltip } = useFeatureOnboarding('gratitude');
return (
<>
{showTooltip && (
<FeatureTooltip
title="Daily Gratitude"
description="Write 3 things you're grateful for. Research shows this builds resilience."
onDismiss={dismissTooltip}
/>
)}
{/* Journal UI */}
</>
);
}
使用骨架屏加载器,切勿使用旋转加载图标
处于危机中的康复用户需要即时反馈:
// ❌ BAD: Spinner creates anxiety
{isLoading && <Loader2 className="animate-spin" />}
// ✅ GOOD: Skeleton shows structure immediately
{isLoading && <MeetingCardSkeleton count={5} />}
// Skeleton implementation
function MeetingCardSkeleton() {
return (
<div className="p-4 bg-leather-800 rounded-lg animate-pulse">
<div className="h-4 bg-leather-700 rounded w-3/4 mb-2" />
<div className="h-3 bg-leather-700 rounded w-1/2 mb-4" />
<div className="flex gap-2">
<div className="h-6 w-16 bg-leather-700 rounded" />
<div className="h-6 w-16 bg-leather-700 rounded" />
</div>
</div>
);
}
康复应用功能类别
核心功能(优先展示)
- 会议查找器——核心实用功能,立即提供价值
- 危机资源——关乎生命安全,始终可访问
- 安全计划——个人支持网络
互动功能(次优先)
- 每日签到——关注 HALT 状态
- 康复日志——反思与成长
- 感恩练习——积极强化
高级功能(逐步引导发现)
- 戒瘾计数器——里程碑追踪
- 社区论坛——同伴支持
- 康复计划——AI 辅助指导
- 线上会议——虚拟选项
功能卡片模式
interface OnboardingFeature {
id: string;
icon: React.ComponentType;
title: string;
description: string;
highlight: string; // Key benefit (e.g., "24/7 support available")
color: string; // Brand color for visual distinction
category: 'essential' | 'engagement' | 'advanced';
}
const FEATURES: OnboardingFeature[] = [
{
id: 'meetings',
icon: MapPin,
title: 'Find Meetings Near You',
description: 'Search thousands of AA, NA, CMA, SMART, and other recovery meetings.',
highlight: '100,000+ meetings nationwide',
color: 'bg-blue-500',
category: 'essential',
},
{
id: 'crisis',
icon: Phone,
title: 'Crisis Resources',
description: 'One-tap access to crisis hotlines and emergency support.',
highlight: '24/7 support available',
color: 'bg-red-500',
category: 'essential',
},
// ... more features
];
危机资源集成
重要提示:只有 45% 的心理健康应用包含危机资源。这对康复应用而言是不可妥协的要求。
始终可见的危机支持入口
// Crisis resources should be accessible from EVERY screen
function Navigation() {
return (
<nav>
{/* Normal nav items */}
<CrisisButton className="fixed bottom-20 right-4" />
</nav>
);
}
// Crisis button design
function CrisisButton({ className }: { className?: string }) {
return (
<Link
href="/crisis"
className={cn(
'flex items-center justify-center',
'w-14 h-14 rounded-full',
'bg-red-600 hover:bg-red-700',
'shadow-lg shadow-red-600/30',
'text-white',
className
)}
aria-label="Crisis resources"
>
<Phone size={24} />
</Link>
);
}
引导流程中的安全计划步骤
// Include safety planning in onboarding, but make it optional
function SafetyPlanStep() {
return (
<div className="space-y-4">
<h2>Your Safety Network</h2>
<p className="text-gray-600">
Having support contacts ready can make all the difference.
This is optional—you can set this up anytime.
</p>
<EmergencyContactInput
label="Emergency Contact"
placeholder="Someone who can help in a crisis"
/>
<SupportPersonInput
label="Support Person"
placeholder="Sponsor, therapist, or trusted friend"
/>
<div className="flex gap-2 mt-6">
<button className="btn-secondary flex-1" onClick={handleSkip}>
Set Up Later
</button>
<button className="btn-primary flex-1" onClick={handleSave}>
Save Contacts
</button>
</div>
</div>
);
}
康复计划选择
支持多选,兼容不同选择
以不带评判的态度支持多种康复路径:
const RECOVERY_PROGRAMS = [
{ id: 'aa', name: 'Alcoholics Anonymous', short: 'AA', color: 'bg-blue-500' },
{ id: 'na', name: 'Narcotics Anonymous', short: 'NA', color: 'bg-purple-500' },
{ id: 'cma', name: 'Crystal Meth Anonymous', short: 'CMA', color: 'bg-pink-500' },
{ id: 'smart', name: 'SMART Recovery', short: 'SMART', color: 'bg-green-500' },
{ id: 'dharma', name: 'Recovery Dharma', short: 'Dharma', color: 'bg-amber-500' },
{ id: 'ha', name: 'Heroin Anonymous', short: 'HA', color: 'bg-red-500' },
{ id: 'oa', name: 'Overeaters Anonymous', short: 'OA', color: 'bg-teal-500' },
{ id: 'other', name: 'Other/Multiple', short: 'Other', color: 'bg-gray-500' },
];
// UI should allow multiple selections
function ProgramSelection({ selected, onChange }) {
return (
<div className="grid grid-cols-2 gap-3">
{RECOVERY_PROGRAMS.map((program) => (
<ProgramCard
key={program.id}
program={program}
isSelected={selected.includes(program.id)}
onToggle={() => toggleProgram(program.id)}
/>
))}
</div>
);
}
引导流程架构
状态管理
interface OnboardingState {
currentStep: number;
completedSteps: string[];
selectedPrograms: string[];
meetingPreferences: {
formats: ('in-person' | 'online' | 'hybrid')[];
days: string[];
times: ('morning' | 'afternoon' | 'evening')[];
};
safetyContacts: {
emergency?: string;
support?: string;
};
skippedSteps: string[];
}
// Persist across sessions
function useOnboardingState() {
return useLocalStorage<OnboardingState>('onboarding', defaultState);
}
步骤导航
function OnboardingWizard() {
const [state, setState] = useOnboardingState();
const currentStep = STEPS[state.currentStep];
const goNext = () => {
setState(prev => ({
...prev,
currentStep: Math.min(prev.currentStep + 1, STEPS.length - 1),
completedSteps: [...prev.completedSteps, currentStep.id],
}));
};
const goBack = () => {
setState(prev => ({
...prev,
currentStep: Math.max(prev.currentStep - 1, 0),
}));
};
const skip = () => {
setState(prev => ({
...prev,
skippedSteps: [...prev.skippedSteps, currentStep.id],
}));
goNext();
};
return (
<div className="min-h-screen flex flex-col">
<ProgressIndicator
current={state.currentStep}
total={STEPS.length}
completedSteps={state.completedSteps}
/>
<main className="flex-1 p-4">
<CurrentStepComponent {...currentStep} state={state} setState={setState} />
</main>
<footer className="p-4 border-t border-leather-700">
<div className="flex gap-3">
{state.currentStep > 0 && (
<button onClick={goBack} className="btn-secondary flex-1">
Back
</button>
)}
{currentStep.skippable && (
<button onClick={skip} className="btn-text">
Skip
</button>
)}
<button onClick={goNext} className="btn-primary flex-1">
{state.currentStep === STEPS.length - 1 ? 'Get Started' : 'Continue'}
</button>
</div>
</footer>
</div>
);
}
微交互
入场动画
// Staggered entrance for lists
function StaggeredList({ items, renderItem }) {
return (
<div className="space-y-3">
{items.map((item, index) => (
<div
key={item.id}
className="animate-fade-in-up"
style={{ animationDelay: `${index * 100}ms` }}
>
{renderItem(item, index)}
</div>
))}
</div>
);
}
轮播导航
// Swipe gesture support for mobile feature tours
function useSwipeNavigation({ onNext, onPrev, threshold = 50 }) {
const [touchStart, setTouchStart] = useState<number | null>(null);
const [touchEnd, setTouchEnd] = useState<number | null>(null);
const onTouchStart = (e: React.TouchEvent) => {
setTouchEnd(null);
setTouchStart(e.targetTouches[0].clientX);
};
const onTouchMove = (e: React.TouchEvent) => {
setTouchEnd(e.targetTouches[0].clientX);
};
const onTouchEnd = () => {
if (!touchStart || !touchEnd) return;
const distance = touchStart - touchEnd;
if (distance > threshold) onNext();
if (distance < -threshold) onPrev();
};
return { onTouchStart, onTouchMove, onTouchEnd };
}
无障碍要求
符合 WCAG AA 标准
- 对比度:字号小于 18px 的文本为 4.5:1,字号大于或等于 18px 的文本为 3:1
- 触控目标:最小尺寸为 44x44px
- 焦点指示器:所有交互元素都要显示轮廓
- 屏幕阅读器:使用
aria-live播报步骤变化
减少动态效果
@media (prefers-reduced-motion: reduce) {
.animate-fade-in-up,
.animate-slide-in {
animation: none;
opacity: 1;
transform: none;
}
}
测试清单
功能测试
- 所有步骤均可完成
- 所有步骤均可跳过
- 返回导航正常
- 进度可在不同会话间保留
- 移动端滑动手势正常
- 键盘导航正常
无障碍测试
- 屏幕阅读器会播报步骤变化
- 所有交互元素均有标签
- 焦点顺序合理
- 颜色对比度符合 WCAG AA 标准
- 触控目标最小尺寸为 44x44px
性能测试
- 初次加载时间少于 2 秒
- 步骤切换时间少于 300ms
- 动画期间没有布局偏移
- 首次加载后可离线使用
情绪体验测试
- 语言不带评判
- 跳过选项清晰易懂
- 不使用施压手段
- 危机资源清晰可见
- 隐私说明清晰
参考资料
详细指南请参阅 /references/:
competitor-analysis.md- I Am Sober、Nomo、Sober Grid 的设计模式wellness-patterns.md- Headspace、Calm、Daylio 的见解crisis-integration.md- 紧急资源最佳实践
评论
支持 Markdown