reviewflow 3.28.0 → 3.30.0
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.md +25 -0
- package/dist/dashboard/index.html +74 -9
- package/dist/dashboard/modules/emberAvatar.d.ts +134 -0
- package/dist/dashboard/modules/emberAvatar.d.ts.map +1 -0
- package/dist/dashboard/modules/emberAvatar.js +192 -0
- package/dist/dashboard/modules/emberAvatar.js.map +1 -0
- package/dist/dashboard/modules/emberAvatarRenderer.d.ts +42 -0
- package/dist/dashboard/modules/emberAvatarRenderer.d.ts.map +1 -0
- package/dist/dashboard/modules/emberAvatarRenderer.js +119 -0
- package/dist/dashboard/modules/emberAvatarRenderer.js.map +1 -0
- package/dist/dashboard/modules/emberChat.d.ts +77 -0
- package/dist/dashboard/modules/emberChat.d.ts.map +1 -0
- package/dist/dashboard/modules/emberChat.js +175 -0
- package/dist/dashboard/modules/emberChat.js.map +1 -0
- package/dist/dashboard/styles.css +126 -0
- package/dist/main/routes.d.ts.map +1 -1
- package/dist/main/routes.js +32 -0
- package/dist/main/routes.js.map +1 -1
- package/dist/modules/ember-chat/entities/emberMessage/emberMessage.guard.d.ts +4 -0
- package/dist/modules/ember-chat/entities/emberMessage/emberMessage.guard.d.ts.map +1 -0
- package/dist/modules/ember-chat/entities/emberMessage/emberMessage.guard.js +4 -0
- package/dist/modules/ember-chat/entities/emberMessage/emberMessage.guard.js.map +1 -0
- package/dist/modules/ember-chat/entities/emberMessage/emberMessage.schema.d.ts +6 -0
- package/dist/modules/ember-chat/entities/emberMessage/emberMessage.schema.d.ts.map +1 -0
- package/dist/modules/ember-chat/entities/emberMessage/emberMessage.schema.js +5 -0
- package/dist/modules/ember-chat/entities/emberMessage/emberMessage.schema.js.map +1 -0
- package/dist/modules/ember-chat/entities/emberSession/emberSession.schema.d.ts +7 -0
- package/dist/modules/ember-chat/entities/emberSession/emberSession.schema.d.ts.map +1 -0
- package/dist/modules/ember-chat/entities/emberSession/emberSession.schema.js +3 -0
- package/dist/modules/ember-chat/entities/emberSession/emberSession.schema.js.map +1 -0
- package/dist/modules/ember-chat/entities/emberSession/emberSessionState.d.ts +13 -0
- package/dist/modules/ember-chat/entities/emberSession/emberSessionState.d.ts.map +1 -0
- package/dist/modules/ember-chat/entities/emberSession/emberSessionState.js +35 -0
- package/dist/modules/ember-chat/entities/emberSession/emberSessionState.js.map +1 -0
- package/dist/modules/ember-chat/entities/emberSession/emberSessionTransport.gateway.d.ts +26 -0
- package/dist/modules/ember-chat/entities/emberSession/emberSessionTransport.gateway.d.ts.map +1 -0
- package/dist/modules/ember-chat/entities/emberSession/emberSessionTransport.gateway.js +2 -0
- package/dist/modules/ember-chat/entities/emberSession/emberSessionTransport.gateway.js.map +1 -0
- package/dist/modules/ember-chat/entities/emberTool/emberTool.gateway.d.ts +11 -0
- package/dist/modules/ember-chat/entities/emberTool/emberTool.gateway.d.ts.map +1 -0
- package/dist/modules/ember-chat/entities/emberTool/emberTool.gateway.js +2 -0
- package/dist/modules/ember-chat/entities/emberTool/emberTool.gateway.js.map +1 -0
- package/dist/modules/ember-chat/interface-adapters/controllers/http/emberChat.routes.d.ts +15 -0
- package/dist/modules/ember-chat/interface-adapters/controllers/http/emberChat.routes.d.ts.map +1 -0
- package/dist/modules/ember-chat/interface-adapters/controllers/http/emberChat.routes.js +61 -0
- package/dist/modules/ember-chat/interface-adapters/controllers/http/emberChat.routes.js.map +1 -0
- package/dist/modules/ember-chat/interface-adapters/gateways/emberReadData.composite.gateway.d.ts +24 -0
- package/dist/modules/ember-chat/interface-adapters/gateways/emberReadData.composite.gateway.d.ts.map +1 -0
- package/dist/modules/ember-chat/interface-adapters/gateways/emberReadData.composite.gateway.js +19 -0
- package/dist/modules/ember-chat/interface-adapters/gateways/emberReadData.composite.gateway.js.map +1 -0
- package/dist/modules/ember-chat/interface-adapters/gateways/emberSessionTransport.claude.gateway.d.ts +13 -0
- package/dist/modules/ember-chat/interface-adapters/gateways/emberSessionTransport.claude.gateway.d.ts.map +1 -0
- package/dist/modules/ember-chat/interface-adapters/gateways/emberSessionTransport.claude.gateway.js +130 -0
- package/dist/modules/ember-chat/interface-adapters/gateways/emberSessionTransport.claude.gateway.js.map +1 -0
- package/dist/modules/ember-chat/interface-adapters/gateways/emberStreamJson.parser.d.ts +18 -0
- package/dist/modules/ember-chat/interface-adapters/gateways/emberStreamJson.parser.d.ts.map +1 -0
- package/dist/modules/ember-chat/interface-adapters/gateways/emberStreamJson.parser.js +33 -0
- package/dist/modules/ember-chat/interface-adapters/gateways/emberStreamJson.parser.js.map +1 -0
- package/dist/modules/ember-chat/interface-adapters/presenters/emberStatus.presenter.d.ts +20 -0
- package/dist/modules/ember-chat/interface-adapters/presenters/emberStatus.presenter.d.ts.map +1 -0
- package/dist/modules/ember-chat/interface-adapters/presenters/emberStatus.presenter.js +28 -0
- package/dist/modules/ember-chat/interface-adapters/presenters/emberStatus.presenter.js.map +1 -0
- package/dist/modules/ember-chat/services/emberSystemPrompt.d.ts +12 -0
- package/dist/modules/ember-chat/services/emberSystemPrompt.d.ts.map +1 -0
- package/dist/modules/ember-chat/services/emberSystemPrompt.js +29 -0
- package/dist/modules/ember-chat/services/emberSystemPrompt.js.map +1 -0
- package/dist/modules/ember-chat/usecases/askEmber/askEmber.usecase.d.ts +23 -0
- package/dist/modules/ember-chat/usecases/askEmber/askEmber.usecase.d.ts.map +1 -0
- package/dist/modules/ember-chat/usecases/askEmber/askEmber.usecase.js +23 -0
- package/dist/modules/ember-chat/usecases/askEmber/askEmber.usecase.js.map +1 -0
- package/dist/modules/ember-chat/usecases/emberSession/emberSessionRegistry.d.ts +36 -0
- package/dist/modules/ember-chat/usecases/emberSession/emberSessionRegistry.d.ts.map +1 -0
- package/dist/modules/ember-chat/usecases/emberSession/emberSessionRegistry.js +59 -0
- package/dist/modules/ember-chat/usecases/emberSession/emberSessionRegistry.js.map +1 -0
- package/dist/tests/acceptance/189-ember-readonly-review-chat.acceptance.test.d.ts +2 -0
- package/dist/tests/acceptance/189-ember-readonly-review-chat.acceptance.test.d.ts.map +1 -0
- package/dist/tests/acceptance/189-ember-readonly-review-chat.acceptance.test.js +124 -0
- package/dist/tests/acceptance/189-ember-readonly-review-chat.acceptance.test.js.map +1 -0
- package/dist/tests/factories/emberMessage.factory.d.ts +5 -0
- package/dist/tests/factories/emberMessage.factory.d.ts.map +1 -0
- package/dist/tests/factories/emberMessage.factory.js +9 -0
- package/dist/tests/factories/emberMessage.factory.js.map +1 -0
- package/dist/tests/stubs/emberReadData.stub.d.ts +20 -0
- package/dist/tests/stubs/emberReadData.stub.d.ts.map +1 -0
- package/dist/tests/stubs/emberReadData.stub.js +31 -0
- package/dist/tests/stubs/emberReadData.stub.js.map +1 -0
- package/dist/tests/stubs/emberSessionTransport.stub.d.ts +18 -0
- package/dist/tests/stubs/emberSessionTransport.stub.d.ts.map +1 -0
- package/dist/tests/stubs/emberSessionTransport.stub.js +75 -0
- package/dist/tests/stubs/emberSessionTransport.stub.js.map +1 -0
- package/dist/tests/units/dashboard/modules/emberAvatar.test.d.ts +2 -0
- package/dist/tests/units/dashboard/modules/emberAvatar.test.d.ts.map +1 -0
- package/dist/tests/units/dashboard/modules/emberAvatar.test.js +74 -0
- package/dist/tests/units/dashboard/modules/emberAvatar.test.js.map +1 -0
- package/dist/tests/units/dashboard/modules/emberAvatarRenderer.test.d.ts +2 -0
- package/dist/tests/units/dashboard/modules/emberAvatarRenderer.test.d.ts.map +1 -0
- package/dist/tests/units/dashboard/modules/emberAvatarRenderer.test.js +44 -0
- package/dist/tests/units/dashboard/modules/emberAvatarRenderer.test.js.map +1 -0
- package/dist/tests/units/dashboard/modules/emberChat.test.d.ts +2 -0
- package/dist/tests/units/dashboard/modules/emberChat.test.d.ts.map +1 -0
- package/dist/tests/units/dashboard/modules/emberChat.test.js +59 -0
- package/dist/tests/units/dashboard/modules/emberChat.test.js.map +1 -0
- package/dist/tests/units/modules/ember-chat/controllers/emberChat.routes.test.d.ts +2 -0
- package/dist/tests/units/modules/ember-chat/controllers/emberChat.routes.test.d.ts.map +1 -0
- package/dist/tests/units/modules/ember-chat/controllers/emberChat.routes.test.js +101 -0
- package/dist/tests/units/modules/ember-chat/controllers/emberChat.routes.test.js.map +1 -0
- package/dist/tests/units/modules/ember-chat/entities/emberMessage.guard.test.d.ts +2 -0
- package/dist/tests/units/modules/ember-chat/entities/emberMessage.guard.test.d.ts.map +1 -0
- package/dist/tests/units/modules/ember-chat/entities/emberMessage.guard.test.js +24 -0
- package/dist/tests/units/modules/ember-chat/entities/emberMessage.guard.test.js.map +1 -0
- package/dist/tests/units/modules/ember-chat/entities/emberSessionState.test.d.ts +2 -0
- package/dist/tests/units/modules/ember-chat/entities/emberSessionState.test.d.ts.map +1 -0
- package/dist/tests/units/modules/ember-chat/entities/emberSessionState.test.js +42 -0
- package/dist/tests/units/modules/ember-chat/entities/emberSessionState.test.js.map +1 -0
- package/dist/tests/units/modules/ember-chat/gateways/emberReadData.composite.gateway.test.d.ts +2 -0
- package/dist/tests/units/modules/ember-chat/gateways/emberReadData.composite.gateway.test.d.ts.map +1 -0
- package/dist/tests/units/modules/ember-chat/gateways/emberReadData.composite.gateway.test.js +75 -0
- package/dist/tests/units/modules/ember-chat/gateways/emberReadData.composite.gateway.test.js.map +1 -0
- package/dist/tests/units/modules/ember-chat/gateways/emberStreamJson.parser.test.d.ts +2 -0
- package/dist/tests/units/modules/ember-chat/gateways/emberStreamJson.parser.test.d.ts.map +1 -0
- package/dist/tests/units/modules/ember-chat/gateways/emberStreamJson.parser.test.js +52 -0
- package/dist/tests/units/modules/ember-chat/gateways/emberStreamJson.parser.test.js.map +1 -0
- package/dist/tests/units/modules/ember-chat/presenters/emberStatus.presenter.test.d.ts +2 -0
- package/dist/tests/units/modules/ember-chat/presenters/emberStatus.presenter.test.d.ts.map +1 -0
- package/dist/tests/units/modules/ember-chat/presenters/emberStatus.presenter.test.js +27 -0
- package/dist/tests/units/modules/ember-chat/presenters/emberStatus.presenter.test.js.map +1 -0
- package/dist/tests/units/modules/ember-chat/services/emberSystemPrompt.test.d.ts +2 -0
- package/dist/tests/units/modules/ember-chat/services/emberSystemPrompt.test.d.ts.map +1 -0
- package/dist/tests/units/modules/ember-chat/services/emberSystemPrompt.test.js +41 -0
- package/dist/tests/units/modules/ember-chat/services/emberSystemPrompt.test.js.map +1 -0
- package/dist/tests/units/modules/ember-chat/usecases/askEmber.usecase.test.d.ts +2 -0
- package/dist/tests/units/modules/ember-chat/usecases/askEmber.usecase.test.d.ts.map +1 -0
- package/dist/tests/units/modules/ember-chat/usecases/askEmber.usecase.test.js +46 -0
- package/dist/tests/units/modules/ember-chat/usecases/askEmber.usecase.test.js.map +1 -0
- package/dist/tests/units/modules/ember-chat/usecases/emberSessionRegistry.test.d.ts +2 -0
- package/dist/tests/units/modules/ember-chat/usecases/emberSessionRegistry.test.d.ts.map +1 -0
- package/dist/tests/units/modules/ember-chat/usecases/emberSessionRegistry.test.js +88 -0
- package/dist/tests/units/modules/ember-chat/usecases/emberSessionRegistry.test.js.map +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,26 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [3.30.0](https://github.com/DGouron/review-flow/compare/reviewflow-v3.29.0...reviewflow-v3.30.0) (2026-05-28)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
* **dashboard:** SPEC-189 Ember flame wireframe avatar + sidebar layout ([#248](https://github.com/DGouron/review-flow/issues/248)) ([7e82a2f](https://github.com/DGouron/review-flow/commit/7e82a2fb385c8990558e3fd15e103b2e8bbd40e7))
|
|
14
|
+
|
|
15
|
+
## [3.29.0](https://github.com/DGouron/review-flow/compare/reviewflow-v3.28.0...reviewflow-v3.29.0) (2026-05-28)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
* **dashboard:** SPEC-189 Ember read-only review chat (Phase A) ([#244](https://github.com/DGouron/review-flow/issues/244)) ([4e0d5b9](https://github.com/DGouron/review-flow/commit/4e0d5b9bb37a68cc2c91454412a35ce34107f1ed))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
* **docs:** unblock vitepress build and refresh setup-wizard docs ([83ce3bb](https://github.com/DGouron/review-flow/commit/83ce3bbbea6e80851742c8af08cfbb912bdc66d5))
|
|
26
|
+
* **docs:** unblock vitepress build and refresh setup-wizard docs ([fc5896d](https://github.com/DGouron/review-flow/commit/fc5896d14cc378d51bf17c9f51ebc01360cbc6bb))
|
|
27
|
+
|
|
8
28
|
## [3.28.0](https://github.com/DGouron/review-flow/compare/reviewflow-v3.27.0...reviewflow-v3.28.0) (2026-05-28)
|
|
9
29
|
|
|
10
30
|
|
|
@@ -16,6 +36,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
16
36
|
* **setup-wizard:** SPEC-184 Iteration B — dashboard wizard interactive forms ([#238](https://github.com/DGouron/review-flow/issues/238)) ([806515b](https://github.com/DGouron/review-flow/commit/806515b4a69efbfaf8022185e545393dc7525623))
|
|
17
37
|
* **setup-wizard:** SPEC-187 read wizard answers from stdin in JSON mode ([#237](https://github.com/DGouron/review-flow/issues/237)) ([8476550](https://github.com/DGouron/review-flow/commit/8476550edbfce72743e882e2c4a907d60c5ab9a4))
|
|
18
38
|
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
* **worktree:** drop false-positive missing-build-artifacts signal ([#239](https://github.com/DGouron/review-flow/issues/239)) ([41a3a79](https://github.com/DGouron/review-flow/commit/41a3a79b5bec924f895ebfa35197d003fde95dd3))
|
|
43
|
+
|
|
19
44
|
## [3.27.0](https://github.com/DGouron/review-flow/compare/reviewflow-v3.26.0...reviewflow-v3.27.0) (2026-05-27)
|
|
20
45
|
|
|
21
46
|
|
|
@@ -101,18 +101,39 @@
|
|
|
101
101
|
|
|
102
102
|
<div class="dashboard-layout">
|
|
103
103
|
<aside class="dashboard-sidebar" aria-label="Project tools">
|
|
104
|
-
<
|
|
105
|
-
<
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
<
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
<
|
|
112
|
-
|
|
104
|
+
<div class="sidebar-tool-buttons">
|
|
105
|
+
<button type="button" id="open-settings-modal-btn" class="sidebar-settings-button" hidden>
|
|
106
|
+
<span class="sidebar-settings-button__prefix">// SETTINGS</span>
|
|
107
|
+
</button>
|
|
108
|
+
<button type="button" id="open-economics-sheet-btn" class="sidebar-settings-button" onclick="openEconomicsSheet()">
|
|
109
|
+
<span class="sidebar-settings-button__prefix">// ECONOMICS</span>
|
|
110
|
+
</button>
|
|
111
|
+
<button type="button" id="open-stats-sheet-btn" class="sidebar-settings-button" onclick="openStatsSheet()" disabled aria-disabled="true">
|
|
112
|
+
<span class="sidebar-settings-button__prefix">// STATS</span>
|
|
113
|
+
</button>
|
|
114
|
+
</div>
|
|
113
115
|
|
|
114
116
|
<span id="config-status" class="config-status hidden"></span>
|
|
115
117
|
|
|
118
|
+
<section id="ember-chat-panel" class="ember-chat-panel" aria-label="Ember">
|
|
119
|
+
<div class="ember-chat-panel__header">
|
|
120
|
+
<span class="ember-chat-panel__prefix">// EMBER</span>
|
|
121
|
+
<canvas id="ember-avatar" class="ember-chat-panel__avatar" width="360" height="360" aria-hidden="true"></canvas>
|
|
122
|
+
</div>
|
|
123
|
+
<output id="ember-answer" class="ember-chat-panel__answer" aria-live="polite"></output>
|
|
124
|
+
<div id="ember-status" class="ember-chat-panel__status" role="status" aria-live="polite"></div>
|
|
125
|
+
<button type="button" id="ember-retry" class="ember-chat-panel__retry" hidden>// RÉESSAYER</button>
|
|
126
|
+
<form id="ember-form" class="ember-chat-panel__form" autocomplete="off">
|
|
127
|
+
<label class="visually-hidden" for="ember-question">Posez une question à Ember</label>
|
|
128
|
+
<input
|
|
129
|
+
type="text"
|
|
130
|
+
id="ember-question"
|
|
131
|
+
class="ember-chat-panel__input"
|
|
132
|
+
placeholder="Posez une question sur vos reviews…"
|
|
133
|
+
/>
|
|
134
|
+
</form>
|
|
135
|
+
</section>
|
|
136
|
+
|
|
116
137
|
<section id="worktree-section" aria-label="Worktree pool"></section>
|
|
117
138
|
</aside>
|
|
118
139
|
|
|
@@ -429,6 +450,8 @@
|
|
|
429
450
|
buildHeaderCapacityViewModel,
|
|
430
451
|
renderHeaderCapacityBadgeHtml,
|
|
431
452
|
} from './modules/headerCapacityBadge.js';
|
|
453
|
+
import { connectEmberStream, shouldSendQuestion } from './modules/emberChat.js';
|
|
454
|
+
import { mountEmberAvatar } from './modules/emberAvatarRenderer.js';
|
|
432
455
|
|
|
433
456
|
const API_URL = window.location.origin;
|
|
434
457
|
const WS_URL = `ws://${window.location.host}/ws`;
|
|
@@ -3838,6 +3861,47 @@
|
|
|
3838
3861
|
}).catch(() => {});
|
|
3839
3862
|
});
|
|
3840
3863
|
|
|
3864
|
+
// Ember read-only chat (SPEC-189). The pure decisions + SSE fold live in
|
|
3865
|
+
// emberChat.js; this humble init just binds DOM and the reused avatar.
|
|
3866
|
+
function initEmberChat() {
|
|
3867
|
+
const form = document.getElementById('ember-form');
|
|
3868
|
+
const input = document.getElementById('ember-question');
|
|
3869
|
+
const answer = document.getElementById('ember-answer');
|
|
3870
|
+
const status = document.getElementById('ember-status');
|
|
3871
|
+
const retry = document.getElementById('ember-retry');
|
|
3872
|
+
const canvas = document.getElementById('ember-avatar');
|
|
3873
|
+
const panel = document.getElementById('ember-chat-panel');
|
|
3874
|
+
if (!form || !input || !answer || !status || !canvas) return;
|
|
3875
|
+
|
|
3876
|
+
const avatar = mountEmberAvatar({ canvas, initialState: 'idle' });
|
|
3877
|
+
|
|
3878
|
+
const ask = async () => {
|
|
3879
|
+
const question = input.value;
|
|
3880
|
+
if (!shouldSendQuestion(question)) {
|
|
3881
|
+
input.focus();
|
|
3882
|
+
return;
|
|
3883
|
+
}
|
|
3884
|
+
panel?.classList.add('ember-chat-panel--active');
|
|
3885
|
+
answer.textContent = '';
|
|
3886
|
+
status.textContent = '';
|
|
3887
|
+
retry.hidden = true;
|
|
3888
|
+
await connectEmberStream({
|
|
3889
|
+
question,
|
|
3890
|
+
onAnswer: (text) => { answer.textContent = text; },
|
|
3891
|
+
onAvatarState: (state) => avatar.setState(state),
|
|
3892
|
+
onAnnounce: (text) => { status.textContent = text; },
|
|
3893
|
+
onRetryVisible: (visible) => { retry.hidden = !visible; },
|
|
3894
|
+
});
|
|
3895
|
+
input.focus();
|
|
3896
|
+
};
|
|
3897
|
+
|
|
3898
|
+
form.addEventListener('submit', (event) => {
|
|
3899
|
+
event.preventDefault();
|
|
3900
|
+
void ask();
|
|
3901
|
+
});
|
|
3902
|
+
retry.addEventListener('click', () => { void ask(); });
|
|
3903
|
+
}
|
|
3904
|
+
|
|
3841
3905
|
// Boot animations & observers exactly once, regardless of readyState timing
|
|
3842
3906
|
let __animationsBooted = false;
|
|
3843
3907
|
function bootOnce() {
|
|
@@ -3845,6 +3909,7 @@
|
|
|
3845
3909
|
__animationsBooted = true;
|
|
3846
3910
|
bootAnimations();
|
|
3847
3911
|
observeCounters();
|
|
3912
|
+
initEmberChat();
|
|
3848
3913
|
}
|
|
3849
3914
|
if (document.readyState !== 'loading') {
|
|
3850
3915
|
bootOnce();
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maps an ember state to its stroke/animation parameters. Keeps the renderer
|
|
3
|
+
* humble: every per-state decision lives here, not in the canvas loop.
|
|
4
|
+
*
|
|
5
|
+
* @param {EmberState} state
|
|
6
|
+
* @returns {EmberVisual}
|
|
7
|
+
*/
|
|
8
|
+
export function emberStateToVisual(state: EmberState): EmberVisual;
|
|
9
|
+
/**
|
|
10
|
+
* Builds the flame wireframe as a surface of revolution: a single tip vertex
|
|
11
|
+
* (index 0) plus `rings` × `meridians` body vertices, joined by tip spokes,
|
|
12
|
+
* vertical meridian lines and horizontal ring loops. Pure and deterministic.
|
|
13
|
+
*
|
|
14
|
+
* @param {{ rings: number; meridians: number }} options
|
|
15
|
+
* @returns {{ vertices: Vertex[]; edges: Array<[number, number]> }}
|
|
16
|
+
*/
|
|
17
|
+
export function buildFlameWireframe(options: {
|
|
18
|
+
rings: number;
|
|
19
|
+
meridians: number;
|
|
20
|
+
}): {
|
|
21
|
+
vertices: Vertex[];
|
|
22
|
+
edges: Array<[number, number]>;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* @typedef {Object} Projection
|
|
26
|
+
* @property {number} tilt Fixed X-axis tilt in radians.
|
|
27
|
+
* @property {number} distance Camera distance for the perspective divide.
|
|
28
|
+
* @property {number} scale Pixels per unit at the projection plane.
|
|
29
|
+
* @property {number} centerX Canvas-space x offset.
|
|
30
|
+
* @property {number} centerY Canvas-space y offset.
|
|
31
|
+
*/
|
|
32
|
+
/**
|
|
33
|
+
* Projects a flame vertex to a 2D canvas point: rotates around Y, leans the
|
|
34
|
+
* upper body sideways by `swayOffset` scaled by height (a candle flame bends at
|
|
35
|
+
* the tip, not the base), applies the fixed tilt, then a perspective divide.
|
|
36
|
+
* Pure and deterministic.
|
|
37
|
+
*
|
|
38
|
+
* @param {Vertex} vertex
|
|
39
|
+
* @param {number} rotationRadians
|
|
40
|
+
* @param {number} swayOffset
|
|
41
|
+
* @param {Projection} projection
|
|
42
|
+
* @returns {{ x: number; y: number }}
|
|
43
|
+
*/
|
|
44
|
+
export function projectFlameVertex(vertex: Vertex, rotationRadians: number, swayOffset: number, projection: Projection): {
|
|
45
|
+
x: number;
|
|
46
|
+
y: number;
|
|
47
|
+
};
|
|
48
|
+
export function emberRadiusFactor(visual: any, time: any): number;
|
|
49
|
+
/**
|
|
50
|
+
* The horizontal lean of the flame tip at a given time. Pure — 0 at time 0,
|
|
51
|
+
* always within ± swayAmount.
|
|
52
|
+
*
|
|
53
|
+
* @param {EmberVisual} visual
|
|
54
|
+
* @param {number} time Milliseconds since the loop started.
|
|
55
|
+
* @returns {number}
|
|
56
|
+
*/
|
|
57
|
+
export function emberSwayOffset(visual: EmberVisual, time: number): number;
|
|
58
|
+
/**
|
|
59
|
+
* Dashboard module — Ember flame wireframe avatar (SPEC-189).
|
|
60
|
+
* Humble object: pure functions, no DOM, no global state. Holds the flame-shaped
|
|
61
|
+
* wireframe geometry and every per-state visual decision, so the
|
|
62
|
+
* requestAnimationFrame loop in emberAvatarRenderer.js carries no branching.
|
|
63
|
+
*
|
|
64
|
+
* Ember is a warm wireframe BRAISE, not the setup wizard's abstract icosahedron:
|
|
65
|
+
* a teardrop flame mesh (pointed tip, rounded base) stroked in amber, that leans
|
|
66
|
+
* and flickers like a live coal — livelier and warmer when spoken to.
|
|
67
|
+
*
|
|
68
|
+
* Visual DNA: "Agentic OS" — dark warm near-black + amber. See
|
|
69
|
+
* project_agentic_os_design_dna.md.
|
|
70
|
+
*/
|
|
71
|
+
/**
|
|
72
|
+
* @typedef {'idle' | 'working' | 'error'} EmberState
|
|
73
|
+
*/
|
|
74
|
+
/** @type {EmberState[]} */
|
|
75
|
+
export const EMBER_STATES: EmberState[];
|
|
76
|
+
/** Top of the flame (the tip) in model space. */
|
|
77
|
+
export const FLAME_TIP_Y: 1.5;
|
|
78
|
+
/** Bottom of the flame (the rounded base) in model space. */
|
|
79
|
+
export const FLAME_BASE_Y: -1.15;
|
|
80
|
+
export type Vertex = [number, number, number];
|
|
81
|
+
export type Projection = {
|
|
82
|
+
/**
|
|
83
|
+
* Fixed X-axis tilt in radians.
|
|
84
|
+
*/
|
|
85
|
+
tilt: number;
|
|
86
|
+
/**
|
|
87
|
+
* Camera distance for the perspective divide.
|
|
88
|
+
*/
|
|
89
|
+
distance: number;
|
|
90
|
+
/**
|
|
91
|
+
* Pixels per unit at the projection plane.
|
|
92
|
+
*/
|
|
93
|
+
scale: number;
|
|
94
|
+
/**
|
|
95
|
+
* Canvas-space x offset.
|
|
96
|
+
*/
|
|
97
|
+
centerX: number;
|
|
98
|
+
/**
|
|
99
|
+
* Canvas-space y offset.
|
|
100
|
+
*/
|
|
101
|
+
centerY: number;
|
|
102
|
+
};
|
|
103
|
+
export type EmberState = "idle" | "working" | "error";
|
|
104
|
+
export type EmberVisual = {
|
|
105
|
+
/**
|
|
106
|
+
* CSS custom-property name driving the warm stroke.
|
|
107
|
+
*/
|
|
108
|
+
color: string;
|
|
109
|
+
/**
|
|
110
|
+
* Stroke width in device pixels.
|
|
111
|
+
*/
|
|
112
|
+
lineWidth: number;
|
|
113
|
+
/**
|
|
114
|
+
* Radians per second of the slow Y rotation.
|
|
115
|
+
*/
|
|
116
|
+
rotationSpeed: number;
|
|
117
|
+
/**
|
|
118
|
+
* Radians per millisecond of the candle-lean sway.
|
|
119
|
+
*/
|
|
120
|
+
swaySpeed: number;
|
|
121
|
+
/**
|
|
122
|
+
* Horizontal lean amplitude at the flame tip.
|
|
123
|
+
*/
|
|
124
|
+
swayAmount: number;
|
|
125
|
+
/**
|
|
126
|
+
* Amplitude of the scale shimmer (the coal breathing).
|
|
127
|
+
*/
|
|
128
|
+
flicker: number;
|
|
129
|
+
/**
|
|
130
|
+
* Stroke shadow-blur in pixels (the warm halo around lines).
|
|
131
|
+
*/
|
|
132
|
+
glow: number;
|
|
133
|
+
};
|
|
134
|
+
//# sourceMappingURL=emberAvatar.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"emberAvatar.d.ts","sourceRoot":"","sources":["../../../src/dashboard/modules/emberAvatar.js"],"names":[],"mappings":"AA4CA;;;;;;GAMG;AACH,0CAHW,UAAU,GACR,WAAW,CAIvB;AAmBD;;;;;;;GAOG;AACH,6CAHW;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAClC;IAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;IAAC,KAAK,EAAE,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;CAAE,CAmClE;AAED;;;;;;;GAOG;AAEH;;;;;;;;;;;GAWG;AACH,2CANW,MAAM,mBACN,MAAM,cACN,MAAM,cACN,UAAU,GACR;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,CAuBpC;AAgBD,kEAMC;AAED;;;;;;;GAOG;AACH,wCAJW,WAAW,QACX,MAAM,GACJ,MAAM,CAIlB;AA/LD;;;;;;;;;;;;GAYG;AAEH;;GAEG;AAEH,2BAA2B;AAC3B,2BADW,UAAU,EAAE,CACkC;AAEzD,iDAAiD;AACjD,0BAA2B,GAAG,CAAC;AAC/B,6DAA6D;AAC7D,2BAA4B,CAAC,IAAI,CAAC;qBAgCrB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;;;;;UA6DvB,MAAM;;;;cACN,MAAM;;;;WACN,MAAM;;;;aACN,MAAM;;;;aACN,MAAM;;yBA1GP,MAAM,GAAG,SAAS,GAAG,OAAO;;;;;WAa3B,MAAM;;;;eACN,MAAM;;;;mBACN,MAAM;;;;eACN,MAAM;;;;gBACN,MAAM;;;;aACN,MAAM;;;;UACN,MAAM"}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard module — Ember flame wireframe avatar (SPEC-189).
|
|
3
|
+
* Humble object: pure functions, no DOM, no global state. Holds the flame-shaped
|
|
4
|
+
* wireframe geometry and every per-state visual decision, so the
|
|
5
|
+
* requestAnimationFrame loop in emberAvatarRenderer.js carries no branching.
|
|
6
|
+
*
|
|
7
|
+
* Ember is a warm wireframe BRAISE, not the setup wizard's abstract icosahedron:
|
|
8
|
+
* a teardrop flame mesh (pointed tip, rounded base) stroked in amber, that leans
|
|
9
|
+
* and flickers like a live coal — livelier and warmer when spoken to.
|
|
10
|
+
*
|
|
11
|
+
* Visual DNA: "Agentic OS" — dark warm near-black + amber. See
|
|
12
|
+
* project_agentic_os_design_dna.md.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {'idle' | 'working' | 'error'} EmberState
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** @type {EmberState[]} */
|
|
20
|
+
export const EMBER_STATES = ['idle', 'working', 'error'];
|
|
21
|
+
|
|
22
|
+
/** Top of the flame (the tip) in model space. */
|
|
23
|
+
export const FLAME_TIP_Y = 1.5;
|
|
24
|
+
/** Bottom of the flame (the rounded base) in model space. */
|
|
25
|
+
export const FLAME_BASE_Y = -1.15;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @typedef {Object} EmberVisual
|
|
29
|
+
* @property {string} color CSS custom-property name driving the warm stroke.
|
|
30
|
+
* @property {number} lineWidth Stroke width in device pixels.
|
|
31
|
+
* @property {number} rotationSpeed Radians per second of the slow Y rotation.
|
|
32
|
+
* @property {number} swaySpeed Radians per millisecond of the candle-lean sway.
|
|
33
|
+
* @property {number} swayAmount Horizontal lean amplitude at the flame tip.
|
|
34
|
+
* @property {number} flicker Amplitude of the scale shimmer (the coal breathing).
|
|
35
|
+
* @property {number} glow Stroke shadow-blur in pixels (the warm halo around lines).
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/** @type {Record<EmberState, EmberVisual>} */
|
|
39
|
+
const EMBER_VISUALS = {
|
|
40
|
+
idle: { color: '--accent', lineWidth: 1.5, rotationSpeed: 0.3, swaySpeed: 0.0011, swayAmount: 0.08, flicker: 0.025, glow: 10 },
|
|
41
|
+
working: { color: '--accent', lineWidth: 1.8, rotationSpeed: 0.6, swaySpeed: 0.0026, swayAmount: 0.22, flicker: 0.16, glow: 16 },
|
|
42
|
+
error: { color: '--danger', lineWidth: 2, rotationSpeed: 0.18, swaySpeed: 0.0018, swayAmount: 0.14, flicker: 0.22, glow: 12 },
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Maps an ember state to its stroke/animation parameters. Keeps the renderer
|
|
47
|
+
* humble: every per-state decision lives here, not in the canvas loop.
|
|
48
|
+
*
|
|
49
|
+
* @param {EmberState} state
|
|
50
|
+
* @returns {EmberVisual}
|
|
51
|
+
*/
|
|
52
|
+
export function emberStateToVisual(state) {
|
|
53
|
+
return EMBER_VISUALS[state];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @typedef {[number, number, number]} Vertex
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The flame radius at a normalised height ringT ∈ [0,1] (0 near the tip, 1 at the
|
|
62
|
+
* base). A teardrop profile: pointed at the top, bulging in the lower third, a
|
|
63
|
+
* small rounded base — the silhouette that reads as a coal/flame rather than a
|
|
64
|
+
* ball.
|
|
65
|
+
*
|
|
66
|
+
* @param {number} ringT
|
|
67
|
+
* @returns {number}
|
|
68
|
+
*/
|
|
69
|
+
function flameRadius(ringT) {
|
|
70
|
+
return Math.sin(Math.PI * ringT ** 1.3) * 0.9 + 0.06;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Builds the flame wireframe as a surface of revolution: a single tip vertex
|
|
75
|
+
* (index 0) plus `rings` × `meridians` body vertices, joined by tip spokes,
|
|
76
|
+
* vertical meridian lines and horizontal ring loops. Pure and deterministic.
|
|
77
|
+
*
|
|
78
|
+
* @param {{ rings: number; meridians: number }} options
|
|
79
|
+
* @returns {{ vertices: Vertex[]; edges: Array<[number, number]> }}
|
|
80
|
+
*/
|
|
81
|
+
export function buildFlameWireframe(options) {
|
|
82
|
+
const { rings, meridians } = options;
|
|
83
|
+
/** @type {Vertex[]} */
|
|
84
|
+
const vertices = [[0, FLAME_TIP_Y, 0]];
|
|
85
|
+
const indexAt = (ring, meridian) => 1 + ring * meridians + meridian;
|
|
86
|
+
|
|
87
|
+
for (let ring = 0; ring < rings; ring += 1) {
|
|
88
|
+
const ringT = (ring + 1) / rings;
|
|
89
|
+
const y = FLAME_TIP_Y + (FLAME_BASE_Y - FLAME_TIP_Y) * ringT;
|
|
90
|
+
const radius = flameRadius(ringT);
|
|
91
|
+
for (let meridian = 0; meridian < meridians; meridian += 1) {
|
|
92
|
+
const angle = (meridian / meridians) * Math.PI * 2;
|
|
93
|
+
vertices.push([Math.cos(angle) * radius, y, Math.sin(angle) * radius]);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** @type {Array<[number, number]>} */
|
|
98
|
+
const edges = [];
|
|
99
|
+
for (let meridian = 0; meridian < meridians; meridian += 1) {
|
|
100
|
+
edges.push([0, indexAt(0, meridian)]);
|
|
101
|
+
}
|
|
102
|
+
for (let ring = 0; ring < rings - 1; ring += 1) {
|
|
103
|
+
for (let meridian = 0; meridian < meridians; meridian += 1) {
|
|
104
|
+
edges.push([indexAt(ring, meridian), indexAt(ring + 1, meridian)]);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
for (let ring = 0; ring < rings; ring += 1) {
|
|
108
|
+
for (let meridian = 0; meridian < meridians; meridian += 1) {
|
|
109
|
+
edges.push([indexAt(ring, meridian), indexAt(ring, (meridian + 1) % meridians)]);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return { vertices, edges };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* @typedef {Object} Projection
|
|
118
|
+
* @property {number} tilt Fixed X-axis tilt in radians.
|
|
119
|
+
* @property {number} distance Camera distance for the perspective divide.
|
|
120
|
+
* @property {number} scale Pixels per unit at the projection plane.
|
|
121
|
+
* @property {number} centerX Canvas-space x offset.
|
|
122
|
+
* @property {number} centerY Canvas-space y offset.
|
|
123
|
+
*/
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Projects a flame vertex to a 2D canvas point: rotates around Y, leans the
|
|
127
|
+
* upper body sideways by `swayOffset` scaled by height (a candle flame bends at
|
|
128
|
+
* the tip, not the base), applies the fixed tilt, then a perspective divide.
|
|
129
|
+
* Pure and deterministic.
|
|
130
|
+
*
|
|
131
|
+
* @param {Vertex} vertex
|
|
132
|
+
* @param {number} rotationRadians
|
|
133
|
+
* @param {number} swayOffset
|
|
134
|
+
* @param {Projection} projection
|
|
135
|
+
* @returns {{ x: number; y: number }}
|
|
136
|
+
*/
|
|
137
|
+
export function projectFlameVertex(vertex, rotationRadians, swayOffset, projection) {
|
|
138
|
+
const [x, y, z] = vertex;
|
|
139
|
+
|
|
140
|
+
const cosY = Math.cos(rotationRadians);
|
|
141
|
+
const sinY = Math.sin(rotationRadians);
|
|
142
|
+
const rotatedX = x * cosY + z * sinY;
|
|
143
|
+
const rotatedZ = -x * sinY + z * cosY;
|
|
144
|
+
|
|
145
|
+
const heightFactor = (y - FLAME_BASE_Y) / (FLAME_TIP_Y - FLAME_BASE_Y);
|
|
146
|
+
const leanedX = rotatedX + swayOffset * heightFactor;
|
|
147
|
+
|
|
148
|
+
const cosTilt = Math.cos(projection.tilt);
|
|
149
|
+
const sinTilt = Math.sin(projection.tilt);
|
|
150
|
+
const tiltedY = y * cosTilt - rotatedZ * sinTilt;
|
|
151
|
+
const tiltedZ = y * sinTilt + rotatedZ * cosTilt;
|
|
152
|
+
|
|
153
|
+
const perspective = projection.scale / (tiltedZ + projection.distance);
|
|
154
|
+
return {
|
|
155
|
+
x: projection.centerX + leanedX * perspective,
|
|
156
|
+
y: projection.centerY - tiltedY * perspective,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* The breathing multiplier applied to the flame scale at a given time: a slow
|
|
162
|
+
* pulse plus a faster two-tone shimmer that reads as flicker. Pure — 1 at time 0,
|
|
163
|
+
* always within 1 ± flicker.
|
|
164
|
+
*
|
|
165
|
+
* @param {EmberVisual} visual
|
|
166
|
+
* @param {number} time Milliseconds since the loop started.
|
|
167
|
+
* @returns {number}
|
|
168
|
+
*/
|
|
169
|
+
const SHIMMER_SLOW_FREQUENCY = 0.013;
|
|
170
|
+
const SHIMMER_FAST_FREQUENCY = 0.031;
|
|
171
|
+
const SHIMMER_SLOW_WEIGHT = 0.6;
|
|
172
|
+
const SHIMMER_FAST_WEIGHT = 0.4;
|
|
173
|
+
|
|
174
|
+
export function emberRadiusFactor(visual, time) {
|
|
175
|
+
const shimmer =
|
|
176
|
+
visual.flicker *
|
|
177
|
+
(SHIMMER_SLOW_WEIGHT * Math.sin(time * SHIMMER_SLOW_FREQUENCY) +
|
|
178
|
+
SHIMMER_FAST_WEIGHT * Math.sin(time * SHIMMER_FAST_FREQUENCY));
|
|
179
|
+
return 1 + shimmer;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* The horizontal lean of the flame tip at a given time. Pure — 0 at time 0,
|
|
184
|
+
* always within ± swayAmount.
|
|
185
|
+
*
|
|
186
|
+
* @param {EmberVisual} visual
|
|
187
|
+
* @param {number} time Milliseconds since the loop started.
|
|
188
|
+
* @returns {number}
|
|
189
|
+
*/
|
|
190
|
+
export function emberSwayOffset(visual, time) {
|
|
191
|
+
return visual.swayAmount * Math.sin(time * visual.swaySpeed);
|
|
192
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"emberAvatar.js","sourceRoot":"","sources":["../../../src/dashboard/modules/emberAvatar.js"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The structural slice of a canvas the renderer depends on. A real
|
|
3
|
+
* HTMLCanvasElement satisfies it; tests pass a lightweight fake without casts.
|
|
4
|
+
*
|
|
5
|
+
* @typedef {Object} EmberCanvas
|
|
6
|
+
* @property {number} width
|
|
7
|
+
* @property {number} height
|
|
8
|
+
* @property {(contextId: '2d') => CanvasRenderingContext2D | null} getContext
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Mounts the animated flame wireframe avatar on a 2D canvas and returns its
|
|
12
|
+
* controls. The page calls setState() on each stream event and destroy() before
|
|
13
|
+
* any teardown so the rAF loop never leaks.
|
|
14
|
+
*
|
|
15
|
+
* @param {Object} options
|
|
16
|
+
* @param {EmberCanvas} options.canvas
|
|
17
|
+
* @param {import('./emberAvatar.js').EmberState} [options.initialState]
|
|
18
|
+
* @param {(callback: FrameRequestCallback) => number} [options.requestFrame]
|
|
19
|
+
* @param {(handle: number) => void} [options.cancelFrame]
|
|
20
|
+
* @param {() => number} [options.now]
|
|
21
|
+
* @returns {{ setState: (state: import('./emberAvatar.js').EmberState) => void; destroy: () => void }}
|
|
22
|
+
*/
|
|
23
|
+
export function mountEmberAvatar(options: {
|
|
24
|
+
canvas: EmberCanvas;
|
|
25
|
+
initialState?: import("./emberAvatar.js").EmberState | undefined;
|
|
26
|
+
requestFrame?: ((callback: FrameRequestCallback) => number) | undefined;
|
|
27
|
+
cancelFrame?: ((handle: number) => void) | undefined;
|
|
28
|
+
now?: (() => number) | undefined;
|
|
29
|
+
}): {
|
|
30
|
+
setState: (state: import("./emberAvatar.js").EmberState) => void;
|
|
31
|
+
destroy: () => void;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* The structural slice of a canvas the renderer depends on. A real
|
|
35
|
+
* HTMLCanvasElement satisfies it; tests pass a lightweight fake without casts.
|
|
36
|
+
*/
|
|
37
|
+
export type EmberCanvas = {
|
|
38
|
+
width: number;
|
|
39
|
+
height: number;
|
|
40
|
+
getContext: (contextId: "2d") => CanvasRenderingContext2D | null;
|
|
41
|
+
};
|
|
42
|
+
//# sourceMappingURL=emberAvatarRenderer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"emberAvatarRenderer.d.ts","sourceRoot":"","sources":["../../../src/dashboard/modules/emberAvatarRenderer.js"],"names":[],"mappings":"AA8BA;;;;;;;;GAQG;AAEH;;;;;;;;;;;;GAYG;AACH,0CAPG;IAA6B,MAAM,EAA3B,WAAW;IACqC,YAAY;IACP,YAAY,eAAtD,oBAAoB,KAAK,MAAM;IACP,WAAW,aAArC,MAAM,KAAK,IAAI;IACD,GAAG,UAApB,MAAM;CACpB,GAAU;IAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,OAAO,kBAAkB,EAAE,UAAU,KAAK,IAAI,CAAC;IAAC,OAAO,EAAE,MAAM,IAAI,CAAA;CAAE,CAmErG;;;;;;WAnFa,MAAM;YACN,MAAM;gBACN,CAAC,SAAS,EAAE,IAAI,KAAK,wBAAwB,GAAG,IAAI"}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard module — Ember flame wireframe renderer (SPEC-189).
|
|
3
|
+
* Humble glue: a thin requestAnimationFrame loop that strokes the flame
|
|
4
|
+
* wireframe with a warm amber glow (shadowBlur), a slow Y rotation, a candle
|
|
5
|
+
* lean and a flicker breathing. It owns the rAF handle and its teardown, and
|
|
6
|
+
* delegates EVERY decision (geometry, colour, line width, rotation, sway,
|
|
7
|
+
* flicker, glow) to the pure emberAvatar.js module. Lifecycle-tested only
|
|
8
|
+
* (browser-only drawing), mirroring setupWizardAvatarRenderer.
|
|
9
|
+
*
|
|
10
|
+
* Visual DNA: "Agentic OS" — dark warm near-black + amber.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
emberStateToVisual,
|
|
15
|
+
buildFlameWireframe,
|
|
16
|
+
projectFlameVertex,
|
|
17
|
+
emberRadiusFactor,
|
|
18
|
+
emberSwayOffset,
|
|
19
|
+
} from './emberAvatar.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {CanvasRenderingContext2D} context
|
|
23
|
+
* @param {string} colorToken
|
|
24
|
+
* @returns {string}
|
|
25
|
+
*/
|
|
26
|
+
function resolveColor(context, colorToken) {
|
|
27
|
+
const value = getComputedStyle(context.canvas).getPropertyValue(colorToken).trim();
|
|
28
|
+
return value === '' ? '#F4A93D' : value;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The structural slice of a canvas the renderer depends on. A real
|
|
33
|
+
* HTMLCanvasElement satisfies it; tests pass a lightweight fake without casts.
|
|
34
|
+
*
|
|
35
|
+
* @typedef {Object} EmberCanvas
|
|
36
|
+
* @property {number} width
|
|
37
|
+
* @property {number} height
|
|
38
|
+
* @property {(contextId: '2d') => CanvasRenderingContext2D | null} getContext
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Mounts the animated flame wireframe avatar on a 2D canvas and returns its
|
|
43
|
+
* controls. The page calls setState() on each stream event and destroy() before
|
|
44
|
+
* any teardown so the rAF loop never leaks.
|
|
45
|
+
*
|
|
46
|
+
* @param {Object} options
|
|
47
|
+
* @param {EmberCanvas} options.canvas
|
|
48
|
+
* @param {import('./emberAvatar.js').EmberState} [options.initialState]
|
|
49
|
+
* @param {(callback: FrameRequestCallback) => number} [options.requestFrame]
|
|
50
|
+
* @param {(handle: number) => void} [options.cancelFrame]
|
|
51
|
+
* @param {() => number} [options.now]
|
|
52
|
+
* @returns {{ setState: (state: import('./emberAvatar.js').EmberState) => void; destroy: () => void }}
|
|
53
|
+
*/
|
|
54
|
+
export function mountEmberAvatar(options) {
|
|
55
|
+
const requestFrame = options.requestFrame ?? globalThis.requestAnimationFrame.bind(globalThis);
|
|
56
|
+
const cancelFrame = options.cancelFrame ?? globalThis.cancelAnimationFrame.bind(globalThis);
|
|
57
|
+
const now = options.now ?? (() => performance.now());
|
|
58
|
+
const context = options.canvas.getContext('2d');
|
|
59
|
+
const startTime = now();
|
|
60
|
+
const flame = buildFlameWireframe({ rings: 7, meridians: 10 });
|
|
61
|
+
|
|
62
|
+
let currentState = options.initialState ?? 'idle';
|
|
63
|
+
/** @type {number | null} */
|
|
64
|
+
let frameHandle = null;
|
|
65
|
+
|
|
66
|
+
const projection = {
|
|
67
|
+
tilt: 0.12,
|
|
68
|
+
distance: 4,
|
|
69
|
+
scale: Math.min(options.canvas.width, options.canvas.height) * 0.55,
|
|
70
|
+
centerX: options.canvas.width / 2,
|
|
71
|
+
centerY: options.canvas.height / 2,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @param {number} time
|
|
76
|
+
*/
|
|
77
|
+
function draw(time) {
|
|
78
|
+
if (context === null) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const elapsed = time - startTime;
|
|
82
|
+
const visual = emberStateToVisual(currentState);
|
|
83
|
+
const rotation = (elapsed * visual.rotationSpeed) / 1000;
|
|
84
|
+
const sway = emberSwayOffset(visual, elapsed);
|
|
85
|
+
const framedProjection = { ...projection, scale: projection.scale * emberRadiusFactor(visual, elapsed) };
|
|
86
|
+
|
|
87
|
+
context.clearRect(0, 0, options.canvas.width, options.canvas.height);
|
|
88
|
+
const color = resolveColor(context, visual.color);
|
|
89
|
+
context.lineWidth = visual.lineWidth;
|
|
90
|
+
context.lineCap = 'round';
|
|
91
|
+
context.strokeStyle = color;
|
|
92
|
+
context.shadowColor = color;
|
|
93
|
+
context.shadowBlur = visual.glow;
|
|
94
|
+
context.beginPath();
|
|
95
|
+
for (const [from, to] of flame.edges) {
|
|
96
|
+
const start = projectFlameVertex(flame.vertices[from], rotation, sway, framedProjection);
|
|
97
|
+
const end = projectFlameVertex(flame.vertices[to], rotation, sway, framedProjection);
|
|
98
|
+
context.moveTo(start.x, start.y);
|
|
99
|
+
context.lineTo(end.x, end.y);
|
|
100
|
+
}
|
|
101
|
+
context.stroke();
|
|
102
|
+
|
|
103
|
+
frameHandle = requestFrame(draw);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
frameHandle = requestFrame(draw);
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
setState: (state) => {
|
|
110
|
+
currentState = state;
|
|
111
|
+
},
|
|
112
|
+
destroy: () => {
|
|
113
|
+
if (frameHandle !== null) {
|
|
114
|
+
cancelFrame(frameHandle);
|
|
115
|
+
frameHandle = null;
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"emberAvatarRenderer.js","sourceRoot":"","sources":["../../../src/dashboard/modules/emberAvatarRenderer.js"],"names":[],"mappings":"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"}
|