1.1.4: 2-column export + welcome rework with auto-load + archive
- Export dialog goes wide (xl modal) and splits into two columns:
Project & code on the left (.ruw, runtime.js, animation.js,
example.html), Media on the right with the per-export size + fps
inputs followed by MP4 / WebM / GIF / APNG / APNG preview. Skip-
hidden toggle still spans the full width up top. Single-column
fallback below 720px.
- Welcome dialog reworked:
- Always auto-loads the last edited project on boot. The welcome
dialog opens on top only when the new "Show on start"
preference is enabled, or unconditionally when there are no
recents (first-run flow).
- Two-column body: Recent (max 5 thumbnail tiles, was 8) on the
left, Archive (compact list view, no thumbnails) on the right.
- Footer toggle binds to the new useShowWelcomeOnStart shared
ref so the user can opt out of the boot dialog inline.
- Document settings → Interface tab grows a matching "Show welcome
screen on start" toggle so users who turned it off can flip it
back on without diving into localStorage.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
1f2812c64d
commit
3005d9310d
10 files changed
+652
-211
No files matched your search
+123
-7
@@ -1461,6 +1461,34 @@ export function registerAllTests(): void {
|
||||
return null
|
||||
}
|
||||
|
||||
test('renders two columns with the project + media headings', async () => {
|
||||
makeFreshStore()
|
||||
const baseline = snapshotDialogs()
|
||||
const handle = mountComponent(ExportDialog, { modelValue: true })
|
||||
try {
|
||||
await nextTick()
|
||||
const dialog = dialogsAddedSince(baseline)[0]
|
||||
const cols = dialog.querySelectorAll('.export-col')
|
||||
expect(cols.length).toBe(2)
|
||||
const headings = Array.from(dialog.querySelectorAll('.col-heading')).map(
|
||||
(el) => el.textContent?.trim() ?? '',
|
||||
)
|
||||
expect(headings).toContain(i18nT('export.colProject'))
|
||||
expect(headings).toContain(i18nT('export.colMedia'))
|
||||
// Media-only controls (target-size + fps) live in the
|
||||
// media column, not the project column.
|
||||
const mediaCol = Array.from(cols).find((c) =>
|
||||
c.querySelector('.col-heading')?.textContent?.includes(
|
||||
i18nT('export.colMedia'),
|
||||
),
|
||||
)
|
||||
expect(mediaCol).toBeTruthy()
|
||||
expect(mediaCol!.querySelector('.target-size')).toBeTruthy()
|
||||
} finally {
|
||||
handle.unmount()
|
||||
}
|
||||
})
|
||||
|
||||
test('renders a GIF, APNG, and APNG preview option row', async () => {
|
||||
makeFreshStore()
|
||||
const baseline = snapshotDialogs()
|
||||
@@ -2939,8 +2967,8 @@ export function registerAllTests(): void {
|
||||
expect(back).toBeNull()
|
||||
})
|
||||
|
||||
test('listAllProjects returns every record — no 8-recents cap', async () => {
|
||||
// Backup flow needs the full dump, not just the top 8 that
|
||||
test('listAllProjects returns every record — no recents cap', async () => {
|
||||
// Backup flow needs the full dump, not just the top N that
|
||||
// the welcome dialog shows.
|
||||
const { listAllProjects } = await import('@/lib/projectStore')
|
||||
const s = makeFreshStore()
|
||||
@@ -2983,6 +3011,74 @@ export function registerAllTests(): void {
|
||||
}
|
||||
}
|
||||
|
||||
test('renders both Recent and Archive columns; archive shows projects past the 8-item recents cap', async () => {
|
||||
const s = makeFreshStore()
|
||||
const prefix = `test-welcome-cols-${Date.now()}-`
|
||||
const ids: string[] = []
|
||||
// Need MORE than MAX_RECENT (5) so the spillover lands in the
|
||||
// archive column. Save 10 to be safe.
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const id = `${prefix}${i}`
|
||||
await saveProjectToStore(s.project, { id, name: `Proj ${i}` })
|
||||
await tick(2)
|
||||
ids.push(id)
|
||||
}
|
||||
const baseline = snapshotDialogs()
|
||||
const handle = mountComponent(WelcomeDialog, { modelValue: true })
|
||||
try {
|
||||
await waitUntil(() => {
|
||||
const d = dialogsAddedSince(baseline)[0]
|
||||
return !!d && d.querySelectorAll('.archive-row').length >= 1
|
||||
})
|
||||
const dialog = dialogsAddedSince(baseline)[0]
|
||||
// Two columns rendered with the new headings.
|
||||
const cols = dialog.querySelectorAll('.col')
|
||||
expect(cols.length).toBe(2)
|
||||
// Recent column gets the .recent thumb tiles, archive column
|
||||
// gets the bare list rows — at least one of each from our
|
||||
// seeded set.
|
||||
expect(dialog.querySelectorAll('.recent').length).toBeGreaterThan(0)
|
||||
expect(dialog.querySelectorAll('.archive-row').length).toBeGreaterThan(0)
|
||||
} finally {
|
||||
for (const id of ids) await deleteStoredProject(id).catch(() => {})
|
||||
handle.unmount()
|
||||
}
|
||||
})
|
||||
|
||||
test('footer "show on start" toggle flips the persisted preference', async () => {
|
||||
const baseline = snapshotDialogs()
|
||||
const handle = mountComponent(WelcomeDialog, { modelValue: true })
|
||||
try {
|
||||
await waitUntil(
|
||||
() => !!dialogsAddedSince(baseline)[0]?.querySelector('.ftr-toggle'),
|
||||
)
|
||||
const dialog = dialogsAddedSince(baseline)[0]
|
||||
const checkbox = dialog.querySelector(
|
||||
'.ftr-toggle input[type="checkbox"]',
|
||||
) as HTMLInputElement
|
||||
expect(checkbox).toBeTruthy()
|
||||
const before = checkbox.checked
|
||||
const { useShowWelcomeOnStart } = await import(
|
||||
'@/composables/useShowWelcomeOnStart'
|
||||
)
|
||||
const pref = useShowWelcomeOnStart()
|
||||
// Drive a change through the input and verify the shared
|
||||
// ref + localStorage flip.
|
||||
checkbox.checked = !before
|
||||
checkbox.dispatchEvent(new Event('change', { bubbles: true }))
|
||||
await nextTick()
|
||||
expect(pref.enabled.value).toBe(!before)
|
||||
expect(localStorage.getItem('animatr.showWelcomeOnStart')).toBe(
|
||||
String(!before),
|
||||
)
|
||||
// Restore so other tests aren't affected.
|
||||
pref.enabled.value = before
|
||||
await nextTick()
|
||||
} finally {
|
||||
handle.unmount()
|
||||
}
|
||||
})
|
||||
|
||||
test('ArrowDown / ArrowUp move focus through the recents list', async () => {
|
||||
// Seed two recents via the real IDB path so the dialog's own
|
||||
// `listRecentProjects` call finds them.
|
||||
@@ -3246,11 +3342,12 @@ export function registerAllTests(): void {
|
||||
) as HTMLButtonElement
|
||||
expect(confirmBtn).toBeTruthy()
|
||||
confirmBtn.click()
|
||||
// Assert by seed name rather than count — MAX_RECENT=8 caps the
|
||||
// visible list, so deleting the top seed may just unmask a
|
||||
// hidden record instead of dropping the total. Also wait for
|
||||
// the list to rerender: reload() briefly flips loading=true,
|
||||
// which hides .recents behind the "loading…" placeholder.
|
||||
// Assert by seed name rather than count — MAX_RECENT caps the
|
||||
// visible recents list, so deleting the top seed may just
|
||||
// unmask a hidden record instead of dropping the total. Also
|
||||
// wait for the list to rerender: reload() briefly flips
|
||||
// loading=true, which hides .recents behind the "loading…"
|
||||
// placeholder.
|
||||
await waitUntil(
|
||||
() =>
|
||||
!welcome.textContent?.includes('DropMe') &&
|
||||
@@ -5895,6 +5992,25 @@ export function registerAllTests(): void {
|
||||
a.enabled.value = initial
|
||||
await nextTick()
|
||||
})
|
||||
|
||||
test('useShowWelcomeOnStart defaults true and persists toggles', async () => {
|
||||
const { useShowWelcomeOnStart } = await import(
|
||||
'@/composables/useShowWelcomeOnStart'
|
||||
)
|
||||
const a = useShowWelcomeOnStart()
|
||||
const b = useShowWelcomeOnStart()
|
||||
expect(a.enabled).toBe(b.enabled)
|
||||
// Default is true (first-run users see the welcome screen).
|
||||
a.enabled.value = true
|
||||
await nextTick()
|
||||
a.enabled.value = false
|
||||
await nextTick()
|
||||
expect(localStorage.getItem('animatr.showWelcomeOnStart')).toBe('false')
|
||||
expect(b.enabled.value).toBe(false)
|
||||
a.enabled.value = true
|
||||
await nextTick()
|
||||
expect(localStorage.getItem('animatr.showWelcomeOnStart')).toBe('true')
|
||||
})
|
||||
})
|
||||
|
||||
suite('clipart drawer — grid pre-layout', () => {
|
||||
|
||||
Reference in new issue
Block a user