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
-12
View File
@@ -1,12 +0,0 @@
{
"compilerOptions": {
"target": "es6",
"lib": ["es2017"],
"strict": true,
"moduleResolution": "node",
"typeRoots": [
"./node_modules/@types",
"./node_modules/@figma"
]
}
}
-6
View File
@@ -1,6 +0,0 @@
{
"extends": "assemblyscript/std/assembly.json",
"include": [
"./**/*.ts"
]
}
-51
View File
@@ -1,51 +0,0 @@
{
"compilerOptions": {
"target": "es2017",
"module": "esNext",
"moduleResolution": "node",
"lib": ["es2017", "dom", "dom.iterable"],
"typeRoots": ["./node_modules/@types", "./node_modules/@figma"],
"declaration": true,
"sourceMap": true,
"inlineSources": true,
"noUnusedLocals": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"experimentalDecorators": true,
"skipLibCheck": true,
"strict": true,
"noImplicitAny": false,
"outDir": "./lib",
"baseUrl": "./packages",
"importHelpers": true,
"plugins": [
{
"name": "ts-lit-plugin",
"rules": {
"no-unknown-tag-name": "error",
"no-unclosed-tag": "error",
"no-unknown-property": "error",
"no-unintended-mixed-binding": "error",
"no-invalid-boolean-binding": "error",
"no-expressionless-property-binding": "error",
"no-noncallable-event-binding": "error",
"no-boolean-in-attribute-binding": "error",
"no-complex-attribute-binding": "error",
"no-nullable-attribute-binding": "error",
"no-incompatible-type-binding": "error",
"no-invalid-directive-binding": "error",
"no-incompatible-property-type": "error",
"no-unknown-property-converter": "error",
"no-invalid-attribute-name": "error",
"no-invalid-tag-name": "error",
"no-unknown-attribute": "off",
"no-unknown-event": "off",
"no-unknown-slot": "off",
"no-invalid-css": "off"
}
}
]
},
"include": ["src/**/*.ts"],
"references": []
}
+6 -699
View File
@@ -1,705 +1,12 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html>
<head> <head>
<meta charset="UTF-8"> <meta http-equiv="refresh" content="0; url=./lit-showcase/index.html" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <script>
<title>Lit Web Components Showcase</title> window.location.replace("./lit-showcase/index.html");
<link rel="preconnect" href="https://fonts.googleapis.com"> </script>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=Plus+Jakarta+Sans:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-gradient: linear-gradient(135deg, #090d16 0%, #111827 100%);
--glass-bg: rgba(17, 24, 39, 0.7);
--glass-border: rgba(255, 255, 255, 0.08);
--text-primary: #f3f4f6;
--text-secondary: #9ca3af;
--accent: linear-gradient(135deg, #6366f1 0%, #a855f7 100%);
--accent-hover: linear-gradient(135deg, #4f46e5 0%, #9333ea 100%);
--shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
--font-main: 'Plus Jakarta Sans', 'Outfit', sans-serif;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: var(--font-main);
background: var(--bg-gradient);
color: var(--text-primary);
min-height: 100vh;
display: flex;
overflow: hidden;
}
/* Sidebar Styles */
.sidebar {
width: 320px;
background: var(--glass-bg);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border-right: 1px solid var(--glass-border);
display: flex;
flex-direction: column;
height: 100vh;
z-index: 10;
}
.sidebar-header {
padding: 24px;
border-bottom: 1px solid var(--glass-border);
}
.sidebar-header h1 {
font-size: 20px;
font-weight: 700;
background: var(--accent);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 8px;
}
.sidebar-header p {
font-size: 12px;
color: var(--text-secondary);
font-weight: 500;
text-transform: uppercase;
letter-spacing: 1px;
}
.search-box {
padding: 16px 24px;
border-bottom: 1px solid var(--glass-border);
}
.search-input {
width: 100%;
padding: 12px 16px;
border-radius: 12px;
border: 1px solid var(--glass-border);
background: rgba(0, 0, 0, 0.2);
color: var(--text-primary);
font-family: var(--font-main);
font-size: 14px;
outline: none;
transition: all 0.3s ease;
}
.search-input:focus {
border-color: #6366f1;
box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.2);
}
.project-list {
flex: 1;
overflow-y: auto;
padding: 16px;
}
/* Custom Scrollbar */
.project-list::-webkit-scrollbar {
width: 6px;
}
.project-list::-webkit-scrollbar-track {
background: transparent;
}
.project-list::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
border-radius: 3px;
}
.project-item {
display: flex;
align-items: center;
padding: 12px 16px;
border-radius: 12px;
margin-bottom: 8px;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border: 1px solid transparent;
}
.project-item:hover {
background: rgba(255, 255, 255, 0.03);
border-color: rgba(255, 255, 255, 0.05);
transform: translateX(4px);
}
.project-item.active {
background: var(--accent);
box-shadow: 0 4px 20px rgba(99, 102, 241, 0.3);
transform: translateX(6px);
}
.project-emoji {
font-size: 22px;
margin-right: 16px;
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
background: rgba(255, 255, 255, 0.05);
border-radius: 10px;
transition: all 0.3s ease;
}
.project-item.active .project-emoji {
background: rgba(255, 255, 255, 0.2);
}
.project-info {
flex: 1;
}
.project-title {
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 2px;
}
.project-meta {
font-size: 11px;
color: var(--text-secondary);
}
.project-item.active .project-title {
color: #ffffff;
}
.project-item.active .project-meta {
color: rgba(255, 255, 255, 0.8);
}
/* Main Area Styles */
.main-content {
flex: 1;
height: 100vh;
display: flex;
flex-direction: column;
position: relative;
}
/* Empty/Hero State */
.hero-state {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px;
text-align: center;
animation: fadeIn 0.6s ease;
}
.hero-card {
background: var(--glass-bg);
backdrop-filter: blur(24px);
border: 1px solid var(--glass-border);
border-radius: 24px;
padding: 48px;
max-width: 600px;
box-shadow: var(--shadow);
position: relative;
overflow: hidden;
}
.hero-card::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(99,102,241,0.15) 0%, transparent 60%);
pointer-events: none;
}
.hero-icon {
font-size: 64px;
margin-bottom: 24px;
display: inline-block;
animation: float 4s ease-in-out infinite;
}
.hero-title {
font-size: 32px;
font-weight: 800;
margin-bottom: 16px;
background: var(--accent);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.hero-desc {
font-size: 16px;
color: var(--text-secondary);
line-height: 1.6;
margin-bottom: 32px;
}
.hero-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
.stat-item {
background: rgba(255, 255, 255, 0.02);
border: 1px solid var(--glass-border);
border-radius: 16px;
padding: 16px;
}
.stat-val {
font-size: 24px;
font-weight: 700;
color: #6366f1;
margin-bottom: 4px;
}
.stat-lbl {
font-size: 11px;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* Iframe Viewport Styles */
.viewport {
display: none;
flex-direction: column;
flex: 1;
height: 100vh;
animation: fadeIn 0.4s ease;
}
.viewport-header {
background: rgba(17, 24, 39, 0.9);
backdrop-filter: blur(8px);
border-bottom: 1px solid var(--glass-border);
padding: 16px 24px;
display: flex;
align-items: center;
justify-content: space-between;
}
.viewport-title-area {
display: flex;
align-items: center;
}
.back-btn {
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--glass-border);
color: var(--text-primary);
width: 36px;
height: 36px;
border-radius: 10px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
margin-right: 16px;
transition: all 0.2s ease;
}
.back-btn:hover {
background: rgba(255, 255, 255, 0.1);
transform: scale(1.05);
}
.viewport-title {
font-size: 18px;
font-weight: 700;
}
.viewport-controls {
display: flex;
align-items: center;
gap: 12px;
}
.size-btn {
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--glass-border);
color: var(--text-secondary);
padding: 8px 12px;
border-radius: 8px;
cursor: pointer;
font-size: 12px;
font-weight: 600;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 6px;
}
.size-btn:hover, .size-btn.active {
background: rgba(255, 255, 255, 0.12);
color: var(--text-primary);
border-color: rgba(255, 255, 255, 0.2);
}
.external-link-btn {
background: var(--accent);
border: none;
color: #ffffff;
padding: 8px 16px;
border-radius: 8px;
cursor: pointer;
font-size: 12px;
font-weight: 600;
text-decoration: none;
display: flex;
align-items: center;
gap: 6px;
transition: all 0.2s ease;
}
.external-link-btn:hover {
background: var(--accent-hover);
box-shadow: 0 4px 12px rgba(168, 85, 247, 0.25);
}
.iframe-wrapper {
flex: 1;
background: #000000;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
overflow: hidden;
position: relative;
}
iframe {
width: 100%;
height: 100%;
border: none;
border-radius: 12px;
background: #ffffff;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Responsiveness viewports */
.iframe-wrapper.desktop iframe {
width: 100%;
height: 100%;
}
.iframe-wrapper.tablet iframe {
width: 768px;
height: 1024px;
max-height: 100%;
}
.iframe-wrapper.mobile iframe {
width: 375px;
height: 667px;
max-height: 100%;
}
/* Keyframes */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-10px); }
}
</style>
</head> </head>
<body> <body>
Redirecting to <a href="./lit-showcase/index.html">Lit Web Components Showcase</a>...
<!-- Sidebar -->
<div class="sidebar">
<div class="sidebar-header">
<h1>Lit Showcase</h1>
<p>Multi-Page App Playground</p>
</div>
<div class="search-box">
<input type="text" class="search-input" placeholder="Search examples..." id="search">
</div>
<div class="project-list" id="projectList">
<div class="project-item" data-path="./lit-3d-piano/index.html" data-name="Lit 3D Piano">
<span class="project-emoji">🎹</span>
<div class="project-info">
<div class="project-title">Lit 3D Piano</div>
<div class="project-meta">WebGL Instrument</div>
</div>
</div>
<div class="project-item" data-path="./lit-calculator/docs/index.html" data-name="Lit Calculator">
<span class="project-emoji">🧮</span>
<div class="project-info">
<div class="project-title">Lit Calculator</div>
<div class="project-meta">Mathematical Engine</div>
</div>
</div>
<div class="project-item" data-path="./lit-code-editor/index.html" data-name="Lit Code Editor">
<span class="project-emoji">💻</span>
<div class="project-info">
<div class="project-title">Lit Code Editor</div>
<div class="project-meta">Monaco Editor Integrator</div>
</div>
</div>
<div class="project-item" data-path="./lit-draggable-dom/index.html" data-name="Lit Draggable Dom">
<span class="project-emoji">🖐️</span>
<div class="project-info">
<div class="project-title">Lit Draggable Dom</div>
<div class="project-meta">Layout Manager</div>
</div>
</div>
<div class="project-item" data-path="./lit-file-based-routing/index.html" data-name="Lit File Based Routing">
<span class="project-emoji">🚦</span>
<div class="project-info">
<div class="project-title">Lit File Based Routing</div>
<div class="project-meta">Application Router</div>
</div>
</div>
<div class="project-item" data-path="./lit-force-graph/index.html" data-name="Lit Force Graph">
<span class="project-emoji">🕸️</span>
<div class="project-info">
<div class="project-title">Lit Force Graph</div>
<div class="project-meta">3D Networking Engine</div>
</div>
</div>
<div class="project-item" data-path="./lit-html-editor/index.html" data-name="Lit HTML Editor">
<span class="project-emoji">✍️</span>
<div class="project-info">
<div class="project-title">Lit HTML Editor</div>
<div class="project-meta">Real-time Markdown & code</div>
</div>
</div>
<div class="project-item" data-path="./lit-html-table/index.html" data-name="Lit HTML Table">
<span class="project-emoji">📊</span>
<div class="project-info">
<div class="project-title">Lit HTML Table</div>
<div class="project-meta">Spreadsheet Grid View</div>
</div>
</div>
<div class="project-item" data-path="./lit-modules/index.html" data-name="Lit Modules">
<span class="project-emoji">📦</span>
<div class="project-info">
<div class="project-title">Lit Modules</div>
<div class="project-meta">NPM Packaging Harness</div>
</div>
</div>
<div class="project-item" data-path="./lit-native/index.html" data-name="Lit Native">
<span class="project-emoji">📱</span>
<div class="project-info">
<div class="project-title">Lit Native</div>
<div class="project-meta">Mobile UI Mockups</div>
</div>
</div>
<div class="project-item" data-path="./lit-node-editor/index.html" data-name="Lit Node Editor">
<span class="project-emoji">🪢</span>
<div class="project-info">
<div class="project-title">Lit Node Editor</div>
<div class="project-meta">Visual Shader graph</div>
</div>
</div>
<div class="project-item" data-path="./lit-sheet-music/index.html" data-name="Lit Sheet Music">
<span class="project-emoji">🎼</span>
<div class="project-info">
<div class="project-title">Lit Sheet Music</div>
<div class="project-meta">OSMD Renderer</div>
</div>
</div>
<div class="project-item" data-path="./lit-starter-ts" data-name="Lit Starter TS">
<span class="project-emoji">🚀</span>
<div class="project-info">
<div class="project-title">Lit Starter TS</div>
<div class="project-meta">TypeScript Project Boilerplate</div>
</div>
</div>
<div class="project-item" data-path="./lit-vscode-extension/index.html" data-name="Lit VSCode Extension">
<span class="project-emoji">🔌</span>
<div class="project-info">
<div class="project-title">Lit VSCode Extension</div>
<div class="project-meta">Webview Core</div>
</div>
</div>
<div class="project-item" data-path="./lit-wmr/public/index.html" data-name="Lit WMR">
<span class="project-emoji"></span>
<div class="project-info">
<div class="project-title">Lit WMR</div>
<div class="project-meta">WMR Bundled Prototype</div>
</div>
</div>
<div class="project-item" data-path="./vite-lit-capacitor/index.html" data-name="Vite Lit Capacitor">
<span class="project-emoji">🔋</span>
<div class="project-info">
<div class="project-title">Vite Lit Capacitor</div>
<div class="project-meta">Hybrid Mobile Container</div>
</div>
</div>
<div class="project-item" data-path="./vite-lit-element-starter/index.html" data-name="Vite Lit Element Starter">
<span class="project-emoji">🌱</span>
<div class="project-info">
<div class="project-title">Vite Lit Element Starter</div>
<div class="project-meta">Vite Bundled Boilerplate</div>
</div>
</div>
<div class="project-item" data-path="./vite-rxdb-lit/index.html" data-name="Vite RxDB Lit">
<span class="project-emoji">💾</span>
<div class="project-info">
<div class="project-title">Vite RxDB Lit</div>
<div class="project-meta">Offline Sync Database</div>
</div>
</div>
<div class="project-item" data-path="./figma-to-lit/ui.html" data-name="Figma To Lit">
<span class="project-emoji">🎨</span>
<div class="project-info">
<div class="project-title">Figma To Lit</div>
<div class="project-meta">Figma Exporter Plugin</div>
</div>
</div>
<div class="project-item" data-path="./figma_lit_example/ui.html" data-name="Figma Lit Example">
<span class="project-emoji">📐</span>
<div class="project-info">
<div class="project-title">Figma Lit Example</div>
<div class="project-meta">Assembly Figma Plugin</div>
</div>
</div>
</div>
</div>
<!-- Main Showcase Content -->
<div class="main-content">
<!-- Hero State -->
<div class="hero-state" id="heroState">
<div class="hero-card">
<span class="hero-icon"></span>
<h2 class="hero-title">Lit Showcase Hub</h2>
<p class="hero-desc">Explore a rich collection of modular Lit Web Components, prototypes, interactive instruments, databases, and custom UI modules. Select any example from the sidebar to preview in real-time with responsive viewport sandboxing.</p>
<div class="hero-stats">
<div class="stat-item">
<div class="stat-val">20</div>
<div class="stat-lbl">Projects</div>
</div>
<div class="stat-item">
<div class="stat-val">100%</div>
<div class="stat-lbl">Vite MPA</div>
</div>
<div class="stat-item">
<div class="stat-val">Nginx</div>
<div class="stat-lbl">Deploy</div>
</div>
</div>
</div>
</div>
<!-- Iframe Viewport -->
<div class="viewport" id="viewport">
<div class="viewport-header">
<div class="viewport-title-area">
<button class="back-btn" id="backBtn"></button>
<span class="viewport-title" id="viewportTitle">Project Name</span>
</div>
<div class="viewport-controls">
<button class="size-btn active" data-size="desktop">💻 Desktop</button>
<button class="size-btn" data-size="tablet">📟 Tablet</button>
<button class="size-btn" data-size="mobile">📱 Mobile</button>
<a href="#" target="_blank" class="external-link-btn" id="extLink">🌐 Open Tab</a>
</div>
</div>
<div class="iframe-wrapper desktop" id="iframeWrapper">
<iframe id="previewIframe" src="about:blank"></iframe>
</div>
</div>
</div>
<script>
const items = document.querySelectorAll('.project-item');
const heroState = document.getElementById('heroState');
const viewport = document.getElementById('viewport');
const iframe = document.getElementById('previewIframe');
const viewportTitle = document.getElementById('viewportTitle');
const extLink = document.getElementById('extLink');
const backBtn = document.getElementById('backBtn');
const sizeBtns = document.querySelectorAll('.size-btn');
const iframeWrapper = document.getElementById('iframeWrapper');
// Click handler for project showcase selection
items.forEach(item => {
item.addEventListener('click', () => {
items.forEach(i => i.classList.remove('active'));
item.classList.add('active');
const path = item.getAttribute('data-path');
const name = item.getAttribute('data-name');
heroState.style.display = 'none';
viewport.style.display = 'flex';
iframe.src = path;
viewportTitle.textContent = name;
extLink.href = path;
});
});
// Back to main showcase
backBtn.addEventListener('click', () => {
items.forEach(i => i.classList.remove('active'));
iframe.src = 'about:blank';
viewport.style.display = 'none';
heroState.style.display = 'flex';
});
// Viewport resizing controls
sizeBtns.forEach(btn => {
btn.addEventListener('click', () => {
sizeBtns.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
const size = btn.getAttribute('data-size');
iframeWrapper.className = 'iframe-wrapper ' + size;
});
});
// Live search filtering
const searchInput = document.getElementById('search');
searchInput.addEventListener('input', (e) => {
const val = e.target.value.toLowerCase();
items.forEach(item => {
const title = item.getAttribute('data-name').toLowerCase();
if (title.includes(val)) {
item.style.display = 'flex';
} else {
item.style.display = 'none';
}
});
});
</script>
</body> </body>
</html> </html>
-5
View File
@@ -1,5 +0,0 @@
node_modules
.DS_Store
dist
types
*.local
-9
View File
@@ -1,9 +0,0 @@
# 3D Piano
[![Demo](https://github.com/rodydavis/lit-3d-piano/actions/workflows/ci.yml/badge.svg)](https://github.com/rodydavis/lit-3d-piano/actions/workflows/ci.yml)
3D Piano built with Lit, Three.js and Tone.js
Online demo [here](https://rodydavis.github.io/lit-3d-piano/).
![](/screenshot.png)
-31
View File
@@ -1,31 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>3D Piano</title>
<script type="module" src="/src/piano-component.ts"></script>
<style>
body {
width: 100%;
min-height: 100vh;
min-height: -webkit-fill-available;
display: flex;
flex-direction: column;
margin: 0;
padding: 0;
}
html {
height: -webkit-fill-available;
}
piano-component {
flex: 1;
}
</style>
</head>
<body>
<piano-component> </piano-component>
</body>
</html>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

-15
View File
@@ -1,15 +0,0 @@
<svg width="410" height="404" viewBox="0 0 410 404" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M399.641 59.5246L215.643 388.545C211.844 395.338 202.084 395.378 198.228 388.618L10.5817 59.5563C6.38087 52.1896 12.6802 43.2665 21.0281 44.7586L205.223 77.6824C206.398 77.8924 207.601 77.8904 208.776 77.6763L389.119 44.8058C397.439 43.2894 403.768 52.1434 399.641 59.5246Z" fill="url(#paint0_linear)"/>
<path d="M292.965 1.5744L156.801 28.2552C154.563 28.6937 152.906 30.5903 152.771 32.8664L144.395 174.33C144.198 177.662 147.258 180.248 150.51 179.498L188.42 170.749C191.967 169.931 195.172 173.055 194.443 176.622L183.18 231.775C182.422 235.487 185.907 238.661 189.532 237.56L212.947 230.446C216.577 229.344 220.065 232.527 219.297 236.242L201.398 322.875C200.278 328.294 207.486 331.249 210.492 326.603L212.5 323.5L323.454 102.072C325.312 98.3645 322.108 94.137 318.036 94.9228L279.014 102.454C275.347 103.161 272.227 99.746 273.262 96.1583L298.731 7.86689C299.767 4.27314 296.636 0.855181 292.965 1.5744Z" fill="url(#paint1_linear)"/>
<defs>
<linearGradient id="paint0_linear" x1="6.00017" y1="32.9999" x2="235" y2="344" gradientUnits="userSpaceOnUse">
<stop stop-color="#41D1FF"/>
<stop offset="1" stop-color="#BD34FE"/>
</linearGradient>
<linearGradient id="paint1_linear" x1="194.651" y1="8.81818" x2="236.076" y2="292.989" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFEA83"/>
<stop offset="0.0833333" stop-color="#FFDD35"/>
<stop offset="1" stop-color="#FFA800"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

-1
View File
@@ -1 +0,0 @@
/// <reference types="vite/client" />
-27
View File
@@ -1,27 +0,0 @@
{
"compilerOptions": {
"module": "esnext",
"target": "esnext",
"lib": [
"esnext",
"dom"
],
"declaration": true,
"emitDeclarationOnly": true,
"outDir": "./types",
"rootDir": "./src",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"forceConsistentCasingInFileNames": true
},
"include": [
"src/**/*.ts"
],
"exclude": []
}
-1
View File
@@ -1 +0,0 @@
node_modules
-1
View File
@@ -1 +0,0 @@
common --@aspect_rules_ts//ts:skipLibCheck=honor_tsconfig
-2
View File
@@ -1,2 +0,0 @@
# Auto detect text files and perform LF normalization
* text=auto
-55
View File
@@ -1,55 +0,0 @@
# ==========================================
# 🌌 Federated Workspaces - Ultimate Ignores
# ==========================================
# --- System & Editor Files ---
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
.vscode/
.idea/
*.swp
*.swo
# --- Bazel Convenience Symlinks & Outputs ---
bazel-*
bazel_out/
# --- C/C++ Outputs ---
*.o
*.obj
*.a
*.lib
*.so
*.dylib
*.dll
*.out
*.exe
# --- Go Outputs ---
*.test
*.prof
__debug_bin
MODULE.bazel.lock
# --- Node / JavaScript / pnpm ---
node_modules/
.npm/
.pnpm-store/
.pnpm-debug.log*
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# --- Dart / Flutter ---
.dart_tool/
.packages
.pub-cache/
.pub/
/build/
*.flutter-plugins
*.flutter-plugins-dependencies
View File
-24
View File
@@ -1,24 +0,0 @@
load("@aspect_rules_js//npm:defs.bzl", "npm_link_package")
load("@npm//:defs.bzl", "npm_link_all_packages")
load("@aspect_rules_ts//ts:defs.bzl", "ts_config", "ts_project")
npm_link_all_packages(name = "node_modules")
ts_config(
name = "tsconfig",
src = "tsconfig.json",
visibility = ["//visibility:public"],
)
ts_project(
name = "build",
srcs = glob(["docs/**/*.ts"]),
declaration = True,
source_map = True,
transpiler = "tsc",
tsconfig = ":tsconfig",
deps = [
"//:node_modules",
],
visibility = ["//visibility:public"],
)
-4
View File
@@ -1,4 +0,0 @@
FROM nginx:alpine
COPY docs /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
-22
View File
@@ -1,22 +0,0 @@
module(
name = "lit_calculator",
version = "1.0.0",
)
bazel_dep(name = "aspect_rules_js", version = "3.1.1")
bazel_dep(name = "aspect_rules_ts", version = "3.1.0")
bazel_dep(name = "rules_nodejs", version = "6.7.3")
npm = use_extension("@aspect_rules_js//npm:extensions.bzl", "npm")
npm.npm_translate_lock(
name = "npm",
pnpm_lock = "//:pnpm-lock.yaml",
verify_node_modules_ignored = "//:.bazelignore",
)
use_repo(npm, "npm")
rules_ts_ext = use_extension("@aspect_rules_ts//ts:extensions.bzl", "ext", dev_dependency = True)
rules_ts_ext.deps()
use_repo(rules_ts_ext, "npm_typescript")
-5
View File
@@ -1,5 +0,0 @@
# Lit Calculator
Reactive calculator made with LitElement and no build system or framework.
Demo: https://rodydavis.github.io/lit-calculator/
File diff suppressed because one or more lines are too long
-158
View File
@@ -1,158 +0,0 @@
import { LitElement, html, css } from "lit";
import { customElement, property } from "lit/decorators.js";
import "./keypad-input.js";
import "./key-pad.js";
import "./display-output.js";
@customElement("app-calculator")
export class AppCalculator extends LitElement {
@property()
width = "500px";
@property()
primaryColor = "red";
@property()
accentColor = "black";
static styles = css`
#base {
border: 2px solid black;
padding-bottom: 20px;
--base-padding: 10px;
}
#actions {
padding-top: var(--base-padding);
padding-left: var(--base-padding);
}
button {
font-size: 17px;
}
`;
connectedCallback() {
super.connectedCallback();
// @ts-ignore
window.addEventListener("select-number", this._handleNumber);
}
disconnectedCallback() {
// @ts-ignore
window.removeEventListener("select-number", this._handleNumber);
super.disconnectedCallback();
}
_staging: string = "";
_value: string = "";
_mode: string = "";
public get mode(): string {
return this._mode;
}
public set mode(v: string) {
const oldMode = this._mode;
const oldVal = this.value;
this._mode = v;
// @ts-ignore
this.requestUpdate("mode", oldMode);
// @ts-ignore
this.requestUpdate("value", oldVal);
}
private get value(): string {
if (this.mode == "") return this._value;
return this._staging;
}
private set value(v: string) {
var oldVal = "";
if (this.mode == "") {
oldVal = this._value;
this._value = v;
} else {
oldVal = this._staging;
this._staging = v;
}
// @ts-ignore
this.requestUpdate("value", oldVal);
}
_handleNumber = (event: CustomEvent) => {
const num = event.detail.value;
event.stopPropagation();
if (num === "=") {
this.calculate();
return;
}
if (this.value === "0") {
this.value = "";
}
if (num === "." && this.value.includes(".")) {
return;
}
this.value += num;
};
clear = () => {
this.value = "";
this._staging = "";
this._value = "";
this.mode = "";
};
calculate = () => {
const staging = Number(this._staging || "0");
const current = Number(this._value || "0");
var result: number = 0;
// Calculate
switch (this.mode) {
case "+":
result = staging + current;
break;
case "-":
result = staging - current;
break;
case "/":
result = staging / current;
break;
case "*":
result = staging * current;
break;
default:
break;
}
const output = result.toString();
this.value = output;
this._staging = output;
};
// Render element DOM by returning a `lit-html` template.
render() {
return html`<div
id="base"
style="width: ${this.width}; background-color: ${this.primaryColor};"
>
<display-output
value="${this.value}"
color="black"
textColor="white"
></display-output>
<div id="actions">
<button
@click=${this.clear}
?disabled=${this.value === "" && this.mode === ""}
>
Clear
</button>
<button @click="${() => (this.mode = "+")}">+</button>
<button @click="${() => (this.mode = "-")}">-</button>
<button @click="${() => (this.mode = "/")}">/</button>
<button @click="${() => (this.mode = "*")}">*</button>
</div>
<key-pad></key-pad>
</div>`;
}
}
-43
View File
@@ -1,43 +0,0 @@
import { LitElement, html, css } from "lit";
import { customElement, property } from "lit/decorators.js";
@customElement("display-output")
export class CalcDisplay extends LitElement {
@property()
color = "black";
@property()
textColor = "white";
@property()
value = "";
static styles = css`
#base {
background-color: black;
text-align: end;
display: inline-block;
margin: var(--base-padding);
width: calc(100% - calc(var(--base-padding) * 2));
border-bottom: 2px solid black;
}
#text-display {
color: white;
margin: 0px;
padding-top: 40px;
padding-right: 10px;
padding-bottom: 10px;
font-size: 36px;
}
`;
// Render element DOM by returning a `lit-html` template.
render() {
return html`<div id="base" style="background-color: ${this.color};">
<div id="text-display" style="color: ${this.textColor};">
${this.value || "0"}
</div>
</div>`;
}
}
-22
View File
@@ -1,22 +0,0 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "node",
"lib": ["esnext", "dom"],
"declaration": true,
"sourceMap": true,
"inlineSources": false,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noImplicitAny": true,
"noImplicitThis": true,
"skipLibCheck": true,
"experimentalDecorators": true
},
"include": ["docs/**/*"],
"exclude": ["node_modules", "**/*.spec.ts"]
}
-5
View File
@@ -1,5 +0,0 @@
node_modules
.DS_Store
dist
types
*.local
-9
View File
@@ -1,9 +0,0 @@
# Lit Code Editor
[![Demo](https://github.com/rodydavis/lit-code-editor/actions/workflows/ci.yml/badge.svg)](https://github.com/rodydavis/lit-code-editor/actions/workflows/ci.yml)
Simple example of Lit + Vite + Monaco Editor as a web component that can be embedded in pages.
Online demo: https://rodydavis.github.io/lit-code-editor/
Another version to look at: https://github.com/vanillawc/wc-monaco-editor
-27
View File
@@ -1,27 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Lit Code Editor</title>
<script type="module" src="/src/code-editor.ts"></script>
<style>
body {
margin: 0;
padding: 0;
width: 100%;
height: 100vh;
}
</style>
</head>
<body>
<code-editor>
<script type="text/javascript">
function x() {
console.log("Hello world! :)");
}
</script>
</code-editor>
</body>
</html>
-15
View File
@@ -1,15 +0,0 @@
<svg width="410" height="404" viewBox="0 0 410 404" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M399.641 59.5246L215.643 388.545C211.844 395.338 202.084 395.378 198.228 388.618L10.5817 59.5563C6.38087 52.1896 12.6802 43.2665 21.0281 44.7586L205.223 77.6824C206.398 77.8924 207.601 77.8904 208.776 77.6763L389.119 44.8058C397.439 43.2894 403.768 52.1434 399.641 59.5246Z" fill="url(#paint0_linear)"/>
<path d="M292.965 1.5744L156.801 28.2552C154.563 28.6937 152.906 30.5903 152.771 32.8664L144.395 174.33C144.198 177.662 147.258 180.248 150.51 179.498L188.42 170.749C191.967 169.931 195.172 173.055 194.443 176.622L183.18 231.775C182.422 235.487 185.907 238.661 189.532 237.56L212.947 230.446C216.577 229.344 220.065 232.527 219.297 236.242L201.398 322.875C200.278 328.294 207.486 331.249 210.492 326.603L212.5 323.5L323.454 102.072C325.312 98.3645 322.108 94.137 318.036 94.9228L279.014 102.454C275.347 103.161 272.227 99.746 273.262 96.1583L298.731 7.86689C299.767 4.27314 296.636 0.855181 292.965 1.5744Z" fill="url(#paint1_linear)"/>
<defs>
<linearGradient id="paint0_linear" x1="6.00017" y1="32.9999" x2="235" y2="344" gradientUnits="userSpaceOnUse">
<stop stop-color="#41D1FF"/>
<stop offset="1" stop-color="#BD34FE"/>
</linearGradient>
<linearGradient id="paint1_linear" x1="194.651" y1="8.81818" x2="236.076" y2="292.989" gradientUnits="userSpaceOnUse">
<stop stop-color="#FFEA83"/>
<stop offset="0.0833333" stop-color="#FFDD35"/>
<stop offset="1" stop-color="#FFA800"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

-1
View File
@@ -1 +0,0 @@
/// <reference types="vite/client" />
-21
View File
@@ -1,21 +0,0 @@
{
"compilerOptions": {
"module": "esnext",
"lib": ["es2017", "dom", "dom.iterable"],
"declaration": true,
"emitDeclarationOnly": true,
"outDir": "./types",
"rootDir": "./src",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*.ts"],
"exclude": []
}
@@ -14,7 +14,7 @@ export class PianoComponent extends LitElement {
scene = new THREE.Scene(); scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera( camera = new THREE.PerspectiveCamera(
70, 70,
window.innerWidth / window.innerHeight, 1,
0.001, 0.001,
10 10
); );
@@ -34,15 +34,17 @@ export class PianoComponent extends LitElement {
user-select: none; user-select: none;
-webkit-user-select: none; -webkit-user-select: none;
-moz-user-select: none; -moz-user-select: none;
position: relative;
} }
canvas { canvas {
width: 100%; width: 100%;
height: 100%; height: 100%;
z-index: 0; z-index: 0;
display: block;
} }
.controls { .controls {
z-index: 1; z-index: 1;
position: fixed; position: absolute;
color: white; color: white;
font-size: 18px; font-size: 18px;
right: 10px; right: 10px;
@@ -50,7 +52,7 @@ export class PianoComponent extends LitElement {
} }
.note { .note {
z-index: 1; z-index: 1;
position: fixed; position: absolute;
color: white; color: white;
font-size: 18px; font-size: 18px;
left: 10px; left: 10px;
@@ -59,9 +61,12 @@ export class PianoComponent extends LitElement {
`; `;
render() { render() {
const tapNote = (x: number, y: number) => { const tapNote = (clientX: number, clientY: number) => {
const dx = (x / window.innerWidth) * 2 - 1; const rect = this.canvas.getBoundingClientRect();
const dy = -(y / window.innerHeight) * 2 + 1; const localX = clientX - rect.left;
const localY = clientY - rect.top;
const dx = (localX / rect.width) * 2 - 1;
const dy = -(localY / rect.height) * 2 + 1;
this.findNote(new THREE.Vector2(dx, dy)); this.findNote(new THREE.Vector2(dx, dy));
}; };
return html`<main> return html`<main>
@@ -233,7 +238,14 @@ export class PianoComponent extends LitElement {
this.controls = new OrbitControls(this.camera, this.renderer.domElement); this.controls = new OrbitControls(this.camera, this.renderer.domElement);
this.controls.screenSpacePanning = true; this.controls.screenSpacePanning = true;
this.controls.enableKeys = true; this.controls.enableKeys = true;
this.renderer!.setSize(window.innerWidth, window.innerHeight);
// Size relative to current component offset bounds
const width = this.offsetWidth || window.innerWidth;
const height = this.offsetHeight || window.innerHeight;
this.renderer.setSize(width, height);
this.camera.aspect = width / height;
this.camera.updateProjectionMatrix();
this.renderer.setAnimationLoop(() => this.paint()); this.renderer.setAnimationLoop(() => this.paint());
this.renderer.setClearColor("red", 1); this.renderer.setClearColor("red", 1);
@@ -284,7 +296,11 @@ export class PianoComponent extends LitElement {
window.addEventListener( window.addEventListener(
"resize", "resize",
() => { () => {
this.renderer!.setSize(window.innerWidth, window.innerHeight); const w = this.offsetWidth || window.innerWidth;
const h = this.offsetHeight || window.innerHeight;
this.renderer!.setSize(w, h);
this.camera.aspect = w / h;
this.camera.updateProjectionMatrix();
}, },
false false
); );
+17
View File
@@ -0,0 +1,17 @@
import { describe, expect, test } from "vitest";
import "./piano-component";
describe("Lit 3D Piano Component", () => {
test("registers piano-component custom element successfully", () => {
expect(customElements.get("piano-component")).toBeDefined();
});
test("mounts piano-component to document body", async () => {
document.body.innerHTML = "<piano-component></piano-component>";
await window.happyDOM.whenAsyncComplete();
const element = document.body.querySelector("piano-component");
expect(element).toBeDefined();
expect(element?.shadowRoot).toBeDefined();
});
});
@@ -0,0 +1,60 @@
import { describe, expect, test } from "vitest";
import { AppCalculator } from "./calculator";
import "./display-output";
import "./keypad-input";
describe("Lit Calculator Component Logic", () => {
test("registers calculator custom elements successfully", () => {
expect(customElements.get("app-calculator")).toBeDefined();
expect(customElements.get("display-output")).toBeDefined();
expect(customElements.get("keypad-input")).toBeDefined();
});
test("calculates addition correctly", () => {
const calc = new AppCalculator();
calc._value = "12";
calc.mode = "+";
calc._staging = "8";
calc.calculate();
expect(calc._value).toBe("20");
expect(calc.mode).toBe("");
expect(calc._staging).toBe("");
});
test("calculates subtraction correctly", () => {
const calc = new AppCalculator();
calc._value = "15";
calc.mode = "-";
calc._staging = "6";
calc.calculate();
expect(calc._value).toBe("9");
expect(calc.mode).toBe("");
});
test("calculates multiplication correctly", () => {
const calc = new AppCalculator();
calc._value = "4";
calc.mode = "*";
calc._staging = "5";
calc.calculate();
expect(calc._value).toBe("20");
});
test("calculates division correctly", () => {
const calc = new AppCalculator();
calc._value = "20";
calc.mode = "/";
calc._staging = "4";
calc.calculate();
expect(calc._value).toBe("5");
});
test("handles division by zero gracefully", () => {
const calc = new AppCalculator();
calc._value = "10";
calc.mode = "/";
calc._staging = "0";
calc.calculate();
expect(calc._value).toBe("0");
});
});
+242
View File
@@ -0,0 +1,242 @@
import { LitElement, html, css } from "lit";
import { customElement, property } from "lit/decorators.js";
import "./keypad-input.js";
import "./key-pad.js";
import "./display-output.js";
@customElement("app-calculator")
export class AppCalculator extends LitElement {
@property()
width = "500px";
@property()
primaryColor = "#1a1f2c";
@property()
accentColor = "black";
static styles = css`
:host {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
min-height: 500px;
box-sizing: border-box;
}
#base {
border: 3px solid #1a1b1f;
border-radius: 20px;
padding: 24px;
background: #2a2c33;
box-shadow:
0 25px 50px -12px rgba(0, 0, 0, 0.7),
inset 0 1px 0 rgba(255, 255, 255, 0.15);
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 18px;
}
#actions {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 12px;
padding: 0 5px;
}
button {
background: #3e424b;
border: none;
border-bottom: 3px solid #1a1b1f;
color: #fff;
padding: 12px;
font-size: 18px;
font-weight: 700;
border-radius: 8px;
cursor: pointer;
transition: all 0.08s ease;
box-shadow: 0 4px 6px rgba(0,0,0,0.3);
display: flex;
align-items: center;
justify-content: center;
}
button:hover {
background: #4a4f59;
}
button:active {
transform: translateY(2px);
border-bottom-width: 1px;
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
}
button.operator {
background: #e76f51;
border-bottom-color: #9d3f27;
color: #ffffff;
}
button.operator:hover {
background: #f4a261;
}
button.clear-btn {
background: #d62828;
border-bottom-color: #7a1212;
color: #ffffff;
}
button.clear-btn:hover {
background: #e63946;
}
button:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none !important;
border-bottom-width: 3px !important;
box-shadow: 0 4px 6px rgba(0,0,0,0.3) !important;
}
`;
connectedCallback() {
super.connectedCallback();
// @ts-ignore
window.addEventListener("select-number", this._handleNumber);
}
disconnectedCallback() {
// @ts-ignore
window.removeEventListener("select-number", this._handleNumber);
super.disconnectedCallback();
}
_staging: string = "";
_value: string = "";
_mode: string = "";
public get mode(): string {
return this._mode;
}
public set mode(v: string) {
const oldMode = this._mode;
const oldVal = this.value;
this._mode = v;
// @ts-ignore
this.requestUpdate("mode", oldMode);
// @ts-ignore
this.requestUpdate("value", oldVal);
}
public get value(): string {
if (this.mode === "") return this._value;
return this._staging;
}
public set value(v: string) {
var oldVal = "";
if (this.mode === "") {
oldVal = this._value;
this._value = v;
} else {
oldVal = this._staging;
this._staging = v;
}
// @ts-ignore
this.requestUpdate("value", oldVal);
}
public get displayValue(): string {
if (this.mode !== "" && this._staging === "") {
return this._value || "0";
}
return this.value || "0";
}
_handleNumber = (event: CustomEvent) => {
const num = event.detail.value;
event.stopPropagation();
if (num === "=") {
this.calculate();
return;
}
if (this.value === "0") {
this.value = "";
}
if (num === "." && this.value.includes(".")) {
return;
}
this.value += num;
};
clear = () => {
this.value = "";
this._staging = "";
this._value = "";
this.mode = "";
};
calculate = () => {
console.log("DEBUG: calculate entered! mode =", this.mode, "_value =", this._value, "_staging =", this._staging);
if (this.mode === "") return;
const op1 = Number(this._value || "0");
const op2 = Number(this._staging || "0");
let result: number = 0;
switch (this.mode) {
case "+":
result = op1 + op2;
break;
case "-":
result = op1 - op2;
break;
case "/":
result = op2 === 0 ? 0 : op1 / op2;
break;
case "*":
result = op1 * op2;
break;
default:
break;
}
const output = result.toString();
console.log("DEBUG: calculation result =", result, "output =", output);
this._value = output;
this._staging = "";
this.mode = "";
};
render() {
return html`<div
id="base"
style="width: ${this.width};"
>
<display-output
value="${this.displayValue}"
color="#8fa882"
textColor="#172412"
></display-output>
<div id="actions">
<button
class="clear-btn"
@click=${this.clear}
?disabled=${this._value === "" && this._staging === "" && this.mode === ""}
>
C
</button>
<button class="operator" @click="${() => (this.mode = "+")}">+</button>
<button class="operator" @click="${() => (this.mode = "-")}">-</button>
<button class="operator" @click="${() => (this.mode = "/")}">/</button>
<button class="operator" @click="${() => (this.mode = "*")}">*</button>
</div>
<key-pad accentColor="#1b1c20" textColor="#f0eedb" actionColor="#f4a261" actionTextColor="#ffffff"></key-pad>
</div>`;
}
}
@@ -0,0 +1,70 @@
import { LitElement, html, css } from "lit";
import { customElement, property } from "lit/decorators.js";
@customElement("display-output")
export class CalcDisplay extends LitElement {
@property()
color = "black";
@property()
textColor = "white";
@property()
value = "";
static styles = css`
@import url('https://fonts.googleapis.com/css2?family=Share+Tech+Mono&display=swap');
#base {
text-align: end;
display: block;
padding: 16px 20px;
border: 3px solid #1a1b1f;
border-radius: 8px;
box-shadow:
inset 0 4px 10px rgba(0, 0, 0, 0.4),
0 1px 0 rgba(255, 255, 255, 0.08);
position: relative;
overflow: hidden;
box-sizing: border-box;
width: 100%;
}
/* Glossy vintage glass glare */
#base::after {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
height: 50%;
background: linear-gradient(
to bottom,
rgba(255, 255, 255, 0.12),
rgba(255, 255, 255, 0)
);
pointer-events: none;
}
#text-display {
font-family: 'Share Tech Mono', 'Courier New', monospace;
font-weight: 700;
font-size: 40px;
letter-spacing: 2px;
text-shadow: 1px 1px 0 rgba(255, 255, 255, 0.12);
margin: 0;
line-height: 1.1;
word-wrap: break-word;
word-break: break-all;
}
`;
// Render element DOM by returning a `lit-html` template.
render() {
return html`<div id="base" style="background-color: ${this.color};">
<div id="text-display" style="color: ${this.textColor};">
${this.value || "0"}
</div>
</div>`;
}
}
@@ -18,14 +18,13 @@ export class CalcKeyPad extends LitElement {
static styles = css` static styles = css`
#base { #base {
display: grid; display: grid;
grid-template-columns: 33% 33% 33%; grid-template-columns: repeat(3, 1fr);
padding-left: calc(var(--base-padding) / 2); gap: 12px;
padding-right: calc(var(--base-padding) / 2); padding: 0 5px;
} }
keypad-input { keypad-input {
display: inline-grid; display: flex;
margin: 5px;
} }
`; `;
@@ -16,25 +16,32 @@ export class CalcNumber extends LitElement {
#base { #base {
width: 100%; width: 100%;
height: 100%; height: 100%;
border-radius: 10px; border-radius: 50%;
text-align: center; text-align: center;
float: left; border: none;
border-bottom: 3px solid rgba(0, 0, 0, 0.45);
font-weight: 700;
font-size: 20px;
cursor: pointer;
aspect-ratio: 1;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.08s ease;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.25);
outline: none;
box-sizing: border-box;
padding: 0;
} }
#base:hover { #base:hover {
box-shadow: 0 0 11px rgba(33, 33, 33, 0.2); filter: brightness(1.1);
} }
#base:active { #base:active {
background-color: lightgrey; transform: translateY(2px);
} border-bottom-width: 1px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
button {
outline: 0 none;
color: white;
margin-top: 10px;
margin-bottom: 10px;
font-size: 20px;
} }
`; `;
@@ -0,0 +1,17 @@
import { describe, expect, test } from "vitest";
import "./code-editor";
describe("Lit Code Editor Component", () => {
test("registers code-editor custom element successfully", () => {
expect(customElements.get("code-editor")).toBeDefined();
});
test("mounts code-editor successfully", async () => {
document.body.innerHTML = '<code-editor code="console.log(1);"></code-editor>';
await window.happyDOM.whenAsyncComplete();
const element = document.body.querySelector("code-editor");
expect(element).toBeDefined();
expect(element?.getAttribute("code")).toBe("console.log(1);");
});
});
@@ -0,0 +1,21 @@
import { describe, expect, test } from "vitest";
import "./draggable-dom";
describe("Lit Draggable DOM Component", () => {
test("registers draggable-dom custom element successfully", () => {
expect(customElements.get("draggable-dom")).toBeDefined();
});
test("mounts draggable-dom successfully and accepts slots", async () => {
document.body.innerHTML = `
<draggable-dom>
<span id="drag">Grab me</span>
</draggable-dom>
`;
await window.happyDOM.whenAsyncComplete();
const element = document.body.querySelector("draggable-dom");
expect(element).toBeDefined();
expect(document.getElementById("drag")?.textContent).toBe("Grab me");
});
});
@@ -15,17 +15,33 @@ export class DraggableDOM extends LitElement {
static styles = css` static styles = css`
:host { :host {
display: block;
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
--offset-x: 0; --offset-x: 0;
--offset-y: 0; --offset-y: 0;
--grid-background-color: white; --grid-background-color: white;
--grid-color: black; --grid-color: rgba(0, 0, 0, 0.1);
--grid-size: 40px; --grid-size: 40px;
--grid-dot-size: 1px; --grid-dot-size: 2px;
} }
main { main {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
overflow: hidden; overflow: hidden;
touch-action: none;
} }
canvas { canvas {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
background-size: var(--grid-size) var(--grid-size); background-size: var(--grid-size) var(--grid-size);
background-image: radial-gradient( background-image: radial-gradient(
circle, circle,
@@ -34,25 +50,33 @@ export class DraggableDOM extends LitElement {
); );
background-position: var(--offset-x) var(--offset-y); background-position: var(--offset-x) var(--offset-y);
z-index: 0; z-index: 0;
pointer-events: none;
} }
.full-size { #children {
width: 100%; width: 100%;
height: 100%; height: 100%;
position: fixed; position: absolute;
transform: translate(var(--dx), var(--dy)); top: 0;
left: 0;
transform: translate(var(--pan-x, 0), var(--pan-y, 0));
pointer-events: none;
} }
.child { .child {
--dx: 0px; display: block;
--dy: 0px; pointer-events: auto;
position: fixed; }
flex-shrink: 1; ::slotted(*) {
z-index: var(--layer, 0); position: absolute !important;
transform: translate(var(--dx), var(--dy)); top: 0;
left: 0;
transform: translate(var(--dx, 0), var(--dy, 0));
pointer-events: auto;
touch-action: none;
} }
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
main { :host {
--grid-background-color: black; --grid-background-color: #0d1117;
--grid-color: grey; --grid-color: rgba(255, 255, 255, 0.1);
} }
} }
`; `;
@@ -60,40 +84,23 @@ export class DraggableDOM extends LitElement {
render() { render() {
return html` return html`
<main <main
class="full-size"
@pointerdown=${(e: any) => this.handleDown(e, "canvas")} @pointerdown=${(e: any) => this.handleDown(e, "canvas")}
@pointermove=${(e: any) => @pointermove=${(e: any) =>
this.handleMove(e, "canvas", (delta) => this.moveCanvas(delta))} this.handleMove(e, "canvas", (delta) => this.moveCanvas(delta))}
@touchstart=${(e: any) => this.handleDown(e, "canvas")}
@touchmove=${(e: any) =>
this.handleMove(e, "canvas", (delta) => this.moveCanvas(delta))}
@pointerup=${(e: any) => this.handleUp(e)} @pointerup=${(e: any) => this.handleUp(e)}
@pointercancel=${(e: any) => this.handleUp(e)}
> >
<canvas class="full-size"></canvas> <canvas></canvas>
<div id="children" class="full-size"> <div id="children">
<slot <slot
class="child" class="child"
draggable="false"
@pointerdown=${(e: any) => this.handleDown(e, "element")} @pointerdown=${(e: any) => this.handleDown(e, "element")}
@pointermove=${(e: any) => @pointermove=${(e: any) =>
this.handleMove(e, "element", (delta) => { this.handleMove(e, "element", (delta) => {
this.moveElement( const target = e.target as HTMLElement;
e.target.parentElement === this const child = target.assignedSlot ? target : target.closest('[slot]') || target;
? e.target this.moveElement(child as SupportedNode, delta);
: e.target.parentElement,
delta
);
})} })}
@touchstart=${(e: any) => this.handleDown(e, "element")}
@touchmove=${(e: any) =>
this.handleMove(e, "element", (delta) =>
this.moveElement(
e.target.parentElement === this
? e.target
: e.target.parentElement,
delta
)
)}
></slot> ></slot>
</div> </div>
</main> </main>
@@ -139,25 +146,24 @@ export class DraggableDOM extends LitElement {
moveCanvas(delta: Offset) { moveCanvas(delta: Offset) {
this.offset.x += delta.x; this.offset.x += delta.x;
this.offset.y += delta.y; this.offset.y += delta.y;
this.root.style.setProperty("--offset-x", `${this.offset.x}px`); this.style.setProperty("--offset-x", `${this.offset.x}px`);
this.root.style.setProperty("--offset-y", `${this.offset.y}px`); this.style.setProperty("--offset-y", `${this.offset.y}px`);
this.container.style.setProperty("--dx", `${this.offset.x}px`); this.container.style.setProperty("--pan-x", `${this.offset.x}px`);
this.container.style.setProperty("--dy", `${this.offset.y}px`); this.container.style.setProperty("--pan-y", `${this.offset.y}px`);
} }
moveElement(child: SupportedNode, delta: Offset) { moveElement(child: SupportedNode, delta: Offset) {
const getNumber = (key: "--dx" | "--dy", fallback: number) => { const getNumber = (key: string) => {
const saved = child.style.getPropertyValue(key); const saved = (child as HTMLElement).style.getPropertyValue(key);
if (saved.length > 0) { if (saved.length > 0) {
return parseFloat(saved.replace("px", "")); return parseFloat(saved.replace("px", ""));
} }
return fallback; return 0;
}; };
const dx = getNumber("--dx", 0) + delta.x; const dx = getNumber("--dx") + delta.x;
const dy = getNumber("--dy", 0) + delta.y; const dy = getNumber("--dy") + delta.y;
child.style.transform = `translate(${dx}px, ${dy}px)`; (child as HTMLElement).style.setProperty("--dx", `${dx}px`);
child.style.setProperty("--dx", `${dx}px`); (child as HTMLElement).style.setProperty("--dy", `${dy}px`);
child.style.setProperty("--dy", `${dy}px`);
} }
} }
@@ -0,0 +1,20 @@
import { describe, expect, test } from "vitest";
import "./generated-app";
describe("Lit File-Based Router Component", () => {
test("registers generated-app custom element successfully", () => {
expect(customElements.get("generated-app")).toBeDefined();
});
test("mounts router application container successfully", async () => {
document.body.innerHTML = "<generated-app></generated-app>";
await window.happyDOM.whenAsyncComplete();
const element = document.body.querySelector("generated-app");
expect(element).toBeDefined();
expect(element?.shadowRoot).toBeDefined();
// Await lazy route imports to complete before ending test and tearing down Happy DOM context
await new Promise((resolve) => setTimeout(resolve, 150));
});
});
@@ -4,6 +4,7 @@ export interface RenderContext {
data: GraphData; data: GraphData;
element: HTMLElement; element: HTMLElement;
onHover: (node?: GraphNode) => void; onHover: (node?: GraphNode) => void;
instance?: any;
} }
export type Renderer = (context: RenderContext) => void; export type Renderer = (context: RenderContext) => any;
@@ -0,0 +1,17 @@
import { describe, expect, test } from "vitest";
import "./lit-force-graph";
describe("Lit Force Graph Component", () => {
test("registers lit-force-graph custom element successfully", () => {
expect(customElements.get("lit-force-graph")).toBeDefined();
});
test("mounts lit-force-graph container successfully", async () => {
document.body.innerHTML = "<lit-force-graph></lit-force-graph>";
await window.happyDOM.whenAsyncComplete();
const element = document.body.querySelector("lit-force-graph");
expect(element).toBeDefined();
expect(element?.shadowRoot).toBeDefined();
});
});
@@ -1,9 +1,7 @@
import { html, css, LitElement } from "lit"; import { html, css, LitElement, PropertyValues } from "lit";
import { customElement, property, query, state } from "lit/decorators.js"; import { customElement, property, query, state } from "lit/decorators.js";
import { Renderer } from "./classes/context"; import { Renderer } from "./classes/context";
import { GraphData, GraphNode } from "./classes/graph"; import { GraphData, GraphNode } from "./classes/graph";
import { render as render2D } from "./modes/mode-2d";
import { render as render3D } from "./modes/mode-3d";
export const tagName = "lit-force-graph"; export const tagName = "lit-force-graph";
@@ -11,24 +9,24 @@ export const tagName = "lit-force-graph";
export class LitForceGraph extends LitElement { export class LitForceGraph extends LitElement {
static styles = css` static styles = css`
:host { :host {
display: block;
background-color: var(--graph-background-color, #000011); background-color: var(--graph-background-color, #000011);
color: var(--graph-foreground-color, #ffffff); color: var(--graph-foreground-color, #ffffff);
width: var(--graph-width, 100%); width: var(--graph-width, 100%);
height: var(--graph-height, 100vh); height: var(--graph-height, 100vh);
position: relative;
} }
#graph { #graph {
width: 100%; width: 100%;
height: 100%; height: 100%;
width: var(--graph-width, 100%);
height: var(--graph-height, 100vh);
} }
#controls { #controls {
position: absolute; position: absolute;
top: 20px; top: 20px;
right: 20px; right: 20px;
z-index: 100 !important; z-index: 100;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: flex-end; align-items: flex-end;
@@ -41,10 +39,11 @@ export class LitForceGraph extends LitElement {
position: absolute; position: absolute;
top: 10px; top: 10px;
left: 10px; left: 10px;
z-index: 100 !important; z-index: 100;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
pointer-events: none;
} }
#tooltips { #tooltips {
@@ -57,6 +56,7 @@ export class LitForceGraph extends LitElement {
align-items: center; align-items: center;
text-align: center; text-align: center;
justify-content: center; justify-content: center;
pointer-events: none;
} }
.node-tooltip { .node-tooltip {
@@ -65,18 +65,12 @@ export class LitForceGraph extends LitElement {
border-radius: 5px; border-radius: 5px;
font-size: 12px; font-size: 12px;
padding: 5px; padding: 5px;
opacity: 0.67; opacity: 0.8;
} }
#graph-description { #graph-description {
opacity: 0.67; opacity: 0.67;
} }
.scene-tooltip {
color: var(--graph-foreground-color, #ffffff);
background-color: transparent;
display: none;
}
`; `;
@query("#graph") graph!: HTMLElement; @query("#graph") graph!: HTMLElement;
@@ -84,15 +78,17 @@ export class LitForceGraph extends LitElement {
@property() mode = "2D"; @property() mode = "2D";
@property({ type: Object }) data?: GraphData; @property({ type: Object }) data?: GraphData;
@state() hovered?: GraphNode; @state() hovered?: GraphNode;
@state() private _instance?: any;
renderers = new Map<string, Renderer>([ renderers = new Map<string, () => Promise<Renderer>>([
["2D", render2D], ["2D", () => import("./modes/mode-2d").then((m) => m.render)],
["3D", render3D], ["3D", () => import("./modes/mode-3d").then((m) => m.render)],
["AR", () => import("./modes/mode-ar").then((m) => m.render)],
["VR", () => import("./modes/mode-vr").then((m) => m.render)],
]); ]);
render() { render() {
return html` <main return html` <main
accept="application/json"
@drop="${this.onDrop}" @drop="${this.onDrop}"
@dragover="${(e: Event) => e.preventDefault()}" @dragover="${(e: Event) => e.preventDefault()}"
> >
@@ -100,16 +96,16 @@ export class LitForceGraph extends LitElement {
<div id="controls"> <div id="controls">
<div> <div>
<label for="render-mode">Render mode</label> <label for="render-mode">Render mode</label>
<select id="render-mode" @change=${this.onChangeMode}> <select id="render-mode" .value=${this.mode} @change=${this.onChangeMode}>
${Array.from(this.renderers.keys()).map((mode) => { ${Array.from(this.renderers.keys()).map((mode) => {
return html` <option value="${mode}">${mode}</option> `; return html` <option value="${mode}" ?selected=${this.mode === mode}>${mode}</option> `;
})} })}
</select> </select>
</div> </div>
</div> </div>
<div id="info"> <div id="info">
<h2 id="graph-name">${this.data?.name}</h2> <h2 id="graph-name">${this.data?.name}</h2>
<div id="graph-description">${this?.data?.description}</div> <div id="graph-description">${this.data?.description}</div>
</div> </div>
<div id="tooltips"> <div id="tooltips">
${this.hovered ${this.hovered
@@ -125,10 +121,25 @@ export class LitForceGraph extends LitElement {
await this.refresh(); await this.refresh();
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)"); const prefersDark = window.matchMedia("(prefers-color-scheme: dark)");
prefersDark.addEventListener("change", () => { prefersDark.addEventListener("change", () => {
this.refresh(); this._updateGraph();
}); });
} }
protected updated(changedProperties: PropertyValues<this>) {
if (changedProperties.has("mode")) {
if (this._instance) {
this.graph.innerHTML = "";
this._instance = undefined;
}
}
if (changedProperties.has("data") || changedProperties.has("mode")) {
this._updateGraph();
}
if (changedProperties.has("src") && this.src) {
this.refresh();
}
}
/** /**
* Set the graph data and update the renderer * Set the graph data and update the renderer
* *
@@ -136,12 +147,24 @@ export class LitForceGraph extends LitElement {
*/ */
setData(data: GraphData) { setData(data: GraphData) {
this.data = data; this.data = data;
const renderer = this.renderers.get(this.mode); }
renderer?.({
private async _updateGraph() {
if (!this.data || !this.graph) return;
const rendererFactory = this.renderers.get(this.mode);
if (rendererFactory) {
try {
const renderer = await rendererFactory();
this._instance = renderer({
element: this.graph, element: this.graph,
data, data: this.data,
onHover: (node) => (this.hovered = node), onHover: (node) => (this.hovered = node),
instance: this._instance,
}); });
} catch (e) {
console.error("Failed to load or run renderer for mode", this.mode, e);
}
}
} }
private async refresh() { private async refresh() {
@@ -149,28 +172,34 @@ export class LitForceGraph extends LitElement {
const children = Array.from(this.children); const children = Array.from(this.children);
const elem = children.find((child) => child.tagName === "SCRIPT"); const elem = children.find((child) => child.tagName === "SCRIPT");
if (elem) { if (elem) {
// Render from script tag contents
if (elem.textContent) { if (elem.textContent) {
try {
const data = JSON.parse(elem.textContent); const data = JSON.parse(elem.textContent);
if (data) this.setData(data); if (data) this.setData(data);
// Render from script tag src } catch (e) {
console.error("Failed to parse graph data from script tag", e);
}
} else if (elem.hasAttribute("src")) { } else if (elem.hasAttribute("src")) {
const url = elem.getAttribute("src")!; const url = elem.getAttribute("src")!;
try {
const data = await fetch(url).then((res) => res.json()); const data = await fetch(url).then((res) => res.json());
if (data) this.setData(data); if (data) this.setData(data);
} catch (e) {
console.error("Failed to fetch graph data from script src", url, e);
} }
} else if (this.src.length > 0) { }
// Render from src attribute } else if (this.src) {
try {
const data = await fetch(this.src).then((res) => res.json()); const data = await fetch(this.src).then((res) => res.json());
if (data) this.setData(data); if (data) this.setData(data);
} catch (e) {
console.error("Failed to fetch graph data from src property", this.src, e);
}
} }
} }
private onChangeMode(e: Event) { private onChangeMode(e: Event) {
const mode = (e.target as HTMLSelectElement).value; this.mode = (e.target as HTMLSelectElement).value;
this.mode = mode;
if (!this.data) return;
this.setData({ ...this.data! });
} }
private onDrop(e: DragEvent) { private onDrop(e: DragEvent) {
@@ -180,34 +209,23 @@ export class LitForceGraph extends LitElement {
const file = files[0]; const file = files[0];
const reader = new FileReader(); const reader = new FileReader();
reader.onload = () => { reader.onload = () => {
try {
const json = JSON.parse(reader.result as string); const json = JSON.parse(reader.result as string);
this.data = json;
this.setData(json); this.setData(json);
} catch (e) {
console.error("Failed to parse dropped graph data", e);
}
}; };
reader.readAsText(file); reader.readAsText(file);
} }
return false; return false;
} }
}
attributeChangedCallback(
name: string, declare global {
_old: string | null, interface HTMLElementTagNameMap {
value: string | null "lit-force-graph": LitForceGraph;
): void { }
if (name === "src" && value) {
this.refresh();
}
if (name === "data" && value) {
this.setData(JSON.parse(value));
}
if (name === "mode" && value) {
this.mode = value;
if (this.data) {
this.setData({ ...this.data! });
}
}
super.attributeChangedCallback(name, _old, value);
}
} }
declare global { declare global {
@@ -2,16 +2,23 @@ import ForceGraph from "force-graph";
import { RenderContext } from "../classes/context"; import { RenderContext } from "../classes/context";
export function render(context: RenderContext) { export function render(context: RenderContext) {
const graph = ForceGraph(); const graph = context.instance || ForceGraph();
const style = getComputedStyle(context.element); const style = getComputedStyle(context.element);
const lineColor = style.getPropertyValue("--graph-line-color").trim(); const lineColor = style.getPropertyValue("--graph-line-color").trim() || "#555";
const bgColor = style.getPropertyValue("--graph-background-color").trim(); const bgColor = style.getPropertyValue("--graph-background-color").trim() || "#000011";
const fgColor = style.getPropertyValue("--graph-foreground-color").trim(); const fgColor = style.getPropertyValue("--graph-foreground-color").trim() || "#ffffff";
const nodeColor = style.getPropertyValue("--graph-node-color").trim(); const nodeColor = style.getPropertyValue("--graph-node-color").trim() || "#999";
graph(context.element) const width = context.element.clientWidth || Number(style.width.slice(0, -2)) || window.innerWidth;
const height = context.element.clientHeight || Number(style.height.slice(0, -2)) || window.innerHeight;
if (!context.instance) {
graph(context.element);
}
graph
.graphData(context.data) .graphData(context.data)
.width(Number(style.width.slice(0, -2))) .width(width)
.height(Number(style.height.slice(0, -2))) .height(height)
.cooldownTicks(100) .cooldownTicks(100)
.backgroundColor(bgColor) .backgroundColor(bgColor)
.linkColor(() => lineColor) .linkColor(() => lineColor)
@@ -21,7 +28,6 @@ export function render(context: RenderContext) {
ctx.beginPath(); ctx.beginPath();
const size = 5 / globalScale; const size = 5 / globalScale;
ctx.arc(node.x, node.y, size, 0, 2 * Math.PI); ctx.arc(node.x, node.y, size, 0, 2 * Math.PI);
// ctx.fillStyle = nodeColor(node, groupColors);
ctx.fillStyle = nodeColor; ctx.fillStyle = nodeColor;
ctx.fill(); ctx.fill();
ctx.lineWidth = 1 / globalScale; ctx.lineWidth = 1 / globalScale;
@@ -55,4 +61,6 @@ export function render(context: RenderContext) {
context.onHover(undefined); context.onHover(undefined);
} }
}); });
return graph;
} }
@@ -2,18 +2,25 @@ import ForceGraph from "3d-force-graph";
import { RenderContext } from "../classes/context.js"; import { RenderContext } from "../classes/context.js";
export function render(context: RenderContext) { export function render(context: RenderContext) {
const graph = ForceGraph({ const graph = context.instance || ForceGraph({
controlType: "trackball", controlType: "trackball",
rendererConfig: { antialias: true, alpha: true }, rendererConfig: { antialias: true, alpha: true },
}); });
const style = getComputedStyle(context.element); const style = getComputedStyle(context.element);
const lineColor = style.getPropertyValue("--graph-line-color").trim(); const lineColor = style.getPropertyValue("--graph-line-color").trim() || "#555";
const bgColor = style.getPropertyValue("--graph-background-color").trim(); const bgColor = style.getPropertyValue("--graph-background-color").trim() || "#000011";
const nodeColor = style.getPropertyValue("--graph-node-color").trim(); const nodeColor = style.getPropertyValue("--graph-node-color").trim() || "#999";
graph(context.element) const width = context.element.clientWidth || Number(style.width.slice(0, -2)) || window.innerWidth;
const height = context.element.clientHeight || Number(style.height.slice(0, -2)) || window.innerHeight;
if (!context.instance) {
graph(context.element);
}
graph
.graphData(context.data) .graphData(context.data)
.width(Number(style.width.slice(0, -2))) .width(width)
.height(Number(style.height.slice(0, -2))) .height(height)
.showNavInfo(false) .showNavInfo(false)
.linkColor(() => lineColor) .linkColor(() => lineColor)
.backgroundColor(bgColor) .backgroundColor(bgColor)
@@ -33,4 +40,6 @@ export function render(context: RenderContext) {
} }
}) })
.cooldownTicks(100); .cooldownTicks(100);
return graph;
} }
@@ -0,0 +1,21 @@
import "aframe";
import "@ar-js-org/ar.js";
import ForceGraph from "3d-force-graph-ar";
import { RenderContext } from "../classes/context.js";
export function render(context: RenderContext) {
const graph = context.instance || ForceGraph();
const style = getComputedStyle(context.element);
if (!context.instance) {
graph(context.element);
}
graph
.graphData(context.data)
.width(Number(style.width.slice(0, -2)) || window.innerWidth)
.height(Number(style.height.slice(0, -2)) || window.innerHeight)
.cooldownTicks(100);
return graph;
}
@@ -0,0 +1,20 @@
import ForceGraph from "3d-force-graph-vr";
import { RenderContext } from "../classes/context.js";
export function render(context: RenderContext) {
const graph = context.instance || ForceGraph();
const style = getComputedStyle(context.element);
if (!context.instance) {
graph(context.element);
}
graph
.graphData(context.data)
.width(Number(style.width.slice(0, -2)) || window.innerWidth)
.height(Number(style.height.slice(0, -2)) || window.innerHeight)
.showNavInfo(false)
.cooldownTicks(100);
return graph;
}
@@ -0,0 +1,19 @@
import { describe, expect, test } from "vitest";
import "./components/rich-text";
import "./components/rich-viewer";
describe("Lit HTML Editor Components", () => {
test("registers rich markdown/HTML custom elements successfully", () => {
expect(customElements.get("rich-text")).toBeDefined();
expect(customElements.get("rich-viewer")).toBeDefined();
});
test("mounts rich-viewer correctly and accepts markdown content", async () => {
document.body.innerHTML = '<rich-viewer value="# Hello World"></rich-viewer>';
await window.happyDOM.whenAsyncComplete();
const element = document.body.querySelector("rich-viewer");
expect(element).toBeDefined();
expect(element?.getAttribute("value")).toBe("# Hello World");
});
});
@@ -0,0 +1,17 @@
import { describe, expect, test } from "vitest";
import "./lit-html-table";
describe("Lit HTML Table Component", () => {
test("registers lit-html-table custom element successfully", () => {
expect(customElements.get("lit-html-table")).toBeDefined();
});
test("mounts lit-html-table spreadsheet grid successfully", async () => {
document.body.innerHTML = "<lit-html-table></lit-html-table>";
await window.happyDOM.whenAsyncComplete();
const element = document.body.querySelector("lit-html-table");
expect(element).toBeDefined();
expect(element?.shadowRoot).toBeDefined();
});
});
@@ -0,0 +1,173 @@
import { html, css, LitElement } from "lit";
import { customElement, property } from "lit/decorators.js";
type ObjectData = { [key: string]: any };
@customElement("lit-html-table")
export class LitHtmlTable extends LitElement {
@property() src = "";
@property({ type: Boolean }) editable = false;
values?: ObjectData[];
static styles = css`
:host {
display: block;
width: 100%;
overflow-x: auto;
font-family: var(--table-font-family, sans-serif);
background-color: var(--table-background-color, #ffffff);
color: var(--table-text-color, #1f2937);
border-radius: var(--table-border-radius, 8px);
box-shadow: var(--table-box-shadow, 0 1px 3px 0 rgba(0, 0, 0, 0.1));
}
table {
width: 100%;
border-collapse: collapse;
text-align: left;
}
thead {
background-color: var(--table-header-bg, #f9fafb);
border-bottom: 2px solid var(--table-border-color, #e5e7eb);
}
th {
padding: var(--table-padding, 12px 16px);
font-weight: 600;
text-transform: uppercase;
font-size: 0.75rem;
letter-spacing: 0.05em;
color: var(--table-header-color, #4b5563);
}
tr {
border-bottom: 1px solid var(--table-border-color, #e5e7eb);
transition: background-color 0.2s;
}
tr:last-child {
border-bottom: none;
}
tbody tr:hover {
background-color: var(--table-row-hover-bg, #f3f4f6);
}
td {
padding: var(--table-padding, 12px 16px);
vertical-align: middle;
font-size: 0.875rem;
}
td > input {
width: 100%;
padding: 8px;
border: 1px solid transparent;
border-radius: 4px;
background: transparent;
font-family: inherit;
font-size: inherit;
color: inherit;
transition: all 0.2s;
}
td > input:focus {
outline: none;
border-color: var(--table-focus-color, #6366f1);
background-color: #ffffff;
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
}
.loading, .empty {
padding: 32px;
text-align: center;
color: var(--table-text-secondary, #6b7280);
font-style: italic;
}
`;
render() {
// Check if data is loaded
if (!this.values) {
return html`<div class="loading"><slot name="loading">Loading data...</slot></div>`;
}
// Check if items are not empty
if (this.values.length === 0) {
return html`<div class="empty"><slot name="empty">No items found.</slot></div>`;
}
// Convert JSON to HTML Table
return html`
<table>
<thead>
<tr>
${Object.keys(this.values[0]).map((key) => {
const name = key.replace(/([A-Z])/g, " $1").replace(/^./, (str) => str.toUpperCase());
return html`<th>
<slot name="${key}">${name}</slot>
</th>`;
})}
</tr>
</thead>
<tbody>
${this.values.map((item, index) => {
return html`
<tr>
${Object.entries(item).map(([key, value]) => {
return html`<td>
${this.editable
? html`<input
.value="${value}"
type="text"
@input=${(e: any) => {
const newValue = e.target.value;
const current = { ...this.values![index] };
current[key] = newValue;
this.values![index] = current;
this.requestUpdate();
this.dispatchEvent(
new CustomEvent("input-cell", {
detail: {
index: index,
data: current,
},
})
);
}}
/>`
: html`${value}`}
</td>`;
})}
</tr>
`;
})}
</tbody>
</table>
`;
}
async firstUpdated() {
await this.fetchData();
}
async fetchData() {
if (this.values && this.values.length > 0) return;
let _data: any;
if (this.src.length > 0) {
_data = await fetch(this.src).then((res) => res.json());
} else {
const elem = this.parentElement?.querySelector(
'script[type="application/json"]'
) as HTMLScriptElement;
if (elem) _data = JSON.parse(elem.innerHTML);
}
_data ??= [];
this.values = this.transform(_data);
this.requestUpdate();
}
transform(data: any) {
return data;
}
}
+21
View File
@@ -0,0 +1,21 @@
import { describe, expect, test } from "vitest";
import "./modules/app";
import "./modules/header";
import "./modules/counter";
describe("Lit Modules App Component", () => {
test("registers modules custom elements successfully", () => {
expect(customElements.get("app-module")).toBeDefined();
expect(customElements.get("header-module")).toBeDefined();
expect(customElements.get("counter-module")).toBeDefined();
});
test("mounts app-module successfully", async () => {
document.body.innerHTML = "<app-module></app-module>";
await window.happyDOM.whenAsyncComplete();
const element = document.body.querySelector("app-module");
expect(element).toBeDefined();
expect(element?.shadowRoot).toBeDefined();
});
});
@@ -0,0 +1,19 @@
import { describe, expect, test } from "vitest";
import "./node-editor";
import "./ui/tree-view";
describe("Lit Node Editor Component", () => {
test("registers visual node editor custom elements successfully", () => {
expect(customElements.get("node-editor")).toBeDefined();
expect(customElements.get("tree-view")).toBeDefined();
});
test("mounts node-editor to the body successfully", async () => {
document.body.innerHTML = "<node-editor></node-editor>";
await new Promise((r) => setTimeout(r, 100));
const element = document.body.querySelector("node-editor");
expect(element).toBeDefined();
expect(element?.shadowRoot).toBeDefined();
});
});
@@ -1,6 +1,52 @@
import { css, html, LitElement, TemplateResult } from "lit"; import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
/**
* Tree view styles
*/
export const styles = css`
ul.tree {
width: 100%;
height: 100%;
overflow-x: scroll;
white-space: nowrap;
}
ul.tree,
ul.tree ul {
list-style: none;
margin: 0;
padding: 0;
}
ul.tree ul {
margin-left: 10px;
}
ul.tree li {
margin: 0;
padding: 0 7px;
line-height: 20px;
color: #369;
font-weight: bold;
border-left: 1px solid rgb(100, 100, 100);
}
ul.tree li:last-child {
border-left: none;
}
ul.tree li:before {
position: relative;
top: -0.3em;
height: 1em;
width: 12px;
color: white;
border-bottom: 1px solid rgb(100, 100, 100);
content: "";
display: inline-block;
left: -7px;
}
ul.tree li:last-child:before {
border-left: 1px solid rgb(100, 100, 100);
}
`;
/** /**
* A tree view component. * A tree view component.
*/ */
@@ -48,52 +94,6 @@ export declare interface BaseTreeNode {
collapsed: boolean; collapsed: boolean;
} }
/**
* Tree view styles
*/
export const styles = css`
ul.tree {
width: 100%;
height: 100%;
overflow-x: scroll;
white-space: nowrap;
}
ul.tree,
ul.tree ul {
list-style: none;
margin: 0;
padding: 0;
}
ul.tree ul {
margin-left: 10px;
}
ul.tree li {
margin: 0;
padding: 0 7px;
line-height: 20px;
color: #369;
font-weight: bold;
border-left: 1px solid rgb(100, 100, 100);
}
ul.tree li:last-child {
border-left: none;
}
ul.tree li:before {
position: relative;
top: -0.3em;
height: 1em;
width: 12px;
color: white;
border-bottom: 1px solid rgb(100, 100, 100);
content: "";
display: inline-block;
left: -7px;
}
ul.tree li:last-child:before {
border-left: 1px solid rgb(100, 100, 100);
}
`;
interface TemplateProps { interface TemplateProps {
tree: Tree; tree: Tree;
onSelect: (node: BaseTreeNode) => void; onSelect: (node: BaseTreeNode) => void;
@@ -0,0 +1,17 @@
import { describe, expect, test } from "vitest";
import "./sheet-music";
describe("Lit Sheet Music Component", () => {
test("registers sheet-music custom element successfully", () => {
expect(customElements.get("sheet-music")).toBeDefined();
});
test("mounts sheet-music successfully", async () => {
document.body.innerHTML = "<sheet-music></sheet-music>";
await window.happyDOM.whenAsyncComplete();
const element = document.body.querySelector("sheet-music");
expect(element).toBeDefined();
expect(element?.shadowRoot).toBeDefined();
});
});
@@ -0,0 +1,52 @@
import { describe, expect, test } from "vitest";
import "./my-element";
describe("MyElement Component", () => {
test("registers my-element custom element successfully", () => {
expect(customElements.get("my-element")).toBeDefined();
});
test("mounts and renders with default properties", async () => {
document.body.innerHTML = "<my-element></my-element>";
await window.happyDOM.whenAsyncComplete();
const element = document.body.querySelector("my-element");
expect(element).toBeDefined();
expect(element?.shadowRoot).toBeDefined();
const h1 = element?.shadowRoot?.querySelector("h1");
expect(h1?.textContent).toBe("Hello, World!");
const button = element?.shadowRoot?.querySelector("button");
expect(button?.textContent?.trim()).toBe("Click Count: 1");
});
test("handles name property change", async () => {
document.body.innerHTML = "<my-element name=\"Vitest\"></my-element>";
await window.happyDOM.whenAsyncComplete();
const element = document.body.querySelector("my-element");
const h1 = element?.shadowRoot?.querySelector("h1");
expect(h1?.textContent).toBe("Hello, Vitest!");
});
test("increments count on click and dispatches event", async () => {
document.body.innerHTML = "<my-element></my-element>";
await window.happyDOM.whenAsyncComplete();
const element = document.body.querySelector("my-element");
const button = element?.shadowRoot?.querySelector("button");
let eventDetail: number | null = null;
element?.addEventListener("count", (e: any) => {
eventDetail = e.detail;
});
button?.click();
await window.happyDOM.whenAsyncComplete();
expect(element?.count).toBe(2);
expect(button?.textContent?.trim()).toBe("Click Count: 2");
expect(eventDetail).toBe(2);
});
});
@@ -0,0 +1,17 @@
import { describe, expect, test } from "vitest";
import "./my-element";
describe("VSCode Webview Custom Element", () => {
test("registers my-element custom element successfully", () => {
expect(customElements.get("my-element")).toBeDefined();
});
test("mounts custom webview element successfully", async () => {
document.body.innerHTML = "<my-element></my-element>";
await window.happyDOM.whenAsyncComplete();
const element = document.body.querySelector("my-element");
expect(element).toBeDefined();
expect(element?.shadowRoot).toBeDefined();
});
});

Some files were not shown because too many files have changed in this diff Show More