mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-23 07:26:30 +00:00
feat(help): in-app help center with per-screen guides and pictures
A help icon on every screen opens a panel with the screen's overview, its sub-screens, task guides with a framed picture per step, search across guides and the wiki, and a switch to any other screen's help. Guides exist for the dashboard, Vacay, Atlas, Collections, Journey (list, journal, studio), Settings (all tabs), Admin (all tabs) and the trip frame; the trip's own screens are in progress. Pictures are produced by Playwright (client/e2e/help), texts in all 23 languages.
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
import type { HelpContext, HelpGuide } from '../types'
|
||||
|
||||
/**
|
||||
* Help for the admin area (`/admin`): one route with tabs, so every tab is a
|
||||
* screen under `admin`, anchored by the page for the tab that is open. The
|
||||
* step actions that produce the pictures live in `e2e/help/admin.guide.ts`.
|
||||
*/
|
||||
|
||||
const OVERVIEW = 'Admin-Panel-Overview'
|
||||
const USERS = 'Admin-Users-and-Invites'
|
||||
const ADDONS = 'Admin-Addons'
|
||||
const CATEGORIES = 'Admin-Categories'
|
||||
const PLUGINS = 'Admin-Plugins'
|
||||
const STORAGE = 'Admin-Storage'
|
||||
const NOTIFICATIONS = 'Notifications'
|
||||
const BACKUP = 'Backups'
|
||||
const MCP = 'Admin-MCP-Tokens'
|
||||
const TEMPLATES = 'Admin-Packing-Templates'
|
||||
const GITHUB = 'Admin-GitHub-Releases'
|
||||
|
||||
const guide = (
|
||||
id: string, context: string, icon: HelpGuide['icon'], size: HelpGuide['size'], steps: number, tips: number,
|
||||
docs: HelpGuide['docs'], related: string[], result = false,
|
||||
): HelpGuide => ({ id, context, icon, size, steps, tips, media: { steps: true, result }, docs, related })
|
||||
|
||||
export const adminGuides: HelpGuide[] = [
|
||||
// Users
|
||||
guide('create-user', 'admin-users', 'userPlus', 'guide', 3, 2, { slug: USERS }, ['edit-user', 'invite-links'], true),
|
||||
guide('edit-user', 'admin-users', 'pencil', 'quick', 2, 2, { slug: USERS }, ['create-user', 'delete-user']),
|
||||
guide('invite-links', 'admin-users', 'link', 'guide', 3, 2, { slug: USERS }, ['create-user'], true),
|
||||
guide('delete-user', 'admin-users', 'trash', 'quick', 1, 2, { slug: USERS }, ['edit-user']),
|
||||
guide('permissions', 'admin-users', 'sliders', 'quick', 2, 2, { slug: 'Admin-Permissions' }, ['edit-user']),
|
||||
// User Defaults
|
||||
guide('default-map', 'admin-defaults', 'map', 'quick', 2, 2, { slug: OVERVIEW }, ['instance-keys']),
|
||||
// Personalization
|
||||
guide('packing-templates', 'admin-config', 'library', 'guide', 3, 2, { slug: TEMPLATES }, ['categories'], true),
|
||||
guide('categories', 'admin-config', 'tags', 'quick', 2, 2, { slug: CATEGORIES }, ['packing-templates']),
|
||||
// Related across screens: the Vacay guide that switches these holidays on.
|
||||
guide('school-holiday-catalog', 'admin-config', 'graduationCap', 'quick', 2, 1, { slug: 'Vacay', anchor: 'manually-maintained-school-holidays' }, ['school-holidays', 'categories']),
|
||||
// Settings
|
||||
guide('auth-methods', 'admin-settings', 'lock', 'guide', 3, 2, { slug: OVERVIEW }, ['oidc']),
|
||||
guide('oidc', 'admin-settings', 'shield', 'guide', 2, 2, { slug: 'OIDC-SSO' }, ['auth-methods']),
|
||||
guide('instance-keys', 'admin-settings', 'key', 'guide', 3, 2, { slug: 'Places-and-Search' }, ['places-transit', 'default-map']),
|
||||
guide('places-transit', 'admin-settings', 'search', 'quick', 2, 2, { slug: 'Places-and-Search' }, ['instance-keys']),
|
||||
guide('file-types', 'admin-settings', 'fileInput', 'quick', 1, 1, { slug: 'Documents-and-Files' }, ['storage-backends']),
|
||||
// Addons
|
||||
guide('toggle-addon', 'admin-addons', 'sliders', 'quick', 2, 2, { slug: ADDONS }, ['install-plugin']),
|
||||
// Plugins
|
||||
guide('install-plugin', 'admin-plugins', 'plug', 'guide', 3, 2, { slug: PLUGINS }, ['toggle-addon']),
|
||||
// Storage
|
||||
guide('storage-backends', 'admin-storage', 'download', 'guide', 3, 2, { slug: STORAGE }, ['create-backup']),
|
||||
// Notifications
|
||||
guide('channels-instance', 'admin-notifications', 'bell', 'guide', 3, 2, { slug: NOTIFICATIONS, anchor: 'notification-channels' }, ['admin-channels']),
|
||||
guide('admin-channels', 'admin-notifications', 'bell', 'quick', 2, 1, { slug: NOTIFICATIONS }, ['channels-instance']),
|
||||
// MCP Access
|
||||
guide('mcp-tokens-admin', 'admin-mcp-tokens', 'key', 'quick', 2, 2, { slug: MCP }, ['auth-methods']),
|
||||
// GitHub
|
||||
guide('release-history', 'admin-github', 'bookOpen', 'quick', 2, 2, { slug: GITHUB }, ['create-backup']),
|
||||
// Backup
|
||||
guide('create-backup', 'admin-backup', 'archive', 'guide', 3, 2, { slug: BACKUP }, ['auto-backup', 'storage-backends'], true),
|
||||
guide('auto-backup', 'admin-backup', 'repeat', 'quick', 2, 2, { slug: BACKUP }, ['create-backup']),
|
||||
// Audit
|
||||
guide('audit-log', 'admin-audit', 'search', 'quick', 2, 2, { slug: 'Audit-Log' }, ['edit-user']),
|
||||
]
|
||||
|
||||
const forContext = (id: string) => adminGuides.filter(g => g.context === id).map(g => g.id)
|
||||
|
||||
export const adminContext: HelpContext = {
|
||||
id: 'admin',
|
||||
route: '/admin',
|
||||
icon: 'shield',
|
||||
bullets: 5,
|
||||
guides: [],
|
||||
docs: [{ slug: OVERVIEW }, { slug: USERS }, { slug: ADDONS }],
|
||||
hero: true,
|
||||
}
|
||||
|
||||
const tab = (id: string, icon: HelpContext['icon'], bullets: number, docs: HelpContext['docs']): HelpContext => ({
|
||||
id, parent: 'admin', route: `/admin?tab=${id.slice('admin-'.length)}`, icon, bullets, guides: forContext(id), docs, hero: true,
|
||||
})
|
||||
|
||||
export const adminTabContexts: HelpContext[] = [
|
||||
tab('admin-users', 'user', 4, [{ slug: USERS }, { slug: 'Admin-Permissions' }]),
|
||||
tab('admin-defaults', 'map', 2, [{ slug: OVERVIEW }]),
|
||||
tab('admin-config', 'sliders', 3, [{ slug: CATEGORIES }, { slug: TEMPLATES }]),
|
||||
tab('admin-settings', 'lock', 4, [{ slug: OVERVIEW }, { slug: 'OIDC-SSO' }, { slug: 'Places-and-Search' }]),
|
||||
tab('admin-addons', 'plug', 3, [{ slug: ADDONS }]),
|
||||
tab('admin-plugins', 'plug', 3, [{ slug: PLUGINS }]),
|
||||
tab('admin-storage', 'download', 3, [{ slug: STORAGE }]),
|
||||
tab('admin-notifications', 'bell', 3, [{ slug: NOTIFICATIONS }]),
|
||||
tab('admin-mcp-tokens', 'key', 2, [{ slug: MCP }]),
|
||||
tab('admin-github', 'bookOpen', 2, [{ slug: GITHUB }, { slug: 'Updating' }]),
|
||||
tab('admin-backup', 'archive', 3, [{ slug: BACKUP }]),
|
||||
tab('admin-audit', 'search', 2, [{ slug: 'Audit-Log' }]),
|
||||
]
|
||||
@@ -0,0 +1,121 @@
|
||||
import type { HelpContext, HelpGuide } from '../types'
|
||||
|
||||
/**
|
||||
* Help for the Atlas addon (`/atlas`): the world map of visited countries,
|
||||
* regions and the bucket list. The step actions that produce the pictures live
|
||||
* in `e2e/help/atlas.guide.ts`, keyed by the same ids.
|
||||
*/
|
||||
|
||||
const ATLAS = 'Atlas'
|
||||
|
||||
export const atlasGuides: HelpGuide[] = [
|
||||
{
|
||||
id: 'mark-country',
|
||||
context: 'atlas',
|
||||
icon: 'mapPin',
|
||||
size: 'guide',
|
||||
steps: 3,
|
||||
tips: 2,
|
||||
media: { steps: true, result: true },
|
||||
docs: { slug: ATLAS, anchor: 'marking-countries-as-visited' },
|
||||
related: ['unmark-country', 'regions', 'bucket-country'],
|
||||
},
|
||||
{
|
||||
id: 'unmark-country',
|
||||
context: 'atlas',
|
||||
icon: 'minusCircle',
|
||||
size: 'quick',
|
||||
steps: 2,
|
||||
tips: 1,
|
||||
media: { steps: true, result: false },
|
||||
docs: { slug: ATLAS, anchor: 'marking-countries-as-visited' },
|
||||
related: ['mark-country'],
|
||||
},
|
||||
{
|
||||
id: 'country-details',
|
||||
context: 'atlas',
|
||||
icon: 'search',
|
||||
size: 'quick',
|
||||
steps: 2,
|
||||
tips: 1,
|
||||
media: { steps: true, result: true },
|
||||
docs: { slug: ATLAS, anchor: 'statistics' },
|
||||
related: ['stats', 'planned-countries'],
|
||||
},
|
||||
{
|
||||
id: 'planned-countries',
|
||||
context: 'atlas',
|
||||
icon: 'eye',
|
||||
size: 'quick',
|
||||
steps: 2,
|
||||
tips: 2,
|
||||
media: { steps: true, result: true },
|
||||
docs: { slug: ATLAS, anchor: 'visited-and-planned-countries' },
|
||||
related: ['country-details', 'mark-country'],
|
||||
},
|
||||
{
|
||||
id: 'regions',
|
||||
context: 'atlas',
|
||||
icon: 'regions',
|
||||
size: 'guide',
|
||||
steps: 3,
|
||||
tips: 2,
|
||||
media: { steps: true, result: true },
|
||||
docs: { slug: ATLAS, anchor: 'sub-national-regions' },
|
||||
related: ['search-place', 'mark-country'],
|
||||
},
|
||||
{
|
||||
id: 'search-place',
|
||||
context: 'atlas',
|
||||
icon: 'landmark',
|
||||
size: 'guide',
|
||||
steps: 3,
|
||||
tips: 1,
|
||||
media: { steps: true, result: true },
|
||||
docs: { slug: ATLAS, anchor: 'marking-countries-as-visited' },
|
||||
related: ['regions', 'bucket-place'],
|
||||
},
|
||||
{
|
||||
id: 'bucket-country',
|
||||
context: 'atlas',
|
||||
icon: 'star',
|
||||
size: 'guide',
|
||||
steps: 3,
|
||||
tips: 2,
|
||||
media: { steps: true, result: true },
|
||||
docs: { slug: ATLAS, anchor: 'bucket-list' },
|
||||
related: ['bucket-place', 'mark-country'],
|
||||
},
|
||||
{
|
||||
id: 'bucket-place',
|
||||
context: 'atlas',
|
||||
icon: 'plus',
|
||||
size: 'guide',
|
||||
steps: 4,
|
||||
tips: 1,
|
||||
media: { steps: true, result: true },
|
||||
docs: { slug: ATLAS, anchor: 'bucket-list' },
|
||||
related: ['bucket-country'],
|
||||
},
|
||||
{
|
||||
id: 'stats',
|
||||
context: 'atlas',
|
||||
icon: 'chart',
|
||||
size: 'quick',
|
||||
steps: 2,
|
||||
tips: 2,
|
||||
media: { steps: true, result: false },
|
||||
docs: { slug: ATLAS, anchor: 'statistics' },
|
||||
related: ['country-details'],
|
||||
},
|
||||
]
|
||||
|
||||
export const atlasContext: HelpContext = {
|
||||
id: 'atlas',
|
||||
route: '/atlas',
|
||||
icon: 'globe',
|
||||
bullets: 6,
|
||||
guides: atlasGuides.map(g => g.id),
|
||||
docs: [{ slug: ATLAS }, { slug: 'Dawarich' }, { slug: 'Admin-Addons' }],
|
||||
hero: true,
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
import type { HelpContext, HelpGuide } from '../types'
|
||||
|
||||
/**
|
||||
* Help for the Collections addon (`/collections`): the place library outside
|
||||
* of any trip. The step actions that produce the pictures live in
|
||||
* `e2e/help/collections.guide.ts`, keyed by the same ids.
|
||||
*/
|
||||
|
||||
const COLLECTIONS = 'Collections'
|
||||
|
||||
export const collectionsGuides: HelpGuide[] = [
|
||||
{
|
||||
id: 'create-list',
|
||||
context: 'collections',
|
||||
icon: 'plus',
|
||||
size: 'guide',
|
||||
steps: 3,
|
||||
tips: 1,
|
||||
media: { steps: true, result: true },
|
||||
docs: { slug: COLLECTIONS, anchor: 'lists' },
|
||||
related: ['add-place', 'edit-list', 'import-from-trip'],
|
||||
},
|
||||
{
|
||||
id: 'add-place',
|
||||
context: 'collections',
|
||||
icon: 'mapPin',
|
||||
size: 'guide',
|
||||
steps: 3,
|
||||
tips: 2,
|
||||
media: { steps: true, result: true },
|
||||
docs: { slug: COLLECTIONS, anchor: 'adding-places' },
|
||||
related: ['import-from-trip', 'place-detail'],
|
||||
},
|
||||
{
|
||||
id: 'import-from-trip',
|
||||
context: 'collections',
|
||||
icon: 'download',
|
||||
size: 'guide',
|
||||
steps: 4,
|
||||
tips: 2,
|
||||
media: { steps: true, result: true },
|
||||
docs: { slug: COLLECTIONS, anchor: 'adding-places' },
|
||||
related: ['add-place', 'copy-to-trip'],
|
||||
},
|
||||
{
|
||||
id: 'place-status',
|
||||
context: 'collections',
|
||||
icon: 'checkCircle',
|
||||
size: 'quick',
|
||||
steps: 2,
|
||||
tips: 2,
|
||||
media: { steps: true, result: true },
|
||||
docs: { slug: COLLECTIONS, anchor: 'place-status' },
|
||||
related: ['place-detail', 'filter-select'],
|
||||
},
|
||||
{
|
||||
id: 'place-detail',
|
||||
context: 'collections',
|
||||
icon: 'panelRight',
|
||||
size: 'quick',
|
||||
steps: 2,
|
||||
tips: 2,
|
||||
media: { steps: true, result: false },
|
||||
docs: { slug: COLLECTIONS, anchor: 'place-detail' },
|
||||
related: ['place-status', 'copy-to-trip', 'labels'],
|
||||
},
|
||||
{
|
||||
id: 'labels',
|
||||
context: 'collections',
|
||||
icon: 'tags',
|
||||
size: 'guide',
|
||||
steps: 4,
|
||||
tips: 2,
|
||||
media: { steps: true, result: true },
|
||||
docs: { slug: COLLECTIONS, anchor: 'custom-labels' },
|
||||
related: ['filter-select', 'place-detail'],
|
||||
},
|
||||
{
|
||||
id: 'filter-select',
|
||||
context: 'collections',
|
||||
icon: 'filter',
|
||||
size: 'guide',
|
||||
steps: 3,
|
||||
tips: 1,
|
||||
media: { steps: true, result: true },
|
||||
docs: { slug: COLLECTIONS, anchor: 'filtering-and-bulk-actions' },
|
||||
related: ['copy-to-trip', 'labels'],
|
||||
},
|
||||
{
|
||||
id: 'copy-to-trip',
|
||||
context: 'collections',
|
||||
icon: 'copy',
|
||||
size: 'guide',
|
||||
steps: 3,
|
||||
tips: 1,
|
||||
media: { steps: true, result: true },
|
||||
docs: { slug: COLLECTIONS, anchor: 'filtering-and-bulk-actions' },
|
||||
related: ['filter-select', 'import-from-trip'],
|
||||
},
|
||||
{
|
||||
id: 'share-list',
|
||||
context: 'collections',
|
||||
icon: 'userPlus',
|
||||
size: 'guide',
|
||||
steps: 3,
|
||||
tips: 2,
|
||||
media: { steps: true, result: true },
|
||||
docs: { slug: COLLECTIONS, anchor: 'sharing-a-list-fusion' },
|
||||
related: ['export-list'],
|
||||
},
|
||||
{
|
||||
id: 'export-list',
|
||||
context: 'collections',
|
||||
icon: 'upload',
|
||||
size: 'quick',
|
||||
steps: 2,
|
||||
tips: 2,
|
||||
media: { steps: true, result: false },
|
||||
docs: { slug: COLLECTIONS, anchor: 'sending-a-list-to-someone-else' },
|
||||
related: ['import-file', 'share-list'],
|
||||
},
|
||||
{
|
||||
id: 'import-file',
|
||||
context: 'collections',
|
||||
icon: 'fileInput',
|
||||
size: 'guide',
|
||||
steps: 3,
|
||||
tips: 2,
|
||||
media: { steps: true, result: true },
|
||||
docs: { slug: COLLECTIONS, anchor: 'sending-a-list-to-someone-else' },
|
||||
related: ['export-list', 'create-list'],
|
||||
},
|
||||
{
|
||||
id: 'edit-list',
|
||||
context: 'collections',
|
||||
icon: 'pencil',
|
||||
size: 'quick',
|
||||
steps: 2,
|
||||
tips: 1,
|
||||
media: { steps: true, result: true },
|
||||
docs: { slug: COLLECTIONS, anchor: 'lists' },
|
||||
related: ['create-list'],
|
||||
},
|
||||
{
|
||||
id: 'all-saved',
|
||||
context: 'collections',
|
||||
icon: 'library',
|
||||
size: 'quick',
|
||||
steps: 2,
|
||||
tips: 1,
|
||||
media: { steps: true, result: false },
|
||||
docs: { slug: COLLECTIONS, anchor: 'lists' },
|
||||
related: ['filter-select'],
|
||||
},
|
||||
]
|
||||
|
||||
export const collectionsContext: HelpContext = {
|
||||
id: 'collections',
|
||||
route: '/collections',
|
||||
icon: 'bookmark',
|
||||
bullets: 6,
|
||||
guides: collectionsGuides.map(g => g.id),
|
||||
docs: [{ slug: COLLECTIONS }, { slug: 'Admin-Categories' }, { slug: 'Admin-Addons' }],
|
||||
hero: true,
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
import type { HelpContext, HelpGuide } from '../types'
|
||||
|
||||
/**
|
||||
* Help for the desktop dashboard (`/dashboard`). The step actions that produce
|
||||
* the pictures for these guides live in `e2e/help/dashboard.guide.ts` and are
|
||||
* keyed by the same ids, so a step added here without an action there fails
|
||||
* the media run.
|
||||
*/
|
||||
|
||||
const DOCS = {
|
||||
dashboard: { slug: 'My-Trips-Dashboard' },
|
||||
creating: { slug: 'Creating-a-Trip' },
|
||||
widgets: { slug: 'Dashboard-Widgets' },
|
||||
feeds: { slug: 'Calendar-Feeds' },
|
||||
} as const
|
||||
|
||||
export const dashboardGuides: HelpGuide[] = [
|
||||
{
|
||||
id: 'create-trip',
|
||||
context: 'dashboard',
|
||||
icon: 'plus',
|
||||
size: 'guide',
|
||||
steps: 5,
|
||||
tips: 2,
|
||||
media: { steps: true, result: true },
|
||||
docs: DOCS.creating,
|
||||
related: ['cover-image', 'edit-trip'],
|
||||
},
|
||||
{
|
||||
id: 'edit-trip',
|
||||
context: 'dashboard',
|
||||
icon: 'pencil',
|
||||
size: 'quick',
|
||||
steps: 3,
|
||||
tips: 1,
|
||||
media: { steps: true, result: false },
|
||||
docs: { slug: 'Creating-a-Trip', anchor: 'fields' },
|
||||
related: ['cover-image', 'duplicate-trip'],
|
||||
},
|
||||
{
|
||||
id: 'cover-image',
|
||||
context: 'dashboard',
|
||||
icon: 'image',
|
||||
size: 'quick',
|
||||
steps: 3,
|
||||
tips: 1,
|
||||
media: { steps: true, result: true },
|
||||
docs: { slug: 'Creating-a-Trip', anchor: 'fields' },
|
||||
related: ['edit-trip'],
|
||||
},
|
||||
{
|
||||
id: 'duplicate-trip',
|
||||
context: 'dashboard',
|
||||
icon: 'copy',
|
||||
size: 'quick',
|
||||
steps: 2,
|
||||
tips: 1,
|
||||
media: { steps: true, result: true },
|
||||
docs: { slug: 'My-Trips-Dashboard', anchor: 'per-trip-actions' },
|
||||
related: ['create-trip'],
|
||||
},
|
||||
{
|
||||
id: 'archive-trip',
|
||||
context: 'dashboard',
|
||||
icon: 'archive',
|
||||
size: 'quick',
|
||||
steps: 3,
|
||||
tips: 0,
|
||||
media: { steps: true, result: false },
|
||||
docs: { slug: 'My-Trips-Dashboard', anchor: 'archived-trips' },
|
||||
related: ['delete-trip', 'filter-and-view'],
|
||||
},
|
||||
{
|
||||
id: 'delete-trip',
|
||||
context: 'dashboard',
|
||||
icon: 'trash',
|
||||
size: 'quick',
|
||||
steps: 2,
|
||||
tips: 0,
|
||||
media: { steps: true, result: false },
|
||||
docs: { slug: 'My-Trips-Dashboard', anchor: 'per-trip-actions' },
|
||||
related: ['archive-trip'],
|
||||
},
|
||||
{
|
||||
id: 'filter-and-view',
|
||||
context: 'dashboard',
|
||||
icon: 'filter',
|
||||
size: 'quick',
|
||||
steps: 2,
|
||||
tips: 0,
|
||||
media: { steps: true, result: true },
|
||||
docs: { slug: 'My-Trips-Dashboard', anchor: 'view-modes' },
|
||||
related: ['archive-trip'],
|
||||
},
|
||||
{
|
||||
id: 'calendar-feed',
|
||||
context: 'dashboard',
|
||||
icon: 'calendar',
|
||||
size: 'quick',
|
||||
steps: 3,
|
||||
tips: 1,
|
||||
media: { steps: true, result: false },
|
||||
docs: { slug: 'Calendar-Feeds', anchor: 'turning-a-feed-on' },
|
||||
},
|
||||
{
|
||||
id: 'widgets',
|
||||
context: 'dashboard',
|
||||
icon: 'layout',
|
||||
size: 'guide',
|
||||
steps: 4,
|
||||
tips: 0,
|
||||
media: { steps: true, result: true },
|
||||
docs: DOCS.widgets,
|
||||
link: '/settings?tab=appearance',
|
||||
related: ['currency-widget', 'timezones-widget'],
|
||||
},
|
||||
{
|
||||
id: 'currency-widget',
|
||||
context: 'dashboard',
|
||||
icon: 'coins',
|
||||
size: 'quick',
|
||||
steps: 2,
|
||||
tips: 1,
|
||||
media: { steps: true, result: false },
|
||||
docs: { slug: 'Dashboard-Widgets', anchor: 'currency-converter' },
|
||||
related: ['widgets'],
|
||||
},
|
||||
{
|
||||
id: 'timezones-widget',
|
||||
context: 'dashboard',
|
||||
icon: 'clock',
|
||||
size: 'quick',
|
||||
steps: 2,
|
||||
tips: 0,
|
||||
media: { steps: true, result: false },
|
||||
docs: { slug: 'Dashboard-Widgets', anchor: 'timezone-clock' },
|
||||
related: ['widgets'],
|
||||
},
|
||||
]
|
||||
|
||||
export const dashboardContext: HelpContext = {
|
||||
id: 'dashboard',
|
||||
route: '/dashboard',
|
||||
icon: 'layout',
|
||||
bullets: 5,
|
||||
guides: dashboardGuides.map(g => g.id),
|
||||
docs: [DOCS.dashboard, DOCS.creating, DOCS.widgets, DOCS.feeds],
|
||||
hero: true,
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
import type { HelpContext, HelpGuide } from '../types'
|
||||
|
||||
/**
|
||||
* Help for the Journey addon: the list of journeys (`/journey`) and one open
|
||||
* journal (`/journey/:id`) are two screens with their own guides. The step
|
||||
* actions that produce the pictures live in `e2e/help/journey.guide.ts`,
|
||||
* keyed by the same ids.
|
||||
*/
|
||||
|
||||
const JOURNEY = 'Journey-Journal'
|
||||
|
||||
export const journeyGuides: HelpGuide[] = [
|
||||
{
|
||||
id: 'create-journey',
|
||||
context: 'journey',
|
||||
icon: 'plus',
|
||||
size: 'guide',
|
||||
steps: 3,
|
||||
tips: 2,
|
||||
media: { steps: true, result: true },
|
||||
docs: { slug: JOURNEY, anchor: 'creating-a-journey' },
|
||||
related: ['continue-writing', 'add-entry'],
|
||||
},
|
||||
{
|
||||
id: 'open-journey',
|
||||
context: 'journey',
|
||||
icon: 'bookOpen',
|
||||
size: 'quick',
|
||||
steps: 1,
|
||||
tips: 1,
|
||||
media: { steps: true, result: false },
|
||||
docs: { slug: JOURNEY, anchor: 'accessing-journey' },
|
||||
related: ['continue-writing'],
|
||||
},
|
||||
{
|
||||
id: 'continue-writing',
|
||||
context: 'journey',
|
||||
icon: 'pencil',
|
||||
size: 'quick',
|
||||
steps: 1,
|
||||
tips: 1,
|
||||
media: { steps: true, result: false },
|
||||
docs: { slug: JOURNEY, anchor: 'getting-around-a-long-journal' },
|
||||
related: ['add-entry', 'open-journey'],
|
||||
},
|
||||
]
|
||||
|
||||
export const journeyContext: HelpContext = {
|
||||
id: 'journey',
|
||||
route: '/journey',
|
||||
icon: 'bookOpen',
|
||||
bullets: 3,
|
||||
guides: journeyGuides.map(g => g.id),
|
||||
docs: [{ slug: JOURNEY }, { slug: 'Admin-Addons' }],
|
||||
hero: true,
|
||||
}
|
||||
|
||||
export const journalGuides: HelpGuide[] = [
|
||||
{
|
||||
id: 'add-entry',
|
||||
context: 'journey-detail',
|
||||
icon: 'plus',
|
||||
size: 'guide',
|
||||
steps: 4,
|
||||
tips: 2,
|
||||
media: { steps: true, result: true },
|
||||
docs: { slug: JOURNEY, anchor: 'journal-entries' },
|
||||
related: ['entry-photos', 'pros-cons', 'suggestions'],
|
||||
},
|
||||
{
|
||||
id: 'entry-photos',
|
||||
context: 'journey-detail',
|
||||
icon: 'image',
|
||||
size: 'guide',
|
||||
steps: 3,
|
||||
tips: 2,
|
||||
media: { steps: true, result: true },
|
||||
docs: { slug: JOURNEY, anchor: 'journal-entries' },
|
||||
related: ['add-entry', 'gallery-map'],
|
||||
},
|
||||
{
|
||||
id: 'suggestions',
|
||||
context: 'journey-detail',
|
||||
icon: 'sparkles',
|
||||
size: 'guide',
|
||||
steps: 3,
|
||||
tips: 2,
|
||||
media: { steps: true, result: true },
|
||||
docs: { slug: JOURNEY, anchor: 'creating-a-journey' },
|
||||
related: ['add-entry', 'link-trip'],
|
||||
},
|
||||
{
|
||||
id: 'add-on-day',
|
||||
context: 'journey-detail',
|
||||
icon: 'calendar',
|
||||
size: 'quick',
|
||||
steps: 2,
|
||||
tips: 1,
|
||||
media: { steps: true, result: false },
|
||||
docs: { slug: JOURNEY, anchor: 'getting-around-a-long-journal' },
|
||||
related: ['add-entry'],
|
||||
},
|
||||
{
|
||||
id: 'pros-cons',
|
||||
context: 'journey-detail',
|
||||
icon: 'thumbsUp',
|
||||
size: 'quick',
|
||||
steps: 2,
|
||||
tips: 1,
|
||||
media: { steps: true, result: true },
|
||||
docs: { slug: JOURNEY, anchor: 'journal-entries' },
|
||||
related: ['entry-fields', 'add-entry'],
|
||||
},
|
||||
{
|
||||
id: 'search-journey',
|
||||
context: 'journey-detail',
|
||||
icon: 'search',
|
||||
size: 'quick',
|
||||
steps: 2,
|
||||
tips: 2,
|
||||
media: { steps: true, result: false },
|
||||
docs: { slug: JOURNEY, anchor: 'getting-around-a-long-journal' },
|
||||
related: ['gallery-map'],
|
||||
},
|
||||
{
|
||||
id: 'gallery-map',
|
||||
context: 'journey-detail',
|
||||
icon: 'gallery',
|
||||
size: 'quick',
|
||||
steps: 2,
|
||||
tips: 2,
|
||||
media: { steps: true, result: false },
|
||||
docs: { slug: JOURNEY, anchor: 'map-view' },
|
||||
related: ['entry-photos', 'share-public'],
|
||||
},
|
||||
{
|
||||
id: 'entry-fields',
|
||||
context: 'journey-detail',
|
||||
icon: 'filter',
|
||||
size: 'quick',
|
||||
steps: 2,
|
||||
tips: 1,
|
||||
media: { steps: true, result: false },
|
||||
docs: { slug: JOURNEY, anchor: 'entry-fields' },
|
||||
related: ['pros-cons'],
|
||||
},
|
||||
{
|
||||
id: 'link-trip',
|
||||
context: 'journey-detail',
|
||||
icon: 'link',
|
||||
size: 'guide',
|
||||
steps: 3,
|
||||
tips: 2,
|
||||
media: { steps: true, result: true },
|
||||
docs: { slug: JOURNEY, anchor: 'creating-a-journey' },
|
||||
related: ['suggestions'],
|
||||
},
|
||||
{
|
||||
id: 'share-public',
|
||||
context: 'journey-detail',
|
||||
icon: 'share',
|
||||
size: 'guide',
|
||||
steps: 3,
|
||||
tips: 2,
|
||||
media: { steps: true, result: false },
|
||||
docs: { slug: JOURNEY, anchor: 'public-sharing' },
|
||||
related: ['contributors', 'gallery-map'],
|
||||
},
|
||||
{
|
||||
id: 'contributors',
|
||||
context: 'journey-detail',
|
||||
icon: 'userPlus',
|
||||
size: 'guide',
|
||||
steps: 3,
|
||||
tips: 1,
|
||||
media: { steps: true, result: false },
|
||||
docs: { slug: JOURNEY, anchor: 'creating-a-journey' },
|
||||
related: ['share-public'],
|
||||
},
|
||||
{
|
||||
id: 'studio',
|
||||
context: 'journey-detail',
|
||||
icon: 'book',
|
||||
size: 'quick',
|
||||
steps: 2,
|
||||
tips: 2,
|
||||
media: { steps: true, result: true },
|
||||
docs: { slug: JOURNEY, anchor: 'trek-studio' },
|
||||
related: ['entry-photos'],
|
||||
},
|
||||
{
|
||||
id: 'archive-journey',
|
||||
context: 'journey-detail',
|
||||
icon: 'archive',
|
||||
size: 'quick',
|
||||
steps: 2,
|
||||
tips: 2,
|
||||
media: { steps: true, result: false },
|
||||
docs: { slug: JOURNEY, anchor: 'creating-a-journey' },
|
||||
related: ['link-trip'],
|
||||
},
|
||||
]
|
||||
|
||||
export const journalContext: HelpContext = {
|
||||
id: 'journey-detail',
|
||||
parent: 'journey',
|
||||
icon: 'bookOpen',
|
||||
bullets: 6,
|
||||
guides: journalGuides.map(g => g.id),
|
||||
docs: [{ slug: JOURNEY }, { slug: 'Public-Share-Links' }],
|
||||
hero: true,
|
||||
}
|
||||
|
||||
/** TREK Studio, the photo-book designer that opens over a journal (`/journey/:id/studio`). */
|
||||
export const studioGuides: HelpGuide[] = [
|
||||
{
|
||||
id: 'studio-auto-layout',
|
||||
context: 'journey-studio',
|
||||
icon: 'sparkles',
|
||||
size: 'guide',
|
||||
steps: 3,
|
||||
tips: 2,
|
||||
media: { steps: true, result: true },
|
||||
docs: { slug: JOURNEY, anchor: 'trek-studio' },
|
||||
related: ['studio-pages', 'studio-layouts'],
|
||||
},
|
||||
{
|
||||
id: 'studio-pages',
|
||||
context: 'journey-studio',
|
||||
icon: 'book',
|
||||
size: 'guide',
|
||||
steps: 3,
|
||||
tips: 2,
|
||||
media: { steps: true, result: true },
|
||||
docs: { slug: JOURNEY, anchor: 'trek-studio' },
|
||||
related: ['studio-layouts', 'studio-spread-file'],
|
||||
},
|
||||
{
|
||||
id: 'studio-layouts',
|
||||
context: 'journey-studio',
|
||||
icon: 'layout',
|
||||
size: 'quick',
|
||||
steps: 2,
|
||||
tips: 1,
|
||||
media: { steps: true, result: true },
|
||||
docs: { slug: JOURNEY, anchor: 'trek-studio' },
|
||||
related: ['studio-auto-layout', 'studio-content'],
|
||||
},
|
||||
{
|
||||
id: 'studio-content',
|
||||
context: 'journey-studio',
|
||||
icon: 'image',
|
||||
size: 'guide',
|
||||
steps: 3,
|
||||
tips: 2,
|
||||
media: { steps: true, result: true },
|
||||
docs: { slug: JOURNEY, anchor: 'trek-studio' },
|
||||
related: ['studio-properties', 'studio-elements'],
|
||||
},
|
||||
{
|
||||
id: 'studio-elements',
|
||||
context: 'journey-studio',
|
||||
icon: 'plus',
|
||||
size: 'quick',
|
||||
steps: 2,
|
||||
tips: 1,
|
||||
media: { steps: true, result: false },
|
||||
docs: { slug: JOURNEY, anchor: 'trek-studio' },
|
||||
related: ['studio-travel', 'studio-properties'],
|
||||
},
|
||||
{
|
||||
id: 'studio-travel',
|
||||
context: 'journey-studio',
|
||||
icon: 'globe',
|
||||
size: 'quick',
|
||||
steps: 2,
|
||||
tips: 1,
|
||||
media: { steps: true, result: false },
|
||||
docs: { slug: JOURNEY, anchor: 'trek-studio' },
|
||||
related: ['studio-elements'],
|
||||
},
|
||||
{
|
||||
id: 'studio-properties',
|
||||
context: 'journey-studio',
|
||||
icon: 'panelRight',
|
||||
size: 'guide',
|
||||
steps: 3,
|
||||
tips: 2,
|
||||
media: { steps: true, result: false },
|
||||
docs: { slug: JOURNEY, anchor: 'trek-studio' },
|
||||
related: ['studio-content', 'studio-layouts'],
|
||||
},
|
||||
{
|
||||
id: 'studio-format',
|
||||
context: 'journey-studio',
|
||||
icon: 'filter',
|
||||
size: 'quick',
|
||||
steps: 2,
|
||||
tips: 2,
|
||||
media: { steps: true, result: false },
|
||||
docs: { slug: JOURNEY, anchor: 'trek-studio' },
|
||||
related: ['studio-export'],
|
||||
},
|
||||
{
|
||||
id: 'studio-export',
|
||||
context: 'journey-studio',
|
||||
icon: 'upload',
|
||||
size: 'guide',
|
||||
steps: 3,
|
||||
tips: 2,
|
||||
media: { steps: true, result: false },
|
||||
docs: { slug: JOURNEY, anchor: 'trek-studio' },
|
||||
related: ['studio-format', 'studio-spread-file'],
|
||||
},
|
||||
{
|
||||
id: 'studio-spread-file',
|
||||
context: 'journey-studio',
|
||||
icon: 'fileInput',
|
||||
size: 'quick',
|
||||
steps: 2,
|
||||
tips: 1,
|
||||
media: { steps: true, result: false },
|
||||
docs: { slug: JOURNEY, anchor: 'trek-studio' },
|
||||
related: ['studio-export', 'studio-pages'],
|
||||
},
|
||||
]
|
||||
|
||||
export const studioContext: HelpContext = {
|
||||
id: 'journey-studio',
|
||||
parent: 'journey',
|
||||
icon: 'book',
|
||||
bullets: 6,
|
||||
guides: studioGuides.map(g => g.id),
|
||||
docs: [{ slug: JOURNEY, anchor: 'trek-studio' }],
|
||||
hero: true,
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import type { HelpContext, HelpGuide } from '../types'
|
||||
|
||||
/**
|
||||
* Help for the user settings (`/settings`). The page is one route with tabs,
|
||||
* so every tab is a screen of its own under `settings`; the page anchors the
|
||||
* tab that is open. The step actions that produce the pictures live in
|
||||
* `e2e/help/settings.guide.ts`, keyed by the same ids.
|
||||
*/
|
||||
|
||||
const SETTINGS = 'User-Settings'
|
||||
const GENERAL = 'Display-Settings'
|
||||
const APPEARANCE = 'Appearance-Settings'
|
||||
const MAP = 'Map-Settings'
|
||||
const NOTIFICATIONS = 'Notifications'
|
||||
const OFFLINE = 'Offline-Mode-and-PWA'
|
||||
|
||||
const guide = (
|
||||
id: string, context: string, icon: HelpGuide['icon'], size: HelpGuide['size'], steps: number, tips: number,
|
||||
docs: HelpGuide['docs'], related: string[], result = true,
|
||||
): HelpGuide => ({ id, context, icon, size, steps, tips, media: { steps: true, result }, docs, related })
|
||||
|
||||
export const settingsGuides: HelpGuide[] = [
|
||||
// General
|
||||
guide('language-region', 'settings-display', 'globe', 'quick', 2, 2, { slug: GENERAL, anchor: 'language' }, ['travel-map-prefs', 'startup'], false),
|
||||
guide('travel-map-prefs', 'settings-display', 'mapPin', 'quick', 2, 1, { slug: GENERAL, anchor: 'always-show-booking-routes' }, ['language-region', 'map-provider'], false),
|
||||
guide('startup', 'settings-display', 'compass', 'quick', 2, 1, { slug: GENERAL, anchor: 'start-page' }, ['language-region'], false),
|
||||
// Appearance
|
||||
guide('theme-scheme', 'settings-appearance', 'palette', 'guide', 3, 2, { slug: APPEARANCE }, ['readability', 'dashboard-widgets']),
|
||||
guide('readability', 'settings-appearance', 'eye', 'quick', 2, 2, { slug: APPEARANCE }, ['theme-scheme'], false),
|
||||
guide('dashboard-widgets', 'settings-appearance', 'layout', 'quick', 2, 2, { slug: APPEARANCE }, ['theme-scheme'], false),
|
||||
// Map
|
||||
guide('map-provider', 'settings-map', 'map', 'guide', 3, 2, { slug: MAP }, ['travel-map-prefs'], false),
|
||||
// Notifications
|
||||
guide('notification-channels', 'settings-notifications', 'bell', 'guide', 3, 2, { slug: NOTIFICATIONS, anchor: 'notification-channels' }, [], false),
|
||||
// Integrations
|
||||
guide('photo-providers', 'settings-integrations', 'image', 'guide', 2, 2, { slug: 'Photo-Providers' }, ['api-keys'], false),
|
||||
guide('api-keys', 'settings-integrations', 'key', 'guide', 2, 2, { slug: SETTINGS, anchor: 'integrations-tab' }, ['mcp-oauth'], false),
|
||||
guide('mcp-oauth', 'settings-integrations', 'plug', 'guide', 3, 2, { slug: 'MCP-Setup' }, ['api-keys'], false),
|
||||
// Offline
|
||||
guide('offline-prepare', 'settings-offline', 'cloudOff', 'guide', 3, 2, { slug: OFFLINE, anchor: 'settings--offline' }, ['offline-conflicts'], false),
|
||||
guide('offline-conflicts', 'settings-offline', 'repeat', 'quick', 2, 1, { slug: OFFLINE, anchor: 'settings--offline' }, ['offline-prepare'], false),
|
||||
// Account
|
||||
guide('profile', 'settings-account', 'user', 'quick', 2, 1, { slug: SETTINGS, anchor: 'account-tab-summary' }, ['password'], false),
|
||||
guide('password', 'settings-account', 'lock', 'quick', 2, 1, { slug: SETTINGS, anchor: 'account-tab-summary' }, ['mfa'], false),
|
||||
guide('mfa', 'settings-account', 'shield', 'guide', 3, 2, { slug: 'Two-Factor-Authentication' }, ['passkeys', 'password']),
|
||||
// Passkeys only exist on https, which the media run is not: words only.
|
||||
{ ...guide('passkeys', 'settings-account', 'fingerprint', 'quick', 2, 2, { slug: 'Passkeys' }, ['mfa'], false), media: { steps: false, result: false } },
|
||||
guide('delete-account', 'settings-account', 'trash', 'quick', 1, 2, { slug: SETTINGS, anchor: 'account-tab-summary' }, ['profile'], false),
|
||||
]
|
||||
|
||||
const forContext = (id: string) => settingsGuides.filter(g => g.context === id).map(g => g.id)
|
||||
|
||||
export const settingsContext: HelpContext = {
|
||||
id: 'settings',
|
||||
route: '/settings',
|
||||
icon: 'sliders',
|
||||
bullets: 4,
|
||||
guides: [],
|
||||
docs: [{ slug: SETTINGS }, { slug: GENERAL }, { slug: APPEARANCE }],
|
||||
hero: true,
|
||||
}
|
||||
|
||||
const tab = (id: string, icon: HelpContext['icon'], bullets: number, docs: HelpContext['docs']): HelpContext => ({
|
||||
id, parent: 'settings', route: `/settings?tab=${id.slice('settings-'.length)}`, icon, bullets, guides: forContext(id), docs, hero: true,
|
||||
})
|
||||
|
||||
export const settingsTabContexts: HelpContext[] = [
|
||||
tab('settings-display', 'sliders', 3, [{ slug: GENERAL }]),
|
||||
tab('settings-appearance', 'palette', 4, [{ slug: APPEARANCE }]),
|
||||
tab('settings-map', 'map', 3, [{ slug: MAP }]),
|
||||
tab('settings-notifications', 'bell', 3, [{ slug: NOTIFICATIONS }]),
|
||||
tab('settings-integrations', 'plug', 4, [{ slug: SETTINGS, anchor: 'integrations-tab' }, { slug: 'MCP-Setup' }, { slug: 'Photo-Providers' }]),
|
||||
tab('settings-offline', 'cloudOff', 4, [{ slug: OFFLINE }]),
|
||||
tab('settings-account', 'user', 4, [{ slug: SETTINGS, anchor: 'account-tab-summary' }, { slug: 'Two-Factor-Authentication' }, { slug: 'Passkeys' }]),
|
||||
]
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { HelpContext, HelpGuide } from '../types'
|
||||
|
||||
/**
|
||||
* Help for a trip (`/trips/:id`). The trip is a family of screens: this one is
|
||||
* the frame (the tabs, the three columns of the plan, the people in the trip),
|
||||
* the columns, the overlays and the other tabs each get their own screen under
|
||||
* it, anchored by the page for whatever is open. The step actions that produce
|
||||
* the pictures live in `e2e/help/trip.guide.ts`.
|
||||
*/
|
||||
|
||||
const SHARING = 'Sharing-and-Collaboration'
|
||||
const PLANNER = 'Trip-Planner'
|
||||
|
||||
const guide = (
|
||||
id: string, icon: HelpGuide['icon'], size: HelpGuide['size'], steps: number, tips: number,
|
||||
docs: HelpGuide['docs'], related: string[], result = false,
|
||||
): HelpGuide => ({ id, context: 'trip', icon, size, steps, tips, media: { steps: true, result }, docs, related })
|
||||
|
||||
export const tripGuides: HelpGuide[] = [
|
||||
guide('add-member', 'userPlus', 'quick', 3, 2, { slug: SHARING }, ['trip-invite-link', 'add-guest'], true),
|
||||
guide('trip-invite-link', 'link', 'quick', 3, 2, { slug: SHARING }, ['add-member', 'invite-links']),
|
||||
guide('add-guest', 'userRound', 'quick', 2, 2, { slug: SHARING, anchor: 'guests' }, ['add-member'], true),
|
||||
guide('public-link', 'share', 'quick', 3, 2, { slug: SHARING, anchor: 'public-share-links' }, ['add-member']),
|
||||
guide('transfer-ownership', 'crown', 'quick', 2, 2, { slug: SHARING }, ['add-member']),
|
||||
guide('collapse-columns', 'panelLeft', 'quick', 3, 2, { slug: PLANNER }, []),
|
||||
guide('undo-change', 'undo', 'quick', 1, 2, { slug: PLANNER }, []),
|
||||
]
|
||||
|
||||
export const tripContext: HelpContext = {
|
||||
id: 'trip',
|
||||
route: '/trips/:id?tab=plan',
|
||||
icon: 'route',
|
||||
bullets: 6,
|
||||
guides: tripGuides.map(g => g.id),
|
||||
docs: [{ slug: PLANNER }, { slug: SHARING }],
|
||||
hero: true,
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import type { HelpContext, HelpGuide } from '../types'
|
||||
|
||||
/**
|
||||
* Help for the days column of a trip's plan: the day cards, the stops on them,
|
||||
* their order and times, notes, the day's route, and everything that leaves
|
||||
* the plan (PDF, calendar, GPX). Bookings and transports show up here too, so
|
||||
* one guide reads them and points at the screens that create them.
|
||||
* A screen under `trip`; the step actions live in `e2e/help/trip-days.guide.ts`.
|
||||
*/
|
||||
|
||||
const DAYS = 'Day-Plans-and-Notes'
|
||||
const MAP = 'Map-Features'
|
||||
|
||||
const guide = (
|
||||
id: string, icon: HelpGuide['icon'], size: HelpGuide['size'], steps: number, tips: number,
|
||||
docs: HelpGuide['docs'], related: string[], result = false,
|
||||
): HelpGuide => ({ id, context: 'trip-days', icon, size, steps, tips, media: { steps: true, result }, docs, related })
|
||||
|
||||
export const tripDaysGuides: HelpGuide[] = [
|
||||
guide('read-day-plan', 'eye', 'guide', 5, 2, { slug: DAYS, anchor: 'day-timeline' }, ['bookings-in-plan', 'day-route']),
|
||||
guide('place-onto-day', 'calendarCheck', 'guide', 4, 3, { slug: DAYS, anchor: 'assigning-places-to-a-day' }, ['place-to-open-day', 'reorder-stops', 'undo-change'], true),
|
||||
guide('reorder-stops', 'repeat', 'quick', 4, 2, { slug: DAYS, anchor: 'assigning-places-to-a-day' }, ['place-onto-day', 'set-stop-times', 'lock-stop']),
|
||||
guide('set-stop-times', 'clock', 'quick', 3, 3, { slug: DAYS, anchor: 'day-timeline' }, ['reorder-stops', 'edit-place']),
|
||||
guide('remove-from-day', 'minusCircle', 'quick', 2, 2, { slug: DAYS, anchor: 'assigning-places-to-a-day' }, ['delete-place', 'undo-change']),
|
||||
guide('lock-stop', 'lock', 'quick', 2, 2, { slug: DAYS, anchor: 'toolbar-actions' }, ['day-route', 'reorder-stops']),
|
||||
guide('day-note', 'pencil', 'quick', 4, 2, { slug: DAYS, anchor: 'day-notes' }, ['read-day-plan'], true),
|
||||
guide('day-route', 'route', 'guide', 5, 3, { slug: MAP, anchor: 'route-lines' }, ['lock-stop', 'read-day-plan']),
|
||||
guide('manage-days', 'calendarDays', 'quick', 4, 2, { slug: DAYS, anchor: 'the-day-plan-sidebar' }, ['edit-trip']),
|
||||
guide('bookings-in-plan', 'ticket', 'guide', 5, 2, { slug: DAYS, anchor: 'multi-day-reservations' }, ['read-day-plan']),
|
||||
guide('export-plan', 'download', 'quick', 4, 3, { slug: MAP, anchor: 'exporting-a-trip-as-gpx' }, ['calendar-feed']),
|
||||
]
|
||||
|
||||
export const tripDaysContext: HelpContext = {
|
||||
id: 'trip-days',
|
||||
parent: 'trip',
|
||||
route: '/trips/:id?tab=plan',
|
||||
icon: 'calendarDays',
|
||||
bullets: 6,
|
||||
guides: tripDaysGuides.map(g => g.id),
|
||||
docs: [{ slug: DAYS }, { slug: MAP }],
|
||||
hero: true,
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { HelpContext, HelpGuide } from '../types'
|
||||
|
||||
/**
|
||||
* Help for the places column of a trip's plan: bringing places in (by hand,
|
||||
* from a file, from a shared list), finding them, changing and deleting them.
|
||||
* A screen under `trip`; the step actions live in `e2e/help/trip-places.guide.ts`.
|
||||
*/
|
||||
|
||||
const PLACES = 'Places-and-Search'
|
||||
const IMPORT = { slug: PLACES, anchor: 'importing-places' }
|
||||
|
||||
const guide = (
|
||||
id: string, icon: HelpGuide['icon'], size: HelpGuide['size'], steps: number, tips: number,
|
||||
docs: HelpGuide['docs'], related: string[], result = false,
|
||||
): HelpGuide => ({ id, context: 'trip-places', icon, size, steps, tips, media: { steps: true, result }, docs, related })
|
||||
|
||||
export const tripPlacesGuides: HelpGuide[] = [
|
||||
guide('create-place', 'plus', 'guide', 5, 3, { slug: PLACES }, ['place-to-open-day', 'edit-place'], true),
|
||||
guide('place-to-open-day', 'calendarCheck', 'quick', 3, 2, { slug: PLACES }, ['create-place']),
|
||||
guide('filter-places', 'filter', 'quick', 4, 2, { slug: PLACES }, ['select-places']),
|
||||
guide('edit-place', 'pencil', 'quick', 3, 2, { slug: PLACES }, ['create-place', 'delete-place']),
|
||||
guide('delete-place', 'trash', 'quick', 2, 2, { slug: PLACES }, ['edit-place', 'select-places', 'undo-change']),
|
||||
guide('select-places', 'checkCircle', 'quick', 4, 2, { slug: PLACES }, ['filter-places', 'delete-place'], true),
|
||||
guide('import-places-file', 'fileInput', 'guide', 4, 2, IMPORT, ['import-places-list', 'filter-places'], true),
|
||||
guide('import-places-list', 'upload', 'quick', 3, 2, IMPORT, ['import-places-file']),
|
||||
]
|
||||
|
||||
export const tripPlacesContext: HelpContext = {
|
||||
id: 'trip-places',
|
||||
parent: 'trip',
|
||||
route: '/trips/:id?tab=plan',
|
||||
icon: 'mapPin',
|
||||
bullets: 6,
|
||||
guides: tripPlacesGuides.map(g => g.id),
|
||||
docs: [{ slug: PLACES }, IMPORT],
|
||||
hero: true,
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
import type { HelpContext, HelpGuide } from '../types'
|
||||
|
||||
/**
|
||||
* Help for the Vacay addon (`/vacay`): the personal leave planner. The step
|
||||
* actions that produce the pictures live in `e2e/help/vacay.guide.ts`, keyed
|
||||
* by the same ids.
|
||||
*/
|
||||
|
||||
const VACAY = 'Vacay'
|
||||
|
||||
export const vacayGuides: HelpGuide[] = [
|
||||
{
|
||||
id: 'log-day',
|
||||
context: 'vacay',
|
||||
icon: 'calendarCheck',
|
||||
size: 'guide',
|
||||
steps: 3,
|
||||
tips: 2,
|
||||
media: { steps: true, result: true },
|
||||
docs: { slug: VACAY, anchor: 'calendar-view' },
|
||||
related: ['half-day', 'comp-day', 'entitlement'],
|
||||
},
|
||||
{
|
||||
id: 'half-day',
|
||||
context: 'vacay',
|
||||
icon: 'halfDay',
|
||||
size: 'quick',
|
||||
steps: 3,
|
||||
tips: 1,
|
||||
media: { steps: true, result: true },
|
||||
docs: { slug: VACAY, anchor: 'half-days-and-comp-days' },
|
||||
related: ['log-day', 'comp-day'],
|
||||
},
|
||||
{
|
||||
id: 'comp-day',
|
||||
context: 'vacay',
|
||||
icon: 'hourglass',
|
||||
size: 'quick',
|
||||
steps: 2,
|
||||
tips: 1,
|
||||
media: { steps: true, result: true },
|
||||
docs: { slug: VACAY, anchor: 'half-days-and-comp-days' },
|
||||
related: ['log-day', 'half-day'],
|
||||
},
|
||||
{
|
||||
id: 'entitlement',
|
||||
context: 'vacay',
|
||||
icon: 'calculator',
|
||||
size: 'quick',
|
||||
steps: 2,
|
||||
tips: 1,
|
||||
media: { steps: true, result: true },
|
||||
docs: { slug: VACAY, anchor: 'year-plans' },
|
||||
related: ['carry-over', 'years'],
|
||||
},
|
||||
{
|
||||
id: 'years',
|
||||
context: 'vacay',
|
||||
icon: 'calendarRange',
|
||||
size: 'quick',
|
||||
steps: 3,
|
||||
tips: 0,
|
||||
media: { steps: true, result: true },
|
||||
docs: { slug: VACAY, anchor: 'year-plans' },
|
||||
related: ['entitlement', 'leave-year'],
|
||||
},
|
||||
{
|
||||
id: 'company-holidays',
|
||||
context: 'vacay',
|
||||
icon: 'building',
|
||||
size: 'guide',
|
||||
steps: 3,
|
||||
tips: 1,
|
||||
media: { steps: true, result: true },
|
||||
docs: { slug: VACAY, anchor: 'calendar-view' },
|
||||
related: ['public-holidays', 'log-day'],
|
||||
},
|
||||
{
|
||||
id: 'public-holidays',
|
||||
context: 'vacay',
|
||||
icon: 'flag',
|
||||
size: 'guide',
|
||||
steps: 3,
|
||||
tips: 1,
|
||||
media: { steps: true, result: true },
|
||||
docs: { slug: VACAY, anchor: 'calendar-view' },
|
||||
related: ['school-holidays', 'company-holidays'],
|
||||
},
|
||||
{
|
||||
id: 'school-holidays',
|
||||
context: 'vacay',
|
||||
icon: 'graduationCap',
|
||||
size: 'guide',
|
||||
steps: 3,
|
||||
tips: 1,
|
||||
media: { steps: true, result: true },
|
||||
docs: { slug: VACAY, anchor: 'manually-maintained-school-holidays' },
|
||||
// The catalogue those holidays come from is kept under Admin.
|
||||
related: ['public-holidays', 'school-holiday-catalog'],
|
||||
},
|
||||
{
|
||||
id: 'weekends',
|
||||
context: 'vacay',
|
||||
icon: 'calendarOff',
|
||||
size: 'quick',
|
||||
steps: 3,
|
||||
tips: 0,
|
||||
media: { steps: true, result: false },
|
||||
docs: { slug: VACAY, anchor: 'calendar-view' },
|
||||
related: ['leave-year'],
|
||||
},
|
||||
{
|
||||
id: 'leave-year',
|
||||
context: 'vacay',
|
||||
icon: 'calendarDays',
|
||||
size: 'quick',
|
||||
steps: 2,
|
||||
tips: 1,
|
||||
media: { steps: true, result: false },
|
||||
docs: { slug: VACAY, anchor: 'leave-year' },
|
||||
related: ['years', 'carry-over'],
|
||||
},
|
||||
{
|
||||
id: 'carry-over',
|
||||
context: 'vacay',
|
||||
icon: 'repeat',
|
||||
size: 'quick',
|
||||
steps: 2,
|
||||
tips: 1,
|
||||
media: { steps: true, result: false },
|
||||
docs: { slug: VACAY, anchor: 'year-plans' },
|
||||
related: ['entitlement'],
|
||||
},
|
||||
{
|
||||
id: 'invite',
|
||||
context: 'vacay',
|
||||
icon: 'userPlus',
|
||||
size: 'guide',
|
||||
steps: 3,
|
||||
tips: 2,
|
||||
media: { steps: true, result: true },
|
||||
docs: { slug: VACAY, anchor: 'inviting-collaborators' },
|
||||
related: ['share-calendar'],
|
||||
},
|
||||
{
|
||||
id: 'share-calendar',
|
||||
context: 'vacay',
|
||||
icon: 'share',
|
||||
size: 'guide',
|
||||
steps: 3,
|
||||
tips: 2,
|
||||
media: { steps: true, result: true },
|
||||
docs: { slug: VACAY, anchor: 'sharing-your-calendar-view-only' },
|
||||
related: ['invite'],
|
||||
},
|
||||
]
|
||||
|
||||
export const vacayContext: HelpContext = {
|
||||
id: 'vacay',
|
||||
route: '/vacay',
|
||||
icon: 'calendarDays',
|
||||
bullets: 5,
|
||||
guides: vacayGuides.map(g => g.id),
|
||||
docs: [{ slug: VACAY }, { slug: 'Admin-Addons' }],
|
||||
hero: true,
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { existsSync } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import en from '@trek/shared/i18n/en'
|
||||
import { dashboardGuides } from './contexts/dashboard'
|
||||
import { vacayGuides } from './contexts/vacay'
|
||||
import { atlasGuides } from './contexts/atlas'
|
||||
import { collectionsGuides } from './contexts/collections'
|
||||
import { journeyGuides, journalGuides, studioGuides } from './contexts/journey'
|
||||
import { settingsGuides } from './contexts/settings'
|
||||
import { adminGuides } from './contexts/admin'
|
||||
import { tripGuides } from './contexts/trip'
|
||||
import { tripPlacesGuides } from './contexts/tripPlaces'
|
||||
import { tripDaysGuides } from './contexts/tripDays'
|
||||
import {
|
||||
HELP_CONTEXTS, HELP_GUIDES, getHelpContext, getHelpGuide, guidesFor, allHelpGuides,
|
||||
ctxKey, ctxBulletKey, guideKey, guideStepKey, guideTipKey, helpMedia, docsRoute,
|
||||
topLevelHelpContexts, childHelpContexts, helpContextTrail,
|
||||
} from './registry'
|
||||
|
||||
/**
|
||||
* The registry is data; these tests are the gate that keeps it honest: every
|
||||
* registered screen and guide has its words in the canonical locale, its
|
||||
* pictures on disk, and its cross-references pointing at things that exist.
|
||||
* A guide that is registered but not written (or not recorded) fails here
|
||||
* rather than rendering a raw key or a broken image to a reader.
|
||||
*/
|
||||
|
||||
const strings = en as Record<string, unknown>
|
||||
const has = (key: string) => typeof strings[key] === 'string' && (strings[key] as string).length > 0
|
||||
const MEDIA_ROOT = path.join(process.cwd(), 'public', 'help-media')
|
||||
const onDisk = (url: string) => existsSync(path.join(MEDIA_ROOT, url.replace(/^\/help-media\//, '')))
|
||||
|
||||
describe('help registry: structure', () => {
|
||||
it('registers the dashboard with its guides', () => {
|
||||
const ctx = getHelpContext('dashboard')
|
||||
expect(ctx).not.toBeNull()
|
||||
expect(ctx!.guides.length).toBeGreaterThan(0)
|
||||
expect(guidesFor(ctx!).map(g => g.id)).toEqual(ctx!.guides)
|
||||
})
|
||||
|
||||
it('answers null for an unknown or missing id', () => {
|
||||
expect(getHelpContext('no-such-screen')).toBeNull()
|
||||
expect(getHelpContext(null)).toBeNull()
|
||||
expect(getHelpContext(undefined)).toBeNull()
|
||||
expect(getHelpGuide('no-such-guide')).toBeNull()
|
||||
expect(getHelpGuide(null)).toBeNull()
|
||||
})
|
||||
|
||||
it('lists every guide of every context, and nothing that belongs nowhere', () => {
|
||||
const listed = new Set([...HELP_CONTEXTS.values()].flatMap(c => c.guides))
|
||||
for (const guide of allHelpGuides()) {
|
||||
expect(listed.has(guide.id), `guide "${guide.id}" is not listed by any context`).toBe(true)
|
||||
expect(HELP_CONTEXTS.has(guide.context), `guide "${guide.id}" names unknown context "${guide.context}"`).toBe(true)
|
||||
expect(HELP_CONTEXTS.get(guide.context)!.guides).toContain(guide.id)
|
||||
}
|
||||
for (const ctx of HELP_CONTEXTS.values()) {
|
||||
for (const id of ctx.guides) expect(HELP_GUIDES.has(id), `context "${ctx.id}" lists unknown guide "${id}"`).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
it('hangs sub-screens under screens that exist, never under themselves', () => {
|
||||
for (const ctx of HELP_CONTEXTS.values()) {
|
||||
if (!ctx.parent) continue
|
||||
expect(HELP_CONTEXTS.has(ctx.parent), `context "${ctx.id}" names unknown parent "${ctx.parent}"`).toBe(true)
|
||||
expect(ctx.parent).not.toBe(ctx.id)
|
||||
expect(helpContextTrail(ctx).map(c => c.id)).toEqual([ctx.parent, ctx.id])
|
||||
}
|
||||
expect(topLevelHelpContexts().every(c => !c.parent)).toBe(true)
|
||||
expect(childHelpContexts('journey').map(c => c.id)).toEqual(['journey-detail', 'journey-studio'])
|
||||
})
|
||||
|
||||
it('only relates guides to guides that exist', () => {
|
||||
for (const guide of allHelpGuides()) {
|
||||
for (const rel of guide.related ?? []) {
|
||||
expect(HELP_GUIDES.has(rel), `guide "${guide.id}" relates to unknown "${rel}"`).toBe(true)
|
||||
expect(rel).not.toBe(guide.id)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('never registers two guides under one id (the pictures live by id)', () => {
|
||||
// Every context file's list, before the registry folds them into a map.
|
||||
const lists = [
|
||||
dashboardGuides, vacayGuides, atlasGuides, collectionsGuides, journeyGuides, journalGuides, studioGuides,
|
||||
settingsGuides, adminGuides, tripGuides, tripDaysGuides, tripPlacesGuides,
|
||||
]
|
||||
const ids = lists.flat().map(g => g.id)
|
||||
const dupes = ids.filter((id, i) => ids.indexOf(id) !== i)
|
||||
expect(dupes).toEqual([])
|
||||
expect(allHelpGuides()).toHaveLength(ids.length)
|
||||
})
|
||||
|
||||
it('has sane counts on every guide', () => {
|
||||
for (const guide of allHelpGuides()) {
|
||||
expect(guide.steps, guide.id).toBeGreaterThan(0)
|
||||
expect(guide.tips, guide.id).toBeGreaterThanOrEqual(0)
|
||||
expect(['quick', 'guide', 'tour']).toContain(guide.size)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('help registry: every word exists in the canonical locale', () => {
|
||||
it('contexts', () => {
|
||||
for (const ctx of HELP_CONTEXTS.values()) {
|
||||
expect(has(ctxKey(ctx.id, 'title')), ctxKey(ctx.id, 'title')).toBe(true)
|
||||
expect(has(ctxKey(ctx.id, 'summary')), ctxKey(ctx.id, 'summary')).toBe(true)
|
||||
for (let n = 1; n <= ctx.bullets; n++) expect(has(ctxBulletKey(ctx.id, n)), ctxBulletKey(ctx.id, n)).toBe(true)
|
||||
expect(has(ctxBulletKey(ctx.id, ctx.bullets + 1)), 'no bullet beyond the declared count').toBe(false)
|
||||
}
|
||||
})
|
||||
|
||||
it('guides', () => {
|
||||
for (const guide of allHelpGuides()) {
|
||||
for (const part of ['title', 'goal', 'result'] as const) expect(has(guideKey(guide.id, part)), guideKey(guide.id, part)).toBe(true)
|
||||
for (let n = 1; n <= guide.steps; n++) expect(has(guideStepKey(guide.id, n)), guideStepKey(guide.id, n)).toBe(true)
|
||||
expect(has(guideStepKey(guide.id, guide.steps + 1)), `${guide.id}: no step beyond the declared count`).toBe(false)
|
||||
for (let n = 1; n <= guide.tips; n++) expect(has(guideTipKey(guide.id, n)), guideTipKey(guide.id, n)).toBe(true)
|
||||
expect(has(guideTipKey(guide.id, guide.tips + 1)), `${guide.id}: no tip beyond the declared count`).toBe(false)
|
||||
expect(has(guideKey(guide.id, 'link')), `${guide.id}: link label iff link`).toBe(Boolean(guide.link))
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('help registry: every promised picture is on disk', () => {
|
||||
it('context heroes', () => {
|
||||
for (const ctx of HELP_CONTEXTS.values()) {
|
||||
if (ctx.hero) expect(onDisk(helpMedia.hero(ctx.id)), helpMedia.hero(ctx.id)).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
it('guide steps and results', () => {
|
||||
for (const guide of allHelpGuides()) {
|
||||
if (guide.media.steps) {
|
||||
for (let n = 1; n <= guide.steps; n++) expect(onDisk(helpMedia.step(guide.id, n)), helpMedia.step(guide.id, n)).toBe(true)
|
||||
}
|
||||
if (guide.media.result) expect(onDisk(helpMedia.result(guide.id)), helpMedia.result(guide.id)).toBe(true)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('help registry: helpers', () => {
|
||||
it('builds media paths under /help-media as webp', () => {
|
||||
expect(helpMedia.hero('dashboard')).toBe('/help-media/ctx/dashboard.webp')
|
||||
expect(helpMedia.step('create-trip', 3)).toBe('/help-media/create-trip/step-3.webp')
|
||||
expect(helpMedia.result('create-trip')).toBe('/help-media/create-trip/result.webp')
|
||||
})
|
||||
|
||||
it('builds wiki routes with and without an anchor', () => {
|
||||
expect(docsRoute({ slug: 'Creating-a-Trip' })).toBe('/help/Creating-a-Trip')
|
||||
expect(docsRoute({ slug: 'Creating-a-Trip', anchor: 'fields' })).toBe('/help/Creating-a-Trip#fields')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,102 @@
|
||||
import type { HelpContext, HelpGuide } from './types'
|
||||
import { dashboardContext, dashboardGuides } from './contexts/dashboard'
|
||||
import { vacayContext, vacayGuides } from './contexts/vacay'
|
||||
import { atlasContext, atlasGuides } from './contexts/atlas'
|
||||
import { collectionsContext, collectionsGuides } from './contexts/collections'
|
||||
import { journeyContext, journeyGuides, journalContext, journalGuides, studioContext, studioGuides } from './contexts/journey'
|
||||
import { settingsContext, settingsTabContexts, settingsGuides } from './contexts/settings'
|
||||
import { adminContext, adminTabContexts, adminGuides } from './contexts/admin'
|
||||
import { tripContext, tripGuides } from './contexts/trip'
|
||||
import { tripPlacesContext, tripPlacesGuides } from './contexts/tripPlaces'
|
||||
import { tripDaysContext, tripDaysGuides } from './contexts/tripDays'
|
||||
|
||||
/**
|
||||
* Everything the help center knows, composed from one file per screen under
|
||||
* `contexts/`. A screen announces which context it is with `<HelpAnchor id>`
|
||||
* (or `useHelpContext`), and the panel looks the rest up here.
|
||||
*/
|
||||
|
||||
// The order of the screen switcher: the trip right under My Trips, where it is opened from.
|
||||
const CONTEXT_LIST: HelpContext[] = [
|
||||
dashboardContext, tripContext, tripDaysContext, tripPlacesContext, vacayContext, atlasContext, collectionsContext, journeyContext, journalContext, studioContext,
|
||||
settingsContext, ...settingsTabContexts, adminContext, ...adminTabContexts,
|
||||
]
|
||||
const GUIDE_LIST: HelpGuide[] = [
|
||||
...dashboardGuides, ...vacayGuides, ...atlasGuides, ...collectionsGuides, ...journeyGuides, ...journalGuides, ...studioGuides,
|
||||
...settingsGuides, ...adminGuides, ...tripGuides, ...tripDaysGuides, ...tripPlacesGuides,
|
||||
]
|
||||
|
||||
export const HELP_CONTEXTS: ReadonlyMap<string, HelpContext> = new Map(CONTEXT_LIST.map(c => [c.id, c]))
|
||||
export const HELP_GUIDES: ReadonlyMap<string, HelpGuide> = new Map(GUIDE_LIST.map(g => [g.id, g]))
|
||||
|
||||
export function getHelpContext(id: string | null | undefined): HelpContext | null {
|
||||
return id ? (HELP_CONTEXTS.get(id) ?? null) : null
|
||||
}
|
||||
|
||||
export function getHelpGuide(id: string | null | undefined): HelpGuide | null {
|
||||
return id ? (HELP_GUIDES.get(id) ?? null) : null
|
||||
}
|
||||
|
||||
/** The guides of a context, in the order the context lists them. */
|
||||
export function guidesFor(context: HelpContext): HelpGuide[] {
|
||||
return context.guides.map(id => HELP_GUIDES.get(id)).filter((g): g is HelpGuide => g !== undefined)
|
||||
}
|
||||
|
||||
export function allHelpGuides(): HelpGuide[] {
|
||||
return GUIDE_LIST
|
||||
}
|
||||
|
||||
/** Every screen that has help, in registration order, for the screen switcher. */
|
||||
export function allHelpContexts(): HelpContext[] {
|
||||
return CONTEXT_LIST
|
||||
}
|
||||
|
||||
/** The screens that stand on their own; their sub-screens hang under them in the switcher. */
|
||||
export function topLevelHelpContexts(): HelpContext[] {
|
||||
return CONTEXT_LIST.filter(c => !c.parent)
|
||||
}
|
||||
|
||||
/** The sub-screens of a screen, in registration order. */
|
||||
export function childHelpContexts(parentId: string): HelpContext[] {
|
||||
return CONTEXT_LIST.filter(c => c.parent === parentId)
|
||||
}
|
||||
|
||||
/** The screen and the screens above it, outermost first, for a breadcrumb. */
|
||||
export function helpContextTrail(context: HelpContext): HelpContext[] {
|
||||
const trail: HelpContext[] = [context]
|
||||
let parent = context.parent ? HELP_CONTEXTS.get(context.parent) : undefined
|
||||
while (parent && !trail.includes(parent)) {
|
||||
trail.unshift(parent)
|
||||
parent = parent.parent ? HELP_CONTEXTS.get(parent.parent) : undefined
|
||||
}
|
||||
return trail
|
||||
}
|
||||
|
||||
// ── Catalogue keys ──────────────────────────────────────────────────────────
|
||||
|
||||
export const ctxKey = (id: string, part: 'title' | 'summary'): string => `help.ctx.${id}.${part}`
|
||||
export const ctxBulletKey = (id: string, n: number): string => `help.ctx.${id}.bullet.${n}`
|
||||
export const guideKey = (id: string, part: 'title' | 'goal' | 'result' | 'link'): string =>
|
||||
`help.guide.${id}.${part}`
|
||||
export const guideStepKey = (id: string, n: number): string => `help.guide.${id}.step.${n}`
|
||||
export const guideTipKey = (id: string, n: number): string => `help.guide.${id}.tip.${n}`
|
||||
|
||||
// ── Media paths ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Generated artwork under `public/help-media/`. WebP and WebM on purpose: the
|
||||
* service worker precaches `png`, and a couple of hundred help pictures do not
|
||||
* belong in every visitor's first download.
|
||||
*/
|
||||
const MEDIA_BASE = '/help-media'
|
||||
|
||||
export const helpMedia = {
|
||||
hero: (contextId: string): string => `${MEDIA_BASE}/ctx/${contextId}.webp`,
|
||||
step: (guideId: string, n: number): string => `${MEDIA_BASE}/${guideId}/step-${n}.webp`,
|
||||
result: (guideId: string): string => `${MEDIA_BASE}/${guideId}/result.webp`,
|
||||
}
|
||||
|
||||
/** Wiki route for a doc link, with the heading anchor when the link names one. */
|
||||
export function docsRoute(link: { slug: string; anchor?: string }): string {
|
||||
return `/help/${link.slug}${link.anchor ? `#${link.anchor}` : ''}`
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* The help center's content model.
|
||||
*
|
||||
* Every screen ("mask") in TREK registers a help context; a context owns a set
|
||||
* of guides. The words live in the i18n catalogue under `help.ctx.<id>.*` and
|
||||
* `help.guide.<id>.*`, the pictures and walkthroughs under
|
||||
* `public/help-media/`, generated by the Playwright run in `e2e/help/` from
|
||||
* the same guide definitions. Nothing here is prose: this file only says what
|
||||
* exists, so a guide that is registered but has no text or no picture fails a
|
||||
* test instead of rendering a raw key.
|
||||
*/
|
||||
|
||||
/** How much of a task a guide covers; drives the chip and the media it carries. */
|
||||
export type HelpGuideSize = 'quick' | 'guide' | 'tour'
|
||||
|
||||
export interface HelpDocLink {
|
||||
/** Wiki slug, e.g. `Creating-a-Trip`. */
|
||||
slug: string
|
||||
/** GitHub-style heading anchor inside that page, without the `#`. */
|
||||
anchor?: string
|
||||
}
|
||||
|
||||
export interface HelpGuideMedia {
|
||||
/** One focus shot per step, `step-<n>.webp`. */
|
||||
steps: boolean
|
||||
/** The state after the last step, `result.webp`. */
|
||||
result: boolean
|
||||
}
|
||||
|
||||
/** Icon names the help components know how to draw (see components/Help/helpIcons.ts). */
|
||||
export type HelpIcon =
|
||||
| 'plus'
|
||||
| 'pencil'
|
||||
| 'image'
|
||||
| 'copy'
|
||||
| 'archive'
|
||||
| 'trash'
|
||||
| 'filter'
|
||||
| 'calendar'
|
||||
| 'layout'
|
||||
| 'coins'
|
||||
| 'clock'
|
||||
| 'compass'
|
||||
| 'calendarCheck'
|
||||
| 'halfDay'
|
||||
| 'hourglass'
|
||||
| 'calculator'
|
||||
| 'calendarRange'
|
||||
| 'building'
|
||||
| 'flag'
|
||||
| 'graduationCap'
|
||||
| 'calendarOff'
|
||||
| 'calendarDays'
|
||||
| 'repeat'
|
||||
| 'userPlus'
|
||||
| 'share'
|
||||
| 'globe'
|
||||
| 'mapPin'
|
||||
| 'search'
|
||||
| 'star'
|
||||
| 'eye'
|
||||
| 'regions'
|
||||
| 'chart'
|
||||
| 'minusCircle'
|
||||
| 'landmark'
|
||||
| 'download'
|
||||
| 'checkCircle'
|
||||
| 'panelRight'
|
||||
| 'tags'
|
||||
| 'upload'
|
||||
| 'fileInput'
|
||||
| 'library'
|
||||
| 'bookmark'
|
||||
| 'bookOpen'
|
||||
| 'sparkles'
|
||||
| 'thumbsUp'
|
||||
| 'gallery'
|
||||
| 'link'
|
||||
| 'book'
|
||||
| 'sliders'
|
||||
| 'palette'
|
||||
| 'map'
|
||||
| 'bell'
|
||||
| 'plug'
|
||||
| 'cloudOff'
|
||||
| 'user'
|
||||
| 'lock'
|
||||
| 'shield'
|
||||
| 'fingerprint'
|
||||
| 'key'
|
||||
| 'route'
|
||||
| 'users'
|
||||
| 'userRound'
|
||||
| 'crown'
|
||||
| 'panelLeft'
|
||||
| 'undo'
|
||||
| 'ticket'
|
||||
| 'train'
|
||||
| 'listChecks'
|
||||
| 'wallet'
|
||||
| 'folder'
|
||||
| 'messages'
|
||||
|
||||
export interface HelpGuide {
|
||||
id: string
|
||||
/** The context the guide belongs to; a guide may be reached from others via `related`. */
|
||||
context: string
|
||||
icon: HelpIcon
|
||||
size: HelpGuideSize
|
||||
/** Number of steps; the texts are `help.guide.<id>.step.1` … `.step.<steps>`. */
|
||||
steps: number
|
||||
/** Number of "good to know" notes; `help.guide.<id>.tip.1` … */
|
||||
tips: number
|
||||
media: HelpGuideMedia
|
||||
/** Where the reference documentation continues. */
|
||||
docs?: HelpDocLink
|
||||
/** An in-app route the guide can take the reader to; label is `help.guide.<id>.link`. */
|
||||
link?: string
|
||||
related?: string[]
|
||||
}
|
||||
|
||||
export interface HelpContext {
|
||||
id: string
|
||||
icon: HelpIcon
|
||||
/**
|
||||
* The screen this one lives under, for the screen switcher: a journal is
|
||||
* reached from the Journey list, a settings tab from Settings. Top-level
|
||||
* screens have none.
|
||||
*/
|
||||
parent?: string
|
||||
/**
|
||||
* Where the screen is in the app, for the "go there" button. Screens that
|
||||
* need an id in the address (a journal, the studio) have none.
|
||||
*/
|
||||
route?: string
|
||||
/** Number of "on this screen" bullets; `help.ctx.<id>.bullet.1` … */
|
||||
bullets: number
|
||||
/** Guide ids in display order. */
|
||||
guides: string[]
|
||||
/** Reference pages for the "in the docs" list. */
|
||||
docs: HelpDocLink[]
|
||||
/** Whether `help-media/ctx/<id>.webp` exists. */
|
||||
hero: boolean
|
||||
}
|
||||
Reference in New Issue
Block a user