@jossuealcala/madre 1.0.2 → 1.0.3
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.
- package/CHANGELOG.en.md +69 -1
- package/CHANGELOG.md +69 -1
- package/README.en.md +11 -15
- package/README.md +3 -3
- package/bin/madre.cjs +40 -0
- package/bin/madre.mjs +5 -0
- package/docs/REFERENCE.md +29 -3
- package/docs/SDK.md +4 -2
- package/docs/en/REFERENCE.md +29 -3
- package/docs/en/SDK.md +4 -2
- package/docs/training/README.md +28 -1
- package/docs/training/train.sh +5 -1
- package/package.json +3 -3
- package/public/app.js +1626 -151
- package/public/attribution.js +44 -1
- package/public/brands.js +20 -5
- package/public/code-tools.js +143 -0
- package/public/editor.js +20 -4
- package/public/es.js +393 -25
- package/public/hardware.js +50 -0
- package/public/index.html +5 -1
- package/public/macroverso.js +187 -96
- package/public/market-art.js +161 -0
- package/public/market-view.js +529 -0
- package/public/market.js +189 -0
- package/public/neural.js +110 -0
- package/public/nostromo-engine.js +412 -47
- package/public/power.js +100 -0
- package/public/resay.js +5 -0
- package/public/salas-mac.css +98 -14
- package/public/salas-mac.html +9 -1
- package/public/salas-mac.js +307 -30
- package/public/styles.css +838 -61
- package/public/tour.js +48 -6
- package/public/troubleshooting.js +6 -1
- package/public/wallpaper.js +275 -0
- package/src/adapters/cursor.mjs +96 -0
- package/src/adapters/gemini.mjs +10 -3
- package/src/adapters/madre.mjs +2 -1
- package/src/auth-probe.mjs +143 -12
- package/src/bridge/policy.mjs +1 -1
- package/src/capabilities.mjs +9 -0
- package/src/checkpoint.mjs +16 -1
- package/src/credentials.mjs +3 -0
- package/src/dataset.mjs +76 -4
- package/src/enlaces.mjs +208 -2
- package/src/extensions.mjs +11 -3
- package/src/i18n.mjs +16 -0
- package/src/looks.mjs +125 -2
- package/src/market-key.mjs +8 -0
- package/src/market-remote.mjs +239 -0
- package/src/models.mjs +4 -0
- package/src/module-scope.mjs +68 -0
- package/src/modules/ahp.mjs +102 -7
- package/src/modules/bishop-exam.mjs +122 -0
- package/src/modules/bishop.mjs +71 -12
- package/src/modules/enlaces.mjs +87 -6
- package/src/modules/image-studio.mjs +10 -1
- package/src/modules/intents.mjs +3 -1
- package/src/modules/local-models.mjs +124 -0
- package/src/modules/ollama.mjs +12 -3
- package/src/modules/playwright.mjs +73 -15
- package/src/modules/sdk.mjs +61 -9
- package/src/npm-launch.mjs +18 -0
- package/src/ollama.mjs +6 -5
- package/src/outbound.mjs +9 -1
- package/src/ports.mjs +4 -2
- package/src/room/control.mjs +1 -1
- package/src/room/prompt.mjs +13 -2
- package/src/room.mjs +14 -4
- package/src/router.mjs +1 -1
- package/src/runs.mjs +4 -1
- package/src/runtime-detection.mjs +9 -0
- package/src/salas.mjs +26 -0
- package/src/server.mjs +617 -48
- package/src/setup.mjs +1 -1
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// MADRE · © 2026 Jossue Alcalá · GNU AGPL v3 with additional terms (Section 7): ADDITIONAL-TERMS.md
|
|
3
|
+
// THE MARKET, full screen (2026-10-18). The same store in Home and in every project, shaped like the
|
|
4
|
+
// stores people already know: the featured modules in a carousel with their pictures, shelves by
|
|
5
|
+
// what they are for, a page per module with its one key, and — in Home — MODULES, where what is on
|
|
6
|
+
// is managed project by project.
|
|
7
|
+
//
|
|
8
|
+
// Where it opens decides what its key does. In a project, a module is turned on for that project
|
|
9
|
+
// (its own word in ~/.pulse/config.json, src/module-scope.mjs), and one that writes into the folder
|
|
10
|
+
// opens its card in the project's MODULES, where the commands are read before they run. In Home,
|
|
11
|
+
// a module is turned on for every project or only in the one chosen; one that writes into a folder
|
|
12
|
+
// opens that project, on its card. Local AI keeps its own place: Settings › Local AI.
|
|
13
|
+
//
|
|
14
|
+
// The host page hands in what only it can do: `localAi()`, `installHere(item)` (a project),
|
|
15
|
+
// `openProject(projectId, item)` (Home), `toast(text)` and `changed()`.
|
|
16
|
+
|
|
17
|
+
import { t, language } from './i18n.js';
|
|
18
|
+
import { appIcon, inMarket, loadMarket, marketEntry, marketExtras, marketFeatured, marketShelves, moduleFace } from './market.js';
|
|
19
|
+
import { marketArt } from './market-art.js';
|
|
20
|
+
|
|
21
|
+
const el = (tag, className = null, text = null) => {
|
|
22
|
+
const node = document.createElement(tag);
|
|
23
|
+
if (className) node.className = className;
|
|
24
|
+
if (text !== null && text !== undefined) node.textContent = text;
|
|
25
|
+
return node;
|
|
26
|
+
};
|
|
27
|
+
const button = (className, text, onClick) => {
|
|
28
|
+
const node = el('button', className, text);
|
|
29
|
+
node.type = 'button';
|
|
30
|
+
if (onClick) node.addEventListener('click', onClick);
|
|
31
|
+
return node;
|
|
32
|
+
};
|
|
33
|
+
const post = async (url, payload = {}) => {
|
|
34
|
+
try {
|
|
35
|
+
const response = await fetch(url, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(payload) });
|
|
36
|
+
return { ok: response.ok, body: await response.json().catch(() => ({})) };
|
|
37
|
+
} catch (error) { return { ok: false, body: { error: error.message } }; }
|
|
38
|
+
};
|
|
39
|
+
const getJson = (url) => fetch(url, { cache: 'no-store' }).then((response) => (response.ok ? response.json() : null)).catch(() => null);
|
|
40
|
+
|
|
41
|
+
const GLYPH = {
|
|
42
|
+
close: '<svg viewBox="0 0 20 20" aria-hidden="true"><path d="M5.5 5.5l9 9M14.5 5.5l-9 9" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg>',
|
|
43
|
+
back: '<svg viewBox="0 0 20 20" aria-hidden="true"><path d="M12.5 4.5L7 10l5.5 5.5" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"/></svg>',
|
|
44
|
+
next: '<svg viewBox="0 0 20 20" aria-hidden="true"><path d="M7.5 4.5L13 10l-5.5 5.5" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"/></svg>',
|
|
45
|
+
search: '<svg viewBox="0 0 20 20" aria-hidden="true"><circle cx="9" cy="9" r="5.2" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M13 13l3.5 3.5" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>',
|
|
46
|
+
};
|
|
47
|
+
const glyph = (node, name) => { node.insertAdjacentHTML('afterbegin', GLYPH[name]); return node; }; // trusted: our own SVG strings
|
|
48
|
+
|
|
49
|
+
// A module from the catalog says its name and what it does in the room's language (market.js).
|
|
50
|
+
const nameOf = (item) => moduleFace(item).name ?? t(item.name ?? item.id);
|
|
51
|
+
const summaryOf = (item) => moduleFace(item).summary ?? t(item.summary ?? '');
|
|
52
|
+
const lineOf = (item) => (moduleFace(item).line ? t(moduleFace(item).line) : t(item.summary ?? ''));
|
|
53
|
+
const priceOf = (item) => (marketEntry(item.id).price ? `$${marketEntry(item.id).price}` : t('Free'));
|
|
54
|
+
const shelfOf = (item) => marketShelves().find((shelf) => shelf.id === marketEntry(item.id).shelf) ?? null;
|
|
55
|
+
// What its key does: a switch MADRE flips, a folder it writes into, or Local AI's own settings. A
|
|
56
|
+
// module from the catalog this computer does not have yet is a switch too: it is got first.
|
|
57
|
+
const kindOf = (item) => (item.id === 'ollama' ? 'local' : item.kind === 'remote' ? 'switch' : item.kind !== 'builtin' || marketEntry(item.id).scope === 'project' ? 'install' : 'switch');
|
|
58
|
+
// Whether this MADRE is new enough for a module from the catalog.
|
|
59
|
+
const fits = (have, need) => {
|
|
60
|
+
if (!need) return true;
|
|
61
|
+
const parts = (value) => String(value).split('.').map((part) => Number.parseInt(part, 10) || 0);
|
|
62
|
+
const [a, b] = [parts(have), parts(need)];
|
|
63
|
+
for (let at = 0; at < 3; at += 1) if ((a[at] ?? 0) !== (b[at] ?? 0)) return (a[at] ?? 0) > (b[at] ?? 0);
|
|
64
|
+
return true;
|
|
65
|
+
};
|
|
66
|
+
const hueOf = (node, item) => { const [a, b] = moduleFace(item).hue ?? []; if (a) { node.style.setProperty('--mk-a', a); node.style.setProperty('--mk-b', b); } return node; };
|
|
67
|
+
const reducedMotion = () => globalThis.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false;
|
|
68
|
+
|
|
69
|
+
let current = null;
|
|
70
|
+
export const marketOpen = () => Boolean(current);
|
|
71
|
+
export function closeMarket() { current?.close(); }
|
|
72
|
+
|
|
73
|
+
export function openMarket({ where = 'home', tab = 'shop', focus = null, project = null, host = {} } = {}) {
|
|
74
|
+
if (current) { current.go({ tab, focus }); return current; }
|
|
75
|
+
const home = where === 'home';
|
|
76
|
+
const before = document.activeElement;
|
|
77
|
+
const root = el('div', 'mk');
|
|
78
|
+
root.setAttribute('role', 'dialog');
|
|
79
|
+
root.setAttribute('aria-modal', 'true');
|
|
80
|
+
root.setAttribute('aria-label', t('Market'));
|
|
81
|
+
root.tabIndex = -1;
|
|
82
|
+
|
|
83
|
+
// The bar: leave, where you are, the search.
|
|
84
|
+
const bar = el('header', 'mk-bar');
|
|
85
|
+
const leave = glyph(button('mk-leave', null, () => close()), 'close');
|
|
86
|
+
leave.setAttribute('aria-label', t('Close'));
|
|
87
|
+
leave.title = t('Close');
|
|
88
|
+
const middle = el('div', 'mk-bar-mid');
|
|
89
|
+
const tabs = {};
|
|
90
|
+
if (home) {
|
|
91
|
+
const seg = el('div', 'mk-tabs');
|
|
92
|
+
seg.setAttribute('role', 'tablist');
|
|
93
|
+
for (const [id, label] of [['shop', t('Market')], ['mine', t('Modules')]]) {
|
|
94
|
+
const key = button('mk-tab', label, () => go({ tab: id }));
|
|
95
|
+
key.setAttribute('role', 'tab');
|
|
96
|
+
tabs[id] = key;
|
|
97
|
+
seg.append(key);
|
|
98
|
+
}
|
|
99
|
+
middle.append(seg);
|
|
100
|
+
} else {
|
|
101
|
+
const here = el('span', 'mk-here');
|
|
102
|
+
here.append(el('b', null, t('Market')), el('span', null, t('for {project}', { project: project?.name ?? t('this project') })));
|
|
103
|
+
middle.append(here);
|
|
104
|
+
}
|
|
105
|
+
// The same search as every other one in MADRE (styles.css, .settings-search).
|
|
106
|
+
const search = glyph(el('label', 'settings-search mk-search'), 'search');
|
|
107
|
+
const input = el('input');
|
|
108
|
+
input.type = 'search';
|
|
109
|
+
input.placeholder = t('Search a module, or what it does');
|
|
110
|
+
input.setAttribute('aria-label', t('Search a module, or what it does'));
|
|
111
|
+
input.spellcheck = false;
|
|
112
|
+
search.append(input);
|
|
113
|
+
bar.append(leave, middle, search);
|
|
114
|
+
const main = el('main', 'mk-main');
|
|
115
|
+
root.append(bar, main);
|
|
116
|
+
document.body.append(root);
|
|
117
|
+
document.documentElement.classList.add('mk-open');
|
|
118
|
+
requestAnimationFrame(() => root.classList.add('in'));
|
|
119
|
+
|
|
120
|
+
let items = [];
|
|
121
|
+
let scope = { projects: [], modules: [] };
|
|
122
|
+
let view = { tab, focus };
|
|
123
|
+
let timer = null;
|
|
124
|
+
const said = (node, text, tone = '') => { node.textContent = text ?? ''; node.dataset.tone = tone; };
|
|
125
|
+
// What went well is said by the page's own notice, the same pill as everywhere else, raised over
|
|
126
|
+
// the Market while it is open (styles.css, html.mk-open .toast).
|
|
127
|
+
const notice = (text) => host.toast?.(text);
|
|
128
|
+
|
|
129
|
+
let madre = null;
|
|
130
|
+
const load = async () => {
|
|
131
|
+
const [extensions, scoped, market] = await Promise.all([getJson('/api/extensions'), home ? getJson('/api/modules/scope') : null, loadMarket({ toast: notice })]);
|
|
132
|
+
madre = market?.madre ?? madre;
|
|
133
|
+
items = (extensions?.extensions ?? []).filter(inMarket);
|
|
134
|
+
items = [...items, ...marketExtras(items)];
|
|
135
|
+
if (scoped) scope = scoped;
|
|
136
|
+
};
|
|
137
|
+
// A module from the catalog is got first — its signed file, checked, for every project — and
|
|
138
|
+
// then turned on wherever the person said.
|
|
139
|
+
const fetchFirst = async (item, line) => {
|
|
140
|
+
if (item.kind !== 'remote') return item;
|
|
141
|
+
said(line, t('Getting {name} from the Market…', { name: nameOf(item) }));
|
|
142
|
+
const answer = await post('/api/market/install', { id: item.id });
|
|
143
|
+
if (!answer.ok) throw new Error(answer.body.error ?? t('{name} could not be installed.', { name: nameOf(item) }));
|
|
144
|
+
await load();
|
|
145
|
+
return items.find((one) => one.id === item.id && one.kind !== 'remote') ?? item;
|
|
146
|
+
};
|
|
147
|
+
const scopeOf = (item) => scope.modules.find((one) => one.id === item.id) ?? null;
|
|
148
|
+
const projectName = (id) => scope.projects.find((one) => one.id === id)?.name ?? id;
|
|
149
|
+
// Whether a module is on: here, in a project; anywhere, in Home.
|
|
150
|
+
const isOn = (item) => {
|
|
151
|
+
if (!home || kindOf(item) === 'local') return Boolean(item.status?.installed);
|
|
152
|
+
const mine = scopeOf(item);
|
|
153
|
+
return mine ? (mine.on?.length ?? 0) > 0 || (kindOf(item) === 'switch' && !scope.projects.length && mine.all) : Boolean(item.status?.installed);
|
|
154
|
+
};
|
|
155
|
+
const reach = (item) => {
|
|
156
|
+
const mine = scopeOf(item);
|
|
157
|
+
if (!home) return item.scope?.all && item.scope?.here === null ? t('On in all your projects') : isOn(item) ? t('On in this project') : t('Off in this project');
|
|
158
|
+
if (kindOf(item) === 'local') return isOn(item) ? t('On on this computer') : t('Off on this computer');
|
|
159
|
+
if (kindOf(item) === 'install') return mine?.on?.length ? t('Installed in {n} of {total} projects', { n: mine.on.length, total: scope.projects.length }) : t('Not installed in any project');
|
|
160
|
+
if (!mine) return isOn(item) ? t('Turned on') : t('Off in every project');
|
|
161
|
+
if (mine.everywhere || (mine.all && !scope.projects.length)) return t('On in all your projects');
|
|
162
|
+
return mine.on.length ? t('On in {n} of {total} projects', { n: mine.on.length, total: scope.projects.length }) : t('Off in every project');
|
|
163
|
+
};
|
|
164
|
+
const refresh = async () => { await load(); host.changed?.(); render(); };
|
|
165
|
+
|
|
166
|
+
// ACTIONS. Each says what happened in the line under the key, and on the page when it went well.
|
|
167
|
+
const turnHere = async (given, on, line, key) => {
|
|
168
|
+
key.disabled = true;
|
|
169
|
+
let item = given;
|
|
170
|
+
try { item = await fetchFirst(given, line); } catch (error) { key.disabled = false; said(line, error.message, 'bad'); return; }
|
|
171
|
+
said(line, on && item.id === 'playwright' ? t('Getting the test browser ready: MADRE downloads what it is missing. It can take a few minutes.') : t('One moment…'));
|
|
172
|
+
const answer = await post(`/api/extensions/${encodeURIComponent(item.id)}/install`, { confirm: true });
|
|
173
|
+
if (!answer.ok) { key.disabled = false; said(line, answer.body.error ?? t('{name} could not be turned on or off.', { name: nameOf(item) }), 'bad'); return; }
|
|
174
|
+
notice(on ? t('{name} is on in this project.', { name: nameOf(item) }) : t('{name} is off in this project.', { name: nameOf(item) }));
|
|
175
|
+
await refresh();
|
|
176
|
+
};
|
|
177
|
+
const turnScope = async (given, change, line, key) => {
|
|
178
|
+
key.disabled = true;
|
|
179
|
+
let item = given;
|
|
180
|
+
try { item = await fetchFirst(given, line); } catch (error) { key.disabled = false; said(line, error.message, 'bad'); return false; }
|
|
181
|
+
const turningOn = change.all === true || Object.values(change.rooms ?? {}).includes(true);
|
|
182
|
+
said(line, turningOn && item.id === 'playwright' ? t('Getting the test browser ready: MADRE downloads what it is missing. It can take a few minutes.') : t('One moment…'));
|
|
183
|
+
const answer = await post(`/api/extensions/${encodeURIComponent(item.id)}/scope`, { ...change, confirm: true });
|
|
184
|
+
if (!answer.ok) { key.disabled = false; said(line, answer.body.error ?? t('{name} could not be turned on or off.', { name: nameOf(item) }), 'bad'); return false; }
|
|
185
|
+
await refresh();
|
|
186
|
+
return true;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
// The key a row and a page share: what one press does, for this module, here.
|
|
190
|
+
const getKey = (item, { onPage = false } = {}) => {
|
|
191
|
+
const on = isOn(item);
|
|
192
|
+
const kind = kindOf(item);
|
|
193
|
+
if (kind === 'local') return button('mk-get', t('Open'), () => { close(); host.localAi?.(); });
|
|
194
|
+
if (!onPage) {
|
|
195
|
+
// On a shelf the key is short: what is on says so and opens its page; what is off gets it.
|
|
196
|
+
if (on) return button('mk-get on', kind === 'install' ? t('Installed') : t('Turned on'), () => go({ tab: 'shop', focus: item.id }));
|
|
197
|
+
if (!home && kind === 'switch') {
|
|
198
|
+
const key = button('mk-get', t('Get'), async (event) => { event.stopPropagation(); const line = el('span'); await turnHere(item, true, line, key); if (line.dataset.tone === 'bad') { notice(line.textContent); key.disabled = false; } });
|
|
199
|
+
return key;
|
|
200
|
+
}
|
|
201
|
+
if (!home && kind === 'install') return button('mk-get', t('Install'), () => { close(); host.installHere?.(item); });
|
|
202
|
+
return button('mk-get', kind === 'install' ? t('Install') : t('Get'), () => go({ tab: 'shop', focus: item.id, choose: true }));
|
|
203
|
+
}
|
|
204
|
+
return null;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const row = (item) => {
|
|
208
|
+
const line = el('div', 'mk-row');
|
|
209
|
+
const open = button('mk-row-open', null, () => go({ tab: 'shop', focus: item.id }));
|
|
210
|
+
const words = el('span', 'mk-row-words');
|
|
211
|
+
words.append(el('b', null, nameOf(item)), el('span', null, lineOf(item)));
|
|
212
|
+
open.append(appIcon(item, 58), words);
|
|
213
|
+
const side = el('span', 'mk-row-side');
|
|
214
|
+
side.append(getKey(item), el('small', null, isOn(item) ? '' : priceOf(item)));
|
|
215
|
+
line.append(open, side);
|
|
216
|
+
return line;
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
// THE CAROUSEL: the featured modules, each on its own colors with its picture.
|
|
220
|
+
const carousel = () => {
|
|
221
|
+
const featured = marketFeatured(items);
|
|
222
|
+
if (!featured.length) return null;
|
|
223
|
+
const box = el('section', 'mk-carousel');
|
|
224
|
+
box.setAttribute('aria-roledescription', t('carousel'));
|
|
225
|
+
box.setAttribute('aria-label', t('Featured'));
|
|
226
|
+
const track = el('div', 'mk-track');
|
|
227
|
+
const dots = el('div', 'mk-dots');
|
|
228
|
+
let index = 0;
|
|
229
|
+
featured.forEach((item, at) => {
|
|
230
|
+
const slide = hueOf(el('article', 'mk-slide'), item);
|
|
231
|
+
slide.setAttribute('aria-roledescription', t('slide'));
|
|
232
|
+
slide.setAttribute('aria-label', t('{n} of {total}', { n: at + 1, total: featured.length }));
|
|
233
|
+
const words = el('div', 'mk-slide-words');
|
|
234
|
+
const get = el('div', 'mk-slide-get');
|
|
235
|
+
const who = el('span', 'mk-slide-who');
|
|
236
|
+
who.append(el('b', null, nameOf(item)), el('span', null, isOn(item) ? reach(item) : priceOf(item)));
|
|
237
|
+
get.append(appIcon(item, 48), who, button('mk-get light', isOn(item) ? t('See it') : kindOf(item) === 'install' ? t('Install') : kindOf(item) === 'local' ? t('Open') : t('Get'), (event) => { event.stopPropagation(); go({ tab: 'shop', focus: item.id, choose: !isOn(item) }); }));
|
|
238
|
+
words.append(el('small', null, t('Featured')), el('h2', null, nameOf(item)), el('p', null, t(marketEntry(item.id).pitch ?? lineOf(item))), get);
|
|
239
|
+
const art = el('div', 'mk-slide-art');
|
|
240
|
+
art.innerHTML = marketArt(item); // trusted: our own drawings (market-art.js)
|
|
241
|
+
slide.append(words, art);
|
|
242
|
+
slide.addEventListener('click', () => go({ tab: 'shop', focus: item.id }));
|
|
243
|
+
track.append(slide);
|
|
244
|
+
const dot = button('mk-dot', null, () => show(at));
|
|
245
|
+
dot.setAttribute('aria-label', t('{n} of {total}', { n: at + 1, total: featured.length }));
|
|
246
|
+
dots.append(dot);
|
|
247
|
+
});
|
|
248
|
+
const show = (at, smooth = true) => {
|
|
249
|
+
index = (at + featured.length) % featured.length;
|
|
250
|
+
track.scrollTo({ left: index * track.clientWidth, behavior: smooth && !reducedMotion() ? 'smooth' : 'auto' });
|
|
251
|
+
mark();
|
|
252
|
+
};
|
|
253
|
+
const mark = () => { [...dots.children].forEach((dot, at) => dot.setAttribute('aria-current', String(at === index))); };
|
|
254
|
+
track.addEventListener('scroll', () => { const at = Math.round(track.scrollLeft / Math.max(1, track.clientWidth)); if (at !== index) { index = at; mark(); } }, { passive: true });
|
|
255
|
+
const prev = glyph(button('mk-arrow prev', null, () => show(index - 1)), 'back');
|
|
256
|
+
const next = glyph(button('mk-arrow next', null, () => show(index + 1)), 'next');
|
|
257
|
+
prev.setAttribute('aria-label', t('Previous'));
|
|
258
|
+
next.setAttribute('aria-label', t('Next'));
|
|
259
|
+
box.append(track, prev, next, dots);
|
|
260
|
+
mark();
|
|
261
|
+
// It moves on by itself, unless the person is reading it or asked for stillness.
|
|
262
|
+
let resting = false;
|
|
263
|
+
for (const [type, value] of [['pointerenter', true], ['pointerleave', false], ['focusin', true], ['focusout', false]]) box.addEventListener(type, () => { resting = value; });
|
|
264
|
+
clearInterval(timer);
|
|
265
|
+
if (!reducedMotion() && featured.length > 1) timer = setInterval(() => { if (!resting && !document.hidden && box.isConnected) show(index + 1); }, 6500);
|
|
266
|
+
return box;
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const shop = () => {
|
|
270
|
+
const words = input.value.trim().toLocaleLowerCase(language());
|
|
271
|
+
const page = el('div', 'mk-page');
|
|
272
|
+
if (words) {
|
|
273
|
+
const found = items.filter((item) => `${nameOf(item)} ${lineOf(item)} ${t(item.summary ?? '')}`.toLocaleLowerCase(language()).includes(words));
|
|
274
|
+
const head = el('section', 'mk-head');
|
|
275
|
+
head.append(el('h1', null, t('Results')));
|
|
276
|
+
page.append(head);
|
|
277
|
+
if (!found.length) page.append(el('p', 'mk-empty', t('No module matches «{query}».', { query: input.value.trim() })));
|
|
278
|
+
else { const list = el('div', 'mk-list'); for (const item of found) list.append(row(item)); page.append(list); }
|
|
279
|
+
return page;
|
|
280
|
+
}
|
|
281
|
+
const head = el('section', 'mk-head');
|
|
282
|
+
head.append(el('h1', null, t('Market')), el('p', null, home
|
|
283
|
+
? t('Modules add what MADRE does: more for your agents, less for the terminal. Free for now. From Home you choose: every project, or only one.')
|
|
284
|
+
: t('Modules add what MADRE does: more for your agents, less for the terminal. Free for now. What you get here is for this project.')));
|
|
285
|
+
page.append(head);
|
|
286
|
+
const featured = carousel();
|
|
287
|
+
if (featured) page.append(featured);
|
|
288
|
+
for (const shelf of marketShelves()) {
|
|
289
|
+
const mine = items.filter((item) => marketEntry(item.id).shelf === shelf.id);
|
|
290
|
+
if (!mine.length) continue;
|
|
291
|
+
const block = el('section', 'mk-shelf');
|
|
292
|
+
const title = el('header');
|
|
293
|
+
title.append(el('h2', null, t(shelf.title)), el('p', null, t(shelf.about)));
|
|
294
|
+
const list = el('div', 'mk-list');
|
|
295
|
+
for (const item of mine) list.append(row(item));
|
|
296
|
+
block.append(title, list);
|
|
297
|
+
page.append(block);
|
|
298
|
+
}
|
|
299
|
+
return page;
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
// A MODULE'S PAGE: its icon, its key, what it costs and where it works, its picture, what it
|
|
303
|
+
// does and what it touches.
|
|
304
|
+
const detail = (item, { choose = false } = {}) => {
|
|
305
|
+
const page = el('div', 'mk-page mk-product');
|
|
306
|
+
page.append(glyph(button('mk-back', t('Market'), () => go({ tab: 'shop' })), 'back'));
|
|
307
|
+
const top = el('section', 'mk-product-top');
|
|
308
|
+
const words = el('div', 'mk-product-words');
|
|
309
|
+
words.append(el('h1', null, nameOf(item)), el('p', null, lineOf(item)));
|
|
310
|
+
const keys = el('div', 'mk-keys');
|
|
311
|
+
const line = el('p', 'mk-said');
|
|
312
|
+
words.append(keys, line);
|
|
313
|
+
top.append(appIcon(item, 128), words);
|
|
314
|
+
page.append(top);
|
|
315
|
+
drawKeys(item, keys, line, { choose });
|
|
316
|
+
|
|
317
|
+
const facts = el('dl', 'mk-facts');
|
|
318
|
+
const fact = (label, value) => { const one = el('div'); one.append(el('dt', null, label), el('dd', null, value)); facts.append(one); };
|
|
319
|
+
fact(t('Price'), priceOf(item));
|
|
320
|
+
fact(home ? t('Where') : t('Here'), reach(item));
|
|
321
|
+
if (shelfOf(item)) fact(t('Shelf'), t(shelfOf(item).title));
|
|
322
|
+
fact(t('Version'), item.version ? `v${String(item.version).replace(/^v/, '')}` : '—');
|
|
323
|
+
page.append(facts);
|
|
324
|
+
|
|
325
|
+
const gallery = el('section', 'mk-section');
|
|
326
|
+
gallery.append(el('h2', null, t('Preview')));
|
|
327
|
+
const shot = hueOf(el('div', 'mk-shot'), item);
|
|
328
|
+
shot.innerHTML = marketArt(item); // trusted: our own drawings (market-art.js)
|
|
329
|
+
gallery.append(shot);
|
|
330
|
+
page.append(gallery);
|
|
331
|
+
if (summaryOf(item)) {
|
|
332
|
+
const about = el('section', 'mk-section');
|
|
333
|
+
about.append(el('h2', null, t('What it does')), el('p', 'mk-about', summaryOf(item)));
|
|
334
|
+
page.append(about);
|
|
335
|
+
}
|
|
336
|
+
const touches = [...(item.creates ?? []).map((text) => [t('Writes'), t(text)]), ...(item.requires ?? []).map((text) => [t('Needs'), t(text)])];
|
|
337
|
+
if (touches.length) {
|
|
338
|
+
const block = el('section', 'mk-section');
|
|
339
|
+
block.append(el('h2', null, t('What it touches')));
|
|
340
|
+
const list = el('div', 'mk-touches');
|
|
341
|
+
for (const [kind, text] of touches) { const one = el('div', 'mk-touch'); one.append(el('span', null, kind), el('span', null, text)); list.append(one); }
|
|
342
|
+
block.append(list);
|
|
343
|
+
page.append(block);
|
|
344
|
+
}
|
|
345
|
+
return page;
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
const projectPicker = (ids) => {
|
|
349
|
+
const select = el('select', 'mk-select');
|
|
350
|
+
select.setAttribute('aria-label', t('Project'));
|
|
351
|
+
for (const id of ids) { const option = el('option', null, projectName(id)); option.value = id; select.append(option); }
|
|
352
|
+
return select;
|
|
353
|
+
};
|
|
354
|
+
const drawKeys = (item, keys, line, { choose = false } = {}) => {
|
|
355
|
+
keys.replaceChildren();
|
|
356
|
+
const kind = kindOf(item);
|
|
357
|
+
const on = isOn(item);
|
|
358
|
+
if (item.preflight && !item.preflight.ok && !on) said(line, item.preflight.problems.map((problem) => t(problem)).join(' '), 'note');
|
|
359
|
+
// A module from the catalog that asks for a newer MADRE says so, and waits for it.
|
|
360
|
+
const needs = item.kind === 'remote' ? item.remote?.minMadre : null;
|
|
361
|
+
if (needs && madre && !fits(madre, needs)) { keys.append(button('mk-get big', t('Needs MADRE {version}', { version: needs }))); keys.lastChild.disabled = true; said(line, t('Update MADRE to get this module.'), 'note'); return; }
|
|
362
|
+
if (kind === 'local') { keys.append(button('mk-get big', t('Open Local AI'), () => { close(); host.localAi?.(); })); return; }
|
|
363
|
+
if (!home) {
|
|
364
|
+
if (kind === 'install') {
|
|
365
|
+
keys.append(button(`mk-get big${on ? ' on' : ''}`, on ? t('See it in MODULES') : t('Install in this project'), () => { close(); host.installHere?.(item); }));
|
|
366
|
+
if (!on) said(line, t('MADRE shows you the commands before it runs them, in this project\'s MODULES.'), 'note');
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
const key = button(`mk-get big${on ? ' quiet' : ''}`, on ? t('Turn off in this project') : t('Get it for this project'), () => turnHere(item, !on, line, key));
|
|
370
|
+
keys.append(key);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
const ids = scope.projects.map((one) => one.id);
|
|
374
|
+
if (kind === 'install') {
|
|
375
|
+
if (!ids.length) { said(line, t('Add a project first: this module is installed into one.'), 'note'); return; }
|
|
376
|
+
const mine = scopeOf(item);
|
|
377
|
+
const left = ids.filter((id) => !(mine?.on ?? []).includes(id));
|
|
378
|
+
if (mine?.on?.length) keys.append(el('span', 'mk-state', t('Installed in: {projects}', { projects: mine.on.map(projectName).join(', ') })));
|
|
379
|
+
if (!left.length) return;
|
|
380
|
+
const select = projectPicker(left);
|
|
381
|
+
const key = button('mk-get big', t('Install in this project'), async () => {
|
|
382
|
+
key.disabled = true;
|
|
383
|
+
said(line, t('Opening the project…'));
|
|
384
|
+
const ok = await host.openProject?.(select.value, item);
|
|
385
|
+
key.disabled = false;
|
|
386
|
+
said(line, ok ? t('It opened in a new tab, on this module: MADRE shows you the commands there before it runs them.') : t('That project did not open.'), ok ? 'good' : 'bad');
|
|
387
|
+
});
|
|
388
|
+
keys.append(select, key);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
const mine = scopeOf(item) ?? { all: on, on: [], everywhere: false, rooms: {} };
|
|
392
|
+
const everywhere = mine.everywhere || (mine.all && !ids.length);
|
|
393
|
+
if (everywhere) {
|
|
394
|
+
const off = button('mk-get big quiet', t('Turn off in every project'), () => turnScope(item, { all: false }, line, off));
|
|
395
|
+
keys.append(off);
|
|
396
|
+
} else {
|
|
397
|
+
const all = button('mk-get big', t('Get it for all your projects'), async () => { if (await turnScope(item, { all: true }, line, all)) notice(t('{name} is on in all your projects.', { name: nameOf(item) })); });
|
|
398
|
+
keys.append(all);
|
|
399
|
+
const left = ids.filter((id) => !mine.on.includes(id));
|
|
400
|
+
if (left.length) {
|
|
401
|
+
const only = el('div', 'mk-only');
|
|
402
|
+
const select = projectPicker(left);
|
|
403
|
+
const key = button('mk-get', t('Only in this one'), async () => { const name = projectName(select.value); if (await turnScope(item, { rooms: { [select.value]: true } }, line, key)) notice(t('{name} is on in {project}.', { name: nameOf(item), project: name })); });
|
|
404
|
+
only.append(el('span', null, t('or only in')), select, key);
|
|
405
|
+
keys.append(only);
|
|
406
|
+
if (choose) requestAnimationFrame(() => all.focus());
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
if (ids.length) keys.append(button('mk-link', t('Choose project by project'), () => go({ tab: 'mine', focus: item.id })));
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
// MODULES, in Home: what is on and where, a switch for every project and one for each.
|
|
413
|
+
const mine = (focusId = null) => {
|
|
414
|
+
const page = el('div', 'mk-page mk-mine');
|
|
415
|
+
const head = el('section', 'mk-head');
|
|
416
|
+
head.append(el('h1', null, t('Modules')), el('p', null, t('What you have on or installed, and in which projects. A change here reaches each project with its next message.')));
|
|
417
|
+
page.append(head);
|
|
418
|
+
const onNow = items.filter((item) => isOn(item));
|
|
419
|
+
const offNow = items.filter((item) => !isOn(item));
|
|
420
|
+
if (onNow.length) {
|
|
421
|
+
const group = el('section', 'mk-section');
|
|
422
|
+
group.append(el('h2', null, t('What is on')));
|
|
423
|
+
for (const item of onNow) group.append(card(item, focusId === item.id));
|
|
424
|
+
page.append(group);
|
|
425
|
+
} else page.append(el('p', 'mk-empty', t('Nothing is on yet. The Market has what each module does.')));
|
|
426
|
+
if (offNow.length) {
|
|
427
|
+
const group = el('section', 'mk-section');
|
|
428
|
+
group.append(el('h2', null, t('Not on')));
|
|
429
|
+
for (const item of offNow) group.append(card(item, focusId === item.id));
|
|
430
|
+
page.append(group);
|
|
431
|
+
}
|
|
432
|
+
return page;
|
|
433
|
+
};
|
|
434
|
+
const switchKey = (checked, label, onFlip) => {
|
|
435
|
+
const key = button(`mk-switch${checked ? ' on' : ''}`, null, onFlip);
|
|
436
|
+
key.setAttribute('role', 'switch');
|
|
437
|
+
key.setAttribute('aria-checked', String(checked));
|
|
438
|
+
key.setAttribute('aria-label', label);
|
|
439
|
+
key.append(el('span', 'mk-knob'));
|
|
440
|
+
return key;
|
|
441
|
+
};
|
|
442
|
+
const card = (item, focused = false) => {
|
|
443
|
+
const box = el('article', `mk-card${focused ? ' focused' : ''}`);
|
|
444
|
+
box.id = `mk-card-${item.id}`;
|
|
445
|
+
const head = el('header', 'mk-card-head');
|
|
446
|
+
const words = el('div', 'mk-card-words');
|
|
447
|
+
words.append(el('b', null, nameOf(item)), el('span', null, reach(item)));
|
|
448
|
+
const open = button('mk-card-open', null, () => go({ tab: 'shop', focus: item.id }));
|
|
449
|
+
open.append(appIcon(item, 44), words);
|
|
450
|
+
head.append(open);
|
|
451
|
+
const line = el('p', 'mk-said');
|
|
452
|
+
box.append(head);
|
|
453
|
+
const kind = kindOf(item);
|
|
454
|
+
if (kind === 'local') {
|
|
455
|
+
head.append(button('mk-get', t('Local AI settings'), () => { close(); host.localAi?.(); }));
|
|
456
|
+
} else if (kind === 'install') {
|
|
457
|
+
head.append(button('mk-get', t('Install in another'), () => go({ tab: 'shop', focus: item.id })));
|
|
458
|
+
const on = scopeOf(item)?.on ?? [];
|
|
459
|
+
if (on.length) {
|
|
460
|
+
const chips = el('div', 'mk-chips');
|
|
461
|
+
for (const id of on) chips.append(el('span', 'mk-chip on', projectName(id)));
|
|
462
|
+
box.append(chips);
|
|
463
|
+
}
|
|
464
|
+
} else {
|
|
465
|
+
const mineScope = scopeOf(item) ?? { all: false, on: [], everywhere: false };
|
|
466
|
+
const allOn = mineScope.everywhere || (mineScope.all && !scope.projects.length);
|
|
467
|
+
const all = el('label', 'mk-all');
|
|
468
|
+
const flip = switchKey(allOn, t('All your projects'), () => turnScope(item, { all: !allOn }, line, flip));
|
|
469
|
+
all.append(el('span', null, t('All your projects')), flip);
|
|
470
|
+
head.append(all);
|
|
471
|
+
if (scope.projects.length) {
|
|
472
|
+
const chips = el('div', 'mk-chips');
|
|
473
|
+
for (const project of scope.projects) {
|
|
474
|
+
const here = mineScope.on.includes(project.id);
|
|
475
|
+
const chip = button(`mk-chip${here ? ' on' : ''}`, project.name, () => turnScope(item, { rooms: { [project.id]: !here } }, line, chip));
|
|
476
|
+
chip.setAttribute('aria-pressed', String(here));
|
|
477
|
+
chip.title = here ? t('On in {project}. Press to turn it off there.', { project: project.name }) : t('Off in {project}. Press to turn it on there.', { project: project.name });
|
|
478
|
+
chips.append(chip);
|
|
479
|
+
}
|
|
480
|
+
box.append(chips);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
box.append(line);
|
|
484
|
+
return box;
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
// WHERE IT IS: a view, the address that reopens it, and the page scrolled to the top.
|
|
488
|
+
const render = () => {
|
|
489
|
+
if (!current) return;
|
|
490
|
+
for (const [id, key] of Object.entries(tabs)) key.setAttribute('aria-selected', String(id === view.tab));
|
|
491
|
+
search.hidden = view.tab === 'mine' || Boolean(view.focus);
|
|
492
|
+
const item = view.focus ? items.find((one) => one.id === view.focus) : null;
|
|
493
|
+
const page = view.tab === 'mine' ? mine(view.focus) : item ? detail(item, { choose: view.choose }) : shop();
|
|
494
|
+
main.replaceChildren(page);
|
|
495
|
+
if (view.tab === 'mine' && view.focus) requestAnimationFrame(() => document.getElementById(`mk-card-${view.focus}`)?.scrollIntoView({ block: 'center' }));
|
|
496
|
+
const hash = view.tab === 'mine' ? '#modulos' : item ? `#mercado=${encodeURIComponent(item.id)}` : '#mercado';
|
|
497
|
+
try { history.replaceState(history.state, '', `${location.pathname}${location.search}${hash}`); } catch { /* the address stays as it was */ }
|
|
498
|
+
};
|
|
499
|
+
const go = ({ tab: nextTab = view.tab, focus: nextFocus = null, choose = false } = {}) => {
|
|
500
|
+
view = { tab: home ? nextTab : 'shop', focus: nextFocus, choose };
|
|
501
|
+
main.scrollTop = 0;
|
|
502
|
+
render();
|
|
503
|
+
};
|
|
504
|
+
const close = () => {
|
|
505
|
+
if (!current) return;
|
|
506
|
+
clearInterval(timer);
|
|
507
|
+
current = null;
|
|
508
|
+
document.documentElement.classList.remove('mk-open');
|
|
509
|
+
root.classList.remove('in');
|
|
510
|
+
try { history.replaceState(history.state, '', `${location.pathname}${location.search}`); } catch { /* fine */ }
|
|
511
|
+
setTimeout(() => root.remove(), reducedMotion() ? 0 : 220);
|
|
512
|
+
before?.focus?.();
|
|
513
|
+
};
|
|
514
|
+
input.addEventListener('input', () => { if (!view.focus && view.tab === 'shop') { const at = input.selectionStart; render(); input.focus(); input.setSelectionRange(at, at); } });
|
|
515
|
+
root.addEventListener('keydown', (event) => {
|
|
516
|
+
if (event.key !== 'Escape') return;
|
|
517
|
+
event.stopPropagation();
|
|
518
|
+
event.preventDefault();
|
|
519
|
+
if (input.value && document.activeElement === input) { input.value = ''; render(); return; }
|
|
520
|
+
if (view.focus && view.tab === 'shop') go({ tab: 'shop' });
|
|
521
|
+
else close();
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
current = { close, go };
|
|
525
|
+
main.append(el('p', 'mk-empty', t('Looking…')));
|
|
526
|
+
root.focus();
|
|
527
|
+
void load().then(() => { if (current) go({ tab, focus, choose: false }); });
|
|
528
|
+
return current;
|
|
529
|
+
}
|