Add side panels, task selection, graph animation, and project docs

- Foldable left panel (user profile) and right panel (task details)
- Clicking a task in the list or graph node selects it and shows details
- Both views (task list + graph) always mounted via absolute inset-0 for
  correct canvas dimensions; tabs toggle visibility with opacity
- Graph node selection animation: other nodes repel outward (charge -600),
  then selected node smoothly slides to center (500ms cubic ease-out),
  then charge restores to -120 and graph stabilizes
- Graph re-fits on tab switch and panel resize via ResizeObserver
- Fix UUID string IDs throughout (backend returns UUIDs, not integers)
- Add TaskDetailPanel, UserPanel components
- Add CLAUDE.md project documentation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Alvis
2026-04-08 11:23:06 +00:00
parent 5c7edd4bbc
commit f1d51b8cc8
23998 changed files with 3242708 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
function cloneEvent(event) {
return new event.constructor(event.type, event);
}
export { cloneEvent };

View File

@@ -0,0 +1,12 @@
function findClosest(element, callback) {
let el = element;
do {
if (callback(el)) {
return el;
}
el = el.parentElement;
}while (el && el !== element.ownerDocument.body)
return undefined;
}
export { findClosest };

View File

@@ -0,0 +1,8 @@
function getDocumentFromNode(el) {
return isDocument(el) ? el : el.ownerDocument;
}
function isDocument(node) {
return node.nodeType === 9;
}
export { getDocumentFromNode };

View File

@@ -0,0 +1,23 @@
function getTreeDiff(a, b) {
const treeA = [];
for(let el = a; el; el = el.parentElement){
treeA.push(el);
}
const treeB = [];
for(let el = b; el; el = el.parentElement){
treeB.push(el);
}
let i = 0;
for(;; i++){
if (i >= treeA.length || i >= treeB.length || treeA[treeA.length - 1 - i] !== treeB[treeB.length - 1 - i]) {
break;
}
}
return [
treeA.slice(0, treeA.length - i),
treeB.slice(0, treeB.length - i),
treeB.slice(treeB.length - i)
];
}
export { getTreeDiff };

View File

@@ -0,0 +1,17 @@
function getWindow(node) {
var _node_ownerDocument;
if (isDocument(node) && node.defaultView) {
return node.defaultView;
} else if ((_node_ownerDocument = node.ownerDocument) === null || _node_ownerDocument === undefined ? undefined : _node_ownerDocument.defaultView) {
return node.ownerDocument.defaultView;
}
throw new Error(`Could not determine window of node. Node was ${describe(node)}`);
}
function isDocument(node) {
return node.nodeType === 9;
}
function describe(val) {
return typeof val === 'function' ? `function ${val.name}` : val === null ? 'null' : String(val);
}
export { getWindow };

View File

@@ -0,0 +1,12 @@
function isDescendantOrSelf(potentialDescendant, potentialAncestor) {
let el = potentialDescendant;
do {
if (el === potentialAncestor) {
return true;
}
el = el.parentElement;
}while (el)
return false;
}
export { isDescendantOrSelf };

View File

@@ -0,0 +1,31 @@
import { isElementType } from './isElementType.js';
// This should probably just rely on the :disabled pseudo-class, but JSDOM doesn't implement it properly.
function isDisabled(element) {
for(let el = element; el; el = el.parentElement){
if (isElementType(el, [
'button',
'input',
'select',
'textarea',
'optgroup',
'option'
])) {
if (el.hasAttribute('disabled')) {
return true;
}
} else if (isElementType(el, 'fieldset')) {
var _el_querySelector;
if (el.hasAttribute('disabled') && !((_el_querySelector = el.querySelector(':scope > legend')) === null || _el_querySelector === undefined ? undefined : _el_querySelector.contains(element))) {
return true;
}
} else if (el.tagName.includes('-')) {
if (el.constructor.formAssociated && el.hasAttribute('disabled')) {
return true;
}
}
}
return false;
}
export { isDisabled };

View File

@@ -0,0 +1,18 @@
function isElementType(element, tag, props) {
if (element.namespaceURI && element.namespaceURI !== 'http://www.w3.org/1999/xhtml') {
return false;
}
tag = Array.isArray(tag) ? tag : [
tag
];
// tagName is uppercase in HTMLDocument and lowercase in XMLDocument
if (!tag.includes(element.tagName.toLowerCase())) {
return false;
}
if (props) {
return Object.entries(props).every(([k, v])=>element[k] === v);
}
return true;
}
export { isElementType };

View File

@@ -0,0 +1,17 @@
import { getWindow } from './getWindow.js';
function isVisible(element) {
const window = getWindow(element);
for(let el = element; el === null || el === undefined ? undefined : el.ownerDocument; el = el.parentElement){
const { display, visibility } = window.getComputedStyle(el);
if (display === 'none') {
return false;
}
if (visibility === 'hidden') {
return false;
}
}
return true;
}
export { isVisible };

View File

@@ -0,0 +1,13 @@
var ApiLevel = /*#__PURE__*/ function(ApiLevel) {
ApiLevel[ApiLevel["Trigger"] = 2] = "Trigger";
ApiLevel[ApiLevel["Call"] = 1] = "Call";
return ApiLevel;
}({});
function setLevelRef(instance, level) {
instance.levelRefs[level] = {};
}
function getLevelRef(instance, level) {
return instance.levelRefs[level];
}
export { ApiLevel, getLevelRef, setLevelRef };

View File

@@ -0,0 +1,12 @@
function wait(config) {
const delay = config.delay;
if (typeof delay !== 'number') {
return;
}
return Promise.all([
new Promise((resolve)=>globalThis.setTimeout(()=>resolve(), delay)),
config.advanceTimers(delay)
]);
}
export { wait };