mirror of
https://github.com/razzant/ouroboros.git
synced 2026-10-03 12:18:39 +00:00
"Take me to the Project this belongs to" was drawn six ways in the Main transcript, in three looks: a card bar reading `in project ↗` or `running in background ↗`, a pill with the Project's name, and a grey button reading `Open Project ↗` — or `Open Project` with no arrow and a `title` the factory silently dropped. Each call truthfully reused a shared primitive, which is why no review, guard or document saw it: the callers still chose the label, the glyph and the class. `web/modules/project_reference.js` is now the only module that builds that control and the only one that raises `ouro:open-project`. Callers pick a layout (`inline`, `bar`, `footer`) and, for the converted card, the one closed state; they pass no label and no class name. The reference names the Project (a row that was never named reads `Project`, never its id), says the same thing to a screen reader as to the eye, and carries a late-arriving name into its text, its accessible name and its press alike. The bound footer, the converted card, the question mirror, the completion mirror, the System lifecycle rows and the routed owner message all use it. `renderProjectChip` and `createSystemMessageAction` are gone with their CSS. The durable row text, the event payload and the listener are unchanged.
55 lines
2.4 KiB
JavaScript
55 lines
2.4 KiB
JavaScript
/**
|
|
* Project lifecycle rows in Main (docs/DESIGN.md "Project completion mirror").
|
|
*
|
|
* A Project root that ended with Ouroboros's own final answer reaches Main as an
|
|
* ordinary Ouroboros message, folded when it is long. Every other lifecycle row (a
|
|
* start, an ending with no answer, a row written before the answer rode the row)
|
|
* keeps its System text. Either way the row ends with the same Project reference:
|
|
* the voice of a row never chooses how the UI points at its Project.
|
|
*/
|
|
import { createSystemMessageActions } from './ui_helpers.js';
|
|
import { projectReference } from './project_reference.js';
|
|
|
|
// The fade says "there is more": it appears only when the fold really hides text. The clamp
|
|
// itself is unconditional CSS, so toggling the class never moves layout. A row mounted while
|
|
// Chat is not the visible page has no layout box (clientHeight 0): it is left alone and
|
|
// measured when Chat is shown; a resize re-measures, because the width decides the height.
|
|
function markFold(bubble) {
|
|
const message = bubble.querySelector('.message');
|
|
if (!message || !message.clientHeight) return;
|
|
bubble.classList.toggle('is-folded', message.scrollHeight > message.clientHeight + 1);
|
|
}
|
|
|
|
// One pair of window listeners for the app's lifetime, however many rows exist: nothing is
|
|
// held per row, so a released row needs no disposer here.
|
|
let watching = false;
|
|
let queued = false;
|
|
function afterLayout(work) {
|
|
if (typeof requestAnimationFrame === 'function') requestAnimationFrame(work);
|
|
}
|
|
function refreshFoldsSoon() {
|
|
if (queued) return;
|
|
queued = true;
|
|
afterLayout(() => {
|
|
queued = false;
|
|
document.querySelectorAll('.chat-bubble.project-answer').forEach(markFold);
|
|
});
|
|
}
|
|
function watchFolds() {
|
|
if (watching || typeof window === 'undefined') return;
|
|
watching = true;
|
|
window.addEventListener('ouro:page-shown', refreshFoldsSoon);
|
|
window.addEventListener('resize', refreshFoldsSoon);
|
|
}
|
|
|
|
export function decorateProjectRow(bubble, { role = 'system', projectId = '', projectName = '' } = {}) {
|
|
if (!bubble || !projectId) return null;
|
|
if (role === 'assistant') {
|
|
bubble.classList.add('project-answer');
|
|
watchFolds();
|
|
afterLayout(() => markFold(bubble));
|
|
}
|
|
const actions = createSystemMessageActions(projectReference({ id: projectId, name: projectName }));
|
|
bubble.querySelector('.message')?.after(actions);
|
|
return actions;
|
|
}
|