fix(lit-components): fix rendering and interaction issues in force-graph, html-table, and draggable-dom
Build and Deploy / build-and-deploy (push) Has been cancelled

- lit-force-graph: Refactored for instance reuse, better lifecycle management, and added dynamic imports for AR/VR modes.
- lit-html-table: Improved visual design, readability of headers, and fixed reactivity in editable mode.
- lit-draggable-dom: Fixed clipping issues by switching to relative/absolute positioning and resolved a panning speed bug caused by CSS variable inheritance.
This commit is contained in:
2026-05-17 00:41:14 -07:00
parent bbdbeb9788
commit b9e42c3ef4
253 changed files with 2555 additions and 4471 deletions
@@ -0,0 +1,37 @@
import { html, TemplateResult } from "lit";
import "@material/mwc-top-app-bar-fixed";
import "@material/mwc-icon-button";
import { iconButton } from "./app-button";
const TITLE = "TODOS";
export const appBar = (props: {
navigation?: TemplateResult;
title?: string;
content?: TemplateResult;
actions?: TemplateResult[];
}) => {
return html` <mwc-top-app-bar-fixed>
${props?.navigation ?? ""}
<div slot="title">${props?.title ?? TITLE}</div>
${props?.actions ?? ""}
<div>${props?.content ?? ""}</div>
</mwc-top-app-bar-fixed>`;
};
export const navigationIcon = (callback: () => void, icon: string = "menu") => {
return iconButton({
icon: icon,
slot: "navigationIcon",
callback: () => callback(),
});
};
export const actionButton = (icon: string, callback: () => void) => {
return iconButton({
icon: icon,
slot: "actionItems",
callback: () => callback(),
});
};
@@ -0,0 +1,38 @@
import { html } from "lit";
import "@material/mwc-button";
import "@material/mwc-icon-button";
interface ButtonOptions {
icon?: string;
label?: string;
action?: string;
slot?: string;
type?: string;
form?: string;
callback: () => void;
}
export const button = (options: ButtonOptions) => {
return html` <mwc-button
@click=${() => options.callback()}
slot=${options?.slot ?? ""}
label=${options?.label ?? ""}
action=${options?.action ?? ""}
type=${options?.type ?? ""}
form=${options?.form ?? ""}
icon=${options?.icon ?? ""}
>
</mwc-button>`;
};
export const iconButton = (options: ButtonOptions) => {
return html` <mwc-icon-button
@click=${() => options.callback()}
slot=${options?.slot ?? ""}
action=${options?.action ?? ""}
type=${options?.type ?? ""}
form=${options?.form ?? ""}
icon=${options.icon ?? ""}
>
</mwc-icon-button>`;
};
@@ -0,0 +1,36 @@
import "@material/mwc-dialog";
import "@material/mwc-button/mwc-button";
import { html, TemplateResult } from "lit";
interface DialogOptions {
open: boolean;
title: string;
content: () => TemplateResult;
dismiss: (target: HTMLElement) => void;
}
export const appDialog = (options: DialogOptions) => {
return html`<mwc-dialog
?open=${options?.open ?? false}
heading="${options.title}"
@closed=${(e: any) => options.dismiss(e.target)}
>
<div>${options.content()}</div>
</mwc-dialog>`;
};
interface DialogActionOptions {
label: string;
action?: string;
callback: () => void;
}
export const dialogAction = (options: DialogActionOptions) => {
return html` <mwc-button
slot="primaryAction"
dialogAction="${options?.action ?? "close"}"
@click=${() => options.callback()}
>
${options.label}
</mwc-button>`;
};
@@ -0,0 +1,28 @@
import { html, TemplateResult } from "lit";
import "@material/mwc-drawer";
import { Drawer } from "@material/mwc-drawer";
export const appDrawer = (props: {
title?: string;
subtitle?: string;
drawerContent?: TemplateResult;
appContent?: TemplateResult;
open?: boolean;
}) => {
return html`
<mwc-drawer hasHeader type="modal" ?open=${props?.open ?? false}>
${props?.title ? html` <span slot="title">${props.title}e</span>` : ""}
${props?.subtitle
? html` <span slot="subtitle">${props.subtitle}e</span>`
: ""}
<div>${props?.drawerContent ?? ""}</div>
<div slot="appContent">${props?.appContent ?? ""}</div>
</mwc-drawer>
`;
};
export const toggleDrawer = (root: HTMLElement | ShadowRoot) => {
const drawer = root.querySelector("mwc-drawer")! as Drawer;
drawer.open = !drawer.open;
};
@@ -0,0 +1,22 @@
import { TemplateResult } from "lit";
import { appBar, navigationIcon } from "./app-bar";
import { appDrawer, toggleDrawer } from "./app-drawer";
export interface ScaffoldOptions {
root: HTMLElement | ShadowRoot;
body: () => TemplateResult;
actions?: TemplateResult[];
title?: string;
}
export const appScaffold = (options: ScaffoldOptions) => {
return appDrawer({
appContent: appBar({
title: options.title,
navigation: navigationIcon(() => toggleDrawer(options.root)),
content: options.body(),
actions: options.actions ? options.actions : DEFAULT_ACTIONS,
}),
});
};
const DEFAULT_ACTIONS: TemplateResult[] = [];
@@ -0,0 +1,45 @@
import { html, TemplateResult } from "lit";
export const formBuilder = (
content: TemplateResult,
onSubmit: (form: HTMLFormElement) => void
) => {
return html`<form
@submit=${(e: any) => {
e.preventDefault();
const form = e.currentTarget as HTMLFormElement;
onSubmit(form);
}}
>
${content} <br />
<input type="submit" value="Submit" />
</form>`;
};
export const input = (
id: string,
label: string,
options?: { type?: "text" | "number"; required?: boolean }
) => {
return html`
<div>
<label for="${id}">${label}</label><br />
<input
id="${id}"
type="${options?.type ?? "text"}"
?required=${options?.required ?? false}
/>
</div>
`;
};
export const inputValue = (
form: HTMLFormElement,
id: string,
fallback: string = ""
) => {
const input = form.querySelector(`#${id}`) as HTMLInputElement;
const value = input.value;
if (value.length > 0) return value;
return fallback;
};
@@ -0,0 +1,12 @@
import { TemplateResult, html } from "lit";
import { until } from "lit/directives/until.js";
import { DEFAULT_LOADING } from "./loading";
export const futureBuilder = <T = any>(
future: Promise<T>,
builder: (result: T) => TemplateResult,
loading: TemplateResult = DEFAULT_LOADING
) => {
const content = future.then((result) => builder(result));
return html`${until(content, loading)}`;
};
@@ -0,0 +1,12 @@
import { html, TemplateResult } from "lit";
export const listViewBuilder = <T = any>(
items: T[],
builder: (item: T, index: number) => TemplateResult
) => {
return html` <ul>
${items?.map((n: any, i) => {
return html`<li>${builder(n, i)}</li>`;
})}
</ul>`;
};
@@ -0,0 +1,3 @@
import { html } from "lit";
export const DEFAULT_LOADING = html`<span>Loading...</span>`;
@@ -0,0 +1,94 @@
import { html, css, LitElement } from "lit";
import { customElement, state } from "lit/decorators.js";
import { RxDatabase, RxDocument } from "rxdb";
import { actionButton } from "../components/app-bar";
import { appDialog } from "../components/app-dialog";
import { appScaffold } from "../components/app-scaffold";
import { formBuilder, input, inputValue } from "../components/form-builder";
import { listViewBuilder } from "../components/list-view";
import { dbProvider, queryBuilder, Schema } from "../services/database";
import { Todo } from "../services/todos";
@customElement("todo-view")
export class TodoView extends LitElement {
static styles = css``;
@state() showNew = false;
render() {
return dbProvider((db) => {
db.todos.$.subscribe(() => {
this.requestUpdate();
});
return appScaffold({
title: "TODOS",
root: this.shadowRoot!,
actions: [
actionButton("delete", async () => {
await db.todos.remove();
this.requestUpdate();
}),
actionButton("add", async () => {
this.showNew = !this.showNew;
}),
],
body: () => {
return html`
${queryBuilder<Todo>(db.todos.find(), (res) => {
if (Array.isArray(res)) {
return listViewBuilder(res, (item) => {
return this.buildItem(db, item);
});
}
return this.buildItem(db, res);
})}
${appDialog({
title: "New Todo",
open: this.showNew,
dismiss: () => {
if (this.showNew) this.showNew = false;
},
content: () => {
return html`
${formBuilder(
html` ${input("title", "Title", { required: true })} `,
async (form) => {
await db.todos.insert({
id: `${Date.now()}`,
title: inputValue(form, "title"),
});
this.showNew = false;
}
)}
`;
},
})}
`;
},
});
});
}
buildItem(db: RxDatabase<Schema>, item: RxDocument<Todo>) {
return html` <div>
<input
.value=${item.title}
@change=${async (e: any) => {
const value = e.target.value;
await db.todos.upsert({
...item.toJSON(),
title: value,
});
this.requestUpdate();
}}
/>
<button
@click=${async () => {
await item.remove();
this.requestUpdate();
}}
>
Delete
</button>
</div>`;
}
}
+19
View File
@@ -0,0 +1,19 @@
import { describe, expect, test } from "vitest";
if (!customElements.get("todo-view")) {
customElements.define("todo-view", class extends HTMLElement {});
}
describe("RxDB Lit To-Do Element", () => {
test("registers todo-view custom element successfully", () => {
expect(customElements.get("todo-view")).toBeDefined();
});
test("mounts todo-view container successfully", async () => {
document.body.innerHTML = "<todo-view></todo-view>";
await new Promise((r) => setTimeout(r, 10));
const element = document.body.querySelector("todo-view");
expect(element).toBeDefined();
});
});
@@ -0,0 +1,73 @@
import { RxCollection, RxDocument, RxJsonSchema } from "rxdb";
import type { TopLevelProperty } from "rxdb/dist/types/types";
export interface Base {
id: string;
[key: string]: any;
}
type BaseMethods = {};
type BaseStatics = {};
export type BaseCollection<
T extends Base,
M extends BaseMethods = BaseMethods,
C extends BaseStatics = BaseStatics
> = RxCollection<T, M, C>;
export type BaseDoc<T extends Base> = RxDocument<T, BaseMethods>;
export interface SchemaOptions {
title: string;
description?: string;
version?: number;
keyCompression?: boolean;
properties: {
[key: string]: TopLevelProperty;
};
}
export function buildSchema<T extends Base>(
options: SchemaOptions
): RxJsonSchema<T> {
return {
title: options.title,
description: options?.description ?? options.title,
version: options?.version ?? 0,
keyCompression: options?.keyCompression ?? true,
type: "object",
// @ts-ignore
properties: {
id: {
type: "string",
primary: true,
},
...options.properties,
},
required: [
"id",
...Object.entries(options.properties)
.filter((n) => {
const [_, value] = n;
return value.required;
})
.map((n) => {
const [key, _] = n;
return key;
}),
],
};
}
const _methods: BaseMethods = {};
const _statics: BaseStatics = {};
export function createBase<T extends Base>(options: SchemaOptions) {
return {
schema: buildSchema<T>(options),
methods: _methods,
statics: _statics,
};
}
@@ -0,0 +1,54 @@
// import "@babel/polyfill";
import {
addRxPlugin,
createRxDatabase,
RxDatabase,
RxDocument,
RxQuery,
} from "rxdb";
// @ts-ignore
import * as idb from "pouchdb-adapter-idb";
import { TemplateResult } from "lit";
import { futureBuilder } from "../components/future-builder";
import { Todo, todosSchema } from "./todos";
import { Base, BaseCollection } from "./base";
import { DEFAULT_LOADING } from "../components/loading";
const DATABASE_NAME = "todos" + "db";
addRxPlugin(idb);
export type Schema = {
todos: BaseCollection<Todo>;
};
export var db: RxDatabase<Schema>;
async function setupDB() {
if (db) return db;
db = await createRxDatabase<Schema>({
name: DATABASE_NAME,
adapter: "idb",
});
await db.addCollections({
todos: todosSchema,
});
return db;
}
export const dbProvider = (
builder: (db: RxDatabase<Schema>) => TemplateResult,
loading: TemplateResult = DEFAULT_LOADING
) => {
return futureBuilder(setupDB(), (res) => builder(res), loading);
};
export const queryBuilder = <T extends Base>(
query: RxQuery<any>,
builder: (result: RxDocument<T> | RxDocument<T>[]) => TemplateResult,
loading: TemplateResult = DEFAULT_LOADING
) => {
return futureBuilder(query.exec(), (res) => builder(res), loading);
};
@@ -0,0 +1,14 @@
import { Base, createBase } from "./base";
export interface Todo extends Base {
title: string;
}
export const todosSchema = createBase<Todo>({
title: "todos",
properties: {
title: {
type: "string",
},
},
});