Skip to content

Commit e744d71

Browse files
JasonWarrenUKclaude
andcommitted
refactor(themis): break down ModuleGenerationList into smaller components
Split the 896-line ModuleGenerationList.svelte into focused, reusable components for better maintainability: - ProgressSummary.svelte (86 lines): Overall generation progress display - ModulePreviewModal.svelte (141 lines): XML preview modal - ModuleCard.svelte (221 lines): Individual module display with actions - ArcSection.svelte (140 lines): Collapsible arc container - moduleStoreHelpers.ts (80 lines): Centralized store update utilities Main component reduced from 896 to 441 lines (51% reduction). Eliminates duplicate store update patterns (addresses roadmap task 1.1.2.7). All existing functionality preserved, build passes successfully. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 2c1ae3e commit e744d71

File tree

6 files changed

+1099
-886
lines changed

6 files changed

+1099
-886
lines changed
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
<script lang="ts">
2+
import { createEventDispatcher } from "svelte";
3+
import type { Arc, ModuleSlot } from "$lib/types/themis";
4+
import ModuleCard from "./ModuleCard.svelte";
5+
6+
export let arc: Arc;
7+
export let isExpanded: boolean;
8+
export let generatingModuleId: string | null;
9+
10+
const dispatch = createEventDispatcher<{
11+
toggle: { arcId: string };
12+
generateModule: { module: ModuleSlot; arc: Arc };
13+
viewPreview: { moduleId: string };
14+
}>();
15+
16+
$: completedCount = arc.modules.filter(m => m.status === 'complete').length;
17+
$: canGenerate = generatingModuleId === null;
18+
19+
function handleToggle() {
20+
dispatch('toggle', { arcId: arc.id });
21+
}
22+
23+
function handleGenerateModule(event: CustomEvent<{ module: ModuleSlot; arc: Arc }>) {
24+
dispatch('generateModule', event.detail);
25+
}
26+
27+
function handleViewPreview(event: CustomEvent<{ moduleId: string }>) {
28+
dispatch('viewPreview', event.detail);
29+
}
30+
</script>
31+
32+
<div class="arc-section">
33+
<button
34+
class="arc-header"
35+
on:click={handleToggle}
36+
class:expanded={isExpanded}
37+
>
38+
<div class="arc-header-content">
39+
<h3>
40+
<span class="arc-icon">{isExpanded ? '' : ''}</span>
41+
Arc {arc.order}: {arc.title}
42+
</h3>
43+
<p class="arc-theme">{arc.theme}</p>
44+
</div>
45+
<div class="arc-meta">
46+
<span class="arc-modules-count">
47+
{completedCount}/{arc.modules.length} modules
48+
</span>
49+
</div>
50+
</button>
51+
52+
{#if isExpanded}
53+
<div class="module-list">
54+
{#each arc.modules as module (module.id)}
55+
<ModuleCard
56+
{module}
57+
{arc}
58+
isGenerating={generatingModuleId === module.id}
59+
canGenerate={canGenerate}
60+
on:generate={handleGenerateModule}
61+
on:viewPreview={handleViewPreview}
62+
/>
63+
{/each}
64+
</div>
65+
{/if}
66+
</div>
67+
68+
<style>
69+
.arc-section {
70+
background: white;
71+
border: 1px solid var(--palette-line);
72+
border-radius: 8px;
73+
overflow: hidden;
74+
}
75+
76+
.arc-header {
77+
width: 100%;
78+
padding: 1.5rem;
79+
background: var(--palette-bg-nav);
80+
border: none;
81+
cursor: pointer;
82+
display: flex;
83+
justify-content: space-between;
84+
align-items: center;
85+
transition: background-color 0.2s;
86+
}
87+
88+
.arc-header:hover {
89+
background: var(--palette-bg-subtle);
90+
}
91+
92+
.arc-header.expanded {
93+
background: var(--palette-bg-subtle);
94+
}
95+
96+
.arc-header-content {
97+
flex: 1;
98+
text-align: left;
99+
}
100+
101+
.arc-header h3 {
102+
font-size: 1.25rem;
103+
color: var(--palette-foreground);
104+
margin: 0 0 0.5rem 0;
105+
display: flex;
106+
align-items: center;
107+
gap: 0.5rem;
108+
}
109+
110+
.arc-icon {
111+
font-size: 0.875rem;
112+
color: var(--palette-foreground-alt);
113+
}
114+
115+
.arc-theme {
116+
color: var(--palette-foreground-alt);
117+
font-size: 0.9rem;
118+
margin: 0;
119+
font-style: italic;
120+
}
121+
122+
.arc-meta {
123+
display: flex;
124+
align-items: center;
125+
gap: 1rem;
126+
}
127+
128+
.arc-modules-count {
129+
font-size: 0.875rem;
130+
color: var(--palette-foreground-alt);
131+
font-weight: 500;
132+
}
133+
134+
.module-list {
135+
padding: 1rem;
136+
display: flex;
137+
flex-direction: column;
138+
gap: 1rem;
139+
}
140+
</style>
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
<script lang="ts">
2+
import { createEventDispatcher } from "svelte";
3+
import type { ModuleSlot, Arc } from "$lib/types/themis";
4+
5+
export let module: ModuleSlot;
6+
export let arc: Arc;
7+
export let isGenerating: boolean = false;
8+
export let canGenerate: boolean = true;
9+
10+
const dispatch = createEventDispatcher<{
11+
generate: { module: ModuleSlot; arc: Arc };
12+
viewPreview: { moduleId: string };
13+
}>();
14+
15+
function getStatusColor(status: ModuleSlot['status']): string {
16+
switch (status) {
17+
case 'complete': return 'var(--palette-foreground)';
18+
case 'generating': return 'var(--palette-foreground-alt)';
19+
case 'error': return 'var(--palette-primary)';
20+
default: return 'var(--palette-foreground-alt)';
21+
}
22+
}
23+
24+
function getStatusIcon(status: ModuleSlot['status']): string {
25+
switch (status) {
26+
case 'complete': return '';
27+
case 'generating': return '';
28+
case 'error': return '!';
29+
default: return '';
30+
}
31+
}
32+
33+
function handleGenerate() {
34+
dispatch('generate', { module, arc });
35+
}
36+
37+
function handleViewPreview() {
38+
dispatch('viewPreview', { moduleId: module.id });
39+
}
40+
</script>
41+
42+
<div class="module-card" class:generating={isGenerating}>
43+
<div class="module-header">
44+
<div class="module-status" style="background-color: {getStatusColor(module.status)}">
45+
{getStatusIcon(module.status)}
46+
</div>
47+
<div class="module-info">
48+
<h4>{module.title}</h4>
49+
<p class="module-description">{module.description}</p>
50+
<div class="module-meta">
51+
<span>{module.durationWeeks} week{module.durationWeeks !== 1 ? 's' : ''}</span>
52+
{#if module.learningObjectives && module.learningObjectives.length > 0}
53+
<span>• {module.learningObjectives.length} objectives</span>
54+
{/if}
55+
</div>
56+
</div>
57+
</div>
58+
59+
{#if module.errorMessage}
60+
<div class="error-message">
61+
<strong>Error:</strong> {module.errorMessage}
62+
</div>
63+
{/if}
64+
65+
<div class="module-actions">
66+
{#if module.status === 'planned' || module.status === 'error'}
67+
<button
68+
class="btn btn-sm btn-generate"
69+
on:click={handleGenerate}
70+
disabled={!canGenerate}
71+
>
72+
{module.status === 'error' ? 'Retry' : 'Generate'}
73+
</button>
74+
{:else if module.status === 'generating'}
75+
<button class="btn btn-sm" disabled>
76+
Generating...
77+
</button>
78+
{:else if module.status === 'complete'}
79+
<button
80+
class="btn btn-sm btn-secondary"
81+
on:click={handleViewPreview}
82+
>
83+
View Module
84+
</button>
85+
<button
86+
class="btn btn-sm"
87+
on:click={handleGenerate}
88+
disabled={!canGenerate}
89+
>
90+
Regenerate
91+
</button>
92+
{/if}
93+
</div>
94+
</div>
95+
96+
<style>
97+
.module-card {
98+
border: 1px solid var(--palette-line);
99+
border-radius: 6px;
100+
padding: 1rem;
101+
transition: box-shadow 0.2s;
102+
}
103+
104+
.module-card.generating {
105+
box-shadow: 0 0 0 2px var(--palette-foreground-alt);
106+
animation: pulse 2s infinite;
107+
}
108+
109+
@keyframes pulse {
110+
0%, 100% { opacity: 1; }
111+
50% { opacity: 0.8; }
112+
}
113+
114+
.module-header {
115+
display: flex;
116+
gap: 1rem;
117+
margin-bottom: 1rem;
118+
}
119+
120+
.module-status {
121+
width: 32px;
122+
height: 32px;
123+
border-radius: 50%;
124+
display: flex;
125+
align-items: center;
126+
justify-content: center;
127+
color: white;
128+
font-weight: bold;
129+
flex-shrink: 0;
130+
background: var(--palette-foreground-alt);
131+
}
132+
133+
.module-info {
134+
flex: 1;
135+
}
136+
137+
.module-info h4 {
138+
font-size: 1.1rem;
139+
color: var(--palette-foreground);
140+
margin: 0 0 0.25rem 0;
141+
}
142+
143+
.module-description {
144+
color: var(--palette-foreground-alt);
145+
font-size: 0.9rem;
146+
margin: 0 0 0.5rem 0;
147+
line-height: 1.5;
148+
}
149+
150+
.module-meta {
151+
display: flex;
152+
gap: 0.5rem;
153+
font-size: 0.875rem;
154+
color: var(--palette-foreground-alt);
155+
}
156+
157+
.error-message {
158+
background: var(--palette-bg-subtle-alt);
159+
border: 1px solid var(--palette-line);
160+
border-radius: 4px;
161+
padding: 0.75rem;
162+
margin-bottom: 1rem;
163+
color: var(--palette-primary);
164+
font-size: 0.875rem;
165+
}
166+
167+
.module-actions {
168+
display: flex;
169+
gap: 0.5rem;
170+
justify-content: flex-end;
171+
}
172+
173+
.btn {
174+
padding: 0.75rem 1.5rem;
175+
border: none;
176+
border-radius: 6px;
177+
font-size: 1rem;
178+
font-weight: 500;
179+
cursor: pointer;
180+
transition: all 0.2s;
181+
}
182+
183+
.btn-sm {
184+
padding: 0.5rem 1rem;
185+
font-size: 0.875rem;
186+
}
187+
188+
.btn:disabled {
189+
opacity: 0.6;
190+
cursor: not-allowed;
191+
}
192+
193+
.btn-secondary {
194+
background: white;
195+
color: var(--palette-foreground);
196+
border: 1px solid var(--palette-foreground);
197+
}
198+
199+
.btn-secondary:hover:not(:disabled) {
200+
background: var(--palette-bg-nav);
201+
}
202+
203+
.btn-generate {
204+
background: var(--palette-foreground-alt);
205+
color: white;
206+
}
207+
208+
.btn-generate:hover:not(:disabled) {
209+
background: var(--palette-foreground);
210+
}
211+
212+
@media (max-width: 768px) {
213+
.module-header {
214+
flex-direction: column;
215+
}
216+
217+
.module-actions {
218+
flex-direction: column;
219+
}
220+
}
221+
</style>

0 commit comments

Comments
 (0)