Skip to main content

Devlog Mar 3: Module System Enhancements

· 8 min read
Eduardez
MoLOS Lead Developer

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

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.