feat(tutorial): update tutorial labels and modal titles to improve clarity
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 3m12s
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 3m12s
- Changed "Skip tour" button to "Cancel" in various tutorial overlays. - Updated modal titles from "Confirm Task" to "Confirm Penalty" where applicable. - Modified tutorial step call-to-action labels from "Got it" and "Next" to "Continue" for consistency. - Added tests for new tutorial overlay behavior and button interactions. - Refactored tutorial controller to allow ignoring tutorial enabled state in step visibility checks. - Enhanced user profile tutorial restart functionality with appropriate modal messages. - Updated user profile and task confirmation dialog components to reflect new titles and messages. - Adjusted configuration files for local development environments.
This commit is contained in:
@@ -2,20 +2,20 @@
|
|||||||
"cookies": [
|
"cookies": [
|
||||||
{
|
{
|
||||||
"name": "refresh_token",
|
"name": "refresh_token",
|
||||||
"value": "qYJH5LMH5O4UK92tdAp7R49rzGACFvTMVj29S-Pzygc",
|
"value": "hW7OJbUjHr4XntQRPtefKNhvvctK014R6pzLz_vyJ8g",
|
||||||
"domain": "localhost",
|
"domain": "localhost",
|
||||||
"path": "/api/auth",
|
"path": "/api/auth",
|
||||||
"expires": 1792435594.088715,
|
"expires": 1792774142.289068,
|
||||||
"httpOnly": true,
|
"httpOnly": true,
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"sameSite": "Strict"
|
"sameSite": "Strict"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "access_token",
|
"name": "access_token",
|
||||||
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZS1jY0B0ZXN0LmNvbSIsInVzZXJfaWQiOiI2NmNlMzgzMy0zMGQ3LTRjMTItYjRhMi1iNmIxMjcxYTI1YzEiLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3ODQ2NzAzOTR9.jkyiFTxBQ0oatY6jCAWFG88E8e6mCrDDlN_8I56RMBA",
|
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZS1jY0B0ZXN0LmNvbSIsInVzZXJfaWQiOiI5MjUwNzY1MS00YjlmLTRkMTMtOTgxYS1hMzA2ZThjY2JmNzciLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3ODUwMDg5NDJ9.15hDvKf9u95pMzyeSbrHesFBDGgTwnxV1FD175p72-w",
|
||||||
"domain": "localhost",
|
"domain": "localhost",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"expires": 1784670394.088677,
|
"expires": 1785008942.288088,
|
||||||
"httpOnly": true,
|
"httpOnly": true,
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"sameSite": "Lax"
|
"sameSite": "Lax"
|
||||||
@@ -27,11 +27,11 @@
|
|||||||
"localStorage": [
|
"localStorage": [
|
||||||
{
|
{
|
||||||
"name": "authSyncEvent",
|
"name": "authSyncEvent",
|
||||||
"value": "{\"type\":\"logout\",\"at\":1784659593888}"
|
"value": "{\"type\":\"logout\",\"at\":1784998141942}"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "parentAuth",
|
"name": "parentAuth",
|
||||||
"value": "{\"expiresAt\":1784832394248}"
|
"value": "{\"expiresAt\":1785170942553}"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,20 +2,20 @@
|
|||||||
"cookies": [
|
"cookies": [
|
||||||
{
|
{
|
||||||
"name": "refresh_token",
|
"name": "refresh_token",
|
||||||
"value": "LZA3Bk15HR3io_q8vfyqD3wlRoyV-GJwY-MjWvtUQzg",
|
"value": "ND-PngBFXIuDrD3eLsaPs1mWqwzusUSI6nPw-M3JjJo",
|
||||||
"domain": "localhost",
|
"domain": "localhost",
|
||||||
"path": "/api/auth",
|
"path": "/api/auth",
|
||||||
"expires": 1792435594.060453,
|
"expires": 1792774029.131567,
|
||||||
"httpOnly": true,
|
"httpOnly": true,
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"sameSite": "Strict"
|
"sameSite": "Strict"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "access_token",
|
"name": "access_token",
|
||||||
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZS10dXRvcmlhbEB0ZXN0LmNvbSIsInVzZXJfaWQiOiJjYjQyMzZkNC1kNDRlLTQ0YjMtOTYzYi0xYTUwMWEzZjM2NTMiLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3ODQ2NzAzOTR9.pPd0JYgaAyn4_0YPOlxKoX1B9Ka4Vq2M3CpNcl7EDAU",
|
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZS10dXRvcmlhbEB0ZXN0LmNvbSIsInVzZXJfaWQiOiJkM2E5OWI5MC1kZDdlLTQ5MjQtYTZkZS1hMWU1OTcxMTE3NDciLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3ODUwMDg4Mjl9.ey7BplN-ghoJ8OGMkdOJavVAYVM_bv_xRBrtL02ASjQ",
|
||||||
"domain": "localhost",
|
"domain": "localhost",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"expires": 1784670394.06041,
|
"expires": 1785008829.130873,
|
||||||
"httpOnly": true,
|
"httpOnly": true,
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"sameSite": "Lax"
|
"sameSite": "Lax"
|
||||||
@@ -27,11 +27,11 @@
|
|||||||
"localStorage": [
|
"localStorage": [
|
||||||
{
|
{
|
||||||
"name": "authSyncEvent",
|
"name": "authSyncEvent",
|
||||||
"value": "{\"type\":\"logout\",\"at\":1784659593886}"
|
"value": "{\"type\":\"logout\",\"at\":1784998028750}"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "parentAuth",
|
"name": "parentAuth",
|
||||||
"value": "{\"expiresAt\":1784832394232}"
|
"value": "{\"expiresAt\":1785170829369}"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,20 +2,20 @@
|
|||||||
"cookies": [
|
"cookies": [
|
||||||
{
|
{
|
||||||
"name": "refresh_token",
|
"name": "refresh_token",
|
||||||
"value": "MYMeAyxeWQy7LVHrJsvF-10FozU5Ko_QoEvmV3bHwPc",
|
"value": "PbjRbjt9aqRjxoNjBDVJTuDsEEzgUPoplaNgzhOOwdQ",
|
||||||
"domain": "localhost",
|
"domain": "localhost",
|
||||||
"path": "/api/auth",
|
"path": "/api/auth",
|
||||||
"expires": 1792435592.056481,
|
"expires": 1792774174.76082,
|
||||||
"httpOnly": true,
|
"httpOnly": true,
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"sameSite": "Strict"
|
"sameSite": "Strict"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "access_token",
|
"name": "access_token",
|
||||||
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZUB0ZXN0LmNvbSIsInVzZXJfaWQiOiJiODJhZDU2Mi03MjMwLTRkN2EtODU3NS04N2FhNzE1ZWNhMjQiLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3ODQ2NzAzOTJ9.iEbCtV3csb9AUWoXL4MJ-GLL1yiJDumvYmKr-vyuwBg",
|
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImUyZUB0ZXN0LmNvbSIsInVzZXJfaWQiOiI4YzU1MWFiNi1lNmMxLTQ1MzgtOTEzNS0xNDgwN2M2NjM0OTQiLCJ0b2tlbl92ZXJzaW9uIjowLCJleHAiOjE3ODUwMDg5NzR9._hCAKJRJgq4gcDKppcjkcWf9nX94wjBfa4J94Lh2ZlI",
|
||||||
"domain": "localhost",
|
"domain": "localhost",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"expires": 1784670392.056434,
|
"expires": 1785008974.760293,
|
||||||
"httpOnly": true,
|
"httpOnly": true,
|
||||||
"secure": true,
|
"secure": true,
|
||||||
"sameSite": "Lax"
|
"sameSite": "Lax"
|
||||||
@@ -27,11 +27,11 @@
|
|||||||
"localStorage": [
|
"localStorage": [
|
||||||
{
|
{
|
||||||
"name": "authSyncEvent",
|
"name": "authSyncEvent",
|
||||||
"value": "{\"type\":\"logout\",\"at\":1784659591903}"
|
"value": "{\"type\":\"logout\",\"at\":1784998174529}"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "parentAuth",
|
"name": "parentAuth",
|
||||||
"value": "{\"expiresAt\":1784832392197}"
|
"value": "{\"expiresAt\":1785170974948}"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ test.describe('Create Child', () => {
|
|||||||
// Navigate to parent list and wait for it to fully load before clicking Add Child
|
// Navigate to parent list and wait for it to fully load before clicking Add Child
|
||||||
await gotoParentList(page)
|
await gotoParentList(page)
|
||||||
// Dismiss tutorial overlay if present — it intercepts pointer events
|
// Dismiss tutorial overlay if present — it intercepts pointer events
|
||||||
const skip = page.getByRole('button', { name: 'Skip tour' })
|
const skip = page.getByRole('button', { name: 'Cancel' })
|
||||||
if (await skip.isVisible({ timeout: 1000 }).catch(() => false)) {
|
if (await skip.isVisible({ timeout: 1000 }).catch(() => false)) {
|
||||||
await skip.click()
|
await skip.click()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ test.describe('Parent profile button – temporary parent mode', () => {
|
|||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await page.goto('/parent')
|
await page.goto('/parent')
|
||||||
// Dismiss tutorial overlay if present — it intercepts pointer events
|
// Dismiss tutorial overlay if present — it intercepts pointer events
|
||||||
const skip = page.getByRole('button', { name: 'Skip tour' })
|
const skip = page.getByRole('button', { name: 'Cancel' })
|
||||||
if (await skip.isVisible({ timeout: 1000 }).catch(() => false)) {
|
if (await skip.isVisible({ timeout: 1000 }).catch(() => false)) {
|
||||||
await skip.click()
|
await skip.click()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ test.describe('Parent profile button – permanent parent mode', () => {
|
|||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await page.goto('/parent')
|
await page.goto('/parent')
|
||||||
// Dismiss tutorial overlay if present — it intercepts pointer events
|
// Dismiss tutorial overlay if present — it intercepts pointer events
|
||||||
const skip = page.getByRole('button', { name: 'Skip tour' })
|
const skip = page.getByRole('button', { name: 'Cancel' })
|
||||||
if (await skip.isVisible({ timeout: 1000 }).catch(() => false)) {
|
if (await skip.isVisible({ timeout: 1000 }).catch(() => false)) {
|
||||||
await skip.click()
|
await skip.click()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -244,7 +244,7 @@ test.describe('Dialog help button and titles', () => {
|
|||||||
await card.click()
|
await card.click()
|
||||||
|
|
||||||
await expect(modalBackdrop(page)).toBeVisible({ timeout: 3000 })
|
await expect(modalBackdrop(page)).toBeVisible({ timeout: 3000 })
|
||||||
await expect(page.locator('.modal-title')).toHaveText('Confirm Task')
|
await expect(page.locator('.modal-title')).toHaveText('Confirm Penalty')
|
||||||
await expect(helpButton(page)).not.toBeVisible()
|
await expect(helpButton(page)).not.toBeVisible()
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Cancel' }).click()
|
await page.getByRole('button', { name: 'Cancel' }).click()
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ async function expectTutorialCard(page: Page, title: string): Promise<void> {
|
|||||||
async function dismissTutorial(page: Page): Promise<void> {
|
async function dismissTutorial(page: Page): Promise<void> {
|
||||||
const card = getTutorialCard(page)
|
const card = getTutorialCard(page)
|
||||||
if (await card.isVisible({ timeout: 1000 }).catch(() => false)) {
|
if (await card.isVisible({ timeout: 1000 }).catch(() => false)) {
|
||||||
// Use "Skip tour" to clear the active step and drain the queue in one
|
// Use "Cancel" to clear the active step and drain the queue in one
|
||||||
// action, avoiding chained steps that would keep the card visible.
|
// action, avoiding chained steps that would keep the card visible.
|
||||||
await page.locator('.tutorial-root .btn-skip').click()
|
await page.locator('.tutorial-root .btn-skip').click()
|
||||||
await expect(card).not.toBeVisible({ timeout: 5000 })
|
await expect(card).not.toBeVisible({ timeout: 5000 })
|
||||||
|
|||||||
@@ -34,16 +34,17 @@ vi.mock('../services/pushSubscription', () => ({
|
|||||||
getPushPermissionState: vi.fn().mockReturnValue('default'),
|
getPushPermissionState: vi.fn().mockReturnValue('default'),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
function stubUserProfile() {
|
const mockResetAllProgress = vi.fn().mockResolvedValue(undefined)
|
||||||
return {
|
vi.mock('@/tutorial/controller', () => ({
|
||||||
template: '<div class="user-profile"><slot /></div>',
|
tutorialEnabled: { value: true },
|
||||||
}
|
setTutorialEnabled: vi.fn(),
|
||||||
}
|
resetAllProgress: () => mockResetAllProgress(),
|
||||||
|
}))
|
||||||
|
|
||||||
function stubModalDialog() {
|
function stubModalDialog() {
|
||||||
return {
|
return {
|
||||||
template: '<div class="mock-modal" v-if="show"><slot /></div>',
|
template: '<div class="mock-modal"><h2 v-if="title">{{ title }}</h2><slot /></div>',
|
||||||
props: ['show'],
|
props: ['title'],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -607,3 +608,64 @@ describe('UserProfile - Notification Toggles', () => {
|
|||||||
expect(body.push_notifications_enabled).toBe(true)
|
expect(body.push_notifications_enabled).toBe(true)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('UserProfile - Tutorial restart', () => {
|
||||||
|
let wrapper: VueWrapper<any>
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
mockResetAllProgress.mockClear()
|
||||||
|
;(global.fetch as any).mockClear()
|
||||||
|
;(global.fetch as any).mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
json: async () => ({
|
||||||
|
image_id: null,
|
||||||
|
first_name: 'Test',
|
||||||
|
last_name: 'User',
|
||||||
|
email: 'test@example.com',
|
||||||
|
email_digest_enabled: true,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
wrapper = mount(UserProfile, {
|
||||||
|
global: {
|
||||||
|
plugins: [mockRouter],
|
||||||
|
stubs: {
|
||||||
|
ProfileSection: stubProfileSection(),
|
||||||
|
ImagePicker: stubImagePicker(),
|
||||||
|
ToggleField: stubToggleField(),
|
||||||
|
ModalDialog: stubModalDialog(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('restart confirm modal uses "Tutorial Restart" title and explanatory wording', async () => {
|
||||||
|
await flushPromises()
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
await wrapper.vm.openRestartConfirm()
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
const modal = wrapper.find('.mock-modal')
|
||||||
|
expect(modal.exists()).toBe(true)
|
||||||
|
expect(modal.text()).toContain('Tutorial Restart')
|
||||||
|
expect(modal.text()).toContain('Start the tour again')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('confirming restart resets tutorial progress and shows success modal', async () => {
|
||||||
|
await flushPromises()
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
await wrapper.vm.openRestartConfirm()
|
||||||
|
await wrapper.vm.confirmRestartTutorial()
|
||||||
|
await flushPromises()
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
expect(mockResetAllProgress).toHaveBeenCalledTimes(1)
|
||||||
|
|
||||||
|
const modal = wrapper.find('.mock-modal')
|
||||||
|
expect(modal.text()).toContain('Tutorial Restart')
|
||||||
|
expect(modal.text()).toContain('Tutorial mode has been restarted')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -28,9 +28,11 @@ const props = defineProps<{
|
|||||||
childName?: string
|
childName?: string
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const dialogTitle = computed(() =>
|
const dialogTitle = computed(() => {
|
||||||
props.task?.type === 'kindness' ? 'Confirm Act' : 'Confirm Task',
|
if (props.task?.type === 'kindness') return 'Confirm Act'
|
||||||
)
|
if (props.task?.type === 'penalty') return 'Confirm Penalty'
|
||||||
|
return 'Confirm Task'
|
||||||
|
})
|
||||||
|
|
||||||
defineEmits<{
|
defineEmits<{
|
||||||
confirm: []
|
confirm: []
|
||||||
|
|||||||
@@ -40,4 +40,20 @@ describe('TaskConfirmDialog', () => {
|
|||||||
|
|
||||||
expect(wrapper.findComponent(ModalDialogStub).props('title')).toBe('Confirm Act')
|
expect(wrapper.findComponent(ModalDialogStub).props('title')).toBe('Confirm Act')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('renders "Confirm Penalty" title for penalties', () => {
|
||||||
|
const task: Task = {
|
||||||
|
id: 'task-3',
|
||||||
|
name: 'No Screen Time',
|
||||||
|
type: 'penalty',
|
||||||
|
points: 5,
|
||||||
|
image_id: '',
|
||||||
|
}
|
||||||
|
const wrapper = mount(TaskConfirmDialog, {
|
||||||
|
props: { task, childName: 'Test Child' },
|
||||||
|
global: { stubs: { ModalDialog: ModalDialogStub } },
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(wrapper.findComponent(ModalDialogStub).props('title')).toBe('Confirm Penalty')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -156,7 +156,7 @@
|
|||||||
<!-- Restart confirmation -->
|
<!-- Restart confirmation -->
|
||||||
<ModalDialog
|
<ModalDialog
|
||||||
v-if="showRestartConfirm"
|
v-if="showRestartConfirm"
|
||||||
title="Restart tutorial?"
|
title="Tutorial Restart"
|
||||||
@close="showRestartConfirm = false"
|
@close="showRestartConfirm = false"
|
||||||
>
|
>
|
||||||
<div class="modal-message">
|
<div class="modal-message">
|
||||||
@@ -169,8 +169,12 @@
|
|||||||
</ModalDialog>
|
</ModalDialog>
|
||||||
|
|
||||||
<!-- Restart success -->
|
<!-- Restart success -->
|
||||||
<ModalDialog v-if="showRestartSuccess" title="Tour reset" @close="showRestartSuccess = false">
|
<ModalDialog
|
||||||
<div class="modal-message">Tour reset — here we go!</div>
|
v-if="showRestartSuccess"
|
||||||
|
title="Tutorial Restart"
|
||||||
|
@close="showRestartSuccess = false"
|
||||||
|
>
|
||||||
|
<div class="modal-message">Tutorial mode has been restarted</div>
|
||||||
<div class="modal-actions">
|
<div class="modal-actions">
|
||||||
<button class="btn btn-primary" @click="showRestartSuccess = false">OK</button>
|
<button class="btn btn-primary" @click="showRestartSuccess = false">OK</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { activeStep, maybeShow, tutorialEnabled, clearChainProgress, modalTutorialStepId, helpButtonHidden } from './controller'
|
import { activeStep, maybeShow, clearChainProgress, modalTutorialStepId, helpButtonHidden } from './controller'
|
||||||
|
|
||||||
// Map route names to the tutorial step that should re-fire when the user taps `?`.
|
// Map route names to the tutorial step that should re-fire when the user taps `?`.
|
||||||
// Keep this list lean — only routes that have a tutorial step actually wired.
|
// Keep this list lean — only routes that have a tutorial step actually wired.
|
||||||
@@ -57,7 +57,6 @@ const targetStepId = computed<string | null>(() => {
|
|||||||
|
|
||||||
const visible = computed(() => {
|
const visible = computed(() => {
|
||||||
if (helpButtonHidden.value) return false
|
if (helpButtonHidden.value) return false
|
||||||
if (!tutorialEnabled.value) return false
|
|
||||||
return targetStepId.value !== null
|
return targetStepId.value !== null
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -69,7 +68,7 @@ function onClick() {
|
|||||||
// Temporarily clear local "seen" for the whole chain so every step replays.
|
// Temporarily clear local "seen" for the whole chain so every step replays.
|
||||||
// The server state is left alone; on dismiss `markStepSeen` simply no-ops.
|
// The server state is left alone; on dismiss `markStepSeen` simply no-ops.
|
||||||
clearChainProgress(id)
|
clearChainProgress(id)
|
||||||
maybeShow(id)
|
maybeShow(id, null, true)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
<p :id="bodyId" class="body" aria-live="polite">{{ step.def.body }}</p>
|
<p :id="bodyId" class="body" aria-live="polite">{{ step.def.body }}</p>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button type="button" class="btn btn-skip" @click="onSkip" ref="skipBtn">
|
<button type="button" class="btn btn-skip" @click="onSkip" ref="skipBtn">
|
||||||
Skip tour
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-primary" @click="onPrimary" ref="primaryBtn">
|
<button type="button" class="btn btn-primary" @click="onPrimary" ref="primaryBtn">
|
||||||
{{ primaryLabel }}
|
{{ primaryLabel }}
|
||||||
@@ -66,12 +66,6 @@ const viewport = ref<{ w: number; h: number }>({
|
|||||||
w: typeof window !== 'undefined' ? window.innerWidth : 1024,
|
w: typeof window !== 'undefined' ? window.innerWidth : 1024,
|
||||||
h: typeof window !== 'undefined' ? window.innerHeight : 768,
|
h: typeof window !== 'undefined' ? window.innerHeight : 768,
|
||||||
})
|
})
|
||||||
const reducedMotion = ref(
|
|
||||||
typeof window !== 'undefined' &&
|
|
||||||
typeof window.matchMedia === 'function' &&
|
|
||||||
window.matchMedia('(prefers-reduced-motion: reduce)').matches,
|
|
||||||
)
|
|
||||||
|
|
||||||
const step = computed<ActiveStep | null>(() => activeStep.value)
|
const step = computed<ActiveStep | null>(() => activeStep.value)
|
||||||
const titleId = 'tutorial-title'
|
const titleId = 'tutorial-title'
|
||||||
const bodyId = 'tutorial-body'
|
const bodyId = 'tutorial-body'
|
||||||
@@ -80,9 +74,9 @@ const isMobile = computed(() => viewport.value.w <= MOBILE_BREAKPOINT)
|
|||||||
|
|
||||||
const primaryLabel = computed(() => {
|
const primaryLabel = computed(() => {
|
||||||
const def = step.value?.def
|
const def = step.value?.def
|
||||||
if (!def) return 'Got it'
|
if (!def) return 'Continue'
|
||||||
if (def.ctaLabel) return def.ctaLabel
|
if (def.ctaLabel) return def.ctaLabel
|
||||||
return def.next ? 'Next' : 'Got it'
|
return 'Continue'
|
||||||
})
|
})
|
||||||
|
|
||||||
function resolveAnchor(): HTMLElement | null {
|
function resolveAnchor(): HTMLElement | null {
|
||||||
|
|||||||
@@ -24,6 +24,12 @@ describe('HelpButton', () => {
|
|||||||
expect(wrapper.find('.help-fab').exists()).toBe(true)
|
expect(wrapper.find('.help-fab').exists()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('renders even when tutorial tips are disabled, so the help system stays reachable', () => {
|
||||||
|
tutorialEnabled.value = false
|
||||||
|
const wrapper = mount(HelpButton)
|
||||||
|
expect(wrapper.find('.help-fab').exists()).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
it('hides while helpButtonHidden is true', async () => {
|
it('hides while helpButtonHidden is true', async () => {
|
||||||
const wrapper = mount(HelpButton)
|
const wrapper = mount(HelpButton)
|
||||||
expect(wrapper.find('.help-fab').exists()).toBe(true)
|
expect(wrapper.find('.help-fab').exists()).toBe(true)
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import { nextTick } from 'vue'
|
||||||
|
import TutorialOverlay from '../TutorialOverlay.vue'
|
||||||
|
import * as controller from '../controller'
|
||||||
|
import { stepRegistry, type StepDef } from '../steps'
|
||||||
|
|
||||||
|
const baseStep: StepDef = stepRegistry['setup-parent-pin']!
|
||||||
|
|
||||||
|
describe('TutorialOverlay', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
controller.activeStep.value = null
|
||||||
|
controller.sessionSkipped.value = false
|
||||||
|
global.fetch = vi.fn().mockResolvedValue({ ok: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
controller.activeStep.value = null
|
||||||
|
document.body.innerHTML = ''
|
||||||
|
})
|
||||||
|
|
||||||
|
function mountOverlay() {
|
||||||
|
return mount(TutorialOverlay, { attachTo: document.body })
|
||||||
|
}
|
||||||
|
|
||||||
|
it('renders "Continue" and "Cancel" labels by default', async () => {
|
||||||
|
controller.activeStep.value = {
|
||||||
|
def: baseStep,
|
||||||
|
anchor: null,
|
||||||
|
}
|
||||||
|
const wrapper = mountOverlay()
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
const root = document.body.querySelector('.tutorial-root')
|
||||||
|
expect(root).not.toBeNull()
|
||||||
|
expect(root!.querySelector('.btn-primary')?.textContent?.trim()).toBe('Continue')
|
||||||
|
expect(root!.querySelector('.btn-skip')?.textContent?.trim()).toBe('Cancel')
|
||||||
|
|
||||||
|
wrapper.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('uses a custom CTA label when the step defines one', async () => {
|
||||||
|
controller.activeStep.value = {
|
||||||
|
def: { ...baseStep, ctaLabel: 'Got it' },
|
||||||
|
anchor: null,
|
||||||
|
}
|
||||||
|
const wrapper = mountOverlay()
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
const root = document.body.querySelector('.tutorial-root')
|
||||||
|
expect(root!.querySelector('.btn-primary')?.textContent?.trim()).toBe('Got it')
|
||||||
|
|
||||||
|
wrapper.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calls dismissActive(true) when the primary button is clicked', async () => {
|
||||||
|
const spy = vi.spyOn(controller, 'dismissActive')
|
||||||
|
controller.activeStep.value = {
|
||||||
|
def: baseStep,
|
||||||
|
anchor: null,
|
||||||
|
}
|
||||||
|
const wrapper = mountOverlay()
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
const root = document.body.querySelector('.tutorial-root')
|
||||||
|
await root!.querySelector('.btn-primary')!.dispatchEvent(new Event('click'))
|
||||||
|
|
||||||
|
expect(spy).toHaveBeenCalledWith(true)
|
||||||
|
|
||||||
|
spy.mockRestore()
|
||||||
|
wrapper.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calls skipSession when the Cancel button is clicked', async () => {
|
||||||
|
const spy = vi.spyOn(controller, 'skipSession')
|
||||||
|
controller.activeStep.value = {
|
||||||
|
def: baseStep,
|
||||||
|
anchor: null,
|
||||||
|
}
|
||||||
|
const wrapper = mountOverlay()
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
const root = document.body.querySelector('.tutorial-root')
|
||||||
|
await root!.querySelector('.btn-skip')!.dispatchEvent(new Event('click'))
|
||||||
|
|
||||||
|
expect(spy).toHaveBeenCalled()
|
||||||
|
|
||||||
|
spy.mockRestore()
|
||||||
|
wrapper.unmount()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -79,8 +79,8 @@ export function hydrateFromProfile(profile: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function shouldShowStep(id: string): boolean {
|
export function shouldShowStep(id: string, ignoreEnabled = false): boolean {
|
||||||
if (!tutorialEnabled.value) return false
|
if (!ignoreEnabled && !tutorialEnabled.value) return false
|
||||||
if (sessionSkipped.value) return false
|
if (sessionSkipped.value) return false
|
||||||
if (tutorialProgress.value[id]) return false
|
if (tutorialProgress.value[id]) return false
|
||||||
return true
|
return true
|
||||||
@@ -116,12 +116,16 @@ function promote() {
|
|||||||
* If anchor is provided but the element is missing at promotion time, the step is dropped.
|
* If anchor is provided but the element is missing at promotion time, the step is dropped.
|
||||||
* If anchor is null, the step renders as a centered modal-style card.
|
* If anchor is null, the step renders as a centered modal-style card.
|
||||||
*/
|
*/
|
||||||
export function maybeShow(id: string, anchor: AnchorSource = null) {
|
export function maybeShow(
|
||||||
|
id: string,
|
||||||
|
anchor: AnchorSource = null,
|
||||||
|
force = false,
|
||||||
|
) {
|
||||||
if (!stepRegistry[id]) {
|
if (!stepRegistry[id]) {
|
||||||
if (import.meta.env.DEV) console.warn(`[tutorial] Unknown step id: ${id}`)
|
if (import.meta.env.DEV) console.warn(`[tutorial] Unknown step id: ${id}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!shouldShowStep(id)) return
|
if (!shouldShowStep(id, force)) return
|
||||||
if (activeStep.value?.def.id === id) return
|
if (activeStep.value?.def.id === id) return
|
||||||
if (queue.some((q) => q.def.id === id)) return
|
if (queue.some((q) => q.def.id === id)) return
|
||||||
queue.push({ def: stepRegistry[id], anchor })
|
queue.push({ def: stepRegistry[id], anchor })
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export interface StepDef {
|
|||||||
/** Chain another step to fire after this one is dismissed. */
|
/** Chain another step to fire after this one is dismissed. */
|
||||||
next?: string
|
next?: string
|
||||||
placement?: Placement
|
placement?: Placement
|
||||||
/** Primary button label. Defaults to "Got it" (or "Next" when `next` is set). */
|
/** Primary button label. Defaults to "Continue". */
|
||||||
ctaLabel?: string
|
ctaLabel?: string
|
||||||
/**
|
/**
|
||||||
* Fallback CSS selector used to resolve the anchor element when a step is
|
* Fallback CSS selector used to resolve the anchor element when a step is
|
||||||
@@ -30,14 +30,14 @@ export const stepRegistry: Record<string, StepDef> = {
|
|||||||
title: 'Welcome! Set up your parent PIN',
|
title: 'Welcome! Set up your parent PIN',
|
||||||
body: 'Tap your avatar to set up a 4–6 digit PIN. The PIN keeps parent mode (where you add chores and rewards) safe from little fingers.',
|
body: 'Tap your avatar to set up a 4–6 digit PIN. The PIN keeps parent mode (where you add chores and rewards) safe from little fingers.',
|
||||||
placement: 'below',
|
placement: 'below',
|
||||||
ctaLabel: 'Got it',
|
ctaLabel: 'Continue',
|
||||||
},
|
},
|
||||||
'create-child': {
|
'create-child': {
|
||||||
id: 'create-child',
|
id: 'create-child',
|
||||||
title: 'Add your child',
|
title: 'Add your child',
|
||||||
body: "Let's add your kiddo. Tap the + button to create a profile — you can pick a fun avatar together.",
|
body: "Let's add your kiddo. Tap the + button to create a profile — you can pick a fun avatar together.",
|
||||||
placement: 'above',
|
placement: 'above',
|
||||||
ctaLabel: 'Got it',
|
ctaLabel: 'Continue',
|
||||||
anchorSelector: '.fab',
|
anchorSelector: '.fab',
|
||||||
},
|
},
|
||||||
'parent-children-list': {
|
'parent-children-list': {
|
||||||
@@ -46,7 +46,7 @@ export const stepRegistry: Record<string, StepDef> = {
|
|||||||
body: 'This is your children list. Each card shows one of your kids — their photo, age, and points. Tap + to add a new child.',
|
body: 'This is your children list. Each card shows one of your kids — their photo, age, and points. Tap + to add a new child.',
|
||||||
placement: 'above',
|
placement: 'above',
|
||||||
next: 'child-points',
|
next: 'child-points',
|
||||||
ctaLabel: 'Next',
|
ctaLabel: 'Continue',
|
||||||
anchorSelector: '.fab',
|
anchorSelector: '.fab',
|
||||||
},
|
},
|
||||||
'child-points': {
|
'child-points': {
|
||||||
@@ -55,7 +55,7 @@ export const stepRegistry: Record<string, StepDef> = {
|
|||||||
body: 'This is how many points your child has earned. They spend these on rewards you assign. Tap the child card to see their details and manage assignments.',
|
body: 'This is how many points your child has earned. They spend these on rewards you assign. Tap the child card to see their details and manage assignments.',
|
||||||
placement: 'below',
|
placement: 'below',
|
||||||
next: 'child-click',
|
next: 'child-click',
|
||||||
ctaLabel: 'Next',
|
ctaLabel: 'Continue',
|
||||||
anchorSelector: '.points',
|
anchorSelector: '.points',
|
||||||
},
|
},
|
||||||
'child-click': {
|
'child-click': {
|
||||||
@@ -64,7 +64,7 @@ export const stepRegistry: Record<string, StepDef> = {
|
|||||||
body: 'Tap any child card to open their page. There you can assign chores, kindness acts, rewards, routines, and penalties.',
|
body: 'Tap any child card to open their page. There you can assign chores, kindness acts, rewards, routines, and penalties.',
|
||||||
placement: 'below',
|
placement: 'below',
|
||||||
next: 'child-kebab',
|
next: 'child-kebab',
|
||||||
ctaLabel: 'Next',
|
ctaLabel: 'Continue',
|
||||||
anchorSelector: '.card',
|
anchorSelector: '.card',
|
||||||
},
|
},
|
||||||
'create-chore': {
|
'create-chore': {
|
||||||
@@ -72,7 +72,7 @@ export const stepRegistry: Record<string, StepDef> = {
|
|||||||
title: 'Create your chore',
|
title: 'Create your chore',
|
||||||
body: 'Tap + to add a chore — give it a name, choose points, and pick a picture so your child can spot it easily.',
|
body: 'Tap + to add a chore — give it a name, choose points, and pick a picture so your child can spot it easily.',
|
||||||
placement: 'above',
|
placement: 'above',
|
||||||
ctaLabel: 'Got it',
|
ctaLabel: 'Continue',
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── Create-flow JIT hints ────────────────────────────────────────────────
|
// ── Create-flow JIT hints ────────────────────────────────────────────────
|
||||||
@@ -82,7 +82,7 @@ export const stepRegistry: Record<string, StepDef> = {
|
|||||||
body: 'Give the chore a short, clear name your child will understand — like "Clean your room" or "Feed the dog".',
|
body: 'Give the chore a short, clear name your child will understand — like "Clean your room" or "Feed the dog".',
|
||||||
placement: 'below',
|
placement: 'below',
|
||||||
next: 'edit-chore-points',
|
next: 'edit-chore-points',
|
||||||
ctaLabel: 'Next',
|
ctaLabel: 'Continue',
|
||||||
anchorSelector: 'input#name',
|
anchorSelector: 'input#name',
|
||||||
},
|
},
|
||||||
'edit-chore-points': {
|
'edit-chore-points': {
|
||||||
@@ -91,7 +91,7 @@ export const stepRegistry: Record<string, StepDef> = {
|
|||||||
body: 'How many points is this chore worth? Bigger or harder chores can be worth more. Your child spends these points on rewards.',
|
body: 'How many points is this chore worth? Bigger or harder chores can be worth more. Your child spends these points on rewards.',
|
||||||
placement: 'below',
|
placement: 'below',
|
||||||
next: 'edit-chore-image',
|
next: 'edit-chore-image',
|
||||||
ctaLabel: 'Next',
|
ctaLabel: 'Continue',
|
||||||
anchorSelector: 'input#points',
|
anchorSelector: 'input#points',
|
||||||
},
|
},
|
||||||
'edit-chore-image': {
|
'edit-chore-image': {
|
||||||
@@ -120,7 +120,7 @@ export const stepRegistry: Record<string, StepDef> = {
|
|||||||
body: 'Tap one or more days to schedule this chore or routine. You can choose a single day or several — whatever fits your week.',
|
body: 'Tap one or more days to schedule this chore or routine. You can choose a single day or several — whatever fits your week.',
|
||||||
placement: 'below',
|
placement: 'below',
|
||||||
next: 'schedule-days-deadline',
|
next: 'schedule-days-deadline',
|
||||||
ctaLabel: 'Next',
|
ctaLabel: 'Continue',
|
||||||
anchorSelector: '.day-chips',
|
anchorSelector: '.day-chips',
|
||||||
},
|
},
|
||||||
'schedule-days-deadline': {
|
'schedule-days-deadline': {
|
||||||
@@ -129,7 +129,7 @@ export const stepRegistry: Record<string, StepDef> = {
|
|||||||
body: 'This is the time your child needs to finish by. Tap "Clear" to remove the deadline — then it can be done anytime that day.',
|
body: 'This is the time your child needs to finish by. Tap "Clear" to remove the deadline — then it can be done anytime that day.',
|
||||||
placement: 'auto',
|
placement: 'auto',
|
||||||
next: 'schedule-days-exception',
|
next: 'schedule-days-exception',
|
||||||
ctaLabel: 'Next',
|
ctaLabel: 'Continue',
|
||||||
anchorSelector: '.default-deadline-row',
|
anchorSelector: '.default-deadline-row',
|
||||||
},
|
},
|
||||||
'schedule-days-exception': {
|
'schedule-days-exception': {
|
||||||
@@ -138,7 +138,7 @@ export const stepRegistry: Record<string, StepDef> = {
|
|||||||
body: 'If one day needs a different deadline, tap "Set different time" for that day. Useful for early-dismissal days or busy afternoons.',
|
body: 'If one day needs a different deadline, tap "Set different time" for that day. Useful for early-dismissal days or busy afternoons.',
|
||||||
placement: 'auto',
|
placement: 'auto',
|
||||||
next: 'schedule-enable-toggle',
|
next: 'schedule-enable-toggle',
|
||||||
ctaLabel: 'Next',
|
ctaLabel: 'Continue',
|
||||||
anchorSelector: '.exception-row',
|
anchorSelector: '.exception-row',
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -149,7 +149,7 @@ export const stepRegistry: Record<string, StepDef> = {
|
|||||||
body: 'Set how many days pass between each time this chore or routine appears. Every 1 day means daily; every 7 days means weekly.',
|
body: 'Set how many days pass between each time this chore or routine appears. Every 1 day means daily; every 7 days means weekly.',
|
||||||
placement: 'below',
|
placement: 'below',
|
||||||
next: 'schedule-interval-start',
|
next: 'schedule-interval-start',
|
||||||
ctaLabel: 'Next',
|
ctaLabel: 'Continue',
|
||||||
anchorSelector: '.stepper',
|
anchorSelector: '.stepper',
|
||||||
},
|
},
|
||||||
'schedule-interval-start': {
|
'schedule-interval-start': {
|
||||||
@@ -158,7 +158,7 @@ export const stepRegistry: Record<string, StepDef> = {
|
|||||||
body: 'Pick the first day this chore or routine is expected. The schedule counts forward from here.',
|
body: 'Pick the first day this chore or routine is expected. The schedule counts forward from here.',
|
||||||
placement: 'below',
|
placement: 'below',
|
||||||
next: 'schedule-interval-deadline',
|
next: 'schedule-interval-deadline',
|
||||||
ctaLabel: 'Next',
|
ctaLabel: 'Continue',
|
||||||
anchorSelector: '[data-tutorial="schedule-interval-start"]',
|
anchorSelector: '[data-tutorial="schedule-interval-start"]',
|
||||||
},
|
},
|
||||||
'schedule-interval-deadline': {
|
'schedule-interval-deadline': {
|
||||||
@@ -167,7 +167,7 @@ export const stepRegistry: Record<string, StepDef> = {
|
|||||||
body: 'This is the time your child needs to finish by. Tap "Clear" to remove the deadline — then it can be done anytime that day.',
|
body: 'This is the time your child needs to finish by. Tap "Clear" to remove the deadline — then it can be done anytime that day.',
|
||||||
placement: 'auto',
|
placement: 'auto',
|
||||||
next: 'schedule-enable-toggle',
|
next: 'schedule-enable-toggle',
|
||||||
ctaLabel: 'Next',
|
ctaLabel: 'Continue',
|
||||||
anchorSelector: '.interval-time-row',
|
anchorSelector: '.interval-time-row',
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -187,7 +187,7 @@ export const stepRegistry: Record<string, StepDef> = {
|
|||||||
body: 'Tap the + button to add a new chore. Give it a name, choose points, and pick a picture so your child can spot it easily.',
|
body: 'Tap the + button to add a new chore. Give it a name, choose points, and pick a picture so your child can spot it easily.',
|
||||||
placement: 'above',
|
placement: 'above',
|
||||||
next: 'list-edit-hint',
|
next: 'list-edit-hint',
|
||||||
ctaLabel: 'Next',
|
ctaLabel: 'Continue',
|
||||||
anchorSelector: '.fab',
|
anchorSelector: '.fab',
|
||||||
},
|
},
|
||||||
'list-kindness-help': {
|
'list-kindness-help': {
|
||||||
@@ -196,7 +196,7 @@ export const stepRegistry: Record<string, StepDef> = {
|
|||||||
body: 'Tap the + button to add a new kindness act. Give it a name, choose points, and pick a picture so your child can spot it easily.',
|
body: 'Tap the + button to add a new kindness act. Give it a name, choose points, and pick a picture so your child can spot it easily.',
|
||||||
placement: 'above',
|
placement: 'above',
|
||||||
next: 'list-edit-hint',
|
next: 'list-edit-hint',
|
||||||
ctaLabel: 'Next',
|
ctaLabel: 'Continue',
|
||||||
anchorSelector: '.fab',
|
anchorSelector: '.fab',
|
||||||
},
|
},
|
||||||
'list-penalty-help': {
|
'list-penalty-help': {
|
||||||
@@ -205,7 +205,7 @@ export const stepRegistry: Record<string, StepDef> = {
|
|||||||
body: 'Tap the + button to add a new penalty. Give it a name, choose points, and pick a picture so your child can spot it easily.',
|
body: 'Tap the + button to add a new penalty. Give it a name, choose points, and pick a picture so your child can spot it easily.',
|
||||||
placement: 'above',
|
placement: 'above',
|
||||||
next: 'list-edit-hint',
|
next: 'list-edit-hint',
|
||||||
ctaLabel: 'Next',
|
ctaLabel: 'Continue',
|
||||||
anchorSelector: '.fab',
|
anchorSelector: '.fab',
|
||||||
},
|
},
|
||||||
'list-reward-help': {
|
'list-reward-help': {
|
||||||
@@ -214,7 +214,7 @@ export const stepRegistry: Record<string, StepDef> = {
|
|||||||
body: 'Tap the + button to add a new reward. Give it a name, choose a cost, and pick a picture so your child can spot it easily.',
|
body: 'Tap the + button to add a new reward. Give it a name, choose a cost, and pick a picture so your child can spot it easily.',
|
||||||
placement: 'above',
|
placement: 'above',
|
||||||
next: 'list-edit-hint',
|
next: 'list-edit-hint',
|
||||||
ctaLabel: 'Next',
|
ctaLabel: 'Continue',
|
||||||
anchorSelector: '.fab',
|
anchorSelector: '.fab',
|
||||||
},
|
},
|
||||||
'list-routine-help': {
|
'list-routine-help': {
|
||||||
@@ -223,7 +223,7 @@ export const stepRegistry: Record<string, StepDef> = {
|
|||||||
body: 'Tap the + button to add a new routine. Give it a name, choose points, and pick a picture so your child can spot it easily.',
|
body: 'Tap the + button to add a new routine. Give it a name, choose points, and pick a picture so your child can spot it easily.',
|
||||||
placement: 'above',
|
placement: 'above',
|
||||||
next: 'list-edit-hint',
|
next: 'list-edit-hint',
|
||||||
ctaLabel: 'Next',
|
ctaLabel: 'Continue',
|
||||||
anchorSelector: '.fab',
|
anchorSelector: '.fab',
|
||||||
},
|
},
|
||||||
'list-edit-hint': {
|
'list-edit-hint': {
|
||||||
@@ -232,7 +232,7 @@ export const stepRegistry: Record<string, StepDef> = {
|
|||||||
body: 'Tap any item in the list to open it and make changes — rename it, adjust points, or swap the picture.',
|
body: 'Tap any item in the list to open it and make changes — rename it, adjust points, or swap the picture.',
|
||||||
placement: 'below',
|
placement: 'below',
|
||||||
next: 'list-delete-hint',
|
next: 'list-delete-hint',
|
||||||
ctaLabel: 'Next',
|
ctaLabel: 'Continue',
|
||||||
anchorSelector: '.list-item, .routine-item',
|
anchorSelector: '.list-item, .routine-item',
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -248,7 +248,7 @@ export const stepRegistry: Record<string, StepDef> = {
|
|||||||
body: 'Give it a clear name your child will understand — like "Share your toys" or "Help a friend."',
|
body: 'Give it a clear name your child will understand — like "Share your toys" or "Help a friend."',
|
||||||
placement: 'below',
|
placement: 'below',
|
||||||
next: 'edit-kindness-points',
|
next: 'edit-kindness-points',
|
||||||
ctaLabel: 'Next',
|
ctaLabel: 'Continue',
|
||||||
anchorSelector: 'input#name',
|
anchorSelector: 'input#name',
|
||||||
},
|
},
|
||||||
'edit-kindness-points': {
|
'edit-kindness-points': {
|
||||||
@@ -257,7 +257,7 @@ export const stepRegistry: Record<string, StepDef> = {
|
|||||||
body: 'How many points is this kindness worth? These work just like chore points — your child saves them up for rewards.',
|
body: 'How many points is this kindness worth? These work just like chore points — your child saves them up for rewards.',
|
||||||
placement: 'below',
|
placement: 'below',
|
||||||
next: 'edit-kindness-image',
|
next: 'edit-kindness-image',
|
||||||
ctaLabel: 'Next',
|
ctaLabel: 'Continue',
|
||||||
anchorSelector: 'input#points',
|
anchorSelector: 'input#points',
|
||||||
},
|
},
|
||||||
'edit-kindness-image': {
|
'edit-kindness-image': {
|
||||||
@@ -275,7 +275,7 @@ export const stepRegistry: Record<string, StepDef> = {
|
|||||||
body: 'Give the penalty a clear name your child will understand — like "No screen time" or "Early bedtime."',
|
body: 'Give the penalty a clear name your child will understand — like "No screen time" or "Early bedtime."',
|
||||||
placement: 'below',
|
placement: 'below',
|
||||||
next: 'edit-penalty-points',
|
next: 'edit-penalty-points',
|
||||||
ctaLabel: 'Next',
|
ctaLabel: 'Continue',
|
||||||
anchorSelector: 'input#name',
|
anchorSelector: 'input#name',
|
||||||
},
|
},
|
||||||
'edit-penalty-points': {
|
'edit-penalty-points': {
|
||||||
@@ -284,7 +284,7 @@ export const stepRegistry: Record<string, StepDef> = {
|
|||||||
body: 'How many points does this penalty cost? When the rule is broken, these points are taken away.',
|
body: 'How many points does this penalty cost? When the rule is broken, these points are taken away.',
|
||||||
placement: 'below',
|
placement: 'below',
|
||||||
next: 'edit-penalty-image',
|
next: 'edit-penalty-image',
|
||||||
ctaLabel: 'Next',
|
ctaLabel: 'Continue',
|
||||||
anchorSelector: 'input#points',
|
anchorSelector: 'input#points',
|
||||||
},
|
},
|
||||||
'edit-penalty-image': {
|
'edit-penalty-image': {
|
||||||
@@ -302,7 +302,7 @@ export const stepRegistry: Record<string, StepDef> = {
|
|||||||
body: 'Give the reward a name your child will get excited about — like "Movie night" or "Ice cream treat."',
|
body: 'Give the reward a name your child will get excited about — like "Movie night" or "Ice cream treat."',
|
||||||
placement: 'below',
|
placement: 'below',
|
||||||
next: 'edit-reward-description',
|
next: 'edit-reward-description',
|
||||||
ctaLabel: 'Next',
|
ctaLabel: 'Continue',
|
||||||
anchorSelector: 'input#name',
|
anchorSelector: 'input#name',
|
||||||
},
|
},
|
||||||
'edit-reward-description': {
|
'edit-reward-description': {
|
||||||
@@ -311,7 +311,7 @@ export const stepRegistry: Record<string, StepDef> = {
|
|||||||
body: 'Add a quick description so you remember the details — like "Pick the movie together" or "Any flavor they want."',
|
body: 'Add a quick description so you remember the details — like "Pick the movie together" or "Any flavor they want."',
|
||||||
placement: 'below',
|
placement: 'below',
|
||||||
next: 'edit-reward-cost',
|
next: 'edit-reward-cost',
|
||||||
ctaLabel: 'Next',
|
ctaLabel: 'Continue',
|
||||||
anchorSelector: 'input#description',
|
anchorSelector: 'input#description',
|
||||||
},
|
},
|
||||||
'edit-reward-cost': {
|
'edit-reward-cost': {
|
||||||
@@ -320,7 +320,7 @@ export const stepRegistry: Record<string, StepDef> = {
|
|||||||
body: 'How many points does this reward cost? Make it feel achievable — small rewards can cost less, big ones more.',
|
body: 'How many points does this reward cost? Make it feel achievable — small rewards can cost less, big ones more.',
|
||||||
placement: 'below',
|
placement: 'below',
|
||||||
next: 'edit-reward-image',
|
next: 'edit-reward-image',
|
||||||
ctaLabel: 'Next',
|
ctaLabel: 'Continue',
|
||||||
anchorSelector: 'input#cost',
|
anchorSelector: 'input#cost',
|
||||||
},
|
},
|
||||||
'edit-reward-image': {
|
'edit-reward-image': {
|
||||||
@@ -338,7 +338,7 @@ export const stepRegistry: Record<string, StepDef> = {
|
|||||||
body: "Enter your child's name or nickname — whatever they like to be called.",
|
body: "Enter your child's name or nickname — whatever they like to be called.",
|
||||||
placement: 'below',
|
placement: 'below',
|
||||||
next: 'edit-child-age',
|
next: 'edit-child-age',
|
||||||
ctaLabel: 'Next',
|
ctaLabel: 'Continue',
|
||||||
anchorSelector: 'input#name',
|
anchorSelector: 'input#name',
|
||||||
},
|
},
|
||||||
'edit-child-age': {
|
'edit-child-age': {
|
||||||
@@ -347,7 +347,7 @@ export const stepRegistry: Record<string, StepDef> = {
|
|||||||
body: 'How old is your child? This helps tailor the experience to their level.',
|
body: 'How old is your child? This helps tailor the experience to their level.',
|
||||||
placement: 'below',
|
placement: 'below',
|
||||||
next: 'edit-child-image',
|
next: 'edit-child-image',
|
||||||
ctaLabel: 'Next',
|
ctaLabel: 'Continue',
|
||||||
anchorSelector: 'input#age',
|
anchorSelector: 'input#age',
|
||||||
},
|
},
|
||||||
'edit-child-image': {
|
'edit-child-image': {
|
||||||
@@ -365,7 +365,7 @@ export const stepRegistry: Record<string, StepDef> = {
|
|||||||
body: 'Give the routine a name your child will understand — like "Morning routine" or "Bedtime checklist."',
|
body: 'Give the routine a name your child will understand — like "Morning routine" or "Bedtime checklist."',
|
||||||
placement: 'below',
|
placement: 'below',
|
||||||
next: 'edit-routine-points',
|
next: 'edit-routine-points',
|
||||||
ctaLabel: 'Next',
|
ctaLabel: 'Continue',
|
||||||
anchorSelector: 'input#name',
|
anchorSelector: 'input#name',
|
||||||
},
|
},
|
||||||
'edit-routine-points': {
|
'edit-routine-points': {
|
||||||
@@ -374,7 +374,7 @@ export const stepRegistry: Record<string, StepDef> = {
|
|||||||
body: 'How many points is the whole routine worth? Your child earns these when they finish every step.',
|
body: 'How many points is the whole routine worth? Your child earns these when they finish every step.',
|
||||||
placement: 'below',
|
placement: 'below',
|
||||||
next: 'edit-routine-image',
|
next: 'edit-routine-image',
|
||||||
ctaLabel: 'Next',
|
ctaLabel: 'Continue',
|
||||||
anchorSelector: 'input#points',
|
anchorSelector: 'input#points',
|
||||||
},
|
},
|
||||||
'edit-routine-image': {
|
'edit-routine-image': {
|
||||||
@@ -383,7 +383,7 @@ export const stepRegistry: Record<string, StepDef> = {
|
|||||||
body: 'Choose an image so your child can spot this routine easily. Tap + or the camera to add one.',
|
body: 'Choose an image so your child can spot this routine easily. Tap + or the camera to add one.',
|
||||||
placement: 'above',
|
placement: 'above',
|
||||||
next: 'create-routine-add-task',
|
next: 'create-routine-add-task',
|
||||||
ctaLabel: 'Next',
|
ctaLabel: 'Continue',
|
||||||
anchorSelector: '.picker',
|
anchorSelector: '.picker',
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -405,7 +405,7 @@ export const stepRegistry: Record<string, StepDef> = {
|
|||||||
body: 'Tap to add each step. Order them the way you want your child to do them — they can be reordered later by dragging.',
|
body: 'Tap to add each step. Order them the way you want your child to do them — they can be reordered later by dragging.',
|
||||||
placement: 'above',
|
placement: 'above',
|
||||||
next: 'routine-task-reorder',
|
next: 'routine-task-reorder',
|
||||||
ctaLabel: 'Next',
|
ctaLabel: 'Continue',
|
||||||
anchorSelector: '.add-task-trigger',
|
anchorSelector: '.add-task-trigger',
|
||||||
},
|
},
|
||||||
'routine-task-reorder': {
|
'routine-task-reorder': {
|
||||||
@@ -414,7 +414,7 @@ export const stepRegistry: Record<string, StepDef> = {
|
|||||||
body: 'Drag the handle up or down to change the order your child sees the tasks in.',
|
body: 'Drag the handle up or down to change the order your child sees the tasks in.',
|
||||||
placement: 'auto',
|
placement: 'auto',
|
||||||
next: 'routine-task-edit',
|
next: 'routine-task-edit',
|
||||||
ctaLabel: 'Next',
|
ctaLabel: 'Continue',
|
||||||
anchorSelector: '[data-tutorial="routine-task-reorder"]',
|
anchorSelector: '[data-tutorial="routine-task-reorder"]',
|
||||||
},
|
},
|
||||||
'routine-task-edit': {
|
'routine-task-edit': {
|
||||||
@@ -423,7 +423,7 @@ export const stepRegistry: Record<string, StepDef> = {
|
|||||||
body: 'Tap Edit to change a task name or picture. Useful for fixing typos or updating the look.',
|
body: 'Tap Edit to change a task name or picture. Useful for fixing typos or updating the look.',
|
||||||
placement: 'auto',
|
placement: 'auto',
|
||||||
next: 'routine-task-delete',
|
next: 'routine-task-delete',
|
||||||
ctaLabel: 'Next',
|
ctaLabel: 'Continue',
|
||||||
anchorSelector: '[data-tutorial="routine-task-edit"]',
|
anchorSelector: '[data-tutorial="routine-task-edit"]',
|
||||||
},
|
},
|
||||||
'routine-task-delete': {
|
'routine-task-delete': {
|
||||||
@@ -455,7 +455,7 @@ export const stepRegistry: Record<string, StepDef> = {
|
|||||||
body: 'Here you can assign chores, rewards, and routines, see their points, and review their progress. Scroll down to see each section.',
|
body: 'Here you can assign chores, rewards, and routines, see their points, and review their progress. Scroll down to see each section.',
|
||||||
placement: 'auto',
|
placement: 'auto',
|
||||||
next: 'assign-chore',
|
next: 'assign-chore',
|
||||||
ctaLabel: 'Next',
|
ctaLabel: 'Continue',
|
||||||
},
|
},
|
||||||
'assign-chore': {
|
'assign-chore': {
|
||||||
id: 'assign-chore',
|
id: 'assign-chore',
|
||||||
@@ -463,7 +463,7 @@ export const stepRegistry: Record<string, StepDef> = {
|
|||||||
body: 'Tap to pick which chores this child does. Assigned chores show up in their view. You can give the same chore to multiple kids.',
|
body: 'Tap to pick which chores this child does. Assigned chores show up in their view. You can give the same chore to multiple kids.',
|
||||||
placement: 'above',
|
placement: 'above',
|
||||||
next: 'assign-kindness',
|
next: 'assign-kindness',
|
||||||
ctaLabel: 'Next',
|
ctaLabel: 'Continue',
|
||||||
anchorSelector: '.assign-buttons button:nth-child(1)',
|
anchorSelector: '.assign-buttons button:nth-child(1)',
|
||||||
},
|
},
|
||||||
'assign-kindness': {
|
'assign-kindness': {
|
||||||
@@ -472,7 +472,7 @@ export const stepRegistry: Record<string, StepDef> = {
|
|||||||
body: 'Kindness acts reward thoughtful behavior — sharing, helping, saying please. Tap to pick which ones this child can do.',
|
body: 'Kindness acts reward thoughtful behavior — sharing, helping, saying please. Tap to pick which ones this child can do.',
|
||||||
placement: 'above',
|
placement: 'above',
|
||||||
next: 'assign-reward',
|
next: 'assign-reward',
|
||||||
ctaLabel: 'Next',
|
ctaLabel: 'Continue',
|
||||||
anchorSelector: '.assign-buttons button:nth-child(4)',
|
anchorSelector: '.assign-buttons button:nth-child(4)',
|
||||||
},
|
},
|
||||||
'assign-reward': {
|
'assign-reward': {
|
||||||
@@ -481,7 +481,7 @@ export const stepRegistry: Record<string, StepDef> = {
|
|||||||
body: "Pick which rewards this child can spend points on. They'll only see rewards you assign here.",
|
body: "Pick which rewards this child can spend points on. They'll only see rewards you assign here.",
|
||||||
placement: 'above',
|
placement: 'above',
|
||||||
next: 'assign-routine',
|
next: 'assign-routine',
|
||||||
ctaLabel: 'Next',
|
ctaLabel: 'Continue',
|
||||||
anchorSelector: '.assign-buttons button:nth-child(3)',
|
anchorSelector: '.assign-buttons button:nth-child(3)',
|
||||||
},
|
},
|
||||||
'assign-routine': {
|
'assign-routine': {
|
||||||
@@ -490,7 +490,7 @@ export const stepRegistry: Record<string, StepDef> = {
|
|||||||
body: 'Routines you assign show up on this child’s page as a checklist they can work through, step by step.',
|
body: 'Routines you assign show up on this child’s page as a checklist they can work through, step by step.',
|
||||||
placement: 'above',
|
placement: 'above',
|
||||||
next: 'assign-penalty',
|
next: 'assign-penalty',
|
||||||
ctaLabel: 'Next',
|
ctaLabel: 'Continue',
|
||||||
anchorSelector: '.assign-buttons button:nth-child(2)',
|
anchorSelector: '.assign-buttons button:nth-child(2)',
|
||||||
},
|
},
|
||||||
'assign-penalty': {
|
'assign-penalty': {
|
||||||
@@ -499,7 +499,7 @@ export const stepRegistry: Record<string, StepDef> = {
|
|||||||
body: 'Penalties subtract points when rules are broken. Use them sparingly — they work best alongside lots of positive tasks.',
|
body: 'Penalties subtract points when rules are broken. Use them sparingly — they work best alongside lots of positive tasks.',
|
||||||
placement: 'above',
|
placement: 'above',
|
||||||
next: 'item-kebab-overview',
|
next: 'item-kebab-overview',
|
||||||
ctaLabel: 'Next',
|
ctaLabel: 'Continue',
|
||||||
anchorSelector: '.assign-buttons button:nth-child(5)',
|
anchorSelector: '.assign-buttons button:nth-child(5)',
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -518,7 +518,7 @@ export const stepRegistry: Record<string, StepDef> = {
|
|||||||
body: 'Tap any chore to check or uncheck it. Checked chores are assigned to this child and show up in their view.',
|
body: 'Tap any chore to check or uncheck it. Checked chores are assigned to this child and show up in their view.',
|
||||||
placement: 'below',
|
placement: 'below',
|
||||||
next: 'assign-submit',
|
next: 'assign-submit',
|
||||||
ctaLabel: 'Next',
|
ctaLabel: 'Continue',
|
||||||
anchorSelector: '.list-item, .routine-item',
|
anchorSelector: '.list-item, .routine-item',
|
||||||
},
|
},
|
||||||
'assign-kindness-list': {
|
'assign-kindness-list': {
|
||||||
@@ -527,7 +527,7 @@ export const stepRegistry: Record<string, StepDef> = {
|
|||||||
body: 'Tap any kindness act to check or uncheck it. Checked acts are assigned to this child and show up in their view.',
|
body: 'Tap any kindness act to check or uncheck it. Checked acts are assigned to this child and show up in their view.',
|
||||||
placement: 'below',
|
placement: 'below',
|
||||||
next: 'assign-submit',
|
next: 'assign-submit',
|
||||||
ctaLabel: 'Next',
|
ctaLabel: 'Continue',
|
||||||
anchorSelector: '.list-item, .routine-item',
|
anchorSelector: '.list-item, .routine-item',
|
||||||
},
|
},
|
||||||
'assign-penalty-list': {
|
'assign-penalty-list': {
|
||||||
@@ -536,7 +536,7 @@ export const stepRegistry: Record<string, StepDef> = {
|
|||||||
body: 'Tap any penalty to check or uncheck it. Checked penalties are assigned to this child and show up in their view.',
|
body: 'Tap any penalty to check or uncheck it. Checked penalties are assigned to this child and show up in their view.',
|
||||||
placement: 'below',
|
placement: 'below',
|
||||||
next: 'assign-submit',
|
next: 'assign-submit',
|
||||||
ctaLabel: 'Next',
|
ctaLabel: 'Continue',
|
||||||
anchorSelector: '.list-item, .routine-item',
|
anchorSelector: '.list-item, .routine-item',
|
||||||
},
|
},
|
||||||
'assign-reward-list': {
|
'assign-reward-list': {
|
||||||
@@ -545,7 +545,7 @@ export const stepRegistry: Record<string, StepDef> = {
|
|||||||
body: 'Tap any reward to check or uncheck it. Checked rewards are assigned to this child and show up in their view.',
|
body: 'Tap any reward to check or uncheck it. Checked rewards are assigned to this child and show up in their view.',
|
||||||
placement: 'below',
|
placement: 'below',
|
||||||
next: 'assign-submit',
|
next: 'assign-submit',
|
||||||
ctaLabel: 'Next',
|
ctaLabel: 'Continue',
|
||||||
anchorSelector: '.list-item, .routine-item',
|
anchorSelector: '.list-item, .routine-item',
|
||||||
},
|
},
|
||||||
'assign-routine-list': {
|
'assign-routine-list': {
|
||||||
@@ -554,7 +554,7 @@ export const stepRegistry: Record<string, StepDef> = {
|
|||||||
body: 'Tap any routine to check or uncheck it. Checked routines are assigned to this child and show up in their view.',
|
body: 'Tap any routine to check or uncheck it. Checked routines are assigned to this child and show up in their view.',
|
||||||
placement: 'below',
|
placement: 'below',
|
||||||
next: 'assign-submit',
|
next: 'assign-submit',
|
||||||
ctaLabel: 'Next',
|
ctaLabel: 'Continue',
|
||||||
anchorSelector: '.list-item, .routine-item',
|
anchorSelector: '.list-item, .routine-item',
|
||||||
},
|
},
|
||||||
'assign-submit': {
|
'assign-submit': {
|
||||||
@@ -591,10 +591,10 @@ export const stepRegistry: Record<string, StepDef> = {
|
|||||||
'chore-kebab-menu': {
|
'chore-kebab-menu': {
|
||||||
id: 'chore-kebab-menu',
|
id: 'chore-kebab-menu',
|
||||||
title: 'Chore actions',
|
title: 'Chore actions',
|
||||||
body: 'Edit the points or change the schedule for this chore. Tap Next to see each option.',
|
body: 'Edit the points or change the schedule for this chore. Tap Continue to see each option.',
|
||||||
placement: 'auto',
|
placement: 'auto',
|
||||||
next: 'chore-edit-points',
|
next: 'chore-edit-points',
|
||||||
ctaLabel: 'Next',
|
ctaLabel: 'Continue',
|
||||||
anchorSelector: '.kebab-menu',
|
anchorSelector: '.kebab-menu',
|
||||||
},
|
},
|
||||||
'chore-edit-points': {
|
'chore-edit-points': {
|
||||||
@@ -603,7 +603,7 @@ export const stepRegistry: Record<string, StepDef> = {
|
|||||||
body: 'Change how many points this chore is worth. Higher points for harder chores.',
|
body: 'Change how many points this chore is worth. Higher points for harder chores.',
|
||||||
placement: 'auto',
|
placement: 'auto',
|
||||||
next: 'chore-schedule',
|
next: 'chore-schedule',
|
||||||
ctaLabel: 'Next',
|
ctaLabel: 'Continue',
|
||||||
anchorSelector: '[data-tutorial~="chore-edit-points"]',
|
anchorSelector: '[data-tutorial~="chore-edit-points"]',
|
||||||
},
|
},
|
||||||
'chore-schedule': {
|
'chore-schedule': {
|
||||||
@@ -644,10 +644,10 @@ export const stepRegistry: Record<string, StepDef> = {
|
|||||||
'routine-kebab-menu': {
|
'routine-kebab-menu': {
|
||||||
id: 'routine-kebab-menu',
|
id: 'routine-kebab-menu',
|
||||||
title: 'Routine actions',
|
title: 'Routine actions',
|
||||||
body: 'Edit the routine, adjust points, or change the schedule. Tap Next to see each option.',
|
body: 'Edit the routine, adjust points, or change the schedule. Tap Continue to see each option.',
|
||||||
placement: 'auto',
|
placement: 'auto',
|
||||||
next: 'routine-edit',
|
next: 'routine-edit',
|
||||||
ctaLabel: 'Next',
|
ctaLabel: 'Continue',
|
||||||
anchorSelector: '.kebab-menu',
|
anchorSelector: '.kebab-menu',
|
||||||
},
|
},
|
||||||
'routine-edit': {
|
'routine-edit': {
|
||||||
@@ -656,7 +656,7 @@ export const stepRegistry: Record<string, StepDef> = {
|
|||||||
body: 'Open the routine to add, remove, or reorder the tasks inside it.',
|
body: 'Open the routine to add, remove, or reorder the tasks inside it.',
|
||||||
placement: 'auto',
|
placement: 'auto',
|
||||||
next: 'routine-edit-points',
|
next: 'routine-edit-points',
|
||||||
ctaLabel: 'Next',
|
ctaLabel: 'Continue',
|
||||||
anchorSelector: '[data-tutorial="routine-edit"]',
|
anchorSelector: '[data-tutorial="routine-edit"]',
|
||||||
},
|
},
|
||||||
'routine-edit-points': {
|
'routine-edit-points': {
|
||||||
@@ -665,7 +665,7 @@ export const stepRegistry: Record<string, StepDef> = {
|
|||||||
body: 'Change how many points the whole routine is worth. Your child earns these when they finish every step.',
|
body: 'Change how many points the whole routine is worth. Your child earns these when they finish every step.',
|
||||||
placement: 'auto',
|
placement: 'auto',
|
||||||
next: 'routine-schedule',
|
next: 'routine-schedule',
|
||||||
ctaLabel: 'Next',
|
ctaLabel: 'Continue',
|
||||||
anchorSelector: '[data-tutorial~="routine-edit-points"]',
|
anchorSelector: '[data-tutorial~="routine-edit-points"]',
|
||||||
},
|
},
|
||||||
'routine-schedule': {
|
'routine-schedule': {
|
||||||
|
|||||||
+7
-12
@@ -5,20 +5,15 @@
|
|||||||
"enabled": true,
|
"enabled": true,
|
||||||
"type": "local",
|
"type": "local",
|
||||||
"command": [
|
"command": [
|
||||||
"docker",
|
"ssh",
|
||||||
"run",
|
"-T",
|
||||||
"-i",
|
"devserver.lan",
|
||||||
"--rm",
|
"/home/ryan/gitea-mcp-wrapper.sh"
|
||||||
"-e",
|
|
||||||
"GITEA_ACCESS_TOKEN",
|
|
||||||
"-e",
|
|
||||||
"GITEA_HOST",
|
|
||||||
"docker.gitea.com/gitea-mcp-server"
|
|
||||||
],
|
],
|
||||||
"environment": {
|
"environment": {
|
||||||
"GITEA_HOST": "https://git.ryankegel.com",
|
"GITEA_HOST": "https://git.ryankegel.com"
|
||||||
"GITEA_ACCESS_TOKEN": "{env:GITEA_ACCESS_TOKEN}"
|
},
|
||||||
}
|
"timeout": 30000
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://opencode.ai/config.json",
|
||||||
|
"mcp": {
|
||||||
|
"gitea-mcp": {
|
||||||
|
"enabled": true,
|
||||||
|
"type": "local",
|
||||||
|
"command": [
|
||||||
|
"docker",
|
||||||
|
"run",
|
||||||
|
"-i",
|
||||||
|
"--rm",
|
||||||
|
"-e",
|
||||||
|
"GITEA_ACCESS_TOKEN",
|
||||||
|
"-e",
|
||||||
|
"GITEA_HOST",
|
||||||
|
"docker.gitea.com/gitea-mcp-server"
|
||||||
|
],
|
||||||
|
"environment": {
|
||||||
|
"GITEA_HOST": "https://git.ryankegel.com",
|
||||||
|
"GITEA_ACCESS_TOKEN": "{env:GITEA_ACCESS_TOKEN}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://opencode.ai/config.json",
|
||||||
|
"mcp": {
|
||||||
|
"gitea-mcp": {
|
||||||
|
"enabled": true,
|
||||||
|
"type": "local",
|
||||||
|
"command": [
|
||||||
|
"ssh",
|
||||||
|
"-T",
|
||||||
|
"devserver.lan",
|
||||||
|
"/home/ryan/gitea-mcp-wrapper.sh"
|
||||||
|
],
|
||||||
|
"environment": {
|
||||||
|
"GITEA_HOST": "https://git.ryankegel.com"
|
||||||
|
},
|
||||||
|
"timeout": 30000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Detect OS and copy the correct opencode.json config."""
|
||||||
|
|
||||||
|
import platform
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
if platform.system() == "Darwin":
|
||||||
|
src = "opencode.mac.json"
|
||||||
|
else:
|
||||||
|
src = "opencode.windows.json"
|
||||||
|
|
||||||
|
shutil.copy(src, "opencode.json")
|
||||||
|
print(f"Copied {src} -> opencode.json")
|
||||||
Reference in New Issue
Block a user