Devlog Mar 3: Module System Enhancements
This week we made significant enhancements to the module system, including a redesigned module selection interface, new UI components for icon and color selection, and improvements to module linking and import patterns.
Module Selection Interface Redesign
Improved Layout and Accessibility
We completely redesigned the module selection interface with better accessibility and layout:
<!-- src/routes/welcome/modules/+page.svelte -->
<script lang="ts">
import ModuleCard from './ModuleCard.svelte';
import { availableModules } from '$lib/stores/modules';
let selectedModules = new Set<string>();
let searchQuery = '';
$: filteredModules = $availableModules.filter(
module =>
module.name.toLowerCase().includes($searchQuery.toLowerCase()) ||
module.description.toLowerCase().includes($searchQuery.toLowerCase())
);
</script>
<div class="module-selection">
<header>
<h1>Select Your Modules</h1>
<input
type="search"
placeholder="Search modules..."
bind:value={searchQuery}
aria-label="Search modules"
class="search-input"
/>
</header>
<div class="module-grid" role="list">
{#each filteredModules as module (module.id)}
<ModuleCard
{module}
selected={$selectedModules.has(module.id)}
onToggle={() => {
if ($selectedModules.has(module.id)) {
$selectedModules.delete(module.id);
} else {
$selectedModules.add(module.id);
}
}}
role="listitem"
/>
{/each}
</div>
<footer>
<p>{$selectedModules.size} modules selected</p>
<button
type="button"
disabled={$selectedModules.size === 0}
class="continue-button"
>
Continue
</button>
</footer>
</div>
<style>
.module-selection {
display: flex;
flex-direction: column;
height: 100vh;
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
.module-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1.5rem;
overflow-y: auto;
flex: 1;
padding: 1rem 0;
}
.search-input {
width: 100%;
padding: 0.75rem 1rem;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 1rem;
transition: border-color 0.2s ease;
}
.search-input:focus {
outline: none;
border-color: #6750a4;
}
</style>
Module Card Component
The new ModuleCard component provides rich information about each module:
<!-- src/routes/welcome/modules/ModuleCard.svelte -->
<script lang="ts">
import type { Module } from '$lib/types/module';
import { createEventDispatcher } from 'svelte';
export let module: Module;
export let selected: boolean;
const dispatch = createEventDispatcher();
function handleToggle() {
dispatch('toggle');
}
function handleKeyDown(event: KeyboardEvent) {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
handleToggle();
}
}
</script>
<button
class="module-card {selected ? 'selected' : ''}"
onclick={handleToggle}
onkeydown={handleKeyDown}
aria-pressed={selected}
aria-labelledby="module-name-{module.id}"
>
<div class="module-header">
<div class="module-icon">{module.icon}</div>
<div class="module-info">
<h2 id="module-name-{module.id}">{module.name}</h2>
<span class="module-version">v{module.version}</span>
</div>
</div>
<p class="module-description">{module.description}</p>
<ul class="module-features">
{#each module.features as feature}
<li>{feature}</li>
{/each}
</ul>
<div class="module-footer">
<span class="module-status">{selected ? 'Selected' : 'Click to select'}</span>
</div>
</button>
<style>
.module-card {
display: flex;
flex-direction: column;
padding: 1.5rem;
border: 2px solid #e0e0e0;
border-radius: 12px;
background: white;
text-align: left;
cursor: pointer;
transition: all 0.2s ease;
}
.module-card:hover {
border-color: #6750a4;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.module-card.selected {
border-color: #6750a4;
background: #f3e8ff;
}
.module-header {
display: flex;
gap: 1rem;
margin-bottom: 1rem;
}
.module-icon {
font-size: 2rem;
}
.module-features {
list-style: none;
padding: 0;
margin: 1rem 0;
}
.module-features li {
padding: 0.25rem 0;
padding-left: 1.5rem;
position: relative;
}
.module-features li::before {
content: '✓';
position: absolute;
left: 0;
color: #6750a4;
}
</style>
New UI Components
Icon Picker Component
We added a new icon picker component for module customization:
<!-- src/components/IconPicker.svelte -->
<script lang="ts">
import { createEventDispatcher } from 'svelte';
const icons = [
'task-alt',
'note',
'calendar',
'folder',
'star',
'bookmark',
'settings',
'dashboard'
];
export let selectedIcon = 'task-alt';
const dispatch = createEventDispatcher();
function selectIcon(icon: string) {
selectedIcon = icon;
dispatch('select', { icon });
}
function handleKeyDown(event: KeyboardEvent, icon: string) {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
selectIcon(icon);
}
}
</script>
<div class="icon-picker">
<label for="icon-grid">Select Icon</label>
<div id="icon-grid" class="icon-grid" role="radiogroup">
{#each icons as icon (icon)}
<button
type="button"
class="icon-button {selectedIcon === icon ? 'selected' : ''}"
onclick={() => selectIcon(icon)}
onkeydown={(e) => handleKeyDown(e, icon)}
role="radio"
aria-checked={selectedIcon === icon}
aria-label={`Select ${icon} icon`}
>
<span class="material-icons">{icon}</span>
</button>
{/each}
</div>
</div>
<style>
.icon-picker {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.icon-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(48px, 1fr));
gap: 0.5rem;
}
.icon-button {
width: 48px;
height: 48px;
border: 2px solid #e0e0e0;
border-radius: 8px;
background: white;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.icon-button:hover {
border-color: #6750a4;
background: #f3e8ff;
}
.icon-button.selected {
border-color: #6750a4;
background: #6750a4;
color: white;
}
.icon-button:focus {
outline: 2px solid #6750a4;
outline-offset: 2px;
}
</style>
Color Picker Component
We implemented a color picker component for module theming:
<!-- src/components/ColorPicker.svelte -->
<script lang="ts">
import { createEventDispatcher } from 'svelte';
const colors = [
{ name: 'Purple', value: '#6750a4' },
{ name: 'Blue', value: '#3866d6' },
{ name: 'Green', value: '#006c4c' },
{ name: 'Red', value: '#b3261e' },
{ name: 'Orange', value: '#ea6800' },
{ name: 'Teal', value: '#00796b' }
];
export let selectedColor = '#6750a4';
const dispatch = createEventDispatcher();
function selectColor(color: string) {
selectedColor = color;
dispatch('select', { color });
}
</script>
<div class="color-picker">
<label for="color-grid">Select Color</label>
<div id="color-grid" class="color-grid" role="radiogroup">
{#each colors as color (color.value)}
<button
type="button"
class="color-button {selectedColor === color.value ? 'selected' : ''}"
style="background-color: {color.value}"
onclick={() => selectColor(color.value)}
role="radio"
aria-checked={selectedColor === color.value}
aria-label="Select {color.name}"
title={color.name}
/>
{/each}
</div>
</div>
<style>
.color-picker {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.color-grid {
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
}
.color-button {
width: 40px;
height: 40px;
border: 3px solid transparent;
border-radius: 50%;
cursor: pointer;
transition: transform 0.2s ease;
}
.color-button:hover {
transform: scale(1.1);
}
.color-button.selected {
border-color: #1d1b20;
}
.color-button:focus {
outline: 2px solid #6750a4;
outline-offset: 2px;
}
</style>
Module Import Enhancements
$module Alias for Internal Imports
We added the $module alias for internal module imports:
// vite.config.ts
export default defineConfig({
plugins: [sveltekit()],
resolve: {
alias: {
'$module': '/modules',
'$lib': '/src/lib',
'$components': '/src/components'
}
}
});
Now modules can import internal resources cleanly:
// In molos-tasks/src/routes/tasks/+page.svelte
import { tasksStore } from '$module/stores';
import TaskCard from '$module/components/TaskCard.svelte';
Unused Module Cleanup
We removed unused modules and cleaned up the module system:
# Remove unused modules
npm run modules:cleanup
# This will:
# - Identify inactive modules
# - Remove unused dependencies
# - Clean up module caches
# - Update module configuration
Module Build and Linking
Simplified Build Process
The module build process is now simplified with better automation:
// scripts/modules/build.ts
export async function buildModules(options: BuildOptions) {
const modules = await getModules(options);
for (const module of modules) {
console.log(`Building ${module.name}...`);
try {
await buildModule(module, options);
// Link module stores and components
await linkModuleResources(module);
console.log(`✓ Built ${module.name}`);
} catch (error) {
console.error(`✗ Failed to build ${module.name}`, error);
throw error;
}
}
}
export async function linkModuleResources(module: Module) {
const storesDir = path.join(module.path, 'src/stores');
const componentsDir = path.join(module.path, 'src/components');
if (existsSync(storesDir)) {
const linkPath = path.join(process.cwd(), 'node_modules', module.id, 'stores');
await fs.symlink(storesDir, linkPath, 'dir');
}
if (existsSync(componentsDir)) {
const linkPath = path.join(process.cwd(), 'node_modules', module.id, 'components');
await fs.symlink(componentsDir, linkPath, 'dir');
}
}
Branch vs Tag Behavior
We distinguished between tag and branch fetching behavior:
// lib/modules/fetch.ts
export async function fetchModule(
moduleName: string,
options: { tag?: string; branch?: string }
): Promise<ModuleSource> {
const repo = await getModuleRepository(moduleName);
if (options.tag) {
// Tag fetch: pinned version, stable
console.log(`Fetching ${moduleName} at tag ${options.tag}`);
return await repo.getTag(options.tag);
} else if (options.branch) {
// Branch fetch: development version, latest commits
console.log(`Fetching ${moduleName} at branch ${options.branch}`);
return await repo.getBranch(options.branch);
} else {
// Default: use main branch
return await repo.getBranch('main');
}
}
Testing Improvements
Module Configuration Tests
We added comprehensive tests for module configuration:
// tests/modules/config.test.ts
describe('Module Configuration', () => {
it('should validate module configuration', () => {
const config = loadModulesConfig();
expect(config).toBeDefined();
expect(config.modules).toBeInstanceOf(Array);
});
it('should resolve module dependencies', () => {
const module = getModuleConfig('molos-tasks');
const dependencies = resolveDependencies(module);
expect(dependencies).toContain('@molos/core');
});
it('should handle branch-based modules', () => {
const config = loadModulesConfig();
const branchModules = config.modules.filter(
m => m.branch !== undefined
);
expect(branchModules.length).toBeGreaterThan(0);
});
});
Key Commits
- refactor(icon-picker): improve component functionality and accessibility
- feat(ui): add icon picker component
- refactor(icon-picker): clean up code and normalize icon selection
- fix(color-picker): deduplicate theme palette colors
- feat(modules): add $module alias for internal imports and remove unused modules
- docs(modules): document $module alias usage for internal imports
- chore(modules): distinguish tag and branch fetching behavior
- feat(modules): add support for linking stores and components directories
What's Next
The module system enhancements provide a better user experience for module management. Coming up:
- Implementing module templates and presets
- Adding module marketplace integration
- Enhancing module discovery with search and filtering
- Implementing module usage analytics
- Adding third-party module support
These module system enhancements improve usability, accessibility, and developer experience for working with MoLOS modules.
