Skip to content

Commit 6f9f373

Browse files
authored
feat(onboarding): add create extension with AI tip to store step (#773)
1 parent d6ce8ec commit 6f9f373

4 files changed

Lines changed: 183 additions & 28 deletions

File tree

asyar-launcher/src/locales/en.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -540,6 +540,8 @@
540540
"featured_title": "Extend your launcher",
541541
"featured_heading": "Essential extensions to start with",
542542
"featured_desc": "Install powerful add-ons created by the community. You can discover hundreds more in the Store anytime.",
543+
"featured_empty_tip_badge": "Store feels empty? Don't be disappointed!",
544+
"featured_empty_store_tip": "If you think the store is empty, don't be disappointed! You can easily create your own extension with the \"Create Extension with AI\" command. That's what people do — they create their own extensions, but forget to push them to the store, so you can easily create your own.",
543545
"clipboard_kicker": "Never lose a copy again"
544546
},
545547
"dev": {

asyar-launcher/src/locales/pt-BR.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,8 @@
539539
"featured_title": "Expanda seu launcher",
540540
"featured_heading": "Extensões essenciais para começar",
541541
"featured_desc": "Instale complementos poderosos criados pela comunidade. Você pode descobrir centenas de outros na Loja a qualquer momento.",
542+
"featured_empty_tip_badge": "Acha que a loja está vazia? Não se decepcione!",
543+
"featured_empty_store_tip": "Se achar que a loja está vazia, não se decepcione! Você pode criar sua própria extensão facilmente com o comando \"Criar Extensão com IA\". É isso que as pessoas fazem — elas criam suas próprias extensões, mas se esquecem de publicá-las na loja, então você pode criar a sua facilmente.",
542544
"clipboard_kicker": "Nunca mais perca o que você copiou"
543545
},
544546
"dev": {

asyar-launcher/src/routes/onboarding/steps/FeaturedExtensions.svelte

Lines changed: 87 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -68,41 +68,79 @@
6868
</script>
6969

7070
<Card>
71-
<h1>{t('onboarding.featured_heading')}</h1>
72-
<p>{t('onboarding.featured_desc')}</p>
71+
<div class="step">
72+
<p class="step__kicker">{t('onboarding.featured_title')}</p>
73+
<h1 class="step__title">{t('onboarding.featured_heading')}</h1>
74+
<p class="step__lede">{t('onboarding.featured_desc')}</p>
7375

74-
{#if loading}
75-
<LoadingState message={t('common.loading')} />
76-
{:else if extensions.length === 0}
77-
<EmptyState message={t('onboarding.featured_store_error')}>
78-
<Button onclick={load}>Retry</Button>
79-
</EmptyState>
80-
{:else}
81-
<ul class="list">
82-
{#each extensions as ext (ext.id)}
83-
<li>
84-
<label>
85-
<input
86-
type="checkbox"
87-
checked={selected.has(ext.id)}
88-
onchange={() => toggle(ext.id)}
89-
disabled={installingIds.has(ext.id)}
90-
/>
91-
<span class="name">{ext.name}</span>
92-
{#if installingIds.has(ext.id)}<span class="hint">Installing…</span>{/if}
93-
{#if failedIds.has(ext.id)}<span class="error">Failed</span>{/if}
94-
</label>
95-
</li>
96-
{/each}
97-
</ul>
98-
{/if}
76+
{#if loading}
77+
<LoadingState message={t('common.loading')} />
78+
{:else}
79+
{#if extensions.length === 0}
80+
<EmptyState message={t('onboarding.featured_store_error')} compact>
81+
<Button onclick={load}>{t('common.retry')}</Button>
82+
</EmptyState>
83+
{:else}
84+
<ul class="list">
85+
{#each extensions as ext (ext.id)}
86+
<li>
87+
<label>
88+
<input
89+
type="checkbox"
90+
checked={selected.has(ext.id)}
91+
onchange={() => toggle(ext.id)}
92+
disabled={installingIds.has(ext.id)}
93+
/>
94+
<span class="name">{ext.name}</span>
95+
{#if installingIds.has(ext.id)}<span class="hint">Installing…</span>{/if}
96+
{#if failedIds.has(ext.id)}<span class="error">Failed</span>{/if}
97+
</label>
98+
</li>
99+
{/each}
100+
</ul>
101+
{/if}
102+
103+
<div class="step__tip">
104+
<span class="step__tip-badge">💡 {t('onboarding.featured_empty_tip_badge')}</span>
105+
<p class="step__tip-text">
106+
{t('onboarding.featured_empty_store_tip')}
107+
</p>
108+
</div>
109+
{/if}
110+
</div>
99111
</Card>
100112

101113
<style>
114+
.step {
115+
display: flex;
116+
flex-direction: column;
117+
gap: var(--space-3);
118+
}
119+
.step__kicker {
120+
margin: 0;
121+
font-size: var(--font-size-sm);
122+
font-weight: 600;
123+
text-transform: uppercase;
124+
letter-spacing: var(--tracking-wide);
125+
color: var(--asyar-brand);
126+
}
127+
.step__title {
128+
margin: 0;
129+
font-size: var(--font-size-display);
130+
font-weight: 600;
131+
letter-spacing: var(--tracking-display);
132+
color: var(--text-primary);
133+
}
134+
.step__lede {
135+
margin: 0;
136+
color: var(--text-secondary);
137+
font-size: var(--font-size-xl);
138+
line-height: 1.6;
139+
}
102140
.list {
103141
list-style: none;
104142
padding: 0;
105-
margin: var(--space-4) 0;
143+
margin: var(--space-2) 0;
106144
}
107145
.list li {
108146
padding: var(--space-2) 0;
@@ -120,4 +158,25 @@
120158
color: var(--accent-danger);
121159
font-size: var(--font-size-sm);
122160
}
161+
.step__tip {
162+
display: flex;
163+
flex-direction: column;
164+
gap: var(--space-1);
165+
border: 1px solid var(--separator);
166+
border-radius: var(--radius-md);
167+
padding: var(--space-3);
168+
background: var(--bg-tertiary);
169+
}
170+
.step__tip-badge {
171+
align-self: flex-start;
172+
font-size: var(--font-size-sm);
173+
font-weight: 600;
174+
color: var(--asyar-brand);
175+
}
176+
.step__tip-text {
177+
margin: 0;
178+
color: var(--text-secondary);
179+
font-size: var(--font-size-md);
180+
line-height: 1.5;
181+
}
123182
</style>
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// @vitest-environment jsdom
2+
import { describe, expect, it, vi, beforeEach } from 'vitest';
3+
import { render, screen } from '@testing-library/svelte';
4+
5+
const { invokeMock, listenMock } = vi.hoisted(() => ({
6+
invokeMock: vi.fn(),
7+
listenMock: vi.fn(() => Promise.resolve(() => {})),
8+
}));
9+
vi.mock('@tauri-apps/api/core', () => ({ invoke: invokeMock }));
10+
vi.mock('@tauri-apps/api/event', () => ({ listen: listenMock }));
11+
12+
const fetchTopExtensions = vi.hoisted(() => vi.fn());
13+
const advanceStep = vi.hoisted(() => vi.fn());
14+
const goBackStep = vi.hoisted(() => vi.fn());
15+
const completeStep = vi.hoisted(() => vi.fn());
16+
const installExtension = vi.hoisted(() => vi.fn());
17+
18+
vi.mock('../stepLogic', () => ({
19+
fetchTopExtensions,
20+
advanceStep,
21+
goBackStep,
22+
completeStep,
23+
}));
24+
25+
vi.mock('@tauri-apps/plugin-os', () => ({
26+
platform: vi.fn(() => 'macos'),
27+
}));
28+
29+
vi.mock('../../../built-in-features/store/index.svelte', () => ({
30+
default: { installExtension },
31+
}));
32+
33+
import FeaturedExtensions from './FeaturedExtensions.svelte';
34+
35+
describe('FeaturedExtensions step', () => {
36+
beforeEach(() => {
37+
vi.clearAllMocks();
38+
});
39+
40+
it('renders extension list and mentions creating custom extensions with AI when store feels empty', async () => {
41+
fetchTopExtensions.mockResolvedValueOnce([
42+
{
43+
id: 101,
44+
name: 'GitHub Assistant',
45+
slug: 'github-assistant',
46+
},
47+
{
48+
id: 102,
49+
name: 'Color Picker',
50+
slug: 'color-picker',
51+
},
52+
]);
53+
54+
render(FeaturedExtensions);
55+
56+
expect(await screen.findByText('GitHub Assistant')).toBeTruthy();
57+
expect(screen.getByText('Color Picker')).toBeTruthy();
58+
59+
// Verify the store-empty tip is displayed
60+
expect(
61+
screen.getByText((content) => content.includes("Store feels empty? Don't be disappointed!")),
62+
).toBeTruthy();
63+
expect(
64+
screen.getByText(
65+
(content) =>
66+
content.includes('Create Extension with AI') &&
67+
content.includes('forget to push them to the store'),
68+
),
69+
).toBeTruthy();
70+
});
71+
72+
it('renders empty state and AI creation tip when no extensions are returned', async () => {
73+
fetchTopExtensions.mockResolvedValueOnce([]);
74+
75+
render(FeaturedExtensions);
76+
77+
expect(await screen.findByText("Couldn't reach the extension store.")).toBeTruthy();
78+
expect(screen.getByRole('button', { name: 'Retry' })).toBeTruthy();
79+
80+
// The tip is still visible to reassure users
81+
expect(
82+
screen.getByText((content) => content.includes("Store feels empty? Don't be disappointed!")),
83+
).toBeTruthy();
84+
expect(
85+
screen.getByText(
86+
(content) =>
87+
content.includes('Create Extension with AI') &&
88+
content.includes('forget to push them to the store'),
89+
),
90+
).toBeTruthy();
91+
});
92+
});

0 commit comments

Comments
 (0)