Help Modal
This commit is contained in:
1 parent
8e8aa5f27c
commit
8d987d1526
11 files changed
+2405
-58
No files matched your search
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "animatr",
|
"name": "ruwio",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.10.7",
|
"version": "0.11.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
Executable
+44
@@ -0,0 +1,44 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Bump the version in package.json. Usage:
|
||||||
|
#
|
||||||
|
# bash scripts/bump.sh patch # 0.10.7 -> 0.10.8
|
||||||
|
# bash scripts/bump.sh minor # 0.10.7 -> 0.11.0
|
||||||
|
# bash scripts/bump.sh major # 0.10.7 -> 1.0.0
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
KIND="${1:-}"
|
||||||
|
case "$KIND" in
|
||||||
|
patch|minor|major) ;;
|
||||||
|
*)
|
||||||
|
echo "Usage: $0 {patch|minor|major}" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
NEW_VERSION=$(node -e "
|
||||||
|
const fs = require('fs');
|
||||||
|
const pkgPath = './package.json';
|
||||||
|
const verPath = './src/version.ts';
|
||||||
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
||||||
|
const m = /^(\d+)\.(\d+)\.(\d+)\$/.exec(pkg.version);
|
||||||
|
if (!m) { console.error('unparseable version:', pkg.version); process.exit(1); }
|
||||||
|
let [_, a, b, c] = m.map((x, i) => i === 0 ? x : Number(x));
|
||||||
|
if (process.argv[1] === 'patch') c += 1;
|
||||||
|
else if (process.argv[1] === 'minor') { b += 1; c = 0; }
|
||||||
|
else if (process.argv[1] === 'major') { a += 1; b = 0; c = 0; }
|
||||||
|
const next = a + '.' + b + '.' + c;
|
||||||
|
pkg.version = next;
|
||||||
|
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
||||||
|
const ver = fs.readFileSync(verPath, 'utf8');
|
||||||
|
const updated = ver.replace(
|
||||||
|
/(APP_VERSION\s*=\s*)(['\"])[^'\"]*\2/,
|
||||||
|
(_m, lhs, q) => lhs + q + next + q,
|
||||||
|
);
|
||||||
|
if (updated === ver) { console.error('APP_VERSION not found in', verPath); process.exit(1); }
|
||||||
|
fs.writeFileSync(verPath, updated);
|
||||||
|
process.stdout.write(next);
|
||||||
|
" "$KIND")
|
||||||
|
|
||||||
|
echo "Bumped to $NEW_VERSION"
|
||||||
@@ -13,12 +13,14 @@ import {
|
|||||||
PhPause,
|
PhPause,
|
||||||
PhPencilSimpleLine,
|
PhPencilSimpleLine,
|
||||||
PhPlay,
|
PhPlay,
|
||||||
|
PhQuestion,
|
||||||
PhSkipBack,
|
PhSkipBack,
|
||||||
PhSparkle,
|
PhSparkle,
|
||||||
PhStop,
|
PhStop,
|
||||||
} from '@phosphor-icons/vue'
|
} from '@phosphor-icons/vue'
|
||||||
import AboutDialog from './components/AboutDialog.vue'
|
import AboutDialog from './components/AboutDialog.vue'
|
||||||
import AssetSidebar from './components/AssetSidebar.vue'
|
import AssetSidebar from './components/AssetSidebar.vue'
|
||||||
|
import HelpDialog from './components/HelpDialog.vue'
|
||||||
import CanvasStage from './components/CanvasStage.vue'
|
import CanvasStage from './components/CanvasStage.vue'
|
||||||
import ClipartDrawer from './components/ClipartDrawer.vue'
|
import ClipartDrawer from './components/ClipartDrawer.vue'
|
||||||
import SceneDrawer from './components/SceneDrawer.vue'
|
import SceneDrawer from './components/SceneDrawer.vue'
|
||||||
@@ -59,6 +61,7 @@ const testsOpen = ref(false)
|
|||||||
const welcomeOpen = ref(false)
|
const welcomeOpen = ref(false)
|
||||||
const fullscreenOpen = ref(false)
|
const fullscreenOpen = ref(false)
|
||||||
const aboutOpen = ref(false)
|
const aboutOpen = ref(false)
|
||||||
|
const helpOpen = ref(false)
|
||||||
const TIMELINE_HEIGHT_KEY = 'animatr:timelineHeight'
|
const TIMELINE_HEIGHT_KEY = 'animatr:timelineHeight'
|
||||||
const DEFAULT_TIMELINE_HEIGHT = 220
|
const DEFAULT_TIMELINE_HEIGHT = 220
|
||||||
const MIN_TIMELINE_HEIGHT = 120
|
const MIN_TIMELINE_HEIGHT = 120
|
||||||
@@ -501,6 +504,9 @@ onBeforeUnmount(() => {
|
|||||||
<PhFloppyDisk :size="16" />
|
<PhFloppyDisk :size="16" />
|
||||||
<span v-if="isDirty" class="dirty-dot" aria-hidden="true"></span>
|
<span v-if="isDirty" class="dirty-dot" aria-hidden="true"></span>
|
||||||
</button>
|
</button>
|
||||||
|
<button class="icon-btn" v-tooltip="t('help.openTooltip')" @click="helpOpen = true">
|
||||||
|
<PhQuestion :size="16" />
|
||||||
|
</button>
|
||||||
<button class="icon-btn" v-tooltip="t('app.documentSettings')" @click="settingsOpen = true">
|
<button class="icon-btn" v-tooltip="t('app.documentSettings')" @click="settingsOpen = true">
|
||||||
<PhGearSix :size="16" />
|
<PhGearSix :size="16" />
|
||||||
</button>
|
</button>
|
||||||
@@ -556,6 +562,7 @@ onBeforeUnmount(() => {
|
|||||||
<TestOverlay v-model="testsOpen" />
|
<TestOverlay v-model="testsOpen" />
|
||||||
<FullscreenPreview v-model="fullscreenOpen" />
|
<FullscreenPreview v-model="fullscreenOpen" />
|
||||||
<AboutDialog v-model="aboutOpen" />
|
<AboutDialog v-model="aboutOpen" />
|
||||||
|
<HelpDialog v-model="helpOpen" />
|
||||||
<DialogsHost />
|
<DialogsHost />
|
||||||
<WelcomeDialog
|
<WelcomeDialog
|
||||||
v-model="welcomeOpen"
|
v-model="welcomeOpen"
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {
|
|||||||
PhFolder,
|
PhFolder,
|
||||||
PhFrameCorners,
|
PhFrameCorners,
|
||||||
PhMoon,
|
PhMoon,
|
||||||
PhPalette,
|
|
||||||
PhSlidersHorizontal,
|
PhSlidersHorizontal,
|
||||||
PhSun,
|
PhSun,
|
||||||
PhX,
|
PhX,
|
||||||
@@ -18,7 +17,8 @@ import { useTooltipPref } from '@/composables/useTooltipPref'
|
|||||||
import { useAssetThumbsPref } from '@/composables/useAssetThumbsPref'
|
import { useAssetThumbsPref } from '@/composables/useAssetThumbsPref'
|
||||||
import { showLocked } from '@/composables/useShowLocked'
|
import { showLocked } from '@/composables/useShowLocked'
|
||||||
import { Modal, confirmDialog, promptDialog } from '@/components/ui'
|
import { Modal, confirmDialog, promptDialog } from '@/components/ui'
|
||||||
import { t } from '@/i18n'
|
import { setLocale, t, useI18n } from '@/i18n'
|
||||||
|
import type { Locale } from '@/i18n'
|
||||||
|
|
||||||
const props = defineProps<{ modelValue: boolean }>()
|
const props = defineProps<{ modelValue: boolean }>()
|
||||||
const emit = defineEmits<{ 'update:modelValue': [value: boolean] }>()
|
const emit = defineEmits<{ 'update:modelValue': [value: boolean] }>()
|
||||||
@@ -28,6 +28,7 @@ const clipart = useClipartStore()
|
|||||||
const { theme, setTheme } = useTheme()
|
const { theme, setTheme } = useTheme()
|
||||||
const { enabled: tooltipsEnabled } = useTooltipPref()
|
const { enabled: tooltipsEnabled } = useTooltipPref()
|
||||||
const { enabled: thumbsEnabled } = useAssetThumbsPref()
|
const { enabled: thumbsEnabled } = useAssetThumbsPref()
|
||||||
|
const { locale } = useI18n()
|
||||||
|
|
||||||
// Re-check the stored clipart folder whenever settings opens — covers the
|
// Re-check the stored clipart folder whenever settings opens — covers the
|
||||||
// case where initial App.vue init raced ahead of IDB or the user opens
|
// case where initial App.vue init raced ahead of IDB or the user opens
|
||||||
@@ -39,13 +40,12 @@ watch(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
type TabId = 'document' | 'clipart' | 'interface' | 'theme'
|
type TabId = 'document' | 'clipart' | 'interface'
|
||||||
|
|
||||||
const tabs: { id: TabId; label: string; icon: typeof PhFrameCorners }[] = [
|
const tabs: { id: TabId; labelKey: string; icon: typeof PhFrameCorners }[] = [
|
||||||
{ id: 'document', label: t('settings.tab.document'), icon: PhFrameCorners },
|
{ id: 'document', labelKey: 'settings.tab.document', icon: PhFrameCorners },
|
||||||
{ id: 'clipart', label: t('settings.tab.clipart'), icon: PhFolder },
|
{ id: 'clipart', labelKey: 'settings.tab.clipart', icon: PhFolder },
|
||||||
{ id: 'interface', label: t('settings.tab.interface'), icon: PhSlidersHorizontal },
|
{ id: 'interface', labelKey: 'settings.tab.interface', icon: PhSlidersHorizontal },
|
||||||
{ id: 'theme', label: t('settings.tab.theme'), icon: PhPalette },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
const activeTab = ref<TabId>('document')
|
const activeTab = ref<TabId>('document')
|
||||||
@@ -195,6 +195,11 @@ const fps = computed({
|
|||||||
set: (v: number) => store.setFps(v),
|
set: (v: number) => store.setFps(v),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const selectedLocale = computed({
|
||||||
|
get: () => locale.value,
|
||||||
|
set: (value: Locale) => setLocale(value),
|
||||||
|
})
|
||||||
|
|
||||||
const presets: { label: string; w: number; h: number }[] = [
|
const presets: { label: string; w: number; h: number }[] = [
|
||||||
{ label: '16:9 · 1280×720', w: 1280, h: 720 },
|
{ label: '16:9 · 1280×720', w: 1280, h: 720 },
|
||||||
{ label: '16:9 · 1920×1080', w: 1920, h: 1080 },
|
{ label: '16:9 · 1920×1080', w: 1920, h: 1080 },
|
||||||
@@ -221,6 +226,15 @@ function applyTheme(t: Theme) {
|
|||||||
:model-value="modelValue"
|
:model-value="modelValue"
|
||||||
:title="t('settings.title')"
|
:title="t('settings.title')"
|
||||||
size="lg"
|
size="lg"
|
||||||
|
:dialog-style="{
|
||||||
|
width: 'min(920px, calc(100% - 32px))',
|
||||||
|
height: 'min(620px, calc(100vh - 48px))',
|
||||||
|
}"
|
||||||
|
:body-style="{
|
||||||
|
display: 'flex',
|
||||||
|
overflow: 'hidden',
|
||||||
|
minHeight: 0,
|
||||||
|
}"
|
||||||
@update:model-value="emit('update:modelValue', $event)"
|
@update:model-value="emit('update:modelValue', $event)"
|
||||||
>
|
>
|
||||||
<div class="body">
|
<div class="body">
|
||||||
@@ -233,7 +247,7 @@ function applyTheme(t: Theme) {
|
|||||||
@click="activeTab = tab.id"
|
@click="activeTab = tab.id"
|
||||||
>
|
>
|
||||||
<component :is="tab.icon" :size="14" />
|
<component :is="tab.icon" :size="14" />
|
||||||
{{ tab.label }}
|
{{ t(tab.labelKey) }}
|
||||||
</button>
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
@@ -362,6 +376,18 @@ function applyTheme(t: Theme) {
|
|||||||
|
|
||||||
<section v-else-if="activeTab === 'interface'">
|
<section v-else-if="activeTab === 'interface'">
|
||||||
<h3>{{ t('settings.tab.interface') }}</h3>
|
<h3>{{ t('settings.tab.interface') }}</h3>
|
||||||
|
<div class="setting-block">
|
||||||
|
<div class="setting-heading">{{ t('settings.languageTitle') }}</div>
|
||||||
|
<div class="setting-desc">{{ t('settings.languageDesc') }}</div>
|
||||||
|
<label class="setting-select">
|
||||||
|
<span class="setting-label">{{ t('settings.languageLabel') }}</span>
|
||||||
|
<select v-model="selectedLocale">
|
||||||
|
<option value="en">{{ t('settings.languageEnglish') }}</option>
|
||||||
|
<option value="de">{{ t('settings.languageGerman') }}</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<label class="row-toggle">
|
<label class="row-toggle">
|
||||||
<span class="switch" :class="{ on: tooltipsEnabled }">
|
<span class="switch" :class="{ on: tooltipsEnabled }">
|
||||||
<input type="checkbox" v-model="tooltipsEnabled" />
|
<input type="checkbox" v-model="tooltipsEnabled" />
|
||||||
@@ -391,41 +417,41 @@ function applyTheme(t: Theme) {
|
|||||||
<div class="row-toggle-desc">{{ t('settings.assetThumbsDesc') }}</div>
|
<div class="row-toggle-desc">{{ t('settings.assetThumbsDesc') }}</div>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
</section>
|
|
||||||
|
|
||||||
<section v-else-if="activeTab === 'theme'">
|
<div class="setting-block">
|
||||||
<h3>{{ t('settings.tab.theme') }}</h3>
|
<div class="setting-heading">{{ t('settings.appearanceTitle') }}</div>
|
||||||
<div class="theme-grid">
|
<div class="setting-desc">{{ t('settings.themeHint') }}</div>
|
||||||
<button
|
<div class="theme-grid">
|
||||||
class="theme-card"
|
<button
|
||||||
:class="{ active: theme === 'dark' }"
|
class="theme-card"
|
||||||
@click="applyTheme('dark')"
|
:class="{ active: theme === 'dark' }"
|
||||||
>
|
@click="applyTheme('dark')"
|
||||||
<div class="theme-preview theme-preview-dark">
|
>
|
||||||
<div class="tp-panel"></div>
|
<div class="theme-preview theme-preview-dark">
|
||||||
<div class="tp-accent"></div>
|
<div class="tp-panel"></div>
|
||||||
</div>
|
<div class="tp-accent"></div>
|
||||||
<div class="theme-label">
|
</div>
|
||||||
<PhMoon :size="14" />
|
<div class="theme-label">
|
||||||
<span>{{ t('settings.themeDark') }}</span>
|
<PhMoon :size="14" />
|
||||||
</div>
|
<span>{{ t('settings.themeDark') }}</span>
|
||||||
</button>
|
</div>
|
||||||
<button
|
</button>
|
||||||
class="theme-card"
|
<button
|
||||||
:class="{ active: theme === 'light' }"
|
class="theme-card"
|
||||||
@click="applyTheme('light')"
|
:class="{ active: theme === 'light' }"
|
||||||
>
|
@click="applyTheme('light')"
|
||||||
<div class="theme-preview theme-preview-light">
|
>
|
||||||
<div class="tp-panel"></div>
|
<div class="theme-preview theme-preview-light">
|
||||||
<div class="tp-accent"></div>
|
<div class="tp-panel"></div>
|
||||||
</div>
|
<div class="tp-accent"></div>
|
||||||
<div class="theme-label">
|
</div>
|
||||||
<PhSun :size="14" />
|
<div class="theme-label">
|
||||||
<span>{{ t('settings.themeLight') }}</span>
|
<PhSun :size="14" />
|
||||||
</div>
|
<span>{{ t('settings.themeLight') }}</span>
|
||||||
</button>
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="hint">{{ t('settings.themeHint') }}</p>
|
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -438,7 +464,8 @@ function applyTheme(t: Theme) {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.body {
|
.body {
|
||||||
display: flex;
|
display: flex;
|
||||||
min-height: 340px;
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabs {
|
.tabs {
|
||||||
@@ -450,6 +477,7 @@ function applyTheme(t: Theme) {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
background: var(--panel-alt);
|
background: var(--panel-alt);
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab {
|
.tab {
|
||||||
@@ -478,6 +506,7 @@ function applyTheme(t: Theme) {
|
|||||||
padding: 16px 20px;
|
padding: 16px 20px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
section {
|
section {
|
||||||
@@ -605,6 +634,56 @@ input[type='number'] {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.setting-block {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background: var(--panel-alt);
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-heading {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-desc {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.45;
|
||||||
|
color: var(--text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-select {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-label {
|
||||||
|
color: var(--text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-select select {
|
||||||
|
width: 100%;
|
||||||
|
background: var(--bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
color: var(--text);
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font: inherit;
|
||||||
|
font-size: 13px;
|
||||||
|
text-transform: none;
|
||||||
|
letter-spacing: normal;
|
||||||
|
}
|
||||||
|
|
||||||
/* Switch + two-line label. Defensive resets defeat the scoped
|
/* Switch + two-line label. Defensive resets defeat the scoped
|
||||||
blanket `label { flex-direction: column; text-transform: uppercase;
|
blanket `label { flex-direction: column; text-transform: uppercase;
|
||||||
font-size: 10px; flex: 1 1 0; }` that form-row labels in this file
|
font-size: 10px; flex: 1 1 0; }` that form-row labels in this file
|
||||||
|
|||||||
@@ -0,0 +1,918 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, nextTick, ref, watch } from 'vue'
|
||||||
|
import type { Component } from 'vue'
|
||||||
|
import {
|
||||||
|
PhCode,
|
||||||
|
PhCursorClick,
|
||||||
|
PhExport,
|
||||||
|
PhFilmStrip,
|
||||||
|
PhFrameCorners,
|
||||||
|
PhImageSquare,
|
||||||
|
PhListBullets,
|
||||||
|
PhMusicNote,
|
||||||
|
PhPencilSimpleLine,
|
||||||
|
PhQuestion,
|
||||||
|
PhSlidersHorizontal,
|
||||||
|
PhSparkle,
|
||||||
|
} from '@phosphor-icons/vue'
|
||||||
|
import { Modal } from '@/components/ui'
|
||||||
|
import { t } from '@/i18n'
|
||||||
|
|
||||||
|
const props = defineProps<{ modelValue: boolean }>()
|
||||||
|
const emit = defineEmits<{ 'update:modelValue': [value: boolean] }>()
|
||||||
|
|
||||||
|
type ChapterId =
|
||||||
|
| 'getting-started'
|
||||||
|
| 'stage'
|
||||||
|
| 'assets'
|
||||||
|
| 'layers-timeline'
|
||||||
|
| 'clips'
|
||||||
|
| 'buttons'
|
||||||
|
| 'scenes'
|
||||||
|
| 'audio'
|
||||||
|
| 'markers'
|
||||||
|
| 'scripts-actions'
|
||||||
|
| 'script-language'
|
||||||
|
| 'export'
|
||||||
|
| 'shortcuts'
|
||||||
|
|
||||||
|
interface HelpListItem {
|
||||||
|
bodyKey: string
|
||||||
|
icon?: Component
|
||||||
|
labelKey?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HelpTableRow {
|
||||||
|
effectKey: string
|
||||||
|
syntaxKey: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type HelpBlock =
|
||||||
|
| { kind: 'paragraph'; bodyKey: string; titleKey?: string }
|
||||||
|
| { items: HelpListItem[]; kind: 'list'; titleKey?: string }
|
||||||
|
| { items: string[]; kind: 'steps'; titleKey: string }
|
||||||
|
| { kind: 'table'; rows: HelpTableRow[]; titleKey: string }
|
||||||
|
| { bodyKey: string; kind: 'code'; titleKey: string }
|
||||||
|
| { kind: 'shortcuts' }
|
||||||
|
|
||||||
|
interface Chapter {
|
||||||
|
blocks: HelpBlock[]
|
||||||
|
icon: Component
|
||||||
|
id: ChapterId
|
||||||
|
introKey: string
|
||||||
|
titleKey: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const CHAPTERS: Chapter[] = [
|
||||||
|
{
|
||||||
|
id: 'getting-started',
|
||||||
|
titleKey: 'help.chapters.gettingStarted',
|
||||||
|
icon: PhSparkle,
|
||||||
|
introKey: 'help.content.gettingStarted.intro',
|
||||||
|
blocks: [
|
||||||
|
{
|
||||||
|
kind: 'paragraph',
|
||||||
|
titleKey: 'help.content.gettingStarted.modelTitle',
|
||||||
|
bodyKey: 'help.content.gettingStarted.modelBody',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'list',
|
||||||
|
titleKey: 'help.content.gettingStarted.whereTitle',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
icon: PhQuestion,
|
||||||
|
labelKey: 'help.content.gettingStarted.where.topBar.label',
|
||||||
|
bodyKey: 'help.content.gettingStarted.where.topBar.body',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: PhImageSquare,
|
||||||
|
labelKey: 'help.content.gettingStarted.where.assets.label',
|
||||||
|
bodyKey: 'help.content.gettingStarted.where.assets.body',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: PhFrameCorners,
|
||||||
|
labelKey: 'help.content.gettingStarted.where.stage.label',
|
||||||
|
bodyKey: 'help.content.gettingStarted.where.stage.body',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: PhSlidersHorizontal,
|
||||||
|
labelKey: 'help.content.gettingStarted.where.properties.label',
|
||||||
|
bodyKey: 'help.content.gettingStarted.where.properties.body',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: PhFilmStrip,
|
||||||
|
labelKey: 'help.content.gettingStarted.where.timeline.label',
|
||||||
|
bodyKey: 'help.content.gettingStarted.where.timeline.body',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: PhListBullets,
|
||||||
|
labelKey: 'help.content.gettingStarted.where.scenes.label',
|
||||||
|
bodyKey: 'help.content.gettingStarted.where.scenes.body',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'steps',
|
||||||
|
titleKey: 'help.content.gettingStarted.quickStartTitle',
|
||||||
|
items: [
|
||||||
|
'help.content.gettingStarted.quickStart.step1',
|
||||||
|
'help.content.gettingStarted.quickStart.step2',
|
||||||
|
'help.content.gettingStarted.quickStart.step3',
|
||||||
|
'help.content.gettingStarted.quickStart.step4',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'stage',
|
||||||
|
titleKey: 'help.chapters.stage',
|
||||||
|
icon: PhFrameCorners,
|
||||||
|
introKey: 'help.content.stage.intro',
|
||||||
|
blocks: [
|
||||||
|
{
|
||||||
|
kind: 'list',
|
||||||
|
titleKey: 'help.content.stage.viewTitle',
|
||||||
|
items: [
|
||||||
|
{ bodyKey: 'help.content.stage.view.zoom' },
|
||||||
|
{ bodyKey: 'help.content.stage.view.pan' },
|
||||||
|
{ bodyKey: 'help.content.stage.view.fit' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'list',
|
||||||
|
titleKey: 'help.content.stage.selectionTitle',
|
||||||
|
items: [
|
||||||
|
{ bodyKey: 'help.content.stage.selection.pick' },
|
||||||
|
{ bodyKey: 'help.content.stage.selection.clear' },
|
||||||
|
{ bodyKey: 'help.content.stage.selection.handles' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'paragraph',
|
||||||
|
titleKey: 'help.content.stage.lockingTitle',
|
||||||
|
bodyKey: 'help.content.stage.lockingBody',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'paragraph',
|
||||||
|
titleKey: 'help.content.stage.drawingTitle',
|
||||||
|
bodyKey: 'help.content.stage.drawingBody',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'assets',
|
||||||
|
titleKey: 'help.chapters.assets',
|
||||||
|
icon: PhImageSquare,
|
||||||
|
introKey: 'help.content.assets.intro',
|
||||||
|
blocks: [
|
||||||
|
{
|
||||||
|
kind: 'list',
|
||||||
|
titleKey: 'help.content.assets.sectionsTitle',
|
||||||
|
items: [
|
||||||
|
{ bodyKey: 'help.content.assets.sections.shapes', icon: PhSparkle },
|
||||||
|
{ bodyKey: 'help.content.assets.sections.images', icon: PhImageSquare },
|
||||||
|
{ bodyKey: 'help.content.assets.sections.clips', icon: PhFilmStrip },
|
||||||
|
{ bodyKey: 'help.content.assets.sections.buttons', icon: PhCursorClick },
|
||||||
|
{ bodyKey: 'help.content.assets.sections.audio', icon: PhMusicNote },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'paragraph',
|
||||||
|
titleKey: 'help.content.assets.thumbsTitle',
|
||||||
|
bodyKey: 'help.content.assets.thumbsBody',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'paragraph',
|
||||||
|
titleKey: 'help.content.assets.foldTitle',
|
||||||
|
bodyKey: 'help.content.assets.foldBody',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'layers-timeline',
|
||||||
|
titleKey: 'help.chapters.layersTimeline',
|
||||||
|
icon: PhFilmStrip,
|
||||||
|
introKey: 'help.content.layersTimeline.intro',
|
||||||
|
blocks: [
|
||||||
|
{
|
||||||
|
kind: 'list',
|
||||||
|
titleKey: 'help.content.layersTimeline.keyframesTitle',
|
||||||
|
items: [
|
||||||
|
{ bodyKey: 'help.content.layersTimeline.keyframes.add' },
|
||||||
|
{ bodyKey: 'help.content.layersTimeline.keyframes.drag' },
|
||||||
|
{ bodyKey: 'help.content.layersTimeline.keyframes.jump' },
|
||||||
|
{ bodyKey: 'help.content.layersTimeline.keyframes.menu' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'paragraph',
|
||||||
|
titleKey: 'help.content.layersTimeline.holdsTitle',
|
||||||
|
bodyKey: 'help.content.layersTimeline.holdsBody',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'paragraph',
|
||||||
|
titleKey: 'help.content.layersTimeline.separateTitle',
|
||||||
|
bodyKey: 'help.content.layersTimeline.separateBody',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'paragraph',
|
||||||
|
titleKey: 'help.content.layersTimeline.groupsTitle',
|
||||||
|
bodyKey: 'help.content.layersTimeline.groupsBody',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'paragraph',
|
||||||
|
titleKey: 'help.content.layersTimeline.snappingTitle',
|
||||||
|
bodyKey: 'help.content.layersTimeline.snappingBody',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'clips',
|
||||||
|
titleKey: 'help.chapters.clips',
|
||||||
|
icon: PhFilmStrip,
|
||||||
|
introKey: 'help.content.clips.intro',
|
||||||
|
blocks: [
|
||||||
|
{
|
||||||
|
kind: 'steps',
|
||||||
|
titleKey: 'help.content.clips.createTitle',
|
||||||
|
items: [
|
||||||
|
'help.content.clips.create.step1',
|
||||||
|
'help.content.clips.create.step2',
|
||||||
|
'help.content.clips.create.step3',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'paragraph',
|
||||||
|
titleKey: 'help.content.clips.editTitle',
|
||||||
|
bodyKey: 'help.content.clips.editBody',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'paragraph',
|
||||||
|
titleKey: 'help.content.clips.trimTitle',
|
||||||
|
bodyKey: 'help.content.clips.trimBody',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'paragraph',
|
||||||
|
titleKey: 'help.content.clips.transformTitle',
|
||||||
|
bodyKey: 'help.content.clips.transformBody',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'buttons',
|
||||||
|
titleKey: 'help.chapters.buttons',
|
||||||
|
icon: PhCursorClick,
|
||||||
|
introKey: 'help.content.buttons.intro',
|
||||||
|
blocks: [
|
||||||
|
{
|
||||||
|
kind: 'paragraph',
|
||||||
|
titleKey: 'help.content.buttons.editingTitle',
|
||||||
|
bodyKey: 'help.content.buttons.editingBody',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'paragraph',
|
||||||
|
titleKey: 'help.content.buttons.stateKeyframesTitle',
|
||||||
|
bodyKey: 'help.content.buttons.stateKeyframesBody',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'paragraph',
|
||||||
|
titleKey: 'help.content.buttons.stillFramesTitle',
|
||||||
|
bodyKey: 'help.content.buttons.stillFramesBody',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'paragraph',
|
||||||
|
titleKey: 'help.content.buttons.hitShapesTitle',
|
||||||
|
bodyKey: 'help.content.buttons.hitShapesBody',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'paragraph',
|
||||||
|
titleKey: 'help.content.buttons.actionsTitle',
|
||||||
|
bodyKey: 'help.content.buttons.actionsBody',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'paragraph',
|
||||||
|
titleKey: 'help.content.buttons.disabledTitle',
|
||||||
|
bodyKey: 'help.content.buttons.disabledBody',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'scenes',
|
||||||
|
titleKey: 'help.chapters.scenes',
|
||||||
|
icon: PhListBullets,
|
||||||
|
introKey: 'help.content.scenes.intro',
|
||||||
|
blocks: [
|
||||||
|
{
|
||||||
|
kind: 'paragraph',
|
||||||
|
titleKey: 'help.content.scenes.drawerTitle',
|
||||||
|
bodyKey: 'help.content.scenes.drawerBody',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'paragraph',
|
||||||
|
titleKey: 'help.content.scenes.switchingTitle',
|
||||||
|
bodyKey: 'help.content.scenes.switchingBody',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'paragraph',
|
||||||
|
titleKey: 'help.content.scenes.referencesTitle',
|
||||||
|
bodyKey: 'help.content.scenes.referencesBody',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'audio',
|
||||||
|
titleKey: 'help.chapters.audio',
|
||||||
|
icon: PhMusicNote,
|
||||||
|
introKey: 'help.content.audio.intro',
|
||||||
|
blocks: [
|
||||||
|
{
|
||||||
|
kind: 'steps',
|
||||||
|
titleKey: 'help.content.audio.addTitle',
|
||||||
|
items: [
|
||||||
|
'help.content.audio.add.step1',
|
||||||
|
'help.content.audio.add.step2',
|
||||||
|
'help.content.audio.add.step3',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'paragraph',
|
||||||
|
titleKey: 'help.content.audio.tracksTitle',
|
||||||
|
bodyKey: 'help.content.audio.tracksBody',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'paragraph',
|
||||||
|
titleKey: 'help.content.audio.movingTitle',
|
||||||
|
bodyKey: 'help.content.audio.movingBody',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'paragraph',
|
||||||
|
titleKey: 'help.content.audio.waveformsTitle',
|
||||||
|
bodyKey: 'help.content.audio.waveformsBody',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'paragraph',
|
||||||
|
titleKey: 'help.content.audio.urlsTitle',
|
||||||
|
bodyKey: 'help.content.audio.urlsBody',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'markers',
|
||||||
|
titleKey: 'help.chapters.markers',
|
||||||
|
icon: PhPencilSimpleLine,
|
||||||
|
introKey: 'help.content.markers.intro',
|
||||||
|
blocks: [
|
||||||
|
{
|
||||||
|
kind: 'list',
|
||||||
|
titleKey: 'help.content.markers.editingTitle',
|
||||||
|
items: [
|
||||||
|
{ bodyKey: 'help.content.markers.editing.add' },
|
||||||
|
{ bodyKey: 'help.content.markers.editing.drag' },
|
||||||
|
{ bodyKey: 'help.content.markers.editing.rename' },
|
||||||
|
{ bodyKey: 'help.content.markers.editing.delete' },
|
||||||
|
{ bodyKey: 'help.content.markers.editing.clear' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'paragraph',
|
||||||
|
titleKey: 'help.content.markers.sceneTitle',
|
||||||
|
bodyKey: 'help.content.markers.sceneBody',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'scripts-actions',
|
||||||
|
titleKey: 'help.chapters.scriptsActions',
|
||||||
|
icon: PhCode,
|
||||||
|
introKey: 'help.content.scriptsActions.intro',
|
||||||
|
blocks: [
|
||||||
|
{
|
||||||
|
kind: 'steps',
|
||||||
|
titleKey: 'help.content.scriptsActions.addTitle',
|
||||||
|
items: [
|
||||||
|
'help.content.scriptsActions.add.step1',
|
||||||
|
'help.content.scriptsActions.add.step2',
|
||||||
|
'help.content.scriptsActions.add.step3',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'list',
|
||||||
|
titleKey: 'help.content.scriptsActions.firingTitle',
|
||||||
|
items: [
|
||||||
|
{ bodyKey: 'help.content.scriptsActions.firing.forward' },
|
||||||
|
{ bodyKey: 'help.content.scriptsActions.firing.exact' },
|
||||||
|
{ bodyKey: 'help.content.scriptsActions.firing.order' },
|
||||||
|
{ bodyKey: 'help.content.scriptsActions.firing.replay' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'paragraph',
|
||||||
|
titleKey: 'help.content.scriptsActions.menuTitle',
|
||||||
|
bodyKey: 'help.content.scriptsActions.menuBody',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'list',
|
||||||
|
titleKey: 'help.content.scriptsActions.typesTitle',
|
||||||
|
items: [
|
||||||
|
{ bodyKey: 'help.content.scriptsActions.types.transport' },
|
||||||
|
{ bodyKey: 'help.content.scriptsActions.types.jump' },
|
||||||
|
{ bodyKey: 'help.content.scriptsActions.types.scene' },
|
||||||
|
{ bodyKey: 'help.content.scriptsActions.types.button' },
|
||||||
|
{ bodyKey: 'help.content.scriptsActions.types.script' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'script-language',
|
||||||
|
titleKey: 'help.chapters.scriptLanguage',
|
||||||
|
icon: PhCode,
|
||||||
|
introKey: 'help.content.scriptLanguage.intro',
|
||||||
|
blocks: [
|
||||||
|
{
|
||||||
|
kind: 'list',
|
||||||
|
titleKey: 'help.content.scriptLanguage.syntaxTitle',
|
||||||
|
items: [
|
||||||
|
{ bodyKey: 'help.content.scriptLanguage.syntax.onePerLine' },
|
||||||
|
{ bodyKey: 'help.content.scriptLanguage.syntax.comments' },
|
||||||
|
{ bodyKey: 'help.content.scriptLanguage.syntax.tokens' },
|
||||||
|
{ bodyKey: 'help.content.scriptLanguage.syntax.case' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'table',
|
||||||
|
titleKey: 'help.content.scriptLanguage.commandsTitle',
|
||||||
|
rows: [
|
||||||
|
{
|
||||||
|
syntaxKey: 'help.content.scriptLanguage.commands.play.syntax',
|
||||||
|
effectKey: 'help.content.scriptLanguage.commands.play.effect',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
syntaxKey: 'help.content.scriptLanguage.commands.pause.syntax',
|
||||||
|
effectKey: 'help.content.scriptLanguage.commands.pause.effect',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
syntaxKey: 'help.content.scriptLanguage.commands.rewind.syntax',
|
||||||
|
effectKey: 'help.content.scriptLanguage.commands.rewind.effect',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
syntaxKey: 'help.content.scriptLanguage.commands.jumpSeconds.syntax',
|
||||||
|
effectKey: 'help.content.scriptLanguage.commands.jumpSeconds.effect',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
syntaxKey: 'help.content.scriptLanguage.commands.jumpFrame.syntax',
|
||||||
|
effectKey: 'help.content.scriptLanguage.commands.jumpFrame.effect',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
syntaxKey: 'help.content.scriptLanguage.commands.jumpMarker.syntax',
|
||||||
|
effectKey: 'help.content.scriptLanguage.commands.jumpMarker.effect',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'code',
|
||||||
|
titleKey: 'help.content.scriptLanguage.exampleTitle',
|
||||||
|
bodyKey: 'help.content.scriptLanguage.exampleBody',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'paragraph',
|
||||||
|
titleKey: 'help.content.scriptLanguage.errorsTitle',
|
||||||
|
bodyKey: 'help.content.scriptLanguage.errorsBody',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'paragraph',
|
||||||
|
titleKey: 'help.content.scriptLanguage.notInDslTitle',
|
||||||
|
bodyKey: 'help.content.scriptLanguage.notInDslBody',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'export',
|
||||||
|
titleKey: 'help.chapters.export',
|
||||||
|
icon: PhExport,
|
||||||
|
introKey: 'help.content.export.intro',
|
||||||
|
blocks: [
|
||||||
|
{
|
||||||
|
kind: 'paragraph',
|
||||||
|
titleKey: 'help.content.export.editorPreviewTitle',
|
||||||
|
bodyKey: 'help.content.export.editorPreviewBody',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'paragraph',
|
||||||
|
titleKey: 'help.content.export.fullscreenPreviewTitle',
|
||||||
|
bodyKey: 'help.content.export.fullscreenPreviewBody',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'list',
|
||||||
|
titleKey: 'help.content.export.formatsTitle',
|
||||||
|
items: [
|
||||||
|
{ bodyKey: 'help.content.export.formats.runtime' },
|
||||||
|
{ bodyKey: 'help.content.export.formats.scene' },
|
||||||
|
{ bodyKey: 'help.content.export.formats.html' },
|
||||||
|
{ bodyKey: 'help.content.export.formats.mp4' },
|
||||||
|
{ bodyKey: 'help.content.export.formats.webm' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'paragraph',
|
||||||
|
titleKey: 'help.content.export.skipHiddenTitle',
|
||||||
|
bodyKey: 'help.content.export.skipHiddenBody',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: 'paragraph',
|
||||||
|
titleKey: 'help.content.export.assetsTitle',
|
||||||
|
bodyKey: 'help.content.export.assetsBody',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'shortcuts',
|
||||||
|
titleKey: 'help.chapters.shortcuts',
|
||||||
|
icon: PhQuestion,
|
||||||
|
introKey: 'help.content.shortcuts.intro',
|
||||||
|
blocks: [{ kind: 'shortcuts' }],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const activeChapter = ref<ChapterId>('getting-started')
|
||||||
|
const contentRef = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
|
const activeChapterDef = computed<Chapter>(
|
||||||
|
() => CHAPTERS.find((chapter) => chapter.id === activeChapter.value) ?? CHAPTERS[0],
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
async (open) => {
|
||||||
|
if (!open) return
|
||||||
|
activeChapter.value = 'getting-started'
|
||||||
|
await nextTick()
|
||||||
|
if (contentRef.value) contentRef.value.scrollTop = 0
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
function selectChapter(id: ChapterId) {
|
||||||
|
activeChapter.value = id
|
||||||
|
if (contentRef.value) contentRef.value.scrollTop = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const APP_SHORTCUTS = [
|
||||||
|
{ labelKey: 'help.shortcut.playPause', keys: 'Space' },
|
||||||
|
{ labelKey: 'help.shortcut.fullscreen', keys: 'Shift+Space' },
|
||||||
|
{ labelKey: 'help.shortcut.undo', keys: 'Ctrl/Cmd+Z' },
|
||||||
|
{ labelKey: 'help.shortcut.redo', keys: 'Ctrl/Cmd+Shift+Z' },
|
||||||
|
{ labelKey: 'help.shortcut.save', keys: 'Ctrl/Cmd+S' },
|
||||||
|
{ labelKey: 'help.shortcut.inPoint', keys: 'I' },
|
||||||
|
{ labelKey: 'help.shortcut.outPoint', keys: 'O' },
|
||||||
|
{ labelKey: 'help.shortcut.rewind', keys: 'R' },
|
||||||
|
{ labelKey: 'help.shortcut.addMarker', keys: 'M' },
|
||||||
|
{ labelKey: 'help.shortcut.deleteSelected', keys: 'Backspace / Delete' },
|
||||||
|
{ labelKey: 'help.shortcut.clearSelection', keys: 'Escape' },
|
||||||
|
{ labelKey: 'help.shortcut.panCanvas', keys: 'Middle-click / Ctrl+drag' },
|
||||||
|
{ labelKey: 'help.shortcut.zoomCanvas', keys: 'Ctrl/Cmd+wheel' },
|
||||||
|
{ labelKey: 'help.shortcut.pasteImage', keys: 'Ctrl/Cmd+V' },
|
||||||
|
{ labelKey: 'help.shortcut.tests', keys: 'Ctrl+Shift+Alt+T' },
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal
|
||||||
|
:model-value="modelValue"
|
||||||
|
size="xl"
|
||||||
|
:title="t('help.title')"
|
||||||
|
:dialog-style="{
|
||||||
|
width: 'min(1100px, calc(100% - 32px))',
|
||||||
|
height: 'min(760px, calc(100vh - 48px))',
|
||||||
|
}"
|
||||||
|
:body-style="{
|
||||||
|
display: 'flex',
|
||||||
|
overflow: 'hidden',
|
||||||
|
minHeight: 0,
|
||||||
|
}"
|
||||||
|
@update:model-value="emit('update:modelValue', $event)"
|
||||||
|
>
|
||||||
|
<div class="help">
|
||||||
|
<aside class="toc" :aria-label="t('help.tocLabel')">
|
||||||
|
<button
|
||||||
|
v-for="chapter in CHAPTERS"
|
||||||
|
:key="chapter.id"
|
||||||
|
class="toc-item"
|
||||||
|
:class="{ active: activeChapter === chapter.id }"
|
||||||
|
@click="selectChapter(chapter.id)"
|
||||||
|
>
|
||||||
|
<component :is="chapter.icon" :size="15" class="toc-icon" />
|
||||||
|
<span>{{ t(chapter.titleKey) }}</span>
|
||||||
|
</button>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<div ref="contentRef" class="content">
|
||||||
|
<section class="chapter">
|
||||||
|
<h3 class="chapter-title">{{ t(activeChapterDef.titleKey) }}</h3>
|
||||||
|
|
||||||
|
<p class="chapter-intro" v-html="t(activeChapterDef.introKey)" />
|
||||||
|
|
||||||
|
<template v-for="block in activeChapterDef.blocks" :key="JSON.stringify(block)">
|
||||||
|
<div v-if="block.kind === 'paragraph'" class="chapter-block">
|
||||||
|
<h4 v-if="block.titleKey">{{ t(block.titleKey) }}</h4>
|
||||||
|
<p v-html="t(block.bodyKey)" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="block.kind === 'list'" class="chapter-block">
|
||||||
|
<h4 v-if="block.titleKey">{{ t(block.titleKey) }}</h4>
|
||||||
|
<ul
|
||||||
|
class="bullets"
|
||||||
|
:class="block.items.some((i) => i.icon) ? 'bullets-icon' : 'bullets-disc'"
|
||||||
|
>
|
||||||
|
<li v-for="item in block.items" :key="item.bodyKey" class="bullet-item">
|
||||||
|
<span v-if="item.icon" class="bullet-icon">
|
||||||
|
<component :is="item.icon" :size="14" />
|
||||||
|
</span>
|
||||||
|
<div class="bullet-copy">
|
||||||
|
<strong v-if="item.labelKey">{{ t(item.labelKey) }}:</strong>
|
||||||
|
<span v-html="t(item.bodyKey)" />
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="block.kind === 'steps'" class="chapter-block">
|
||||||
|
<h4>{{ t(block.titleKey) }}</h4>
|
||||||
|
<ol class="bullets bullets-steps">
|
||||||
|
<li v-for="item in block.items" :key="item" v-html="t(item)" />
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="block.kind === 'table'" class="chapter-block">
|
||||||
|
<h4>{{ t(block.titleKey) }}</h4>
|
||||||
|
<table class="cmd-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{ t('help.table.syntax') }}</th>
|
||||||
|
<th>{{ t('help.table.effect') }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="row in block.rows" :key="row.syntaxKey">
|
||||||
|
<td v-html="t(row.syntaxKey)" />
|
||||||
|
<td v-html="t(row.effectKey)" />
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="block.kind === 'code'" class="chapter-block">
|
||||||
|
<h4>{{ t(block.titleKey) }}</h4>
|
||||||
|
<pre class="code">{{ t(block.bodyKey) }}</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="block.kind === 'shortcuts'" class="chapter-block">
|
||||||
|
<ul class="shortcuts">
|
||||||
|
<li v-for="shortcut in APP_SHORTCUTS" :key="shortcut.keys + shortcut.labelKey">
|
||||||
|
<span class="shortcut-label">{{ t(shortcut.labelKey) }}</span>
|
||||||
|
<kbd>{{ shortcut.keys }}</kbd>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.help {
|
||||||
|
display: flex;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc {
|
||||||
|
flex: 0 0 220px;
|
||||||
|
border-right: 1px solid var(--border);
|
||||||
|
padding: 10px 8px;
|
||||||
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
background: var(--panel-alt);
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
text-align: left;
|
||||||
|
padding: 8px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-item:hover:not(.active) {
|
||||||
|
background: var(--panel-hover);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toc-item.active {
|
||||||
|
background: var(--accent);
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 18px 22px 22px;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chapter {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chapter-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chapter-intro {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chapter-block {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chapter h4 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
color: var(--text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chapter p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.55;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bullets {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bullets-disc {
|
||||||
|
list-style: disc;
|
||||||
|
padding-left: 22px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bullets-disc::marker,
|
||||||
|
.bullets-disc li::marker {
|
||||||
|
color: var(--text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bullets-icon {
|
||||||
|
list-style: none;
|
||||||
|
padding-left: 4px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bullets-steps {
|
||||||
|
padding-left: 22px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bullets-icon .bullet-item {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bullet-icon {
|
||||||
|
width: 18px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bullet-copy {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bullet-copy strong {
|
||||||
|
color: var(--text);
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chapter code {
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
background: var(--panel-alt);
|
||||||
|
padding: 1px 5px;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chapter em {
|
||||||
|
font-style: italic;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.code {
|
||||||
|
margin: 0;
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: var(--panel-alt);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cmd-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cmd-table th,
|
||||||
|
.cmd-table td {
|
||||||
|
text-align: left;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cmd-table th {
|
||||||
|
font-size: 10px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
color: var(--text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shortcuts {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 6px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shortcuts li {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 5px 8px;
|
||||||
|
background: var(--panel-alt);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shortcut-label {
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
kbd {
|
||||||
|
background: var(--panel);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-dim-strong);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
||||||
|
import type { StyleValue } from 'vue'
|
||||||
import { PhX } from '@phosphor-icons/vue'
|
import { PhX } from '@phosphor-icons/vue'
|
||||||
import { t } from '@/i18n'
|
import { t } from '@/i18n'
|
||||||
|
|
||||||
type Size = 'sm' | 'md' | 'lg'
|
type Size = 'sm' | 'md' | 'lg' | 'xl'
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
@@ -13,12 +14,16 @@ const props = withDefaults(
|
|||||||
closeOnBackdrop?: boolean
|
closeOnBackdrop?: boolean
|
||||||
closeOnEsc?: boolean
|
closeOnEsc?: boolean
|
||||||
dismissable?: boolean
|
dismissable?: boolean
|
||||||
|
dialogStyle?: StyleValue
|
||||||
|
bodyStyle?: StyleValue
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
size: 'md',
|
size: 'md',
|
||||||
closeOnBackdrop: true,
|
closeOnBackdrop: true,
|
||||||
closeOnEsc: true,
|
closeOnEsc: true,
|
||||||
dismissable: true,
|
dismissable: true,
|
||||||
|
dialogStyle: undefined,
|
||||||
|
bodyStyle: undefined,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -99,7 +104,14 @@ onBeforeUnmount(() => document.removeEventListener('keydown', onKeyDown))
|
|||||||
<Teleport to="body">
|
<Teleport to="body">
|
||||||
<Transition name="modal-fade">
|
<Transition name="modal-fade">
|
||||||
<div v-if="modelValue" class="backdrop" @mousedown="onBackdropMouseDown">
|
<div v-if="modelValue" class="backdrop" @mousedown="onBackdropMouseDown">
|
||||||
<div ref="dialogRef" class="dialog" :class="`size-${size}`" role="dialog" aria-modal="true">
|
<div
|
||||||
|
ref="dialogRef"
|
||||||
|
class="dialog"
|
||||||
|
:class="`size-${size}`"
|
||||||
|
:style="dialogStyle"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
>
|
||||||
<header v-if="title || $slots.header" class="hdr">
|
<header v-if="title || $slots.header" class="hdr">
|
||||||
<slot name="header">
|
<slot name="header">
|
||||||
<h2>{{ title }}</h2>
|
<h2>{{ title }}</h2>
|
||||||
@@ -114,7 +126,7 @@ onBeforeUnmount(() => document.removeEventListener('keydown', onKeyDown))
|
|||||||
<PhX :size="14" weight="bold" />
|
<PhX :size="14" weight="bold" />
|
||||||
</button>
|
</button>
|
||||||
</header>
|
</header>
|
||||||
<div class="body" tabindex="-1">
|
<div class="body" :style="bodyStyle" tabindex="-1">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
<footer v-if="$slots.footer" class="ftr">
|
<footer v-if="$slots.footer" class="ftr">
|
||||||
@@ -161,6 +173,10 @@ onBeforeUnmount(() => document.removeEventListener('keydown', onKeyDown))
|
|||||||
width: min(720px, calc(100% - 32px));
|
width: min(720px, calc(100% - 32px));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dialog.size-xl {
|
||||||
|
width: min(960px, calc(100% - 32px));
|
||||||
|
}
|
||||||
|
|
||||||
.hdr {
|
.hdr {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
import { PhArrowClockwise, PhCheck, PhClipboardText, PhWarning, PhX } from '@phosphor-icons/vue'
|
import { PhArrowClockwise, PhCheck, PhClipboardText, PhWarning, PhX } from '@phosphor-icons/vue'
|
||||||
import { useSceneStore } from '@/stores/scene'
|
import { useSceneStore } from '@/stores/scene'
|
||||||
import { t } from '@/i18n'
|
import { t, useI18n } from '@/i18n'
|
||||||
import { runAll, type TestResult } from './testRunner'
|
import { runAll, type TestResult } from './testRunner'
|
||||||
import { registerAllTests } from './tests'
|
import { registerAllTests } from './tests'
|
||||||
|
|
||||||
@@ -140,6 +140,13 @@ async function run() {
|
|||||||
clipboardStatus.value = 'idle'
|
clipboardStatus.value = 'idle'
|
||||||
startedAt.value = performance.now()
|
startedAt.value = performance.now()
|
||||||
const snapshot = JSON.parse(JSON.stringify(store.$state))
|
const snapshot = JSON.parse(JSON.stringify(store.$state))
|
||||||
|
// The test harness asserts against English UI strings (aria-labels,
|
||||||
|
// section titles, help dialog headings, "yet"/"unsaved" substrings).
|
||||||
|
// Pin the locale to 'en' for the duration of the run so a German
|
||||||
|
// browser default doesn't flip them all red. Restore on finally.
|
||||||
|
const { locale } = useI18n()
|
||||||
|
const prevLocale = locale.value
|
||||||
|
locale.value = 'en'
|
||||||
try {
|
try {
|
||||||
await runAll(registerAllTests, {
|
await runAll(registerAllTests, {
|
||||||
onProgress: (d, t) => {
|
onProgress: (d, t) => {
|
||||||
@@ -160,6 +167,7 @@ async function run() {
|
|||||||
})
|
})
|
||||||
} finally {
|
} finally {
|
||||||
store.$state = snapshot
|
store.$state = snapshot
|
||||||
|
locale.value = prevLocale
|
||||||
finishedAt.value = performance.now()
|
finishedAt.value = performance.now()
|
||||||
running.value = false
|
running.value = false
|
||||||
}
|
}
|
||||||
|
|||||||
+116
-1
@@ -1,4 +1,4 @@
|
|||||||
import { createApp, h, nextTick, type Component, type Slot } from 'vue'
|
import { createApp, defineComponent, h, nextTick, ref, type Component, type Slot } from 'vue'
|
||||||
import {
|
import {
|
||||||
createBlankProject,
|
createBlankProject,
|
||||||
createBlankScene,
|
createBlankScene,
|
||||||
@@ -46,6 +46,7 @@ import ActionEditor from '@/components/ActionEditor.vue'
|
|||||||
import Timeline from '@/components/Timeline.vue'
|
import Timeline from '@/components/Timeline.vue'
|
||||||
import AssetSidebar from '@/components/AssetSidebar.vue'
|
import AssetSidebar from '@/components/AssetSidebar.vue'
|
||||||
import FullscreenPreview from '@/components/FullscreenPreview.vue'
|
import FullscreenPreview from '@/components/FullscreenPreview.vue'
|
||||||
|
import HelpDialog from '@/components/HelpDialog.vue'
|
||||||
import { createAudioRuntime } from '@/composables/useAudioRuntime'
|
import { createAudioRuntime } from '@/composables/useAudioRuntime'
|
||||||
import {
|
import {
|
||||||
clearWaveformCache,
|
clearWaveformCache,
|
||||||
@@ -1813,6 +1814,120 @@ export function registerAllTests(): void {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
suite('ui — HelpDialog', () => {
|
||||||
|
const snapshotDialogs = () =>
|
||||||
|
new Set(Array.from(document.querySelectorAll('[role="dialog"]')))
|
||||||
|
const dialogsAddedSince = (baseline: Set<Element>) =>
|
||||||
|
Array.from(document.querySelectorAll('[role="dialog"]')).filter(
|
||||||
|
(el) => !baseline.has(el),
|
||||||
|
) as HTMLElement[]
|
||||||
|
|
||||||
|
test('opens on modelValue=true and renders a TOC with all 13 chapters', async () => {
|
||||||
|
const baseline = snapshotDialogs()
|
||||||
|
const handle = mountComponent(HelpDialog, { modelValue: true })
|
||||||
|
await nextTick()
|
||||||
|
const added = dialogsAddedSince(baseline)
|
||||||
|
expect(added.length).toBe(1)
|
||||||
|
const tocItems = added[0].querySelectorAll('.toc-item')
|
||||||
|
expect(tocItems.length).toBe(13)
|
||||||
|
handle.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('defaults to the Getting started chapter', async () => {
|
||||||
|
const baseline = snapshotDialogs()
|
||||||
|
const handle = mountComponent(HelpDialog, { modelValue: true })
|
||||||
|
await nextTick()
|
||||||
|
const dialog = dialogsAddedSince(baseline)[0]
|
||||||
|
const activeToc = dialog.querySelector('.toc-item.active') as HTMLElement
|
||||||
|
expect(activeToc).toBeTruthy()
|
||||||
|
expect(activeToc.textContent?.trim()).toBe('Getting started')
|
||||||
|
// Content heading matches.
|
||||||
|
const heading = dialog.querySelector('.chapter h3')
|
||||||
|
expect(heading?.textContent?.trim()).toBe('Getting started')
|
||||||
|
handle.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('clicking a TOC item swaps the right-pane content', async () => {
|
||||||
|
const baseline = snapshotDialogs()
|
||||||
|
const handle = mountComponent(HelpDialog, { modelValue: true })
|
||||||
|
await nextTick()
|
||||||
|
const dialog = dialogsAddedSince(baseline)[0]
|
||||||
|
const items = Array.from(dialog.querySelectorAll('.toc-item')) as HTMLButtonElement[]
|
||||||
|
const scripting = items.find((el) => el.textContent?.includes('Script language'))
|
||||||
|
expect(scripting).toBeTruthy()
|
||||||
|
scripting!.click()
|
||||||
|
await nextTick()
|
||||||
|
const heading = dialog.querySelector('.chapter h3')
|
||||||
|
expect(heading?.textContent?.trim()).toBe('Script language')
|
||||||
|
// Active highlight moved.
|
||||||
|
const active = dialog.querySelector('.toc-item.active')
|
||||||
|
expect(active?.textContent?.trim()).toBe('Script language')
|
||||||
|
handle.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Script language chapter documents every DSL command', async () => {
|
||||||
|
// Pin the parser/help alignment — if a new command ships in
|
||||||
|
// scriptDsl.ts, this test reminds us to document it here too.
|
||||||
|
const baseline = snapshotDialogs()
|
||||||
|
const handle = mountComponent(HelpDialog, { modelValue: true })
|
||||||
|
await nextTick()
|
||||||
|
const dialog = dialogsAddedSince(baseline)[0]
|
||||||
|
const items = Array.from(dialog.querySelectorAll('.toc-item')) as HTMLButtonElement[]
|
||||||
|
items.find((el) => el.textContent?.includes('Script language'))!.click()
|
||||||
|
await nextTick()
|
||||||
|
const body = dialog.querySelector('.chapter')!.textContent ?? ''
|
||||||
|
expect(body).toContain('play')
|
||||||
|
expect(body).toContain('pause')
|
||||||
|
expect(body).toContain('rewind')
|
||||||
|
expect(body).toContain('jump')
|
||||||
|
expect(body).toContain('#')
|
||||||
|
expect(body).toContain('@')
|
||||||
|
expect(body).toContain('//')
|
||||||
|
handle.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Shortcuts chapter renders one row per shortcut with a kbd key', async () => {
|
||||||
|
const baseline = snapshotDialogs()
|
||||||
|
const handle = mountComponent(HelpDialog, { modelValue: true })
|
||||||
|
await nextTick()
|
||||||
|
const dialog = dialogsAddedSince(baseline)[0]
|
||||||
|
const items = Array.from(dialog.querySelectorAll('.toc-item')) as HTMLButtonElement[]
|
||||||
|
items.find((el) => el.textContent?.includes('Keyboard shortcuts'))!.click()
|
||||||
|
await nextTick()
|
||||||
|
const rows = dialog.querySelectorAll('.shortcuts li')
|
||||||
|
expect(rows.length).toBeGreaterThan(9)
|
||||||
|
// Every row ends with a kbd element.
|
||||||
|
for (const li of Array.from(rows)) {
|
||||||
|
expect(li.querySelector('kbd')).toBeTruthy()
|
||||||
|
}
|
||||||
|
handle.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('reopening resets to the first chapter (no stale selection)', async () => {
|
||||||
|
// Regression: a user who leaves the dialog on "Script language"
|
||||||
|
// and reopens it expects to land on the intro, not mid-content.
|
||||||
|
const open = ref(true)
|
||||||
|
const Wrapper = defineComponent({
|
||||||
|
render: () => h(HelpDialog, { modelValue: open.value }),
|
||||||
|
})
|
||||||
|
const handle = mountComponent(Wrapper, {})
|
||||||
|
await nextTick()
|
||||||
|
const items = Array.from(
|
||||||
|
document.querySelectorAll('.toc-item'),
|
||||||
|
) as HTMLButtonElement[]
|
||||||
|
items.find((el) => el.textContent?.includes('Script language'))!.click()
|
||||||
|
await nextTick()
|
||||||
|
open.value = false
|
||||||
|
await nextTick()
|
||||||
|
open.value = true
|
||||||
|
await nextTick()
|
||||||
|
await nextTick()
|
||||||
|
const heading = document.querySelector('.chapter h3')
|
||||||
|
expect(heading?.textContent?.trim()).toBe('Getting started')
|
||||||
|
handle.unmount()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
suite('ui — IconButton', () => {
|
suite('ui — IconButton', () => {
|
||||||
test('renders a button element', async () => {
|
test('renders a button element', async () => {
|
||||||
// IconButton now uses the custom v-tooltip directive instead of
|
// IconButton now uses the custom v-tooltip directive instead of
|
||||||
|
|||||||
+28
-6
@@ -1,10 +1,25 @@
|
|||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { en, type Messages } from './messages'
|
import { de, en } from './messages'
|
||||||
|
|
||||||
export type Locale = 'en'
|
export type Locale = 'en' | 'de'
|
||||||
|
|
||||||
const activeLocale = ref<Locale>('en')
|
const LOCALE_KEY = 'animatr:locale'
|
||||||
const messagesByLocale: Record<Locale, Messages> = { en }
|
const messagesByLocale: Record<Locale, unknown> = { en, de }
|
||||||
|
|
||||||
|
function readInitialLocale(): Locale {
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem(LOCALE_KEY)
|
||||||
|
if (stored === 'en' || stored === 'de') return stored
|
||||||
|
} catch {
|
||||||
|
/* noop */
|
||||||
|
}
|
||||||
|
if (typeof navigator !== 'undefined' && navigator.language.toLowerCase().startsWith('de')) {
|
||||||
|
return 'de'
|
||||||
|
}
|
||||||
|
return 'en'
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeLocale = ref<Locale>(readInitialLocale())
|
||||||
|
|
||||||
function resolveAt(obj: unknown, path: string): unknown {
|
function resolveAt(obj: unknown, path: string): unknown {
|
||||||
let cur = obj
|
let cur = obj
|
||||||
@@ -28,11 +43,13 @@ function interpolate(template: string, params?: Record<string, string | number>)
|
|||||||
|
|
||||||
export function t(key: string, params?: Record<string, string | number>): string {
|
export function t(key: string, params?: Record<string, string | number>): string {
|
||||||
const raw = resolveAt(messagesByLocale[activeLocale.value], key)
|
const raw = resolveAt(messagesByLocale[activeLocale.value], key)
|
||||||
if (typeof raw !== 'string') {
|
if (typeof raw === 'string') return interpolate(raw, params)
|
||||||
|
const fallback = resolveAt(en, key)
|
||||||
|
if (typeof fallback !== 'string') {
|
||||||
if (import.meta.env.DEV) console.warn(`[i18n] Missing key: ${key}`)
|
if (import.meta.env.DEV) console.warn(`[i18n] Missing key: ${key}`)
|
||||||
return key
|
return key
|
||||||
}
|
}
|
||||||
return interpolate(raw, params)
|
return interpolate(fallback, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useI18n() {
|
export function useI18n() {
|
||||||
@@ -41,4 +58,9 @@ export function useI18n() {
|
|||||||
|
|
||||||
export function setLocale(l: Locale): void {
|
export function setLocale(l: Locale): void {
|
||||||
activeLocale.value = l
|
activeLocale.value = l
|
||||||
|
try {
|
||||||
|
localStorage.setItem(LOCALE_KEY, l)
|
||||||
|
} catch {
|
||||||
|
/* noop */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+1139
-1
File diff suppressed because it is too large.
Load diff
+1
-1
@@ -1,2 +1,2 @@
|
|||||||
export const APP_VERSION = '0.8.5'
|
export const APP_VERSION = '0.11.0'
|
||||||
export const APP_NAME = 'RUWIO'
|
export const APP_NAME = 'RUWIO'
|
||||||
Reference in new issue
Block a user