1
-# Agent Zero Component System
2
-
3
-> Generated from codebase reconnaissance on 2026-01-10
4
-> Scope: `webui/components/` - Self-contained Alpine.js component architecture
5
-
6
-## Quick Reference
7
-
8
-| Aspect | Value |
9
-|--------|-------|
10
-| Tech Stack | Alpine.js, ES Modules, CSS Variables |
11
-| Component Tag | `<x-component path="...">` |
12
-| State Management | `createStore(name, model)` from `/js/AlpineStore.js` |
13
-| Modals | `openModal(path)` / `closeModal()` from `/js/modals.js` |
14
-| API Layer | `callJsonApi()` / `fetchApi()` from `/js/api.js` |
15
-
16
----
17
-
18
-## Table of Contents
19
-
20
-1. [Architecture Overview](#1-architecture-overview)
21
-2. [Component Structure](#2-component-structure)
22
-3. [Store Pattern](#3-store-pattern)
23
-4. [Lifecycle Management](#4-lifecycle-management)
24
-5. [Integration Layer](#5-integration-layer)
25
-6. [Alpine.js Directives](#6-alpinejs-directives)
26
-7. [Patterns and Conventions](#7-patterns-and-conventions)
27
-8. [Pitfalls and Anti-Patterns](#8-pitfalls-and-anti-patterns)
28
-9. [Porting Guide](#9-porting-guide)
29
-
30
----
31
-
32
-## 1. Architecture Overview
33
-
34
-### Core Files (Integration Layer)
35
-
36
-| File | Purpose |
37
-|------|---------|
38
-| `/js/components.js` | Component loader - hydrates `<x-component>` tags |
39
-| `/js/AlpineStore.js` | Store factory with Alpine proxy |
40
-| `/js/modals.js` | Modal stack management |
41
-| `/js/initFw.js` | Bootstrap: loads Alpine, registers custom directives |
42
-| `/js/api.js` | CSRF-protected API client (`callJsonApi`, `fetchApi`) |
43
-
44
-### Component Resolution
45
-
46
-```
47
-<x-component path="sidebar/left-sidebar.html">
48
- ↓
49
- Resolves to: components/sidebar/left-sidebar.html
50
- ↓
51
- Loader: importComponent() fetches, parses, injects
52
-```
53
-
54
-- Path auto-prefixes `components/` if not present
55
-- Component HTML cached after first fetch
56
-- Module scripts cached by virtual URL
57
-- MutationObserver auto-loads dynamically inserted components
58
-
59
-### Data Flow
60
-
61
-```
62
-Component HTML
63
- ↓
64
-imports Store module
65
- ↓
66
-createStore() registers with Alpine
67
- ↓
68
-Template binds via $store.name
69
- ↓
70
-User actions → store methods → state updates → reactive UI
71
-```
72
-
73
----
74
-
75
-## 2. Component Structure
76
-
77
-### Anatomy of a Component
78
-
79
-```html
80
-<html>
81
-<head>
82
- <!-- Module imports MUST be in <head> -->
83
- <script type="module">
84
- import { store } from "/components/feature/feature-store.js";
85
- </script>
86
-</head>
87
-
88
-<body>
89
- <!-- Store gate: prevents render until store registered -->
90
- <div x-data>
91
- <template x-if="$store.featureStore">
92
- <!-- Single root element inside template (mandatory) -->
93
- <div class="feature-container">
94
- <p x-text="$store.featureStore.value"></p>
95
- <button @click="$store.featureStore.action()">Do Thing</button>
96
- </div>
97
- </template>
98
- </div>
99
-
100
- <!-- Inline styles scoped to component -->
101
- <style>
102
- .feature-container {
103
- color: var(--color-text);
104
- }
105
- </style>
106
-</body>
107
-</html>
108
-```
109
-
110
-### Key Rules
111
-
112
-| Rule | Rationale |
113
-|------|-----------|
114
-| Scripts in `<head>`, content in `<body>` | Loader extracts separately |
115
-| Use `type="module"` for scripts | Enables ES imports, caching |
116
-| Wrap with `x-data` + `x-if="$store.X"` | Prevents render before store ready |
117
-| `<template>` has ONE root element | Alpine limitation |
118
-| Styles inline in component | Self-contained, no global CSS files |
119
-
120
-### Nesting Components
121
-
122
-```html
123
-<div class="parent-container">
124
- <x-component path="child/child-component.html"></x-component>
125
-</div>
126
-```
127
-
128
-Components can nest other components. Loader recursively processes `x-component` tags.
129
-
130
----
131
-
132
-## 3. Store Pattern
133
-
134
-### Creating a Store
135
-
136
-```javascript
137
-// /components/feature/feature-store.js
138
-import { createStore } from "/js/AlpineStore.js";
139
-
140
-const model = {
141
- // State
142
- items: [],
143
- loading: false,
144
- _initialized: false,
145
-
146
- // Lifecycle (called once by Alpine when store registers)
147
- init() {
148
- if (this._initialized) return;
149
- this._initialized = true;
150
- this.load();
151
- },
152
-
153
- // Actions
154
- async load() {
155
- this.loading = true;
156
- // ... fetch data
157
- this.loading = false;
158
- },
159
-
160
- // Computed-like getters (Alpine reactivity works)
161
- get itemCount() {
162
- return this.items.length;
163
- }
164
-};
165
-
166
-export const store = createStore("featureStore", model);
167
-```
168
-
169
-### Store Proxy Behavior
170
-
171
-`createStore()` returns a proxy that:
172
-- Before Alpine boots: reads/writes directly to `model` object
173
-- After Alpine boots: reads/writes through `Alpine.store(name)`
174
-
175
-This enables safe module-level initialization before Alpine loads.
176
-
177
-### Store Access
178
-
179
-| Context | Syntax |
180
-|---------|--------|
181
-| Template (Alpine) | `$store.featureStore.prop` |
182
-| Module import | `import { store } from "./feature-store.js"; store.prop` |
183
-| Global (avoid) | `Alpine.store("featureStore").prop` |
184
-
185
-Prefer module imports over global lookups.
186
-
187
-### Persistence Helpers
188
-
189
-```javascript
190
-import { saveState, loadState } from "/js/AlpineStore.js";
191
-
192
-// Save to localStorage (exclude functions automatically)
193
-const snapshot = saveState(store, [], ["transientField"]);
194
-localStorage.setItem("myStore", JSON.stringify(snapshot));
195
-
196
-// Restore
197
-const saved = JSON.parse(localStorage.getItem("myStore"));
198
-loadState(store, saved);
199
-```
200
-
201
----
202
-
203
-## 4. Lifecycle Management
204
-
205
-### Custom Alpine Directives
206
-
207
-Registered in `/js/initFw.js`:
208
-
209
-| Directive | When Fires | Use Case |
210
-|-----------|------------|----------|
211
-| `x-create` | Once on mount | Initialize, subscribe to events |
212
-| `x-destroy` | On unmount/cleanup | Unsubscribe, clear timers |
213
-| `x-every-second` | Every 1s while mounted | Polling, countdowns |
214
-| `x-every-minute` | Every 60s while mounted | Low-frequency updates |
215
-| `x-every-hour` | Every 3600s while mounted | Rare periodic tasks |
216
-
217
-### Usage Pattern
218
-
219
-```html
220
-<div
221
- x-create="$store.myStore.onOpen()"
222
- x-destroy="$store.myStore.cleanup()"
223
->
224
- <!-- content -->
225
-</div>
226
-```
227
-
228
-### Store Lifecycle Pattern
229
-
230
-```javascript
231
-const model = {
232
- _initialized: false,
233
- resizeHandler: null,
234
-
235
- init() {
236
- // Guard: runs only once per app lifetime
237
- if (this._initialized) return;
238
- this._initialized = true;
239
- // Global setup: event listeners, intervals
240
- this.resizeHandler = () => this.handleResize();
241
- window.addEventListener("resize", this.resizeHandler);
242
- },
243
-
244
- // Called via x-create when component mounts (can run multiple times)
245
- onOpen() {
246
- this.loadData();
247
- },
248
-
249
- // Called via x-destroy when component unmounts
250
- cleanup() {
251
- // Clear component-specific state, not global listeners
252
- },
253
-
254
- // For full teardown (rarely needed)
255
- destroy() {
256
- if (this.resizeHandler) {
257
- window.removeEventListener("resize", this.resizeHandler);
258
- this.resizeHandler = null;
259
- }
260
- this._initialized = false;
261
- }
262
-};
263
-```
264
-
265
-Key distinction:
266
-- `init()` → once per app load (store registration)
267
-- `onOpen()` → each time component mounts (modal opens, etc.)
268
-- `cleanup()`/`destroy()` → teardown resources
269
-
270
----
271
-
272
-## 5. Integration Layer
273
-
274
-### API Calls
275
-
276
-```javascript
277
-import { callJsonApi, fetchApi } from "/js/api.js";
278
-
279
-// JSON POST with CSRF
280
-const result = await callJsonApi("/endpoint", { key: "value" });
281
-
282
-// Raw fetch with CSRF
283
-const response = await fetchApi("/endpoint", {
284
- method: "GET",
285
- headers: { "Accept": "application/json" }
286
-});
287
-```
288
-
289
-- `callJsonApi`: JSON-in, JSON-out, throws on non-2xx
290
-- `fetchApi`: Adds CSRF header, handles 403 retry, redirects to `/login`
291
-
292
-### Modals
293
-
294
-```javascript
295
-import { openModal, closeModal } from "/js/modals.js";
296
-
297
-// Open (returns Promise that resolves when modal closes)
298
-await openModal("feature/feature-modal.html");
299
-
300
-// Close topmost modal
301
-closeModal();
302
-
303
-// Close specific modal by path
304
-closeModal("feature/feature-modal.html");
305
-```
306
-
307
-Modal component receives title from `<title>` tag:
308
-```html
309
-<head>
310
- <title>My Modal Title</title>
311
-</head>
312
-```
313
-
314
-Modal footer (outside scroll area):
315
-```html
316
-<div data-modal-footer>
317
- <button @click="closeModal()">Close</button>
318
-</div>
319
-```
320
-
321
-### Attribute Inheritance
322
-
323
-Parent `x-component` attributes accessible via `globalThis.xAttrs(element)`:
324
-
325
-```html
326
-<!-- Parent -->
327
-<x-component path="child.html" mydata='{"id": 123}'></x-component>
328
-
329
-<!-- Child can access -->
330
-<script type="module">
331
- const attrs = globalThis.xAttrs(document.currentScript);
332
- console.log(attrs.mydata.id); // 123
333
-</script>
334
-```
335
-
336
----
337
-
338
-## 6. Alpine.js Directives
339
-
340
-### Common Patterns
341
-
342
-| Pattern | Syntax |
343
-|---------|--------|
344
-| Reactive text | `x-text="$store.s.value"` |
345
-| Conditional render | `x-if="$store.s.condition"` |
346
-| Visibility toggle | `x-show="$store.s.visible"` |
347
-| Class binding | `:class="{'active': $store.s.isActive}"` |
348
-| Event handler | `@click="$store.s.action()"` |
349
-| Two-way bind | `x-model="$store.s.inputValue"` |
350
-| List iteration | `<template x-for="item in $store.s.items">` |
351
-| Init expression | `x-init="$store.s.load()"` |
352
-
353
-### Store Gating (Critical Pattern)
354
-
355
-```html
356
-<div x-data>
357
- <template x-if="$store.myStore">
358
- <!-- Renders only when store exists -->
359
- </template>
360
-</div>
361
-```
362
-
363
-Always gate components that depend on stores. Prevents errors during initial load race.
364
-
365
----
366
-
367
-## 7. Patterns and Conventions
368
-
369
-### ✅ DO
370
-
371
-| Pattern | Example |
372
-|---------|---------|
373
-| Self-contained components | All HTML/CSS/JS in one component folder |
374
-| Module imports with absolute paths | `import { store } from "/components/..."` |
375
-| CSS variables for theming | `color: var(--color-text)` |
376
-| Guard `init()` with `_initialized` | Prevents duplicate setup |
377
-| Use `display: contents` for flex chains | Wrapper doesn't break parent flex |
378
-| Inline component styles | `<style>` in component `<body>` |
379
-| Import stores in `<head>` | Ensures registration before render |
380
-| Name stores uniquely | `createStore("featureStore", ...)` |
381
-
382
-### CSS Variable Theming
383
-
384
-```css
385
-.component {
386
- background: var(--color-panel);
387
- color: var(--color-text);
388
- border: 1px solid var(--color-border);
389
- padding: var(--spacing-md);
390
- transition: all var(--transition-speed) ease-in-out;
391
- font-size: var(--font-size-normal);
392
-}
393
-```
394
-
395
-### Flex Chain Preservation
396
-
397
-When `x-component` wrapper would break flex layout:
398
-
399
-```css
400
-#parent-container > x-component,
401
-#parent-container > x-component > div[x-data] {
402
- display: contents;
403
-}
404
-```
405
-
406
----
407
-
408
-## 8. Pitfalls and Anti-Patterns
409
-
410
-### 🚫 DON'T
411
-
412
-| Anti-Pattern | Why | Fix |
413
-|--------------|-----|-----|
414
-| Global CSS files | Breaks encapsulation | Inline styles per component |
415
-| `window.Alpine.store()` lookups | Timing issues, coupling | Import store module directly |
416
-| Call `init()` from `x-init` | Runs multiple times | Use guard, or use `x-create` for per-mount |
417
-| Multiple roots in `<template>` | Alpine breaks | Wrap in single `<div>` |
418
-| `.catch(() => null)` for errors | Hides bugs | Let errors surface, use notifications |
419
-| Scripts outside `<head>` | May not load before template | Move to `<head>` with `type="module"` |
420
-| Hardcoded colors | Breaks theming | Use CSS variables |
421
-| Relative imports `./file.js` | Path resolution issues | Use absolute `/components/...` |
422
-
423
-### Common Mistakes
424
-
425
-Race condition: store not ready
426
-```html
427
-<!-- ❌ BAD: No gate -->
428
-<div x-data>
429
- <p x-text="$store.myStore.value"></p>
430
-</div>
431
-
432
-<!-- ✅ GOOD: Store gate -->
433
-<div x-data>
434
- <template x-if="$store.myStore">
435
- <p x-text="$store.myStore.value"></p>
436
- </template>
437
-</div>
438
-```
439
-
440
-Duplicate initialization
441
-```javascript
442
-// ❌ BAD: Runs every time store accessed
443
-init() {
444
- window.addEventListener("resize", this.handler);
445
-}
446
-
447
-// ✅ GOOD: Guard pattern
448
-init() {
449
- if (this._initialized) return;
450
- this._initialized = true;
451
- window.addEventListener("resize", this.handler);
452
-}
453
-```
454
-
455
-Leaking listeners
456
-```javascript
457
-// ❌ BAD: No cleanup
458
-init() {
459
- this.interval = setInterval(() => this.tick(), 1000);
460
-}
461
-
462
-// ✅ GOOD: With cleanup
463
-init() {
464
- this.interval = setInterval(() => this.tick(), 1000);
465
-},
466
-destroy() {
467
- clearInterval(this.interval);
468
-}
469
-```
470
-
471
----
472
-
473
-## 9. Porting Guide
474
-
475
-### Minimum Requirements for External Apps
476
-
477
-1. Files to copy:
478
- ```
479
- /js/components.js # Component loader
480
- /js/AlpineStore.js # Store factory
481
- /js/modals.js # Modal system (optional)
482
- /js/initFw.js # Alpine bootstrap + directives
483
- ```
484
-
485
-2. Dependencies:
486
- - Alpine.js (vendor or CDN)
487
- - CSS variables (define your theme)
488
-
489
-3. Bootstrap sequence:
490
- ```javascript
491
- // initFw.js pattern:
492
- await import("path/to/alpine.min.js");
493
-
494
- // Register custom directives
495
- Alpine.directive("destroy", ...);
496
- Alpine.directive("create", ...);
497
- // etc.
498
- ```
499
-
500
-4. HTML entry point:
501
- ```html
502
- <script type="module" src="/js/initFw.js"></script>
503
- <x-component path="app/root.html"></x-component>
504
- ```
505
-
506
-### Adaptation Checklist
507
-
508
-- [ ] Define CSS variables for theming (`--color-*`, `--spacing-*`, etc.)
509
-- [ ] Set up component directory structure
510
-- [ ] Configure build tool to serve `/components/` path (or adjust loader)
511
-- [ ] Create API wrapper matching your backend (replace `api.js`)
512
-- [ ] Test MutationObserver behavior with your router/SPA framework
513
-- [ ] Verify module caching behavior in production build
514
-
515
-### Integration with Frameworks
516
-
517
-| Framework | Consideration |
518
-|-----------|--------------|
519
-| Vanilla/Static | Works directly, include initFw.js |
520
-| Electron | Works, may need CSP adjustments for Blob URLs |
521
-| React/Vue | Mount Alpine in specific container, avoid conflicts |
522
-| SPA Routers | MutationObserver handles dynamic inserts |
523
-
524
----
525
-
526
-## Directory Structure Reference
527
-
528
-```
529
-webui/components/
530
-├── _examples/ # Reference implementations
531
-│ ├── _example-component.html
532
-│ └── _example-store.js
533
-├── chat/
534
-│ ├── input/
535
-│ │ ├── chat-bar.html
536
-│ │ └── input-store.js
537
-│ └── ...
538
-├── sidebar/
539
-│ ├── sidebar-store.js
540
-│ ├── left-sidebar.html
541
-│ └── chats/
542
-│ └── chats-list.html
543
-├── modals/
544
-│ └── file-browser/
545
-│ ├── file-browser.html
546
-│ └── file-browser-store.js
547
-├── notifications/
548
-│ ├── notification-store.js
549
-│ └── notification-toast-stack.html
550
-└── settings/
551
- └── ...
552
-```
553
-
554
-Naming conventions:
555
-- Components: `feature-name.html`
556
-- Stores: `feature-store.js` or `feature-name-store.js`
557
-- Modals: placed in `modals/` or feature folder
558
-
559
----
560
-
561
-## Key Exports Summary
562
-
563
-### `/js/components.js`
564
-```javascript
565
-export async function importComponent(path, targetElement)
566
-export async function loadComponents(roots)
567
-export function getParentAttributes(el)
568
-// Global: globalThis.xAttrs
569
-```
570
-
571
-### `/js/AlpineStore.js`
572
-```javascript
573
-export function createStore(name, initialState)
574
-export function getStore(name)
575
-export function saveState(store, include, exclude)
576
-export function loadState(store, state, include, exclude)
577
-```
578
-
579
-### `/js/modals.js`
580
-```javascript
581
-export function openModal(modalPath)
582
-export function closeModal(modalPath?)
583
-export function scrollModal(id)
584
-// Globals: globalThis.openModal, closeModal, scrollModal
585
-```
586
-
587
-### `/js/api.js`
588
-```javascript
589
-export async function callJsonApi(endpoint, data)
590
-export async function fetchApi(url, request)
591
-```
592
-
593
----
594
-
595
-## Addendum: Additional Patterns
596
-
597
-### Alpine Transitions
598
-
599
-Use `x-transition` for enter/leave animations:
600
-
601
-```html
602
-<div x-show="visible"
603
- x-transition:enter="fade-enter"
604
- x-transition:leave="fade-leave">
605
-```
606
-
607
-### Two-Click Confirmation (`$confirmClick`)
608
-
609
-Magic helper for destructive actions:
610
-
611
-```html
612
-<button @click="$confirmClick($event, () => $store.myStore.delete(item.id))">
613
- <span class="material-symbols-outlined">delete</span>
614
-</button>
615
-```
616
-
617
-First click arms (icon changes to checkmark), second click confirms. Auto-resets after 2s.
618
-
619
-### Device Detection
620
-
621
-Body receives `device-touch` or `device-mouse` class via `/js/initializer.js`. Use for input-type-specific styling:
622
-
623
-```css
624
-.device-touch .hover-only { display: none; }
625
-```
626
-
627
-### CSS Variables Reference
628
-
629
-Defined in `/webui/index.css`:
630
-
631
-| Variable | Purpose |
632
-|----------|---------|
633
-| `--color-background` | Page background |
634
-| `--color-text` | Primary text |
635
-| `--color-primary` | Headings, emphasis |
636
-| `--color-panel` | Card/sidebar backgrounds |
637
-| `--color-border` | Borders, dividers |
638
-| `--color-accent` | Highlights, actions |
639
-| `--color-input` | Form field backgrounds |
640
-| `--spacing-xs/sm/md/lg` | Consistent spacing scale |
641
-| `--font-size-small/normal/large` | Typography scale |
642
-| `--transition-speed` | Animation duration (0.3s) |
643
-
644
-Theme switching via `.light-mode` class on root element.
645
-
646
----
647
-
648
-*End of Component System Documentation*