anentrypoint-design 0.0.73 → 0.0.75

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,112 +1,9 @@
1
- import * as webjsx from '../../vendor/webjsx/index.js';
2
- export const h = webjsx.createElement;
3
- import { Panel, Row, Hero, Receipt, Kpi, Table, Form } from './content.js';
4
- import { Chip } from './shell.js';
5
- import { EmptyState } from './files.js';
1
+ export { skillLabel, getRecentPaths, saveRecentPath, renderChatMessages } from './freddie/helpers.js';
2
+ export { home, sessions, projects, agents, analytics } from './freddie/pages-core.js';
3
+ export { chat } from './freddie/pages-chat.js';
4
+ export { models, cron, skills, config, env, tools, batch, gateway } from './freddie/pages-config.js';
6
5
 
7
- export const skillLabel = s => (s.name||'').replace(/^gm:/,'').replace(/-/g,' ');
8
- export const getRecentPaths = () => { try { return JSON.parse(localStorage.getItem('fd_recent_cwds')||'[]'); } catch { return []; } };
9
- export const saveRecentPath = p => { if (!p) return; const a = getRecentPaths().filter(x=>x!==p); a.unshift(p); localStorage.setItem('fd_recent_cwds', JSON.stringify(a.slice(0,5))); };
10
- export function renderChatMessages(el, msgs) {
11
- if (!el) return; el.innerHTML = '';
12
- for (const m of msgs) {
13
- const div = document.createElement('div');
14
- div.className = 'fd-msg'+(m.role==='assistant'?' fd-msg-assistant':'');
15
- if (m.role==='tool') { const det=document.createElement('details'); det.className='fd-tool-call'; const sum=document.createElement('summary'); sum.textContent=(m.name||'tool')+(m.argsSummary?': '+m.argsSummary:''); const pre=document.createElement('pre'); pre.className='fd-tool-body'; pre.textContent=m.content||''; det.appendChild(sum); det.appendChild(pre); div.appendChild(det); }
16
- else { const pre=document.createElement('pre'); pre.className='fd-pre'; pre.textContent=m.content||''; div.appendChild(pre); }
17
- el.appendChild(div);
18
- }
19
- el.scrollTop = el.scrollHeight;
20
- }
21
- export async function home(h0) {
22
- const sessions=await h0.pi.sessions.list(); const health=h0.pi.health();
23
- return [Hero({title:'freddie',body:'open js agent harness.',accent:h0.version||'web'}),Kpi({items:[[sessions.length,'sessions'],[h0.pi.tools.size,'tools'],[h0.pi.skills.size,'skills']]}),Panel({title:'quick start',children:Receipt({rows:[['open chat',"click 'chat' — set a working directory"],['pick skill','software dev, research, planning'],['set api key','keys tab → click chip'],['add cron','cron tab → form']]})}),Panel({title:'host',children:Receipt({rows:Object.entries(health).map(([k,v])=>[k,String(v)])})})];
24
- }
25
- export async function chat(h0) {
26
- const skills=[...h0.pi.skills.values()]; const providers=await fetch('/api/providers').then(r=>r.json()).catch(()=>[]); const configured=providers.filter(p=>p.configured);
27
- const cs=window.__fd_chatState=window.__fd_chatState||{cwd:'',skill:'',provider:'',model:'',messages:[],busy:false,sessionId:null};
28
- if (!cs.cwd) cs.cwd=(getRecentPaths()[0]||'');
29
- const root=document.getElementById('app'); const getMsgs=()=>root.querySelector('#fd-chat-msgs');
30
- const newSession=()=>{ if(cs.busy)return; cs.messages=[]; cs.sessionId=null; renderChatMessages(getMsgs(),cs.messages); };
31
- const parseSse=text=>{ const evs=[]; let ev=null,data=''; for(const line of text.split('\n')){ if(line.startsWith('event: '))ev=line.slice(7).trim(); else if(line.startsWith('data: '))data=line.slice(6).trim(); else if(line===''&&ev){try{evs.push({event:ev,data:JSON.parse(data)});}catch{}ev=null;data='';} } return evs; };
32
- const sendChat=async ev=>{ ev.preventDefault(); if(cs.busy)return; const promptEl=ev.target.elements.prompt; const prompt=promptEl.value.trim(); if(!prompt)return;
33
- cs.messages.push({role:'user',content:prompt}); promptEl.value=''; promptEl.style.height='auto'; cs.busy=true; saveRecentPath(cs.cwd); renderChatMessages(getMsgs(),cs.messages);
34
- try { const body={prompt,cwd:cs.cwd||undefined,skill:cs.skill||undefined,provider:cs.provider||undefined,model:cs.model||undefined,sessionId:cs.sessionId||undefined};
35
- const resp=await fetch('/api/chat',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify(body)}); const text=await resp.text(); const events=parseSse(text); let ac='';
36
- for(const{event,data}of events){ if(event==='start'&&data.sessionId)cs.sessionId=data.sessionId; if(event==='done'&&data.sessionId)cs.sessionId=data.sessionId;
37
- if(event==='message'){ if(data.role==='assistant'){ const content=Array.isArray(data.content)?data.content:[{type:'text',text:String(data.content||'')}]; for(const block of content){ if(block.type==='text')ac+=block.text; if(block.type==='tool_use'){if(ac){cs.messages.push({role:'assistant',content:ac});ac='';}cs.messages.push({role:'tool',name:block.name,argsSummary:JSON.stringify(block.input||{}).slice(0,60),content:JSON.stringify(block.input||{},null,2)});} } }
38
- else if(data.role==='tool'){const tc=Array.isArray(data.content)?data.content[0]:data;cs.messages.push({role:'tool',name:'result',argsSummary:'',content:String(tc?.content||tc?.text||JSON.stringify(tc))});} }
39
- if(event==='done'&&data.result&&!ac)ac=data.result; if(event==='error')ac='error: '+(data.error||'unknown'); }
40
- if(ac)cs.messages.push({role:'assistant',content:ac}); if(!events.length)cs.messages.push({role:'assistant',content:'(no response)'});
41
- } catch(e){cs.messages.push({role:'assistant',content:'error: '+e.message});}
42
- cs.busy=false; renderChatMessages(getMsgs(),cs.messages); };
43
- const byCat=skills.reduce((a,s)=>{const c=s.category||'other';(a[c]=a[c]||[]).push(s);return a;},{});
44
- setTimeout(()=>renderChatMessages(getMsgs(),cs.messages),50);
45
- return [Panel({title:'chat',right:h('button',{class:'btn-primary',onclick:ev=>{ev.preventDefault();newSession();}},'+ new'),children:[
46
- h('form',{class:'fd-chat-form',onsubmit:sendChat},h('label',{class:'fd-label'},'WORKING DIRECTORY'),h('input',{name:'cwd',type:'text',placeholder:'e.g. C:/dev/myproject',value:cs.cwd,oninput:ev=>{cs.cwd=ev.target.value;}}),
47
- h('div',{class:'fd-row'},h('div',{class:'fd-col'},h('label',{class:'fd-label'},'SKILL'),h('select',{name:'skill',onchange:ev=>{cs.skill=ev.target.value;}},h('option',{value:''},'— no skill —'),...Object.entries(byCat).map(([cat,ss])=>h('optgroup',{label:cat},...ss.map(s=>h('option',{value:s.name,selected:cs.skill===s.name?'true':null},skillLabel(s))))))),
48
- h('div',{class:'fd-col'},h('label',{class:'fd-label'},'PROVIDER'),h('select',{name:'provider',onchange:ev=>{cs.provider=ev.target.value;}},h('option',{value:''},configured.length?'— auto —':'— none configured —'),...configured.map(p=>h('option',{value:p.name,selected:cs.provider===p.name?'true':null},(p.available?'● ':'○ ')+p.name)))),
49
- h('div',{class:'fd-col'},h('label',{class:'fd-label'},'MODEL'),h('input',{name:'model',type:'text',placeholder:'default',value:cs.model,oninput:ev=>{cs.model=ev.target.value;}}))),
50
- h('div',{class:'fd-chat-send-row'},h('textarea',{name:'prompt',placeholder:'describe what you want…',rows:4,oninput:ev=>{ev.target.style.height='auto';ev.target.style.height=Math.min(ev.target.scrollHeight,240)+'px';}}),h('button',{type:'submit',class:'btn-primary',disabled:cs.busy?'true':null},cs.busy?'…':'send'))),
51
- h('div',{id:'fd-chat-msgs',class:'fd-chat-thread'})]}),
52
- configured.length===0?Panel({title:'no providers configured',children:Receipt({rows:[['set API key','keys tab → click chip'],['or use acptoapi','run acptoapi server on localhost:4800']]})}):Panel({title:'providers',children:h('div',{class:'fd-chips'},...providers.map(p=>Chip({tone:p.configured?(p.available?'ok':'warn'):'miss',children:p.name+(p.configured?(p.available?' ●':' ○'):'')})))})];
53
- }
54
- export async function sessions(h0) {
55
- const list=await h0.pi.sessions.list();
56
- const rows=list.map(s=>{const cont=h('button',{class:'btn-primary',onclick:async()=>{const msgs=await h0.pi.sessions.getMessages(s.id);const cs=window.__fd_chatState=window.__fd_chatState||{messages:[],busy:false,sessionId:null,cwd:'',skill:'',provider:'',model:''};cs.sessionId=s.id;cs.messages=msgs.map(m=>({role:m.role,content:String(m.content||'')}));if(s.cwd)cs.cwd=s.cwd;if(s.skill)cs.skill=s.skill;if(typeof window.__fd_nav==='function')window.__fd_nav('chat');}},'continue');return[(s.id||'').slice(0,8),s.title||'—',s.platform||'—',s.model||'—',s.cwd?s.cwd.slice(-30):'—',s.skill?skillLabel({name:s.skill}):'—',cont];});
57
- return [Kpi({items:[[list.length,'sessions']]}),Panel({title:'sessions',count:list.length,children:list.length===0?EmptyState({text:'no sessions yet',glyph:'✉'}):Table({headers:['id','title','platform','model','cwd','skill',''],rows})})];
58
- }
59
- export async function projects(h0) {
60
- const list=h0.pi.projects.list(); const active=h0.pi.projects.active();
61
- const rows=list.map(p=>Row({key:p.name,code:p.name===active?.name?'●':'○',title:p.name+(p.name===active?.name?' (active)':''),meta:p.path,onClick:()=>{if(p.name!==active?.name)h0.pi.projects.setActive(p.name);}}));
62
- return [Hero({title:'projects',body:'each project is its own ~/.freddie home.',accent:active?'active · '+active.name:'no active project'}),Kpi({items:[[list.length,'projects'],[active?.name||'—','active']]}),Panel({title:'add project',children:Form({fields:[{name:'name',placeholder:'name',required:true},{name:'path',placeholder:'/abs/path'}],submit:'add',onSubmit:ev=>{h0.pi.projects.create({name:ev.target.elements.name.value,path:ev.target.elements.path.value});}})}),Panel({title:'all projects',count:list.length,children:rows.length?rows:EmptyState({text:'no projects',glyph:'◆'})})];
63
- }
64
- export async function agents(h0) {
65
- const a=typeof h0.pi.agents==='function'?await h0.pi.agents():{count:0,turns:0,active:null};
66
- return [Kpi({items:[[a.count||0,'active'],[a.turns||0,'turns']]}),Panel({title:'agents',children:Receipt({rows:[['active session',a.active||'(none)'],['total turns',String(a.turns||0)]]})})];
67
- }
68
- export async function analytics(h0) {
69
- const list=await h0.pi.sessions.list(); const tools=[...h0.pi.tools.values()];
70
- const byPlat=list.reduce((a,s)=>{const k=s.platform||'?';a[k]=(a[k]||0)+1;return a;},{}); const byModel=list.reduce((a,s)=>{const k=s.model||'?';a[k]=(a[k]||0)+1;return a;},{});
71
- return [Kpi({items:[[list.length,'sessions'],[tools.length,'tools']]}),Panel({title:'by platform',children:Object.keys(byPlat).length===0?EmptyState({text:'no data',glyph:'◉'}):Table({headers:['platform','count'],rows:Object.entries(byPlat).sort((a,b)=>b[1]-a[1])})}),Panel({title:'by model',children:Object.keys(byModel).length===0?EmptyState({text:'no data',glyph:'◎'}):Table({headers:['model','count'],rows:Object.entries(byModel).sort((a,b)=>b[1]-a[1])})})];
72
- }
73
- export async function models(h0) {
74
- const cfg=typeof h0.pi.config?.load==='function'?await h0.pi.config.load():{};
75
- const providers=await fetch('/api/providers').then(r=>r.json()).catch(()=>[]); const configured=providers.filter(p=>p.configured);
76
- const probeState=window.__fd_probeState=window.__fd_probeState||{};
77
- async function probeAll(){await Promise.allSettled(configured.map(async p=>{probeState[p.name]='loading';try{const r=await fetch('/api/providers/'+p.name+'/probe',{method:'POST'}).then(x=>x.json());probeState[p.name]=r.models||r.error||'?';}catch(e){probeState[p.name]='error: '+e.message;}}));if(typeof window.__fd_nav==='function')window.__fd_nav('models');}
78
- const modelPanels=configured.map(p=>{const models=Array.isArray(probeState[p.name])?probeState[p.name]:p.models;const loading=probeState[p.name]==='loading';const children=loading?h('span',{},'probing…'):models&&models.length>0?Table({headers:['model id'],rows:models.map(m=>[m])}):h('span',{class:'fd-muted'},p.modelsError?'error: '+p.modelsError:'not probed — click "probe all"');return Panel({title:p.name+(p.available?' ●':' ○'),children});});
79
- return [Kpi({items:[[configured.length,'configured'],[providers.filter(p=>p.available).length,'available']]}),Panel({title:'change active model',children:Form({fields:[{name:'provider',placeholder:'provider',value:cfg.agent?.provider||''},{name:'model',placeholder:'model id',value:cfg.agent?.model||''}],submit:'update',onSubmit:async ev=>{await h0.pi.config.saveValue('agent.provider',ev.target.elements.provider.value);await h0.pi.config.saveValue('agent.model',ev.target.elements.model.value);}})}),Panel({title:'providers',right:h('button',{class:'btn-primary',onclick:ev=>{ev.preventDefault();probeAll();}},'probe all'),children:h('div',{class:'fd-chips'},...providers.map(p=>Chip({tone:p.configured?(p.available?'ok':'warn'):'miss',children:p.name+(p.configured?(p.available?' ●':' ○'):' ·')})))}),...modelPanels];
80
- }
81
- export async function cron(h0) {
82
- const list=await h0.pi.cron.list();
83
- return [Kpi({items:[[list.length,'jobs']]}),Panel({title:'add job',children:Form({fields:[{name:'cron',placeholder:'* * * * *',required:true},{name:'prompt',placeholder:'prompt',required:true}],submit:'create',onSubmit:async ev=>{await h0.pi.cron.create({cron:ev.target.elements.cron.value,prompt:ev.target.elements.prompt.value});}})}),Panel({title:'jobs',count:list.length,children:list.length===0?EmptyState({text:'no cron jobs',glyph:'◷'}):Table({headers:['id','cron','prompt','enabled'],rows:list.map(j=>[j.id,j.cron,(j.prompt||'').slice(0,40),j.enabled?'yes':'no'])})})];
84
- }
85
- export async function skills(h0) {
86
- const list=[...h0.pi.skills.values()]; const byCat=list.reduce((a,s)=>{(a[s.category||'other']=a[s.category||'other']||[]).push(s);return a;},{});
87
- return [Kpi({items:[[list.length,'skills'],[Object.keys(byCat).length,'categories']]}),list.length===0?EmptyState({text:'no skills — add SKILL.md files to ~/.freddie/skills/',glyph:'◈'}):null,...Object.entries(byCat).map(([cat,ss])=>Panel({title:cat,count:ss.length,children:Table({headers:['name','description'],rows:ss.map(s=>[skillLabel(s),(s.description||'').slice(0,120)])})}))].filter(Boolean);
88
- }
89
- export async function config(h0) {
90
- const cfg=typeof h0.pi.config?.load==='function'?await h0.pi.config.load():{};
91
- const commands=typeof h0.pi.cli?.values==='function'?[...h0.pi.cli.values()]:[];
92
- return [Kpi({items:[[commands.length,'commands'],[cfg._config_version||0,'config version']]}),Panel({title:'set config value',children:Form({fields:[{name:'key',placeholder:'dotted.key',required:true},{name:'value',placeholder:'value (json or string)',required:true}],submit:'save',onSubmit:async ev=>{let v=ev.target.elements.value.value;try{v=JSON.parse(v);}catch{}await h0.pi.config.saveValue(ev.target.elements.key.value,v);}})}),Panel({title:'commands',count:commands.length,children:Table({headers:['name','description'],rows:commands.map(c=>[c.name,c.description||''])})}),Panel({title:'active config',children:Receipt({rows:Object.entries(cfg).map(([k,v])=>[k,typeof v==='object'&&v!==null?JSON.stringify(v):String(v??'')])})})];
93
- }
94
- export async function env(h0) {
95
- const list=typeof h0.pi.env?.list==='function'?h0.pi.env.list():[];
96
- const setCount=list.filter(k=>k.set).length;
97
- return [Kpi({items:[[setCount,'set'],[list.length-setCount,'missing'],[list.length,'total']]}),Panel({title:'environment variables',children:h('div',{class:'fd-chips'},...list.map(k=>h('span',{key:k.key,onclick:()=>{const v=prompt('set '+k.key+' (empty to unset):');if(v==null)return;if(typeof h0.pi.env.set==='function'){h0.pi.env.set(k.key,v);}},class:'fd-chip-wrap'},Chip({tone:k.set?'ok':'miss',children:k.key+(k.set?' ✓':' ·')}))))})];
98
- }
99
- export async function tools(h0) {
100
- const list=[...h0.pi.tools.values()]; const bySet=list.reduce((a,t)=>{(a[t.toolset||'core']=a[t.toolset||'core']||[]).push(t);return a;},{});
101
- return [Kpi({items:[[list.length,'tools'],[Object.keys(bySet).length,'toolsets']]}), ...Object.entries(bySet).map(([ts,items])=>Panel({title:'toolset · '+ts,count:items.length,children:items.map(t=>Row({key:t.name,code:'⚒',title:t.name,sub:(t.description||(t.schema&&t.schema.description)||'').slice(0,80)}))}))];
102
- }
103
- export async function batch(h0) {
104
- const out=h('div',{id:'fd-batch-out'});
105
- return [Panel({title:'run batch',children:Form({fields:[{name:'prompts',kind:'textarea',placeholder:'one prompt per line',rows:6},{name:'concurrency',type:'number',value:'4'}],submit:'run',onSubmit:async ev=>{const prompts=ev.target.elements.prompts.value.split('\n').map(s=>s.trim()).filter(Boolean);if(!prompts.length)return;const root=document.getElementById('app');const node=root.querySelector('#fd-batch-out');if(node)node.textContent='running…';try{const r=await h0.pi.batch.run({prompts,concurrency:Number(ev.target.elements.concurrency.value)||4});if(node){const results=Array.isArray(r)?r:(r&&typeof r==='object'?Object.entries(r).map(([k,v])=>({prompt:k,result:v})):[]);const tbl=document.createElement('table');const thead=tbl.createTHead();const hr=thead.insertRow();['#','prompt','result','status'].forEach(ht=>{const th=document.createElement('th');th.textContent=ht;hr.appendChild(th);});const tbody=tbl.createTBody();results.forEach((item,i)=>{const row=tbody.insertRow();[i+1,(item.prompt||'').slice(0,60),(item.result||item.error||'').slice(0,120),item.error?'error':'ok'].forEach(v=>{const td=row.insertCell();td.textContent=String(v);});});node.innerHTML='';node.appendChild(tbl);}}catch(e){if(node)node.textContent='error: '+(e.message||e);}}})}),Panel({title:'results',children:out})];
106
- }
107
- export async function gateway(h0) {
108
- const platforms=typeof h0.pi.gateway?.platforms==='function'?h0.pi.gateway.platforms():[];
109
- const active=platforms.filter(p=>p.enabled);
110
- return [Kpi({items:[[platforms.length,'platforms'],[active.length,'active']]}),Panel({title:'platforms',count:platforms.length,right:active.length>0?Chip({tone:'ok',children:active.length+' active'}):Chip({tone:'miss',children:'none active'}),children:platforms.length===0?EmptyState({text:'no platforms registered',glyph:'⇌'}):platforms.map(p=>Row({key:p.name,code:p.enabled?'●':'○',title:p.name,sub:p.note||'',meta:p.enabled?'enabled':''}))})];
111
- }
6
+ import { home, sessions, projects, agents, analytics } from './freddie/pages-core.js';
7
+ import { chat } from './freddie/pages-chat.js';
8
+ import { models, cron, skills, config, env, tools, batch, gateway } from './freddie/pages-config.js';
112
9
  export const FREDDIE_PAGES = { home, chat, sessions, projects, agents, analytics, models, cron, skills, config, env, tools, batch, gateway };
@@ -1,3 +1,5 @@
1
1
  export { icons } from './icons.js';
2
2
  export { createDesktopShell } from './shell.js';
3
+ export { renderWindow } from './wm.js';
4
+ export { renderDock } from './launcher.js';
3
5
  export const themeUrl = new URL('./theme.css', import.meta.url).href;
@@ -0,0 +1,87 @@
1
+ // Launcher dock paint surface — pure DOM rendering, no lifecycle.
2
+ // Consumer (thebird) owns instance creation, fs/worker/shell wiring, teardown.
3
+ // renderDock returns a handle whose setInstances/setActive are called from
4
+ // lifecycle code. Visuals are bible-aligned: panel-select bg + accent inset
5
+ // rail for active, tonal hover, lowercase mono labels.
6
+
7
+ export function renderDock(opts = {}) {
8
+ const { root = document.body, callbacks = {} } = opts;
9
+
10
+ const el = document.createElement('div');
11
+ el.className = 'launcher-dock';
12
+
13
+ const addBtn = document.createElement('button');
14
+ addBtn.className = 'launcher-btn launcher-add';
15
+ addBtn.textContent = '+';
16
+ addBtn.title = 'new instance';
17
+ addBtn.addEventListener('click', () => callbacks.onNewInstance && callbacks.onNewInstance());
18
+ el.appendChild(addBtn);
19
+
20
+ const instancesHost = document.createElement('div');
21
+ instancesHost.className = 'launcher-instances';
22
+ instancesHost.style.display = 'flex';
23
+ instancesHost.style.flexDirection = 'column';
24
+ instancesHost.style.gap = '8px';
25
+ instancesHost.style.alignItems = 'center';
26
+ el.appendChild(instancesHost);
27
+
28
+ root.appendChild(el);
29
+
30
+ let activeId = null;
31
+ const buttons = new Map();
32
+
33
+ function clear() {
34
+ while (instancesHost.firstChild) instancesHost.removeChild(instancesHost.firstChild);
35
+ buttons.clear();
36
+ }
37
+
38
+ function setInstances(list) {
39
+ clear();
40
+ for (const inst of list) {
41
+ const row = document.createElement('div');
42
+ row.className = 'launcher-row';
43
+ row.dataset.instanceId = inst.id;
44
+ row.style.display = 'flex';
45
+ row.style.flexDirection = 'column';
46
+ row.style.gap = '2px';
47
+ row.style.alignItems = 'center';
48
+
49
+ const selBtn = document.createElement('button');
50
+ selBtn.className = 'launcher-btn';
51
+ selBtn.textContent = inst.label || inst.id;
52
+ selBtn.title = 'instance ' + inst.id;
53
+ selBtn.dataset.role = 'select';
54
+ selBtn.dataset.instanceId = inst.id;
55
+ if (inst.active || inst.id === activeId) selBtn.classList.add('active');
56
+ selBtn.addEventListener('click', () => callbacks.onSelectInstance && callbacks.onSelectInstance(inst.id));
57
+
58
+ const closeBtn = document.createElement('button');
59
+ closeBtn.className = 'launcher-btn launcher-close';
60
+ closeBtn.textContent = 'x';
61
+ closeBtn.title = 'close ' + inst.id;
62
+ closeBtn.dataset.role = 'close';
63
+ closeBtn.dataset.instanceId = inst.id;
64
+ closeBtn.addEventListener('click', e => {
65
+ e.stopPropagation();
66
+ callbacks.onCloseInstance && callbacks.onCloseInstance(inst.id);
67
+ });
68
+
69
+ row.append(selBtn, closeBtn);
70
+ instancesHost.appendChild(row);
71
+ buttons.set(inst.id, { selBtn, closeBtn, row });
72
+ }
73
+ if (activeId && !buttons.has(activeId)) activeId = null;
74
+ }
75
+
76
+ function setActive(id) {
77
+ activeId = id;
78
+ for (const [iid, b] of buttons) b.selBtn.classList.toggle('active', iid === id);
79
+ }
80
+
81
+ function dispose() {
82
+ el.remove();
83
+ buttons.clear();
84
+ }
85
+
86
+ return { el, setInstances, setActive, dispose };
87
+ }