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.