@bongos/core 1.20.77 → 1.20.78
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/.bongos-core.json +79 -54
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +3 -0
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/adr/0136-update-channel-subscription-policy.md +1 -1
- package/docs/adr/0360-bongos-follows-every-candidate-every-other-project-follows-releases.md +66 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +70 -3
- package/docs/api-reference.md +3 -2
- package/docs/copy-inventory.md +22 -14
- package/docs/copy-registry.json +125 -41
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-inventory.json +5 -1
- package/docs/page-readings.json +150 -137
- package/docs/recipes/upgrading-the-core.md +11 -2
- package/modules/copy-desk/artist-stats.js +15 -1
- package/modules/copy-desk/module.json +1 -1
- package/modules/copy-desk/page-status.js +41 -0
- package/modules/copy-desk/routes/copy-desk-artist.js +44 -0
- package/modules/copy-desk/tests/copy_no_cms.mjs +11 -2
- package/modules/hall-ui/public/deploy.states.json +1 -1
- package/modules/hall-ui/public/profile-crafts.js +231 -0
- package/modules/hall-ui/public/profile.css +60 -0
- package/modules/hall-ui/public/profile.html +31 -12
- package/modules/hall-ui/public/profile.js +180 -75
- package/modules/hall-ui/public/profile.states.json +34 -0
- package/modules/hall-ui/public/sky-draw.js +66 -39
- package/modules/hall-ui/public/sky-interaction.js +1 -1
- package/modules/hall-ui/public/sky-panel.js +1 -1
- package/modules/hall-ui/public/thinking.css +52 -24
- package/modules/hall-ui/records/profile-roles.md +18 -0
- package/modules/npm-release/public/work.js +72 -0
- package/modules/npm-release/work.js +67 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +18 -0
- package/scripts/gds/update-channel.js +68 -1
- package/scripts/gds/update-sweep.js +100 -4
- package/scripts/migrate.sh +6 -2
- package/src/module-api.js +1 -1
- package/tests/hall_sky.mjs +77 -0
- package/tests/helpers.mjs +32 -7
- package/tests/migrate_applied_check.mjs +94 -0
- package/tests/npm_release_page.mjs +67 -0
- package/tests/npm_release_work.mjs +82 -0
- package/tests/profile_activity.mjs +23 -15
- package/tests/profile_role_headline.mjs +4 -4
- package/tests/profile_role_tabs.mjs +163 -0
- package/tests/update_channel.mjs +80 -1
- package/tests/update_subscription_engine.mjs +99 -0
|
@@ -406,22 +406,17 @@
|
|
|
406
406
|
// OTBKit's server-offset pager mode — the kit's own mode, added by this task,
|
|
407
407
|
// rather than an eleventh hand-rolled pager (hall-ui/CLAUDE.md's hard rule).
|
|
408
408
|
|
|
409
|
-
// The
|
|
410
|
-
//
|
|
411
|
-
//
|
|
412
|
-
//
|
|
413
|
-
//
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
];
|
|
421
|
-
// Filing an idea is ideator work whatever craft the resulting task turned out
|
|
422
|
-
// to be, so the trail belongs to the ideator view — and to 'all', which is the
|
|
423
|
-
// whole record by definition. It is not a per-craft slice of the ideas.
|
|
424
|
-
const TRAIL_TABS = new Set(['all', 'ideator']);
|
|
409
|
+
// The role tabs (task 1004435, the approved mock): one per role the person
|
|
410
|
+
// HAS — their main role and every other role that has earned credits, the
|
|
411
|
+
// same `roles.shown` the header's chips read. Each tab is that craft's own
|
|
412
|
+
// record, never "tasks that happen to carry its label":
|
|
413
|
+
// engineer the works they completed and the goals they created, every
|
|
414
|
+
// craft of task included (most of the corpus is 'unclassified',
|
|
415
|
+
// and a filter to 'engineer' would read as "you did less")
|
|
416
|
+
// artist the pages they rewrote in Tweak Mode that were approved
|
|
417
|
+
// ideator the room's public trail: every thought they filed, in the sky's
|
|
418
|
+
// five states, drawn and listed (the old idea trail, folded in)
|
|
419
|
+
const ROLE_TAB_WORD = { engineer: 'Engineer', artist: 'Artist', ideator: 'Ideator' };
|
|
425
420
|
|
|
426
421
|
// Every href below goes through BOTH encodings, and it needs both: the ids are
|
|
427
422
|
// DB integers today, but encodeURIComponent leaves a literal `'` alone, so on
|
|
@@ -586,10 +581,12 @@
|
|
|
586
581
|
// not rebuild the DOM (which would drop the reader's per-page preference).
|
|
587
582
|
let activity = null;
|
|
588
583
|
|
|
589
|
-
|
|
584
|
+
// `view` is the person as the header reads them (task 1004435): their name,
|
|
585
|
+
// their main role, the roles to give a tab and the count beside each.
|
|
586
|
+
function buildActivity(builderId, view = {}) {
|
|
590
587
|
const kit = window.OTBKit;
|
|
591
588
|
if (!kit) return null;
|
|
592
|
-
|
|
589
|
+
const { name: ownerName, main: mainRole, roles: roleTabs, counts: roleCounts } = view;
|
|
593
590
|
|
|
594
591
|
const tasks = makeActivityList({
|
|
595
592
|
mountId: 'activity-tasks',
|
|
@@ -603,7 +600,7 @@
|
|
|
603
600
|
emptyMsg: `No completed works in this ${window.OTB.roleAxisLabel().toLowerCase()}.`,
|
|
604
601
|
fetchPage: async (offset, limit) => {
|
|
605
602
|
const res = await api.request('GET', `${API}/builders/${encodeURIComponent(builderId)}/activity`
|
|
606
|
-
+ `?section=tasks&limit=${limit}&offset=${offset}
|
|
603
|
+
+ `?section=tasks&limit=${limit}&offset=${offset}`);
|
|
607
604
|
if (!res.ok) throw new Error(`activity tasks → ${res.status}`);
|
|
608
605
|
return { rows: res.data.rows || [], total: Number(res.data.total) || 0 };
|
|
609
606
|
},
|
|
@@ -633,7 +630,7 @@
|
|
|
633
630
|
emptyMsg: `No goals created in this ${window.OTB.roleAxisLabel().toLowerCase()}.`,
|
|
634
631
|
fetchPage: async (offset, limit) => {
|
|
635
632
|
const res = await api.request('GET', `${API}/builders/${encodeURIComponent(builderId)}/activity`
|
|
636
|
-
+ `?section=goals&limit=${limit}&offset=${offset}
|
|
633
|
+
+ `?section=goals&limit=${limit}&offset=${offset}`);
|
|
637
634
|
if (!res.ok) throw new Error(`activity goals → ${res.status}`);
|
|
638
635
|
return { rows: res.data.rows || [], total: Number(res.data.total) || 0 };
|
|
639
636
|
},
|
|
@@ -654,83 +651,179 @@
|
|
|
654
651
|
},
|
|
655
652
|
});
|
|
656
653
|
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
654
|
+
// ---- the ideator: the room's public trail (task 1004435) ----------------
|
|
655
|
+
// ONE read of up to the route's cap, drawn whole and listed under the
|
|
656
|
+
// drawing, so every mark has its row and every row its mark (the sky's
|
|
657
|
+
// accessibility rule). The ledger pages that list in the browser; past the
|
|
658
|
+
// cap the foot says how many the drawing leaves out.
|
|
659
|
+
const crafts = window.OTBProfileCrafts;
|
|
660
|
+
const TRAIL_CAP = 100;
|
|
661
|
+
let trailRows = [];
|
|
662
|
+
const trailPager = kit.makePager('activity-ideas', 'otb.profile.trailPerPage', { defaultPerPage: 10, minPerPage: 10 });
|
|
663
|
+
const trailLedger = crafts && $('#activity-ideas') ? kit.makeLedger({
|
|
664
|
+
mount: $('#activity-ideas'),
|
|
665
|
+
columns: ['Thought', 'State', { label: 'Size', class: 'ledger__r' }, { label: 'Still alive', class: 'ledger__r' }, 'What became of it'],
|
|
666
|
+
emptyMsg: 'No thoughts filed yet.',
|
|
667
|
+
pager: trailPager,
|
|
668
|
+
rowHtml: (row) => crafts.thoughtRowHtml(row, trailRows.indexOf(row), { outcomeHtml: ideaOutcomeHtml, ideaHref }),
|
|
669
|
+
}) : null;
|
|
670
|
+
let trailLoaded = false;
|
|
671
|
+
async function loadTrail() {
|
|
672
|
+
if (!crafts || !trailLedger || trailLoaded) return;
|
|
673
|
+
trailLoaded = true;
|
|
674
|
+
const first = String(ownerName || 'This builder').split(' ')[0];
|
|
675
|
+
const lede = $('#trail-lede');
|
|
676
|
+
if (lede) lede.textContent = crafts.thoughtsLede(first, mainRole === 'ideator');
|
|
677
|
+
try {
|
|
678
|
+
const res = await api.request('GET', `${API}/inbox/by-builder/${encodeURIComponent(builderId)}?limit=${TRAIL_CAP}&offset=0`);
|
|
670
679
|
if (!res.ok) throw new Error(`idea trail → ${res.status}`);
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
680
|
+
trailRows = res.data.ideas || [];
|
|
681
|
+
const total = Number(res.data.total) || trailRows.length;
|
|
682
|
+
const fig = $('#trail-figure');
|
|
683
|
+
if (fig) {
|
|
684
|
+
fig.innerHTML = crafts.trailSvgHtml(trailRows, { width: fig.clientWidth || 860, height: matchMedia('(max-width: 640px)').matches ? 190 : 230 });
|
|
685
|
+
fig.hidden = !trailRows.length;
|
|
686
|
+
}
|
|
687
|
+
const legend = $('#trail-legend');
|
|
688
|
+
if (legend) legend.innerHTML = trailRows.length ? crafts.legendHtml() : '';
|
|
689
|
+
trailLedger.render(trailRows);
|
|
690
|
+
const foot = $('#trail-foot');
|
|
691
|
+
if (foot) {
|
|
692
|
+
const parts = [];
|
|
693
|
+
if (total > trailRows.length) parts.push(`The newest ${trailRows.length} of ${total} thoughts are drawn here.`);
|
|
694
|
+
if (trailRows.some((r) => r.size && r.size.historical === true && Number(r.size.estimated) > 0)) parts.push('~ marks a size that includes a historical estimate.');
|
|
695
|
+
foot.textContent = parts.join(' ');
|
|
696
|
+
foot.hidden = !parts.length;
|
|
697
|
+
}
|
|
698
|
+
} catch (err) {
|
|
699
|
+
trailLoaded = false;
|
|
700
|
+
console.error('[profile] idea trail failed', err);
|
|
701
|
+
trailLedger.render([]);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
// Hovering or focusing a mark lights its row, and a row its mark: one
|
|
705
|
+
// listener per mount, read off data-i, so a re-render needs no re-wiring.
|
|
706
|
+
function hot(i) {
|
|
707
|
+
document.querySelectorAll('#trail-figure .pt-m').forEach((m) => m.classList.toggle('is-hot', m.getAttribute('data-i') === String(i)));
|
|
708
|
+
document.querySelectorAll('#activity-ideas tr[data-i]').forEach((r) => r.classList.toggle('is-hot', r.getAttribute('data-i') === String(i)));
|
|
709
|
+
}
|
|
710
|
+
const hotFrom = (e) => { const el = e.target.closest && e.target.closest('[data-i]'); hot(el ? el.getAttribute('data-i') : -1); };
|
|
711
|
+
['#trail-figure', '#activity-ideas'].forEach((sel) => {
|
|
712
|
+
const el = $(sel);
|
|
713
|
+
if (!el) return;
|
|
714
|
+
el.addEventListener('mouseover', hotFrom);
|
|
715
|
+
el.addEventListener('focusin', hotFrom);
|
|
716
|
+
el.addEventListener('mouseleave', () => hot(-1));
|
|
679
717
|
});
|
|
718
|
+
const fig = $('#trail-figure');
|
|
719
|
+
if (fig) {
|
|
720
|
+
fig.addEventListener('click', (e) => {
|
|
721
|
+
const m = e.target.closest && e.target.closest('.pt-m[data-i]');
|
|
722
|
+
const row = m && document.getElementById(`pt-row-${m.getAttribute('data-i')}`);
|
|
723
|
+
if (row) row.focus();
|
|
724
|
+
});
|
|
725
|
+
}
|
|
680
726
|
|
|
681
|
-
//
|
|
682
|
-
//
|
|
683
|
-
//
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
727
|
+
// ---- the artist: approved pages (task 1004435) ---------------------------
|
|
728
|
+
// A few at a time, newest first, each with its first changed line before and
|
|
729
|
+
// after and, when the round has them, its approval renders.
|
|
730
|
+
const PAGES_STEP = 4;
|
|
731
|
+
let pagesShown = 0;
|
|
732
|
+
let pagesHtml = '';
|
|
733
|
+
let pagesTotal = 0;
|
|
734
|
+
async function visualsFor(taskId) {
|
|
735
|
+
try {
|
|
736
|
+
const res = await api.request('GET', `${API}/tasks/${encodeURIComponent(taskId)}/visuals`);
|
|
737
|
+
if (!res.ok) return null;
|
|
738
|
+
const by = {};
|
|
739
|
+
for (const s of (res.data && res.data.slots) || []) if (s && s.slot) by[s.slot] = s;
|
|
740
|
+
return by;
|
|
741
|
+
} catch { return null; }
|
|
742
|
+
}
|
|
743
|
+
async function loadPages(more) {
|
|
744
|
+
const list = $('#activity-pages');
|
|
745
|
+
if (!crafts || !list) return;
|
|
746
|
+
if (!more) { pagesShown = 0; pagesHtml = ''; list.innerHTML = ''; }
|
|
747
|
+
const first = String(ownerName || 'This builder').split(' ')[0];
|
|
748
|
+
try {
|
|
749
|
+
const res = await api.request('GET', `${API}/copy-desk/artists/${encodeURIComponent(builderId)}/pages?limit=${PAGES_STEP}&offset=${pagesShown}`);
|
|
750
|
+
if (!res.ok) throw new Error(`artist pages → ${res.status}`);
|
|
751
|
+
const rows = res.data.rows || [];
|
|
752
|
+
pagesTotal = Number(res.data.total) || 0;
|
|
753
|
+
const vis = await Promise.all(rows.map((p) => visualsFor(p.task_id)));
|
|
754
|
+
const thumb = window.OTBVisual ? window.OTBVisual.shipVisualThumbHtml : null;
|
|
755
|
+
pagesHtml += rows.map((p, k) => crafts.pageRowHtml(p, { visuals: vis[k], thumb })).join('');
|
|
756
|
+
list.innerHTML = pagesHtml;
|
|
757
|
+
pagesShown += rows.length;
|
|
758
|
+
const lede = $('#pages-lede');
|
|
759
|
+
if (lede) lede.textContent = crafts.pagesLede(first, pagesTotal);
|
|
760
|
+
} catch (err) {
|
|
761
|
+
console.error('[profile] artist pages failed', err);
|
|
762
|
+
const lede = $('#pages-lede');
|
|
763
|
+
if (lede && !pagesShown) lede.textContent = 'The approved pages could not be read just now.';
|
|
764
|
+
}
|
|
765
|
+
const moreEl = $('#pages-more');
|
|
766
|
+
if (moreEl) {
|
|
767
|
+
const left = pagesTotal - pagesShown;
|
|
768
|
+
moreEl.innerHTML = left > 0
|
|
769
|
+
? `<p class="pt-foot">Showing ${pagesShown} of ${pagesTotal}. <button type="button" class="btn-ghost" id="pages-more-btn">Show ${Math.min(PAGES_STEP, left)} more</button></p>`
|
|
770
|
+
: '';
|
|
771
|
+
const btn = $('#pages-more-btn');
|
|
772
|
+
if (btn) btn.addEventListener('click', () => loadPages(true), { once: true });
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// ---- the tabs ---------------------------------------------------------------
|
|
777
|
+
// One panel per role tab, so makeTabs' own panel toggle does the showing; a
|
|
778
|
+
// tab's record loads the first time it is opened.
|
|
779
|
+
const shownRoles = (roleTabs && roleTabs.length ? roleTabs : ['engineer']).filter((c) => ROLE_TAB_WORD[c]);
|
|
780
|
+
const loaded = new Set();
|
|
781
|
+
function load(id) {
|
|
782
|
+
if (id === 'engineer') {
|
|
783
|
+
[tasks, goals].forEach((l) => { if (l) { l.reset(); l.load(); } });
|
|
784
|
+
} else if (id === 'ideator') {
|
|
785
|
+
loadTrail();
|
|
786
|
+
} else if (id === 'artist' && !loaded.has(id)) {
|
|
787
|
+
loadPages(false);
|
|
788
|
+
}
|
|
789
|
+
loaded.add(id);
|
|
790
|
+
}
|
|
687
791
|
const tabs = kit.makeTabs({
|
|
688
792
|
mount: $('#activity-tabs'),
|
|
689
|
-
tabs:
|
|
690
|
-
onChange: (id) =>
|
|
691
|
-
const tab = CRAFT_TABS.find((t) => t.id === id) || CRAFT_TABS[0];
|
|
692
|
-
role = tab.role;
|
|
693
|
-
show(id);
|
|
694
|
-
},
|
|
793
|
+
tabs: shownRoles.map((c) => ({ id: c, label: ROLE_TAB_WORD[c], panel: `activity-${c}-panel` })),
|
|
794
|
+
onChange: (id) => load(id),
|
|
695
795
|
});
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
const trail = TRAIL_TABS.has(tabId);
|
|
699
|
-
const ideasPanel = $('#activity-ideas-panel');
|
|
700
|
-
if (ideasPanel) ideasPanel.hidden = !trail;
|
|
701
|
-
[tasks, goals].forEach((l) => { if (l) { l.reset(); l.load(); } });
|
|
702
|
-
if (trail && ideas) { ideas.reset(); ideas.load(); }
|
|
703
|
-
}
|
|
796
|
+
// The count beside each tab: the same numbers the header leads with.
|
|
797
|
+
for (const c of shownRoles) if (roleCounts && roleCounts[c] != null) tabs.setCount(c, roleCounts[c]);
|
|
704
798
|
|
|
705
799
|
// makeTabs activates its initial tab during construction, which already ran
|
|
706
|
-
// onChange →
|
|
707
|
-
return { show, active: () => tabs.active() ||
|
|
800
|
+
// onChange → load(). So the caller must NOT kick it again on first build.
|
|
801
|
+
return { show: (id) => load(id), active: () => tabs.active() || shownRoles[0] };
|
|
708
802
|
}
|
|
709
803
|
|
|
710
|
-
function renderActivity(builderId, signedIn) {
|
|
804
|
+
function renderActivity(builderId, signedIn, view) {
|
|
711
805
|
const section = $('#activity-scroll');
|
|
712
806
|
const placeholder = $('#activity-placeholder');
|
|
713
807
|
if (!section) return;
|
|
808
|
+
const PANELS = ['activity-engineer-panel', 'activity-artist-panel', 'activity-ideator-panel'];
|
|
714
809
|
if (!signedIn) {
|
|
715
|
-
//
|
|
716
|
-
//
|
|
717
|
-
//
|
|
810
|
+
// Owner decision (a), 2026-09-30: "no one should be able to open up the
|
|
811
|
+
// hood of data without being signed in." The identity band, the tally and
|
|
812
|
+
// By the numbers stay public; every role's record says so instead.
|
|
813
|
+
const first = String((view && view.name) || 'This builder').split(' ')[0];
|
|
718
814
|
if (placeholder) {
|
|
719
815
|
placeholder.hidden = false;
|
|
720
|
-
placeholder.innerHTML =
|
|
721
|
-
+ `<a href="${API}/auth/web/start">sign in</a> to see
|
|
816
|
+
placeholder.innerHTML = `${escapeHtml(first)}'s activity is visible to signed-in builders of this project — `
|
|
817
|
+
+ `<a href="${API}/auth/web/start">sign in</a> to see it.`;
|
|
722
818
|
}
|
|
723
|
-
|
|
724
|
-
.forEach((id) => { const el = $(`#${id}`); if (el) el.hidden = true; });
|
|
819
|
+
PANELS.forEach((id) => { const el = $(`#${id}`); if (el) el.hidden = true; });
|
|
725
820
|
const tabsMount = $('#activity-tabs');
|
|
726
821
|
if (tabsMount) tabsMount.innerHTML = '';
|
|
727
822
|
return;
|
|
728
823
|
}
|
|
729
824
|
if (placeholder) { placeholder.hidden = true; placeholder.innerHTML = ''; }
|
|
730
|
-
['activity-tasks-panel', 'activity-goals-panel']
|
|
731
|
-
.forEach((id) => { const el = $(`#${id}`); if (el) el.hidden = false; });
|
|
732
825
|
if (!activity) {
|
|
733
|
-
activity = buildActivity(builderId); // its makeTabs already loaded
|
|
826
|
+
activity = buildActivity(builderId, view); // its makeTabs already loaded the first tab
|
|
734
827
|
return;
|
|
735
828
|
}
|
|
736
829
|
activity.show(activity.active());
|
|
@@ -831,7 +924,19 @@
|
|
|
831
924
|
buildWorksLedger(builder.id, selfId != null, data.recent_ships);
|
|
832
925
|
// The activity log needs a session (private goals + the idea trail), so it
|
|
833
926
|
// keys off whether /me resolved — the same signal the self-detect uses.
|
|
834
|
-
|
|
927
|
+
// The Activity tabs are the header's roles (task 1004435): one per role
|
|
928
|
+
// shown, each counted by the number its craft leads with.
|
|
929
|
+
const r = data.roles || {};
|
|
930
|
+
renderActivity(builder.id, selfId != null, {
|
|
931
|
+
name: builder.display_name || builder.github_login || 'This builder',
|
|
932
|
+
main: view.crafts[0].craft,
|
|
933
|
+
roles: view.crafts.map((c) => c.craft),
|
|
934
|
+
counts: {
|
|
935
|
+
engineer: Number(data.shipped_count) || 0,
|
|
936
|
+
artist: r.artist && Number.isFinite(r.artist.pages_approved) ? r.artist.pages_approved : null,
|
|
937
|
+
ideator: r.ideator && Number.isFinite(r.ideator.filed) ? r.ideator.filed : null,
|
|
938
|
+
},
|
|
939
|
+
});
|
|
835
940
|
// Connections (section 6, task 1002296) — a separate hub-only script that
|
|
836
941
|
// reveals its own section or leaves it hidden. Optional by design: a page
|
|
837
942
|
// that does not load it simply has no connections section.
|
|
@@ -73,6 +73,40 @@
|
|
|
73
73
|
]
|
|
74
74
|
}
|
|
75
75
|
},
|
|
76
|
+
"activity-artist": {
|
|
77
|
+
"_": "The artist's Activity tab (task 1004435): approved pages, each with its before/after. Fixtures builders__2001__profile + copy-desk__artists__2001__pages.",
|
|
78
|
+
"auth": true,
|
|
79
|
+
"url": "/profile?id=2001#activity",
|
|
80
|
+
"actions": [
|
|
81
|
+
[
|
|
82
|
+
"wait",
|
|
83
|
+
1500
|
|
84
|
+
]
|
|
85
|
+
],
|
|
86
|
+
"expect": {
|
|
87
|
+
"visible": [
|
|
88
|
+
"#activity-tabs",
|
|
89
|
+
"#activity-pages"
|
|
90
|
+
]
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
"activity-ideator": {
|
|
94
|
+
"_": "The ideator's Activity tab (task 1004435): the room's public trail, every mark also a row. Fixtures builders__2002__profile + inbox__by-builder__2002.",
|
|
95
|
+
"auth": true,
|
|
96
|
+
"url": "/profile?id=2002#activity",
|
|
97
|
+
"actions": [
|
|
98
|
+
[
|
|
99
|
+
"wait",
|
|
100
|
+
1500
|
|
101
|
+
]
|
|
102
|
+
],
|
|
103
|
+
"expect": {
|
|
104
|
+
"visible": [
|
|
105
|
+
"#activity-tabs",
|
|
106
|
+
"#trail-figure"
|
|
107
|
+
]
|
|
108
|
+
}
|
|
109
|
+
},
|
|
76
110
|
"other": {
|
|
77
111
|
"auth": true,
|
|
78
112
|
"url": "/profile?id=1000025",
|
|
@@ -20,6 +20,31 @@
|
|
|
20
20
|
|
|
21
21
|
const ACC = [251, 149, 105], CHART = [242, 210, 122];
|
|
22
22
|
const rgba = (c, a) => `rgba(${c[0]},${c[1]},${c[2]},${a})`;
|
|
23
|
+
// THE PALETTE (task 1004500). The template's white-on-void, read off the stage's
|
|
24
|
+
// --sky-*-rgb channels (thinking.css) so the light theme paints ink on paper.
|
|
25
|
+
// The arrays are filled IN PLACE: SKY.ACC / SKY.CHART stay the same objects.
|
|
26
|
+
// Re-read only when the theme flips (shell.js writes html[data-theme]).
|
|
27
|
+
const INK = [255, 255, 255], VOID = [0, 0, 0], COLD = [20, 20, 24]; let TEMP = 1;
|
|
28
|
+
// ink(a) is called per mark per frame, so each alpha's string is built once per palette.
|
|
29
|
+
const inkCache = new Map();
|
|
30
|
+
const ink = (a) => { let s = inkCache.get(a); if (!s) { s = rgba(INK, a); inkCache.set(a, s); } return s; };
|
|
31
|
+
let paletteStale = true;
|
|
32
|
+
function readPalette() {
|
|
33
|
+
paletteStale = false; inkCache.clear();
|
|
34
|
+
const st = typeof document.querySelector === 'function' && document.querySelector('.sky-stage');
|
|
35
|
+
if (!st || typeof getComputedStyle !== 'function') return;
|
|
36
|
+
const cs = getComputedStyle(st); if (!cs || typeof cs.getPropertyValue !== 'function') return;
|
|
37
|
+
const fill = (name, into) => { const v = String(cs.getPropertyValue(name) || '').split(',').map((n) => Number(n.trim())); if (v.length === 3 && v.every((n) => Number.isFinite(n))) into.splice(0, 3, ...v); };
|
|
38
|
+
fill('--sky-ink-rgb', INK); fill('--sky-void-rgb', VOID); fill('--sky-cold-rgb', COLD); fill('--sky-accent-rgb', ACC); fill('--sky-chart-rgb', CHART);
|
|
39
|
+
const t = Number(String(cs.getPropertyValue('--sky-temp') || '').trim()); TEMP = Number.isFinite(t) && t > 0 ? t : 1;
|
|
40
|
+
}
|
|
41
|
+
const tone = (c) => (TEMP === 1 ? c : c.map((v) => Math.round(v * TEMP)));
|
|
42
|
+
{
|
|
43
|
+
const stale = () => { paletteStale = true; S.dirty = true; };
|
|
44
|
+
if (typeof MutationObserver === 'function' && document.documentElement) new MutationObserver(stale).observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
|
|
45
|
+
const mq = typeof window.matchMedia === 'function' && window.matchMedia('(prefers-color-scheme: dark)');
|
|
46
|
+
if (mq && typeof mq.addEventListener === 'function') mq.addEventListener('change', stale);
|
|
47
|
+
}
|
|
23
48
|
function goalAlpha(g, now) {
|
|
24
49
|
if (!present(g)) return 0;
|
|
25
50
|
let a = 1;
|
|
@@ -31,7 +56,7 @@
|
|
|
31
56
|
function shuttle(x, y, ang, s, glow) {
|
|
32
57
|
ctx.save(); ctx.translate(x, y); ctx.rotate(ang);
|
|
33
58
|
if (glow) { ctx.shadowColor = rgba(CHART, .7); ctx.shadowBlur = 10; }
|
|
34
|
-
ctx.fillStyle = glow ?
|
|
59
|
+
ctx.fillStyle = glow ? ink(1) : ink(.85); ctx.strokeStyle = glow ? rgba(CHART, 1) : ink(.5); ctx.lineWidth = 1;
|
|
35
60
|
ctx.beginPath(); ctx.moveTo(8 * s, 0); ctx.lineTo(2 * s, -2.2 * s); ctx.lineTo(-3 * s, -2.2 * s); ctx.lineTo(-5 * s, -6 * s); ctx.lineTo(-7 * s, -6 * s); ctx.lineTo(-6 * s, -1.6 * s); ctx.lineTo(-7 * s, 0); ctx.lineTo(-6 * s, 1.6 * s); ctx.lineTo(-7 * s, 6 * s); ctx.lineTo(-5 * s, 6 * s); ctx.lineTo(-3 * s, 2.2 * s); ctx.lineTo(2 * s, 2.2 * s); ctx.closePath(); ctx.fill(); ctx.stroke();
|
|
36
61
|
ctx.restore();
|
|
37
62
|
}
|
|
@@ -40,7 +65,8 @@
|
|
|
40
65
|
const { W, H, DPR, cam, lens, hover, picked, gravityGoal, tourSpot, charted, goals, edges, ideas, BANDS } = S;
|
|
41
66
|
const tt = (now - S.t0) / 1000;
|
|
42
67
|
ctx.setTransform(DPR, 0, 0, DPR, 0, 0);
|
|
43
|
-
|
|
68
|
+
if (paletteStale) readPalette();
|
|
69
|
+
ctx.fillStyle = rgba(VOID, 1); ctx.fillRect(0, 0, W, H);
|
|
44
70
|
const k = cam.k;
|
|
45
71
|
const visible = (x, y, r) => { const [sx, sy] = toScreen(x, y); const sr = r * k; return sx + sr > -40 && sx - sr < W + 40 && sy + sr > -40 && sy - sr < H + 40; };
|
|
46
72
|
|
|
@@ -48,9 +74,9 @@
|
|
|
48
74
|
const [cx0, cy0] = toScreen(0, 0);
|
|
49
75
|
for (let bi = lens === 'simple' ? -1 : BANDS.length - 1; bi >= 0; bi--) {
|
|
50
76
|
const b = BANDS[bi]; const hot = hover && hover.kind === 'band' && hover.ref === b;
|
|
51
|
-
ctx.fillStyle =
|
|
77
|
+
ctx.fillStyle = ink((hot ? b.tone * 1.6 : b.tone).toFixed(4));
|
|
52
78
|
ctx.beginPath(); ctx.ellipse(cx0, cy0, b.r1 * k * S.ASPECT, b.r1 * k, 0, 0, TAU); if (b.r0) ctx.ellipse(cx0, cy0, b.r0 * k * S.ASPECT, b.r0 * k, 0, 0, TAU, true); ctx.fill('evenodd');
|
|
53
|
-
ctx.strokeStyle = hot ?
|
|
79
|
+
ctx.strokeStyle = hot ? ink(.14) : ink(.05); ctx.lineWidth = 1; ctx.setLineDash([]);
|
|
54
80
|
ctx.beginPath(); ctx.ellipse(cx0, cy0, b.r1 * k * S.ASPECT, b.r1 * k, 0, 0, TAU); ctx.stroke();
|
|
55
81
|
}
|
|
56
82
|
|
|
@@ -59,7 +85,7 @@
|
|
|
59
85
|
for (const e of (lens === 'simple' ? [] : edges)) {
|
|
60
86
|
if (!present(e.a) || !present(e.b)) continue;
|
|
61
87
|
const c = curve(e); const job = charted.get(e.id);
|
|
62
|
-
if (!job) { if (lens === 'all' && k > 0.03 && seen(e.a) && seen(e.b)) { ctx.strokeStyle =
|
|
88
|
+
if (!job) { if (lens === 'all' && k > 0.03 && seen(e.a) && seen(e.b)) { ctx.strokeStyle = ink(.02); ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(c.ax, c.ay); ctx.quadraticCurveTo(c.mx, c.my, c.bx, c.by); ctx.stroke(); } continue; }
|
|
63
89
|
const rev = job.from === e.b; const P = (t) => pointOn(c, rev ? 1 - t : t);
|
|
64
90
|
let p = job.done ? 1 : clamp((now - job.t0) / job.ms, 0, 1);
|
|
65
91
|
if (p >= 1 && !job.done) { job.done = true; reveal(rev ? e.a : e.b); }
|
|
@@ -87,23 +113,23 @@
|
|
|
87
113
|
const has = g.thoughts.some(shown);
|
|
88
114
|
if (g.band !== BANDS[0] && !has) { g._stubs = []; ctx.globalAlpha = 1; continue; }
|
|
89
115
|
ctx.globalAlpha = has ? 1 : 0.55; ctx.lineWidth = 1; ctx.setLineDash([]);
|
|
90
|
-
ctx.strokeStyle = isPicked || isHover ?
|
|
91
|
-
ctx.strokeStyle =
|
|
116
|
+
ctx.strokeStyle = isPicked || isHover ? ink(.22) : ink(.085); ctx.beginPath(); ctx.arc(sx, sy, sr, 0, TAU); ctx.stroke();
|
|
117
|
+
ctx.strokeStyle = ink(.05); ctx.beginPath(); ctx.arc(sx, sy, sr * 0.5, 0, TAU); ctx.stroke();
|
|
92
118
|
if (sr > 26 && (has || g.status === 'open')) labels.push({ x: sx, y: sy - sr - 6, text: short(g.title).toLowerCase(), sub: '', big: false, small: true, hot: isPicked || isHover, alpha: 1 });
|
|
93
119
|
g._stubs = []; ctx.globalAlpha = 1; continue;
|
|
94
120
|
}
|
|
95
121
|
if (!seen(g)) { // a silhouette: something is there, and you have not been
|
|
96
|
-
ctx.strokeStyle =
|
|
122
|
+
ctx.strokeStyle = ink(.5); ctx.lineWidth = 1; ctx.setLineDash([2, 6]); ctx.beginPath(); ctx.arc(sx, sy, Math.max(3, sr), 0, TAU); ctx.stroke(); ctx.setLineDash([]);
|
|
97
123
|
if (isHover) labels.push({ x: sx, y: sy - sr - 8, text: 'unexplored', sub: 'follow a route to reach it', big: false, hot: true, alpha: 1 });
|
|
98
124
|
g._stubs = []; ctx.globalAlpha = 1; continue;
|
|
99
125
|
}
|
|
100
126
|
if (g.status === 'open') {
|
|
101
127
|
// alive: a builder holding a task now, or work landing this week — the one accent a well can wear
|
|
102
128
|
if (g.live > 0) { const br = reduce ? 1 : 0.85 + 0.15 * Math.sin(tt * 0.9 + hash(g.id) * 6); const hg = ctx.createRadialGradient(sx, sy, sr * 0.92, sx, sy, sr * 1.35); hg.addColorStop(0, rgba(ACC, 0.04 + 0.14 * g.live * br)); hg.addColorStop(1, rgba(ACC, 0)); ctx.fillStyle = hg; ctx.beginPath(); ctx.arc(sx, sy, sr * 1.35, 0, TAU); ctx.fill(); }
|
|
103
|
-
const grd = ctx.createRadialGradient(sx, sy, sr * 0.2, sx, sy, sr); grd.addColorStop(0,
|
|
129
|
+
const grd = ctx.createRadialGradient(sx, sy, sr * 0.2, sx, sy, sr); grd.addColorStop(0, ink(.05)); grd.addColorStop(1, ink(.012));
|
|
104
130
|
ctx.fillStyle = grd; ctx.beginPath(); ctx.arc(sx, sy, sr, 0, TAU); ctx.fill();
|
|
105
131
|
ctx.lineWidth = 1; ctx.setLineDash([]);
|
|
106
|
-
ctx.strokeStyle = isGrav || isSpot ? rgba(ACC, .8) : (isPicked || isHover) ?
|
|
132
|
+
ctx.strokeStyle = isGrav || isSpot ? rgba(ACC, .8) : (isPicked || isHover) ? ink(.4) : g.live > 0 ? rgba(ACC, 0.18 + 0.3 * g.live) : ink(.14);
|
|
107
133
|
if (isGrav || isSpot) { ctx.shadowColor = rgba(ACC, .9); ctx.shadowBlur = 22; }
|
|
108
134
|
ctx.beginPath(); ctx.arc(sx, sy, sr, 0, TAU); ctx.stroke(); ctx.shadowBlur = 0;
|
|
109
135
|
if (sr > 22) for (let i = 0; i < n; i++) {
|
|
@@ -111,8 +137,8 @@
|
|
|
111
137
|
const hot = (hoverRing && hoverRing.goal === g && hoverRing.index === i) || (tourSpot && tourSpot.kind === 'ring' && tourSpot.goal === g && tourSpot.index === i);
|
|
112
138
|
ctx.beginPath(); ctx.arc(sx, sy, rr, 0, TAU);
|
|
113
139
|
if (hot) { ctx.setLineDash([]); ctx.strokeStyle = rgba(ACC, .9); ctx.lineWidth = ringWidth(c) + 0.5; ctx.shadowColor = rgba(ACC, .8); ctx.shadowBlur = 14; }
|
|
114
|
-
else if (c.satisfied) { ctx.setLineDash([]); ctx.strokeStyle =
|
|
115
|
-
else { ctx.setLineDash([3, 5]); ctx.strokeStyle =
|
|
140
|
+
else if (c.satisfied) { ctx.setLineDash([]); ctx.strokeStyle = ink(.42); ctx.lineWidth = ringWidth(c); }
|
|
141
|
+
else { ctx.setLineDash([3, 5]); ctx.strokeStyle = ink(.13); ctx.lineWidth = ringWidth(c); }
|
|
116
142
|
ctx.stroke(); ctx.shadowBlur = 0;
|
|
117
143
|
if (c.bornOf && sr > 40) { const a = hash(c.bornOf) * TAU; ctx.setLineDash([]); ctx.strokeStyle = rgba(ACC, .9); ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(sx + Math.cos(a) * (rr - 7), sy + Math.sin(a) * (rr - 7)); ctx.lineTo(sx + Math.cos(a) * (rr + 7), sy + Math.sin(a) * (rr + 7)); ctx.stroke(); }
|
|
118
144
|
}
|
|
@@ -120,30 +146,30 @@
|
|
|
120
146
|
const tasksNow = g.tasks.filter(present);
|
|
121
147
|
if (tasksNow.length && sr > 60) {
|
|
122
148
|
const bodies = smooth(sr, 170, 330);
|
|
123
|
-
if (bodies < 1) { ctx.globalAlpha = al * (1 - bodies); for (let i = -1; i < n; i++) { const rr = (i < 0 ? g.r * 0.99 : ringR(g, i, n)) * k; const cnt = tasksNow.filter((t) => (i < 0 ? t.ci < 0 || t.ci >= n : t.ci === i)).length; if (!cnt) continue; ctx.strokeStyle =
|
|
149
|
+
if (bodies < 1) { ctx.globalAlpha = al * (1 - bodies); for (let i = -1; i < n; i++) { const rr = (i < 0 ? g.r * 0.99 : ringR(g, i, n)) * k; const cnt = tasksNow.filter((t) => (i < 0 ? t.ci < 0 || t.ci >= n : t.ci === i)).length; if (!cnt) continue; ctx.strokeStyle = ink(Math.min(.3, 0.03 + cnt * 0.004)); ctx.lineWidth = Math.min(7, 1.5 + cnt * 0.03); ctx.beginPath(); ctx.arc(sx, sy, rr, 0, TAU); ctx.stroke(); } ctx.lineWidth = 1; ctx.globalAlpha = al; }
|
|
124
150
|
if (bodies > 0) { ctx.globalAlpha = al * bodies; for (const t of tasksNow) { const [ox, oy] = taskAt(t, tt); const [tx, ty] = toScreen(g.x + ox, g.y + oy); t._sx = tx; t._sy = ty; drawTask(t, tx, ty, tt, picked && picked.kind === 'task' && picked.ref === t, hover && hover.kind === 'task' && hover.ref === t); } ctx.globalAlpha = al; }
|
|
125
151
|
}
|
|
126
152
|
const done = tasksNow.filter((t) => ['shipped', 'confirmed', 'completed'].includes(t.status)).length;
|
|
127
153
|
const speaks = sr > 200 || (sr > 34 && g.mass >= 1500) || isPicked || isHover || isGrav || isSpot;
|
|
128
154
|
if (speaks) labels.push({ x: sx, y: sr > 200 ? sy - sr + 44 : sy - sr - 8, text: g.title, sub: `${S.clock < NOW - 3600000 ? done : g.taskDone}/${S.clock < NOW - 3600000 ? tasksNow.length : g.taskTotal} tasks · ${g.credits ? g.credits + ' credits · ' : ''}${g.crit.filter((c) => c.satisfied).length} of ${g.crit.length} rings closed${g.live > 0 ? ' · alive' : ''}`, big: sr > 120, hot: isPicked || isHover || isGrav || isSpot, alpha: al });
|
|
129
155
|
} else if (g.status === 'achieved') {
|
|
130
|
-
const grd = ctx.createRadialGradient(sx, sy, 0, sx, sy, Math.max(4, sr)); grd.addColorStop(0,
|
|
156
|
+
const grd = ctx.createRadialGradient(sx, sy, 0, sx, sy, Math.max(4, sr)); grd.addColorStop(0, ink(.16)); grd.addColorStop(0.5, ink(.04)); grd.addColorStop(1, ink(0));
|
|
131
157
|
ctx.fillStyle = grd; ctx.beginPath(); ctx.arc(sx, sy, Math.max(4, sr), 0, TAU); ctx.fill();
|
|
132
|
-
ctx.lineWidth = 1; ctx.setLineDash([]); ctx.strokeStyle = (isPicked || isHover || isSpot) ?
|
|
158
|
+
ctx.lineWidth = 1; ctx.setLineDash([]); ctx.strokeStyle = (isPicked || isHover || isSpot) ? ink(.6) : ink(.22);
|
|
133
159
|
ctx.beginPath(); ctx.arc(sx, sy, Math.max(3, sr), 0, TAU); ctx.stroke();
|
|
134
|
-
if (sr > 22) for (let i = 0; i < n; i++) { const c = g.crit[i]; const rr = ringR(g, i, n) * k; const hot = hoverRing && hoverRing.goal === g && hoverRing.index === i; ctx.strokeStyle = hot ? rgba(ACC, .9) :
|
|
160
|
+
if (sr > 22) for (let i = 0; i < n; i++) { const c = g.crit[i]; const rr = ringR(g, i, n) * k; const hot = hoverRing && hoverRing.goal === g && hoverRing.index === i; ctx.strokeStyle = hot ? rgba(ACC, .9) : ink(.3); ctx.lineWidth = ringWidth(c); ctx.beginPath(); ctx.arc(sx, sy, rr, 0, TAU); ctx.stroke(); if (c.bornOf && sr > 40) { const a = hash(c.bornOf) * TAU; ctx.strokeStyle = rgba(ACC, .9); ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(sx + Math.cos(a) * (rr - 7), sy + Math.sin(a) * (rr - 7)); ctx.lineTo(sx + Math.cos(a) * (rr + 7), sy + Math.sin(a) * (rr + 7)); ctx.stroke(); } }
|
|
135
161
|
ctx.lineWidth = 1;
|
|
136
162
|
if (g.tasks.length && sr > 170) { const bodies = smooth(sr, 170, 330); ctx.globalAlpha = al * bodies; for (const t of g.tasks.filter(present)) { const [ox, oy] = taskAt(t, tt); const [tx, ty] = toScreen(g.x + ox, g.y + oy); t._sx = tx; t._sy = ty; drawTask(t, tx, ty, tt, false, hover && hover.kind === 'task' && hover.ref === t); } ctx.globalAlpha = al; }
|
|
137
|
-
if (sr > 8) { ctx.strokeStyle =
|
|
138
|
-
const core = Math.max(2.4, Math.min(sr * 0.14, 9)); const cg = ctx.createRadialGradient(sx, sy, 0, sx, sy, core * 3); cg.addColorStop(0,
|
|
139
|
-
ctx.fillStyle = cg; ctx.beginPath(); ctx.arc(sx, sy, core * 3, 0, TAU); ctx.fill(); ctx.fillStyle =
|
|
163
|
+
if (sr > 8) { ctx.strokeStyle = ink(.05); ctx.beginPath(); ctx.arc(sx, sy, sr * 1.35, 0, TAU); ctx.stroke(); ctx.strokeStyle = ink(.03); ctx.beginPath(); ctx.arc(sx, sy, sr * 1.7, 0, TAU); ctx.stroke(); }
|
|
164
|
+
const core = Math.max(2.4, Math.min(sr * 0.14, 9)); const cg = ctx.createRadialGradient(sx, sy, 0, sx, sy, core * 3); cg.addColorStop(0, ink(.5)); cg.addColorStop(1, ink(0));
|
|
165
|
+
ctx.fillStyle = cg; ctx.beginPath(); ctx.arc(sx, sy, core * 3, 0, TAU); ctx.fill(); ctx.fillStyle = ink(.92); ctx.beginPath(); ctx.arc(sx, sy, core, 0, TAU); ctx.fill();
|
|
140
166
|
const speaks = sr > 200 || (sr > 40 && g.mass >= 1500) || isPicked || isHover || isSpot;
|
|
141
167
|
if (speaks) labels.push({ x: sx, y: sr > 200 ? sy - sr + 44 : sy - sr - 8, text: g.title, sub: `settled · ${g.taskDone} tasks${g.credits ? ' · ' + g.credits + ' credits' : ''} · ${g.crit.length} rings closed`, big: sr > 120, hot: isPicked || isHover || isSpot, small: sr < 60, alpha: al });
|
|
142
168
|
} else {
|
|
143
|
-
ctx.fillStyle =
|
|
144
|
-
ctx.strokeStyle = (isPicked || isHover || isSpot) ?
|
|
145
|
-
if (sr > 5) { ctx.strokeStyle =
|
|
146
|
-
if (sr > 30) { const grd = ctx.createRadialGradient(sx, sy, sr * 0.6, sx, sy, sr); grd.addColorStop(0,
|
|
169
|
+
ctx.fillStyle = rgba(VOID, 1); ctx.beginPath(); ctx.arc(sx, sy, Math.max(2, sr), 0, TAU); ctx.fill();
|
|
170
|
+
ctx.strokeStyle = (isPicked || isHover || isSpot) ? ink(.6) : ink(.22); ctx.lineWidth = 1; ctx.setLineDash([]); ctx.beginPath(); ctx.arc(sx, sy, Math.max(2, sr), 0, TAU); ctx.stroke();
|
|
171
|
+
if (sr > 5) { ctx.strokeStyle = ink(.06); ctx.beginPath(); ctx.arc(sx, sy, sr * 1.5, 0, TAU); ctx.stroke(); }
|
|
172
|
+
if (sr > 30) { const grd = ctx.createRadialGradient(sx, sy, sr * 0.6, sx, sy, sr); grd.addColorStop(0, ink(0)); grd.addColorStop(1, ink(.05)); ctx.fillStyle = grd; ctx.beginPath(); ctx.arc(sx, sy, sr, 0, TAU); ctx.fill(); }
|
|
147
173
|
const speaks = sr > 160 || isPicked || isHover || isSpot;
|
|
148
174
|
if (speaks) labels.push({ x: sx, y: sr > 160 ? sy - sr + 40 : sy - sr - 8, text: g.title, sub: `collapsed · ${g.taskDone} of ${g.taskTotal} tasks`, big: false, hot: isPicked || isHover || isSpot, small: sr < 60, alpha: al });
|
|
149
175
|
}
|
|
@@ -155,7 +181,7 @@
|
|
|
155
181
|
const o = e.a === g ? e.b : e.a; if (!present(o)) continue; const a = Math.atan2(o.y - g.y, o.x - g.x);
|
|
156
182
|
const r0 = sr + 5, r1 = sr + 19; const x0 = sx + Math.cos(a) * r0, y0 = sy + Math.sin(a) * r0, x1 = sx + Math.cos(a) * r1, y1 = sy + Math.sin(a) * r1;
|
|
157
183
|
const hot = hover && hover.kind === 'stub' && hover.edge === e && hover.goal === g; const done = charted.has(e.id);
|
|
158
|
-
ctx.strokeStyle = hot ? rgba(CHART, 1) : done ? rgba(CHART, .5) :
|
|
184
|
+
ctx.strokeStyle = hot ? rgba(CHART, 1) : done ? rgba(CHART, .5) : ink(.3); ctx.lineWidth = hot ? 1.6 : 1; ctx.setLineDash([]);
|
|
159
185
|
ctx.beginPath(); ctx.moveTo(x0, y0); ctx.lineTo(x1, y1); ctx.stroke();
|
|
160
186
|
const out = e.a === g; const hx = out ? x1 : x0, hy = out ? y1 : y0, ha = out ? a : a + Math.PI;
|
|
161
187
|
ctx.beginPath(); ctx.moveTo(hx, hy); ctx.lineTo(hx - Math.cos(ha - 0.5) * 5, hy - Math.sin(ha - 0.5) * 5); ctx.moveTo(hx, hy); ctx.lineTo(hx - Math.cos(ha + 0.5) * 5, hy - Math.sin(ha + 0.5) * 5); ctx.stroke();
|
|
@@ -179,7 +205,7 @@
|
|
|
179
205
|
if (S.flying) { const f = S.flying; const p = clamp((now - f.t0) / f.ms, 0, 1); const e = ease(p); const [sx0, sy0] = [W / 2, 150]; const [sx1, sy1] = toScreen(f.idea.x, f.idea.y); const sx = lerp(sx0, sx1, e), sy = lerp(sy0, sy1, e) - Math.sin(p * Math.PI) * 40; drawThought(f.idea, sx, sy, k, tt, false, false); if (p >= 1) { f.idea.landed = true; S.flying = null; } }
|
|
180
206
|
if (picked && picked.kind === 'idea' && picked.ref.lineage.length && picked.ref.x != null) {
|
|
181
207
|
const i = picked.ref; const [sx, sy] = toScreen(i.x, i.y); const base = Math.max(22, diameter(i) * Math.min(1.6, k) + 10);
|
|
182
|
-
i.lineage.forEach((t, n) => { const orb = base + n * 14; const a = hash(t.id) * TAU + tt * (reduce ? 0 : 0.05) * (n % 2 ? -1 : 1); ctx.strokeStyle =
|
|
208
|
+
i.lineage.forEach((t, n) => { const orb = base + n * 14; const a = hash(t.id) * TAU + tt * (reduce ? 0 : 0.05) * (n % 2 ? -1 : 1); ctx.strokeStyle = ink(.08); ctx.lineWidth = 1; ctx.setLineDash([]); ctx.beginPath(); ctx.arc(sx, sy, orb, 0, TAU); ctx.stroke(); const tx = sx + Math.cos(a) * orb, ty = sy + Math.sin(a) * orb; drawTask({ status: t.status }, tx, ty, tt, false, hover && hover.kind === 'ltask' && hover.ref === t); t._sx = tx; t._sy = ty; });
|
|
183
209
|
}
|
|
184
210
|
for (const s of shuttles) shuttle(s.x, s.y, s.ang, 1.1, s.live || (hover && hover.kind === 'shuttle' && hover.ref === s.e));
|
|
185
211
|
S.shuttles = shuttles;
|
|
@@ -196,9 +222,9 @@
|
|
|
196
222
|
let y = l.y; let rect = { x0: l.x - w / 2, x1: l.x + w / 2, y0: y - h, y1: y };
|
|
197
223
|
if (collides(rect)) { y = l.y + h + 6; rect = { x0: l.x - w / 2, x1: l.x + w / 2, y0: y - h, y1: y }; if (collides(rect) && !l.hot) continue; }
|
|
198
224
|
placed.push(rect);
|
|
199
|
-
ctx.fillStyle = l.hot ?
|
|
225
|
+
ctx.fillStyle = l.hot ? ink(.95) : l.small ? ink(.4) : ink(.6);
|
|
200
226
|
ctx.fillText(text, l.x, y - (l.small ? 0 : 14));
|
|
201
|
-
if (!l.small && l.sub) { ctx.font = '400 10px "JetBrains Mono", monospace'; ctx.fillStyle = l.hot ?
|
|
227
|
+
if (!l.small && l.sub) { ctx.font = '400 10px "JetBrains Mono", monospace'; ctx.fillStyle = l.hot ? ink(.6) : ink(.3); ctx.fillText(l.sub.toUpperCase(), l.x, y); }
|
|
202
228
|
}
|
|
203
229
|
}
|
|
204
230
|
|
|
@@ -206,21 +232,21 @@
|
|
|
206
232
|
const d = diameter(i) * clamp(k, 0.55, 1.6); const breathe = reduce ? 1 : 0.9 + 0.1 * Math.sin(tt * 1.3 + hash(i.id) * 6); ctx.setLineDash([]);
|
|
207
233
|
if (i.shaped) { ctx.strokeStyle = rgba(ACC, .7); ctx.lineWidth = 1; ctx.beginPath(); ctx.arc(sx, sy, Math.max(d / 2 + 6, 10), 0, TAU); ctx.stroke(); }
|
|
208
234
|
if (i.st === 'star') { const a = 0.32 + 0.68 * i.lit; const glow = d * (0.8 + 1.6 * i.lit) * breathe; const grd = ctx.createRadialGradient(sx, sy, 0, sx, sy, glow); grd.addColorStop(0, rgba(ACC, 0.55 * i.lit)); grd.addColorStop(1, rgba(ACC, 0)); ctx.fillStyle = grd; ctx.beginPath(); ctx.arc(sx, sy, glow, 0, TAU); ctx.fill(); ctx.fillStyle = rgba(ACC, a); ctx.beginPath(); ctx.arc(sx, sy, d / 2, 0, TAU); ctx.fill(); }
|
|
209
|
-
else if (i.st === 'remnant') { ctx.strokeStyle =
|
|
210
|
-
else if (i.st === 'protostar') { const aw = i.awaiting || (i.state && i.state.awaitingNod); const pulse = aw && !reduce ? 0.5 + 0.5 * Math.sin(tt * 2 + hash(i.id) * 6) : 0; const c = SKY.temp(i); ctx.fillStyle = rgba(c, .06); ctx.beginPath(); ctx.arc(sx, sy, 7 + pulse * 6, 0, TAU); ctx.fill(); ctx.fillStyle = aw ? rgba(ACC, 0.55 + 0.45 * pulse) : rgba(c, i.well ? .6 : .5); ctx.beginPath(); ctx.arc(sx, sy, 3.2, 0, TAU); ctx.fill(); }
|
|
211
|
-
else if (i.st === 'satellite') { ctx.fillStyle =
|
|
212
|
-
else { ctx.fillStyle =
|
|
213
|
-
if (isPicked || isHover) { ctx.strokeStyle = isPicked ? rgba(ACC, .7) :
|
|
235
|
+
else if (i.st === 'remnant') { ctx.strokeStyle = ink(.4); ctx.lineWidth = 1; ctx.beginPath(); ctx.arc(sx, sy, d / 2 * 1.15, 0, TAU); ctx.stroke(); }
|
|
236
|
+
else if (i.st === 'protostar') { const aw = i.awaiting || (i.state && i.state.awaitingNod); const pulse = aw && !reduce ? 0.5 + 0.5 * Math.sin(tt * 2 + hash(i.id) * 6) : 0; const c = tone(SKY.temp(i)); ctx.fillStyle = rgba(c, .06); ctx.beginPath(); ctx.arc(sx, sy, 7 + pulse * 6, 0, TAU); ctx.fill(); ctx.fillStyle = aw ? rgba(ACC, 0.55 + 0.45 * pulse) : rgba(c, i.well ? .6 : .5); ctx.beginPath(); ctx.arc(sx, sy, 3.2, 0, TAU); ctx.fill(); }
|
|
237
|
+
else if (i.st === 'satellite') { ctx.fillStyle = ink(.4); ctx.beginPath(); ctx.arc(sx, sy, 2.6, 0, TAU); ctx.fill(); ctx.strokeStyle = ink(.16); ctx.lineWidth = 1; ctx.beginPath(); ctx.arc(sx, sy, 6.5, 0, TAU); ctx.stroke(); if (i.host && i.host.x != null) { const [hx, hy] = toScreen(i.host.x, i.host.y); ctx.strokeStyle = ink(.1); ctx.beginPath(); ctx.moveTo(sx, sy); ctx.lineTo(hx, hy); ctx.stroke(); } }
|
|
238
|
+
else { ctx.fillStyle = rgba(COLD, 1); ctx.beginPath(); ctx.arc(sx, sy, 3, 0, TAU); ctx.fill(); ctx.strokeStyle = ink(.18); ctx.lineWidth = 1; ctx.beginPath(); ctx.arc(sx, sy, 3, 0, TAU); ctx.stroke(); }
|
|
239
|
+
if (isPicked || isHover) { ctx.strokeStyle = isPicked ? rgba(ACC, .7) : ink(.35); ctx.lineWidth = 1; ctx.beginPath(); ctx.arc(sx, sy, Math.max(d / 2 + 8, 12), 0, TAU); ctx.stroke(); }
|
|
214
240
|
}
|
|
215
241
|
function drawTask(t, sx, sy, tt, isPicked, isHover) {
|
|
216
242
|
ctx.setLineDash([]); ctx.lineWidth = 1; const s = t.status;
|
|
217
243
|
if (s === 'active') { const g = ctx.createRadialGradient(sx, sy, 0, sx, sy, 9); g.addColorStop(0, rgba(ACC, .6)); g.addColorStop(1, rgba(ACC, 0)); ctx.fillStyle = g; ctx.beginPath(); ctx.arc(sx, sy, 9, 0, TAU); ctx.fill(); ctx.fillStyle = rgba(ACC, 1); ctx.beginPath(); ctx.arc(sx, sy, 2.6, 0, TAU); ctx.fill(); }
|
|
218
|
-
else if (s === 'shipped' || s === 'confirmed' || s === 'completed') { ctx.fillStyle =
|
|
219
|
-
else if (s === 'ready') { ctx.strokeStyle =
|
|
220
|
-
else if (s === 'backlog') { ctx.strokeStyle =
|
|
221
|
-
else if (s === 'blocked') { ctx.strokeStyle =
|
|
222
|
-
else { ctx.strokeStyle =
|
|
223
|
-
if (isPicked || isHover) { ctx.strokeStyle = isPicked ? rgba(ACC, .7) :
|
|
244
|
+
else if (s === 'shipped' || s === 'confirmed' || s === 'completed') { ctx.fillStyle = ink(.55); ctx.beginPath(); ctx.arc(sx, sy, 1.8, 0, TAU); ctx.fill(); }
|
|
245
|
+
else if (s === 'ready') { ctx.strokeStyle = ink(.7); ctx.beginPath(); ctx.arc(sx, sy, 2.6, 0, TAU); ctx.stroke(); }
|
|
246
|
+
else if (s === 'backlog') { ctx.strokeStyle = ink(.28); ctx.beginPath(); ctx.arc(sx, sy, 2.4, 0, TAU); ctx.stroke(); }
|
|
247
|
+
else if (s === 'blocked') { ctx.strokeStyle = ink(.5); ctx.beginPath(); ctx.arc(sx, sy, 2.6, 0, TAU); ctx.stroke(); ctx.beginPath(); ctx.moveTo(sx - 4, sy + 4); ctx.lineTo(sx + 4, sy - 4); ctx.stroke(); }
|
|
248
|
+
else { ctx.strokeStyle = ink(.22); ctx.beginPath(); ctx.moveTo(sx - 3, sy); ctx.lineTo(sx + 3, sy); ctx.stroke(); }
|
|
249
|
+
if (isPicked || isHover) { ctx.strokeStyle = isPicked ? rgba(ACC, .7) : ink(.4); ctx.beginPath(); ctx.arc(sx, sy, 9, 0, TAU); ctx.stroke(); }
|
|
224
250
|
}
|
|
225
251
|
|
|
226
252
|
// ---------------------------------------------------------------- hit testing
|
|
@@ -241,5 +267,6 @@
|
|
|
241
267
|
return null;
|
|
242
268
|
}
|
|
243
269
|
|
|
244
|
-
|
|
270
|
+
// INK/VOID/ink/tone/readPalette are exported for tests/hall_sky.mjs; COLD and TEMP stay private.
|
|
271
|
+
Object.assign(SKY, { ctx, ACC, CHART, INK, VOID, rgba, ink, tone, readPalette, goalAlpha, draw, drawThought, drawTask, hit });
|
|
245
272
|
})();
|