refactor lit components to lustre and vanilla js
This commit is contained in:
parent
1d95b6f917
commit
316a3d8414
40 changed files with 1018 additions and 1617 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -3,7 +3,6 @@ target
|
|||
.DS_Store
|
||||
node_modules
|
||||
css_dist
|
||||
glm_dist
|
||||
js_dist
|
||||
pgdata
|
||||
.vite
|
||||
|
|
|
|||
|
|
@ -1,16 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["deno.ns", "dom"],
|
||||
"experimentalDecorators": true,
|
||||
"useDefineForClassFields": false
|
||||
},
|
||||
"tasks": {
|
||||
"dev": "deno run -A --node-modules-dir npm:vite",
|
||||
"build": "deno run -A --node-modules-dir npm:vite build"
|
||||
},
|
||||
"imports": {
|
||||
"@lit/context": "npm:@lit/context@^1.1.2",
|
||||
"lit": "npm:lit@^3.2.0",
|
||||
"vite": "npm:vite@^5.2.10"
|
||||
}
|
||||
}
|
||||
353
components/deno.lock
generated
353
components/deno.lock
generated
|
|
@ -1,353 +0,0 @@
|
|||
{
|
||||
"version": "5",
|
||||
"specifiers": {
|
||||
"jsr:@std/path@*": "1.0.8",
|
||||
"npm:@lit/context@^1.1.2": "1.1.2",
|
||||
"npm:lit@^3.2.0": "3.2.1",
|
||||
"npm:vite@*": "5.4.5",
|
||||
"npm:vite@^5.2.10": "5.4.5"
|
||||
},
|
||||
"jsr": {
|
||||
"@std/path@1.0.8": {
|
||||
"integrity": "548fa456bb6a04d3c1a1e7477986b6cffbce95102d0bb447c67c4ee70e0364be"
|
||||
}
|
||||
},
|
||||
"npm": {
|
||||
"@esbuild/aix-ppc64@0.21.5": {
|
||||
"integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
|
||||
"os": ["aix"],
|
||||
"cpu": ["ppc64"]
|
||||
},
|
||||
"@esbuild/android-arm64@0.21.5": {
|
||||
"integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
|
||||
"os": ["android"],
|
||||
"cpu": ["arm64"]
|
||||
},
|
||||
"@esbuild/android-arm@0.21.5": {
|
||||
"integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
|
||||
"os": ["android"],
|
||||
"cpu": ["arm"]
|
||||
},
|
||||
"@esbuild/android-x64@0.21.5": {
|
||||
"integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
|
||||
"os": ["android"],
|
||||
"cpu": ["x64"]
|
||||
},
|
||||
"@esbuild/darwin-arm64@0.21.5": {
|
||||
"integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
|
||||
"os": ["darwin"],
|
||||
"cpu": ["arm64"]
|
||||
},
|
||||
"@esbuild/darwin-x64@0.21.5": {
|
||||
"integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
|
||||
"os": ["darwin"],
|
||||
"cpu": ["x64"]
|
||||
},
|
||||
"@esbuild/freebsd-arm64@0.21.5": {
|
||||
"integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
|
||||
"os": ["freebsd"],
|
||||
"cpu": ["arm64"]
|
||||
},
|
||||
"@esbuild/freebsd-x64@0.21.5": {
|
||||
"integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
|
||||
"os": ["freebsd"],
|
||||
"cpu": ["x64"]
|
||||
},
|
||||
"@esbuild/linux-arm64@0.21.5": {
|
||||
"integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
|
||||
"os": ["linux"],
|
||||
"cpu": ["arm64"]
|
||||
},
|
||||
"@esbuild/linux-arm@0.21.5": {
|
||||
"integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
|
||||
"os": ["linux"],
|
||||
"cpu": ["arm"]
|
||||
},
|
||||
"@esbuild/linux-ia32@0.21.5": {
|
||||
"integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
|
||||
"os": ["linux"],
|
||||
"cpu": ["ia32"]
|
||||
},
|
||||
"@esbuild/linux-loong64@0.21.5": {
|
||||
"integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
|
||||
"os": ["linux"],
|
||||
"cpu": ["loong64"]
|
||||
},
|
||||
"@esbuild/linux-mips64el@0.21.5": {
|
||||
"integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
|
||||
"os": ["linux"],
|
||||
"cpu": ["mips64el"]
|
||||
},
|
||||
"@esbuild/linux-ppc64@0.21.5": {
|
||||
"integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
|
||||
"os": ["linux"],
|
||||
"cpu": ["ppc64"]
|
||||
},
|
||||
"@esbuild/linux-riscv64@0.21.5": {
|
||||
"integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
|
||||
"os": ["linux"],
|
||||
"cpu": ["riscv64"]
|
||||
},
|
||||
"@esbuild/linux-s390x@0.21.5": {
|
||||
"integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
|
||||
"os": ["linux"],
|
||||
"cpu": ["s390x"]
|
||||
},
|
||||
"@esbuild/linux-x64@0.21.5": {
|
||||
"integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
|
||||
"os": ["linux"],
|
||||
"cpu": ["x64"]
|
||||
},
|
||||
"@esbuild/netbsd-x64@0.21.5": {
|
||||
"integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
|
||||
"os": ["netbsd"],
|
||||
"cpu": ["x64"]
|
||||
},
|
||||
"@esbuild/openbsd-x64@0.21.5": {
|
||||
"integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
|
||||
"os": ["openbsd"],
|
||||
"cpu": ["x64"]
|
||||
},
|
||||
"@esbuild/sunos-x64@0.21.5": {
|
||||
"integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
|
||||
"os": ["sunos"],
|
||||
"cpu": ["x64"]
|
||||
},
|
||||
"@esbuild/win32-arm64@0.21.5": {
|
||||
"integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
|
||||
"os": ["win32"],
|
||||
"cpu": ["arm64"]
|
||||
},
|
||||
"@esbuild/win32-ia32@0.21.5": {
|
||||
"integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
|
||||
"os": ["win32"],
|
||||
"cpu": ["ia32"]
|
||||
},
|
||||
"@esbuild/win32-x64@0.21.5": {
|
||||
"integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
|
||||
"os": ["win32"],
|
||||
"cpu": ["x64"]
|
||||
},
|
||||
"@lit-labs/ssr-dom-shim@1.2.1": {
|
||||
"integrity": "sha512-wx4aBmgeGvFmOKucFKY+8VFJSYZxs9poN3SDNQFF6lT6NrQUnHiPB2PWz2sc4ieEcAaYYzN+1uWahEeTq2aRIQ=="
|
||||
},
|
||||
"@lit/context@1.1.2": {
|
||||
"integrity": "sha512-S0nw2C6Tkm7fVX5TGYqeROGD+Z9Coa2iFpW+ysYBDH3YvCqOY3wVQvSgwbaliLJkjTnSEYCBe9qFqKV8WUFpVw==",
|
||||
"dependencies": [
|
||||
"@lit/reactive-element"
|
||||
]
|
||||
},
|
||||
"@lit/reactive-element@2.0.4": {
|
||||
"integrity": "sha512-GFn91inaUa2oHLak8awSIigYz0cU0Payr1rcFsrkf5OJ5eSPxElyZfKh0f2p9FsTiZWXQdWGJeXZICEfXXYSXQ==",
|
||||
"dependencies": [
|
||||
"@lit-labs/ssr-dom-shim"
|
||||
]
|
||||
},
|
||||
"@rollup/rollup-android-arm-eabi@4.21.3": {
|
||||
"integrity": "sha512-MmKSfaB9GX+zXl6E8z4koOr/xU63AMVleLEa64v7R0QF/ZloMs5vcD1sHgM64GXXS1csaJutG+ddtzcueI/BLg==",
|
||||
"os": ["android"],
|
||||
"cpu": ["arm"]
|
||||
},
|
||||
"@rollup/rollup-android-arm64@4.21.3": {
|
||||
"integrity": "sha512-zrt8ecH07PE3sB4jPOggweBjJMzI1JG5xI2DIsUbkA+7K+Gkjys6eV7i9pOenNSDJH3eOr/jLb/PzqtmdwDq5g==",
|
||||
"os": ["android"],
|
||||
"cpu": ["arm64"]
|
||||
},
|
||||
"@rollup/rollup-darwin-arm64@4.21.3": {
|
||||
"integrity": "sha512-P0UxIOrKNBFTQaXTxOH4RxuEBVCgEA5UTNV6Yz7z9QHnUJ7eLX9reOd/NYMO3+XZO2cco19mXTxDMXxit4R/eQ==",
|
||||
"os": ["darwin"],
|
||||
"cpu": ["arm64"]
|
||||
},
|
||||
"@rollup/rollup-darwin-x64@4.21.3": {
|
||||
"integrity": "sha512-L1M0vKGO5ASKntqtsFEjTq/fD91vAqnzeaF6sfNAy55aD+Hi2pBI5DKwCO+UNDQHWsDViJLqshxOahXyLSh3EA==",
|
||||
"os": ["darwin"],
|
||||
"cpu": ["x64"]
|
||||
},
|
||||
"@rollup/rollup-linux-arm-gnueabihf@4.21.3": {
|
||||
"integrity": "sha512-btVgIsCjuYFKUjopPoWiDqmoUXQDiW2A4C3Mtmp5vACm7/GnyuprqIDPNczeyR5W8rTXEbkmrJux7cJmD99D2g==",
|
||||
"os": ["linux"],
|
||||
"cpu": ["arm"]
|
||||
},
|
||||
"@rollup/rollup-linux-arm-musleabihf@4.21.3": {
|
||||
"integrity": "sha512-zmjbSphplZlau6ZTkxd3+NMtE4UKVy7U4aVFMmHcgO5CUbw17ZP6QCgyxhzGaU/wFFdTfiojjbLG3/0p9HhAqA==",
|
||||
"os": ["linux"],
|
||||
"cpu": ["arm"]
|
||||
},
|
||||
"@rollup/rollup-linux-arm64-gnu@4.21.3": {
|
||||
"integrity": "sha512-nSZfcZtAnQPRZmUkUQwZq2OjQciR6tEoJaZVFvLHsj0MF6QhNMg0fQ6mUOsiCUpTqxTx0/O6gX0V/nYc7LrgPw==",
|
||||
"os": ["linux"],
|
||||
"cpu": ["arm64"]
|
||||
},
|
||||
"@rollup/rollup-linux-arm64-musl@4.21.3": {
|
||||
"integrity": "sha512-MnvSPGO8KJXIMGlQDYfvYS3IosFN2rKsvxRpPO2l2cum+Z3exiExLwVU+GExL96pn8IP+GdH8Tz70EpBhO0sIQ==",
|
||||
"os": ["linux"],
|
||||
"cpu": ["arm64"]
|
||||
},
|
||||
"@rollup/rollup-linux-powerpc64le-gnu@4.21.3": {
|
||||
"integrity": "sha512-+W+p/9QNDr2vE2AXU0qIy0qQE75E8RTwTwgqS2G5CRQ11vzq0tbnfBd6brWhS9bCRjAjepJe2fvvkvS3dno+iw==",
|
||||
"os": ["linux"],
|
||||
"cpu": ["ppc64"]
|
||||
},
|
||||
"@rollup/rollup-linux-riscv64-gnu@4.21.3": {
|
||||
"integrity": "sha512-yXH6K6KfqGXaxHrtr+Uoy+JpNlUlI46BKVyonGiaD74ravdnF9BUNC+vV+SIuB96hUMGShhKV693rF9QDfO6nQ==",
|
||||
"os": ["linux"],
|
||||
"cpu": ["riscv64"]
|
||||
},
|
||||
"@rollup/rollup-linux-s390x-gnu@4.21.3": {
|
||||
"integrity": "sha512-R8cwY9wcnApN/KDYWTH4gV/ypvy9yZUHlbJvfaiXSB48JO3KpwSpjOGqO4jnGkLDSk1hgjYkTbTt6Q7uvPf8eg==",
|
||||
"os": ["linux"],
|
||||
"cpu": ["s390x"]
|
||||
},
|
||||
"@rollup/rollup-linux-x64-gnu@4.21.3": {
|
||||
"integrity": "sha512-kZPbX/NOPh0vhS5sI+dR8L1bU2cSO9FgxwM8r7wHzGydzfSjLRCFAT87GR5U9scj2rhzN3JPYVC7NoBbl4FZ0g==",
|
||||
"os": ["linux"],
|
||||
"cpu": ["x64"]
|
||||
},
|
||||
"@rollup/rollup-linux-x64-musl@4.21.3": {
|
||||
"integrity": "sha512-S0Yq+xA1VEH66uiMNhijsWAafffydd2X5b77eLHfRmfLsRSpbiAWiRHV6DEpz6aOToPsgid7TI9rGd6zB1rhbg==",
|
||||
"os": ["linux"],
|
||||
"cpu": ["x64"]
|
||||
},
|
||||
"@rollup/rollup-win32-arm64-msvc@4.21.3": {
|
||||
"integrity": "sha512-9isNzeL34yquCPyerog+IMCNxKR8XYmGd0tHSV+OVx0TmE0aJOo9uw4fZfUuk2qxobP5sug6vNdZR6u7Mw7Q+Q==",
|
||||
"os": ["win32"],
|
||||
"cpu": ["arm64"]
|
||||
},
|
||||
"@rollup/rollup-win32-ia32-msvc@4.21.3": {
|
||||
"integrity": "sha512-nMIdKnfZfzn1Vsk+RuOvl43ONTZXoAPUUxgcU0tXooqg4YrAqzfKzVenqqk2g5efWh46/D28cKFrOzDSW28gTA==",
|
||||
"os": ["win32"],
|
||||
"cpu": ["ia32"]
|
||||
},
|
||||
"@rollup/rollup-win32-x64-msvc@4.21.3": {
|
||||
"integrity": "sha512-fOvu7PCQjAj4eWDEuD8Xz5gpzFqXzGlxHZozHP4b9Jxv9APtdxL6STqztDzMLuRXEc4UpXGGhx029Xgm91QBeA==",
|
||||
"os": ["win32"],
|
||||
"cpu": ["x64"]
|
||||
},
|
||||
"@types/estree@1.0.5": {
|
||||
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw=="
|
||||
},
|
||||
"@types/trusted-types@2.0.7": {
|
||||
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="
|
||||
},
|
||||
"esbuild@0.21.5": {
|
||||
"integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
|
||||
"optionalDependencies": [
|
||||
"@esbuild/aix-ppc64",
|
||||
"@esbuild/android-arm",
|
||||
"@esbuild/android-arm64",
|
||||
"@esbuild/android-x64",
|
||||
"@esbuild/darwin-arm64",
|
||||
"@esbuild/darwin-x64",
|
||||
"@esbuild/freebsd-arm64",
|
||||
"@esbuild/freebsd-x64",
|
||||
"@esbuild/linux-arm",
|
||||
"@esbuild/linux-arm64",
|
||||
"@esbuild/linux-ia32",
|
||||
"@esbuild/linux-loong64",
|
||||
"@esbuild/linux-mips64el",
|
||||
"@esbuild/linux-ppc64",
|
||||
"@esbuild/linux-riscv64",
|
||||
"@esbuild/linux-s390x",
|
||||
"@esbuild/linux-x64",
|
||||
"@esbuild/netbsd-x64",
|
||||
"@esbuild/openbsd-x64",
|
||||
"@esbuild/sunos-x64",
|
||||
"@esbuild/win32-arm64",
|
||||
"@esbuild/win32-ia32",
|
||||
"@esbuild/win32-x64"
|
||||
],
|
||||
"scripts": true,
|
||||
"bin": true
|
||||
},
|
||||
"fsevents@2.3.3": {
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"os": ["darwin"],
|
||||
"scripts": true
|
||||
},
|
||||
"lit-element@4.1.1": {
|
||||
"integrity": "sha512-HO9Tkkh34QkTeUmEdNYhMT8hzLid7YlMlATSi1q4q17HE5d9mrrEHJ/o8O2D0cMi182zK1F3v7x0PWFjrhXFew==",
|
||||
"dependencies": [
|
||||
"@lit-labs/ssr-dom-shim",
|
||||
"@lit/reactive-element",
|
||||
"lit-html"
|
||||
]
|
||||
},
|
||||
"lit-html@3.2.1": {
|
||||
"integrity": "sha512-qI/3lziaPMSKsrwlxH/xMgikhQ0EGOX2ICU73Bi/YHFvz2j/yMCIrw4+puF2IpQ4+upd3EWbvnHM9+PnJn48YA==",
|
||||
"dependencies": [
|
||||
"@types/trusted-types"
|
||||
]
|
||||
},
|
||||
"lit@3.2.1": {
|
||||
"integrity": "sha512-1BBa1E/z0O9ye5fZprPtdqnc0BFzxIxTTOO/tQFmyC/hj1O3jL4TfmLBw0WEwjAokdLwpclkvGgDJwTIh0/22w==",
|
||||
"dependencies": [
|
||||
"@lit/reactive-element",
|
||||
"lit-element",
|
||||
"lit-html"
|
||||
]
|
||||
},
|
||||
"nanoid@3.3.7": {
|
||||
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
|
||||
"bin": true
|
||||
},
|
||||
"picocolors@1.1.0": {
|
||||
"integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw=="
|
||||
},
|
||||
"postcss@8.4.47": {
|
||||
"integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
|
||||
"dependencies": [
|
||||
"nanoid",
|
||||
"picocolors",
|
||||
"source-map-js"
|
||||
]
|
||||
},
|
||||
"rollup@4.21.3": {
|
||||
"integrity": "sha512-7sqRtBNnEbcBtMeRVc6VRsJMmpI+JU1z9VTvW8D4gXIYQFz0aLcsE6rRkyghZkLfEgUZgVvOG7A5CVz/VW5GIA==",
|
||||
"dependencies": [
|
||||
"@types/estree"
|
||||
],
|
||||
"optionalDependencies": [
|
||||
"@rollup/rollup-android-arm-eabi",
|
||||
"@rollup/rollup-android-arm64",
|
||||
"@rollup/rollup-darwin-arm64",
|
||||
"@rollup/rollup-darwin-x64",
|
||||
"@rollup/rollup-linux-arm-gnueabihf",
|
||||
"@rollup/rollup-linux-arm-musleabihf",
|
||||
"@rollup/rollup-linux-arm64-gnu",
|
||||
"@rollup/rollup-linux-arm64-musl",
|
||||
"@rollup/rollup-linux-powerpc64le-gnu",
|
||||
"@rollup/rollup-linux-riscv64-gnu",
|
||||
"@rollup/rollup-linux-s390x-gnu",
|
||||
"@rollup/rollup-linux-x64-gnu",
|
||||
"@rollup/rollup-linux-x64-musl",
|
||||
"@rollup/rollup-win32-arm64-msvc",
|
||||
"@rollup/rollup-win32-ia32-msvc",
|
||||
"@rollup/rollup-win32-x64-msvc",
|
||||
"fsevents"
|
||||
],
|
||||
"bin": true
|
||||
},
|
||||
"source-map-js@1.2.1": {
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="
|
||||
},
|
||||
"vite@5.4.5": {
|
||||
"integrity": "sha512-pXqR0qtb2bTwLkev4SE3r4abCNioP3GkjvIDLlzziPpXtHgiJIjuKl+1GN6ESOT3wMjG3JTeARopj2SwYaHTOA==",
|
||||
"dependencies": [
|
||||
"esbuild",
|
||||
"postcss",
|
||||
"rollup"
|
||||
],
|
||||
"optionalDependencies": [
|
||||
"fsevents"
|
||||
],
|
||||
"bin": true
|
||||
}
|
||||
},
|
||||
"workspace": {
|
||||
"dependencies": [
|
||||
"npm:@lit/context@^1.1.2",
|
||||
"npm:lit@^3.2.0",
|
||||
"npm:vite@^5.2.10"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -1,132 +0,0 @@
|
|||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import { createRef, ref } from "lit/directives/ref.js";
|
||||
|
||||
import "./add-selection-modal-contents.tsx";
|
||||
|
||||
@customElement("add-selection-button")
|
||||
export class AddSelectionButton extends LitElement {
|
||||
@property({ attribute: true })
|
||||
columns = "";
|
||||
|
||||
@state()
|
||||
private _active = false;
|
||||
|
||||
private _labelInputRef = createRef<HTMLInputElement>();
|
||||
|
||||
private _nameInputRef = createRef<HTMLInputElement>();
|
||||
|
||||
private _typePopoverRef = createRef<HTMLDivElement>();
|
||||
|
||||
static override styles = css`
|
||||
:host {
|
||||
height: 100%;
|
||||
--shadow: 0 0.5rem 0.5rem #3333;
|
||||
}
|
||||
|
||||
button.main {
|
||||
appearance: none;
|
||||
border: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-weight: inherit;
|
||||
font-size: inherit;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
}
|
||||
|
||||
div.th {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
border: dashed 1px #ccc;
|
||||
border-left: none;
|
||||
border-top: none;
|
||||
font-family: "Funnel Sans";
|
||||
background: #0001;
|
||||
box-sizing: border-box;
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
|
||||
input#name-input {
|
||||
appearance: none;
|
||||
background: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
button#type-config-button {
|
||||
anchor-name: --type-config-button;
|
||||
}
|
||||
|
||||
.config-popover:popover-open {
|
||||
box-sizing: border-box;
|
||||
inset: unset;
|
||||
margin: 0;
|
||||
margin-top: 1rem;
|
||||
position: fixed;
|
||||
display: block;
|
||||
width: 20rem;
|
||||
border: solid 1px #ccc;
|
||||
border-radius: 0.25rem;
|
||||
filter: drop-shadow(var(--shadow));
|
||||
}
|
||||
|
||||
#type-popover {
|
||||
position-anchor: --type-config-button;
|
||||
position-area: bottom;
|
||||
}
|
||||
`;
|
||||
|
||||
protected override updated() {
|
||||
if (this._active && this._labelInputRef.value) {
|
||||
this._labelInputRef.value.focus();
|
||||
}
|
||||
}
|
||||
|
||||
private _activate() {
|
||||
this._active = true;
|
||||
}
|
||||
|
||||
private _handleLabelChange(ev: InputEvent) {
|
||||
}
|
||||
|
||||
private _showTypePopover() {
|
||||
this._typePopoverRef.value?.showPopover();
|
||||
}
|
||||
|
||||
protected override render() {
|
||||
if (this._active) {
|
||||
return html`
|
||||
<div class="th">
|
||||
<input
|
||||
type="text"
|
||||
${ref(
|
||||
this._labelInputRef,
|
||||
)}
|
||||
id="label-input"
|
||||
name="name-input"
|
||||
@change="${this._handleLabelChange}"
|
||||
>
|
||||
<button type="button" id="type-config-button" @click="${this
|
||||
._showTypePopover}">
|
||||
abc
|
||||
</button>
|
||||
<button type="submit">Create</button>
|
||||
</div>
|
||||
<div ${ref(
|
||||
this._typePopoverRef,
|
||||
)} id="type-popover" class="config-popover" popover="auto">
|
||||
<input type="text" ${ref(this._nameInputRef)} name="name">
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<button type="button" class="main" @click="${this._activate}">+</button>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import { classMap } from "lit/directives/class-map.js";
|
||||
import { styleMap } from "lit/directives/style-map.js";
|
||||
|
||||
@customElement("add-selection-modal-content")
|
||||
export class AddSelectionModalContents extends LitElement {
|
||||
@property({ attribute: true, type: Array })
|
||||
columns: {
|
||||
attname: string;
|
||||
atttypid: number;
|
||||
}[] = [];
|
||||
|
||||
@state()
|
||||
private _tab: "existing_col" | "new_col" = "existing_col";
|
||||
|
||||
static override styles = css`
|
||||
.container {}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
button.tab {
|
||||
appearance: none;
|
||||
background: none;
|
||||
border: none;
|
||||
border-bottom: solid 2px #4474;
|
||||
font: inherit;
|
||||
padding: 0.25rem 0.5rem;
|
||||
|
||||
&.tab-active {
|
||||
border-bottom-color: #447;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
setTab(key: typeof this._tab) {
|
||||
this._tab = key;
|
||||
}
|
||||
|
||||
protected override render() {
|
||||
return html`
|
||||
<div class="container">
|
||||
<div class="tabs">
|
||||
<button
|
||||
class="${classMap({
|
||||
tab: true,
|
||||
"tab-active": this._tab === "existing_col",
|
||||
})}"
|
||||
type="button"
|
||||
@click="${() => this.setTab("existing_col")}"
|
||||
>
|
||||
Select Existing
|
||||
</button>
|
||||
<button
|
||||
class="${classMap({
|
||||
tab: true,
|
||||
"tab-active": this._tab === "new_col",
|
||||
})}"
|
||||
type="button"
|
||||
@click="${() => this.setTab("new_col")}"
|
||||
>
|
||||
Create Column
|
||||
</button>
|
||||
</div>
|
||||
<div class="tab-controlled">
|
||||
<div style="${styleMap({
|
||||
display: this._tab === "existing_col" ? "block" : "none",
|
||||
})}">
|
||||
<form method="post" action="add-selection">
|
||||
<label for="column-select">Column:</label>
|
||||
<select name="column" id="column-select">
|
||||
${this.columns.map((column) =>
|
||||
html`
|
||||
<option value="${column.attname}">${column.attname}</option>
|
||||
`
|
||||
)}
|
||||
</select>
|
||||
<button type="submit">Add to Table</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,129 +0,0 @@
|
|||
import { css, html, LitElement, type TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import { createRef, ref } from "lit/directives/ref.js";
|
||||
|
||||
@customElement("cell-text")
|
||||
export class CellText extends LitElement {
|
||||
/**
|
||||
* When present, a JSON object with key "c" mapped to cell contents.
|
||||
*/
|
||||
@property({ attribute: true, reflect: true })
|
||||
value = "null";
|
||||
|
||||
@property({ attribute: true, type: Boolean })
|
||||
editable = false;
|
||||
|
||||
@property({ attribute: true })
|
||||
column = "";
|
||||
|
||||
@property()
|
||||
pkeyJson = "";
|
||||
|
||||
@state()
|
||||
private _editing = false;
|
||||
|
||||
@state()
|
||||
private _updating = false;
|
||||
|
||||
private _inputRef = createRef<HTMLTextAreaElement>();
|
||||
|
||||
static override styles = css`
|
||||
.outer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-family: "Funnel Sans";
|
||||
}
|
||||
`;
|
||||
|
||||
protected override updated() {
|
||||
if (this._editing && this._inputRef.value) {
|
||||
this._inputRef.value.focus();
|
||||
}
|
||||
}
|
||||
|
||||
private get _contents(): string | undefined {
|
||||
return JSON.parse(this.value)?.c ?? undefined;
|
||||
}
|
||||
|
||||
startEdit() {
|
||||
if (!this._updating) {
|
||||
this._editing = true;
|
||||
}
|
||||
}
|
||||
|
||||
cancelEdit() {
|
||||
this._editing = false;
|
||||
}
|
||||
|
||||
confirmEdit(value: string) {
|
||||
// TODO how to handle null vs empty string
|
||||
(async () => {
|
||||
this._editing = false;
|
||||
this._updating = true;
|
||||
const response = await fetch("update-value", {
|
||||
method: "post",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
pkeys: JSON.parse(this.pkeyJson),
|
||||
column: this.column,
|
||||
value: { t: "Text", c: value },
|
||||
}),
|
||||
});
|
||||
// TODO retry logic
|
||||
if (response.ok) {
|
||||
this.value = JSON.stringify({ t: "Text", c: value });
|
||||
this._updating = false;
|
||||
}
|
||||
})()
|
||||
.catch(console.error);
|
||||
}
|
||||
|
||||
private _handleClick() {
|
||||
if (!this._editing && !this._updating) {
|
||||
this.startEdit();
|
||||
}
|
||||
}
|
||||
|
||||
private _handleBlur(ev: Event) {
|
||||
this.confirmEdit((ev.target as HTMLTextAreaElement).value);
|
||||
}
|
||||
|
||||
protected override render() {
|
||||
let inner: TemplateResult = html`
|
||||
|
||||
`;
|
||||
|
||||
if (this._updating) {
|
||||
inner = html`
|
||||
Loading...
|
||||
`;
|
||||
}
|
||||
|
||||
if (this._editing) {
|
||||
inner = html`
|
||||
<div>
|
||||
<textarea ${ref(this._inputRef)} @blur="${this
|
||||
._handleBlur}">${this._contents ??
|
||||
""}</textarea>
|
||||
<button type="button" @click="${this.cancelEdit}">
|
||||
<custom-icon name="x-mark" alt="cancel"></custom-icon>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
} else if (this._contents === null) {
|
||||
inner = html`
|
||||
<code>NULL</code>
|
||||
`;
|
||||
} else {
|
||||
inner = html`
|
||||
${this._contents}
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="outer" @click="${this._handleClick}">
|
||||
${inner}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,128 +0,0 @@
|
|||
import { css, html, LitElement, type TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import { createRef, ref } from "lit/directives/ref.js";
|
||||
|
||||
@customElement("cell-uuid")
|
||||
export class CellUuid extends LitElement {
|
||||
/**
|
||||
* When present, a JSON object with key "c" mapped to cell contents.
|
||||
*/
|
||||
@property({ attribute: true, reflect: true })
|
||||
value = "null";
|
||||
|
||||
@property({ attribute: true })
|
||||
column = "";
|
||||
|
||||
@property()
|
||||
pkeyJson = "";
|
||||
|
||||
@state()
|
||||
private _editing = false;
|
||||
|
||||
@state()
|
||||
private _updating = false;
|
||||
|
||||
private _inputRef = createRef<HTMLTextAreaElement>();
|
||||
|
||||
static override styles = css`
|
||||
.outer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-family: "Funnel Sans";
|
||||
}
|
||||
`;
|
||||
|
||||
protected override updated() {
|
||||
if (this._editing && this._inputRef.value) {
|
||||
this._inputRef.value.focus();
|
||||
}
|
||||
}
|
||||
|
||||
private get _contents(): string | undefined {
|
||||
return JSON.parse(this.value)?.c ?? undefined;
|
||||
}
|
||||
|
||||
startEdit() {
|
||||
if (!this._updating) {
|
||||
this._editing = true;
|
||||
}
|
||||
}
|
||||
|
||||
cancelEdit() {
|
||||
this._editing = false;
|
||||
}
|
||||
|
||||
confirmEdit(value: string) {
|
||||
// TODO how to handle null vs empty string
|
||||
(async () => {
|
||||
this._editing = false;
|
||||
this._updating = true;
|
||||
const response = await fetch("update-value", {
|
||||
method: "post",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
pkeys: JSON.parse(this.pkeyJson),
|
||||
column: this.column,
|
||||
value: { t: "Uuid", c: value },
|
||||
}),
|
||||
});
|
||||
// TODO retry logic
|
||||
if (response.ok) {
|
||||
// FIXME If this is a primary key, need to freeze the whole row until
|
||||
// edit is accepted by the server, and then update the pkeys value
|
||||
// locally.
|
||||
this.value = JSON.stringify({ t: "Uuid", c: value });
|
||||
this._updating = false;
|
||||
}
|
||||
})()
|
||||
.catch(console.error);
|
||||
}
|
||||
|
||||
private _handleClick() {
|
||||
if (!this._editing && !this._updating) {
|
||||
this.startEdit();
|
||||
}
|
||||
}
|
||||
|
||||
private _handleBlur(ev: Event) {
|
||||
this.confirmEdit((ev.target as HTMLTextAreaElement).value);
|
||||
}
|
||||
|
||||
protected override render() {
|
||||
let inner: TemplateResult = html`
|
||||
|
||||
`;
|
||||
|
||||
if (this._updating) {
|
||||
inner = html`
|
||||
Loading...
|
||||
`;
|
||||
}
|
||||
|
||||
if (this._editing) {
|
||||
inner = html`
|
||||
<div>
|
||||
<textarea ${ref(this._inputRef)} @blur="${this
|
||||
._handleBlur}">${this._contents ?? ""}</textarea>
|
||||
<button type="button" @click="${this.cancelEdit}">
|
||||
<custom-icon name="x-mark" alt="cancel"></custom-icon>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
} else if (this._contents === null) {
|
||||
inner = html`
|
||||
<code>NULL</code>
|
||||
`;
|
||||
} else {
|
||||
inner = html`
|
||||
<code>${this._contents}</code>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="outer" @click="${this._handleClick}">
|
||||
${inner}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
export { CellText } from "../cell-text.ts";
|
||||
export { CellUuid } from "../cell-uuid.ts";
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
@customElement("custom-icon")
|
||||
export class CustomIcon extends LitElement {
|
||||
@property({ attribute: true })
|
||||
name = "";
|
||||
|
||||
@property({ attribute: true })
|
||||
alt?: string;
|
||||
|
||||
static override styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
img {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
|
||||
protected override render() {
|
||||
const path = `../heroicons/16/solid/${this.name}.svg`;
|
||||
return html`
|
||||
<img src="${new URL(
|
||||
path,
|
||||
import.meta.url,
|
||||
).href}" alt="${this.alt}">
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,118 +0,0 @@
|
|||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import { classMap } from "lit/directives/class-map.js";
|
||||
|
||||
@customElement("dev-reloader")
|
||||
export class DevReloader extends LitElement {
|
||||
@property({ attribute: true })
|
||||
ws = "";
|
||||
|
||||
@property({ attribute: true, type: Boolean, reflect: true })
|
||||
auto = false;
|
||||
|
||||
@state()
|
||||
private _connected = false;
|
||||
|
||||
private _armed = false;
|
||||
|
||||
private _socket?: WebSocket;
|
||||
|
||||
static override styles = css`
|
||||
button {
|
||||
appearance: none;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: none;
|
||||
}
|
||||
|
||||
.widget {
|
||||
position: fixed;
|
||||
z-index: 999;
|
||||
bottom: 1rem;
|
||||
left: 1rem;
|
||||
border: solid 1px #0002;
|
||||
padding: 1rem;
|
||||
border-radius: 9999px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-shadow: 0 0.5rem 1rem #0002;
|
||||
opacity: 0.5;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.indicator {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 9999px;
|
||||
background: #f60;
|
||||
|
||||
&.connected {
|
||||
background: #06f;
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._connected = true;
|
||||
this._handleDisconnect();
|
||||
}
|
||||
|
||||
private _handleDisconnect() {
|
||||
if (this._connected) {
|
||||
console.log("dev-reloader: disconnected");
|
||||
this._connected = false;
|
||||
this._socket = undefined;
|
||||
const intvl = setInterval(() => {
|
||||
if (!this._socket || this._socket.readyState === WebSocket.CLOSED) {
|
||||
try {
|
||||
this._socket = new WebSocket(this.ws);
|
||||
this._socket.addEventListener("open", () => {
|
||||
if (this.auto && this._armed) {
|
||||
globalThis.location.reload();
|
||||
}
|
||||
console.log("dev-reloader: connected");
|
||||
this._connected = true;
|
||||
this._armed = true;
|
||||
clearInterval(intvl);
|
||||
});
|
||||
this._socket.addEventListener(
|
||||
"close",
|
||||
this._handleDisconnect.bind(this),
|
||||
);
|
||||
this._socket.addEventListener(
|
||||
"error",
|
||||
this._handleDisconnect.bind(this),
|
||||
);
|
||||
} catch { /* no-op */ }
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
private _toggleAuto() {
|
||||
this.auto = !this.auto;
|
||||
}
|
||||
|
||||
protected override render() {
|
||||
return html`
|
||||
<button type="button" class="widget" @click="${this._toggleAuto}">
|
||||
<div class="${classMap({
|
||||
indicator: true,
|
||||
connected: this._connected,
|
||||
})}"></div>
|
||||
<div class="label">
|
||||
${this.auto ? "Disable" : "Enable"} auto-reload
|
||||
</div>
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { LensControls } from "../lens-controls.ts";
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { AddSelectionButton } from "../add-selection-button.ts";
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
document.addEventListener("DOMContentLoaded", () => {
|
||||
document.querySelectorAll("table.viewer > tbody > tr").forEach((tr) => {
|
||||
const pkeyJson = tr.getAttribute("data-pkey");
|
||||
if (pkeyJson !== null) {
|
||||
tr.querySelectorAll(".cell").forEach((node) => {
|
||||
console.log(node);
|
||||
(node as unknown as { pkeyJson: string }).pkeyJson = pkeyJson;
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -1,214 +0,0 @@
|
|||
import { css, html, LitElement } from "lit";
|
||||
import { classMap } from "lit/directives/class-map.js";
|
||||
import { createRef, ref } from "lit/directives/ref.js";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
|
||||
import "./entrypoints/custom-icon.ts";
|
||||
|
||||
export type TabItem = {
|
||||
icon: string;
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export class SelectTabEvent extends Event {
|
||||
value: string;
|
||||
|
||||
constructor(value: string) {
|
||||
super("select-tab");
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement("lens-controls-shell")
|
||||
export class LensControlsShell extends LitElement {
|
||||
@property({ attribute: true, type: Array })
|
||||
tabs: TabItem[] = [];
|
||||
|
||||
@property({ attribute: "active-tab" })
|
||||
activeTab?: string;
|
||||
|
||||
@state()
|
||||
private _isOpen = false;
|
||||
|
||||
private _controlPanelRef = createRef<HTMLDivElement>();
|
||||
|
||||
static override styles = css`
|
||||
:host {
|
||||
--shadow: 0 0.5rem 0.5rem #3333;
|
||||
--background: #fff;
|
||||
--border-color: #ccc;
|
||||
--border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
#container-positioner {
|
||||
position: fixed;
|
||||
bottom: 2rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-end;
|
||||
overflow: visible;
|
||||
width: 100%;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
#container {
|
||||
display: grid;
|
||||
grid-template-columns: max-content max-content 1rem max-content;
|
||||
filter: drop-shadow(var(--shadow));
|
||||
}
|
||||
|
||||
#tab-box {
|
||||
height: 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
list-style-type: none;
|
||||
padding: 0.5rem;
|
||||
margin: 0;
|
||||
border: solid 1px var(--border-color);
|
||||
border-right: none;
|
||||
border-top-left-radius: var(--border-radius);
|
||||
border-bottom-left-radius: var(--border-radius);
|
||||
background: var(--background);
|
||||
grid-row: 2;
|
||||
|
||||
& button {
|
||||
appearance: none;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: inherit;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
& button.active {
|
||||
background: #39f3;
|
||||
}
|
||||
}
|
||||
|
||||
#control-bar {
|
||||
height: 3rem;
|
||||
flex-shrink: 0;
|
||||
border: solid 1px var(--border-color);
|
||||
border-top-right-radius: var(--border-radius);
|
||||
border-bottom-right-radius: var(--border-radius);
|
||||
overflow: hidden;
|
||||
width: 40rem;
|
||||
grid-row: 2;
|
||||
background: var(--background);
|
||||
|
||||
&.open {
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
#control-buttons {
|
||||
height: 3rem;
|
||||
grid-row: 2;
|
||||
grid-column: 4;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#control-panel-positioner {
|
||||
grid-template-columns: subgrid;
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
/* Flexbox positioning is required for Safari */
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
anchor-name: --control-bar;
|
||||
}
|
||||
|
||||
#control-panel-container:popover-open {
|
||||
inset: unset;
|
||||
border: solid 1px var(--border-color);
|
||||
border-bottom: none;
|
||||
border-top-left-radius: var(--border-radius);
|
||||
border-top-right-radius: var(--border-radius);
|
||||
margin: 0;
|
||||
position: fixed;
|
||||
display: block;
|
||||
width: 40rem;
|
||||
/* Anchor positioning is required for Chromium */
|
||||
position-anchor: --control-bar;
|
||||
position-area: top;
|
||||
padding: 0;
|
||||
background: var(--background);
|
||||
box-shadow: var(--shadow);
|
||||
/* Clip drop shadow */
|
||||
clip-path: polygon(
|
||||
-100% -100%,
|
||||
200% -100%,
|
||||
200% 200%,
|
||||
100% 200%,
|
||||
100% 100%,
|
||||
-100% 100%
|
||||
);
|
||||
}
|
||||
|
||||
#control-panel {
|
||||
padding: 0.5rem;
|
||||
overflow: auto;
|
||||
max-height: 8rem;
|
||||
}
|
||||
`;
|
||||
|
||||
open(): void {
|
||||
this._controlPanelRef.value?.showPopover();
|
||||
}
|
||||
|
||||
protected override render() {
|
||||
return html`
|
||||
<div id="container-positioner">
|
||||
<div id="container">
|
||||
<ul id="tab-box" @click="${this.open}">
|
||||
${this.tabs.map(({ icon, label, value }) =>
|
||||
html`
|
||||
<li>
|
||||
<button type="button" class="${classMap({
|
||||
active: this.activeTab === value,
|
||||
})}" @click="${() => {
|
||||
this.dispatchEvent(new SelectTabEvent(value));
|
||||
}}">
|
||||
${icon
|
||||
? html`<custom-icon name=${icon} alt=${label}></custom-label>`
|
||||
: label}
|
||||
</button>
|
||||
</li>
|
||||
`
|
||||
)}
|
||||
</ul>
|
||||
<div id="control-bar" class="${classMap({
|
||||
open: this._isOpen,
|
||||
})}" @click="${this.open}">
|
||||
<slot name="control-bar"></slot>
|
||||
</div>
|
||||
<div id="control-buttons">
|
||||
<slot name="control-buttons"></slot>
|
||||
</div>
|
||||
<div id="control-panel-positioner">
|
||||
<div
|
||||
id="control-panel-container"
|
||||
popover="auto"
|
||||
${ref(
|
||||
this._controlPanelRef,
|
||||
)}
|
||||
@toggle="${(ev: ToggleEvent) => {
|
||||
this._isOpen = ev.newState === "open";
|
||||
}}"
|
||||
>
|
||||
<div id="control-panel">
|
||||
<slot name="control-panel"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,191 +0,0 @@
|
|||
import { css, html, LitElement } from "lit";
|
||||
import { createRef, ref } from "lit/directives/ref.js";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
import "./lens-controls-shell.ts";
|
||||
import "./entrypoints/custom-icon.ts";
|
||||
import { type LensControlsShell } from "./lens-controls-shell.ts";
|
||||
import { type Selection } from "./selections.ts";
|
||||
export { SelectionFilters } from "./selection-filters.ts";
|
||||
|
||||
const TABS = [
|
||||
{ icon: "funnel", label: "Filters", value: "filters" },
|
||||
{ icon: "view-columns", label: "Columns", value: "columns" },
|
||||
];
|
||||
|
||||
@customElement("lens-controls")
|
||||
export class LensControls extends LitElement {
|
||||
@property({ attribute: true, type: Array })
|
||||
selections: Selection[] = [];
|
||||
|
||||
private _shellRef = createRef<LensControlsShell>();
|
||||
|
||||
static override styles = css`
|
||||
#control-bar {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
|
||||
& input {
|
||||
appearance: none;
|
||||
flex: 1;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-size: inherit;
|
||||
font-family: "Funnel Sans";
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
& .actions {
|
||||
flex: 0;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
#control-buttons {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
|
||||
& button {
|
||||
appearance: none;
|
||||
background: #447;
|
||||
border: none;
|
||||
color: #fff;
|
||||
font-size: inherit;
|
||||
font-family: inherit;
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
#selections {
|
||||
display: grid;
|
||||
grid-template-columns: max-content max-content max-content max-content 1fr;
|
||||
grid-gap: 0 1rem;
|
||||
}
|
||||
|
||||
.selection {
|
||||
display: grid;
|
||||
grid-column: 1 / 7;
|
||||
grid-template-columns: subgrid;
|
||||
justify-content: start;
|
||||
align-items: center;
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.25rem;
|
||||
transition: background 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: #9991;
|
||||
}
|
||||
|
||||
& .selection-filters {
|
||||
font-family: "Funnel Sans";
|
||||
}
|
||||
}
|
||||
|
||||
.label input {
|
||||
font-family: "Funnel Sans";
|
||||
font-size: inherit;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
border: none;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
private _handleVisibilityInput(ev: InputEvent, selectionId: string): void {
|
||||
this.selections = this.selections.map((
|
||||
selection,
|
||||
) => (selection.id === selectionId
|
||||
? {
|
||||
...selection,
|
||||
visible: (ev.target as HTMLInputElement).checked,
|
||||
}
|
||||
: selection)
|
||||
);
|
||||
}
|
||||
|
||||
private _handleSelectionFiltersChange(ev: Event, selectionId: string): void {
|
||||
// TODO
|
||||
}
|
||||
|
||||
protected override render() {
|
||||
return html`
|
||||
<form method="post" action="update-lens">
|
||||
<lens-controls-shell tabs="${JSON.stringify(
|
||||
TABS,
|
||||
)}" active-tab="columns" ${ref(this._shellRef)}>
|
||||
<div slot="control-bar" id="control-bar">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Configure columns..."
|
||||
@keydown="${() => {
|
||||
this._shellRef.value?.open();
|
||||
}}"
|
||||
>
|
||||
</div>
|
||||
<div slot="control-buttons" id="control-buttons">
|
||||
<button type="submit">Apply</button>
|
||||
</div>
|
||||
<div slot="control-panel" id="selections">
|
||||
${this.selections.map((selection) =>
|
||||
html`
|
||||
<div class="selection">
|
||||
<div class="visibility">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="selection_visibility_${selection.id}"
|
||||
?checked="${selection.visible}"
|
||||
@input="${(ev: InputEvent) =>
|
||||
this._handleVisibilityInput(ev, selection.id)}"
|
||||
>
|
||||
</div>
|
||||
<div class="selection-filters">
|
||||
<selection-filters
|
||||
selection-id="${selection.id}"
|
||||
filters="${JSON.stringify(selection.attr_filters)}"
|
||||
@change="${(ev: Event) =>
|
||||
this._handleSelectionFiltersChange(ev, selection.id)}"
|
||||
></selection-filters>
|
||||
</div>
|
||||
<div>Text</div>
|
||||
<div class="cast">
|
||||
<custom-icon name="arrow-right"></custom-icon>
|
||||
</div>
|
||||
<div class="label">
|
||||
<input
|
||||
type="text"
|
||||
name="selection_label_${selection.id}"
|
||||
@change="${(
|
||||
ev: Event,
|
||||
) => {
|
||||
this.selections = this.selections.map((
|
||||
iterSelection,
|
||||
) => (iterSelection.id === selection.id
|
||||
? {
|
||||
...iterSelection,
|
||||
label: (ev.target as HTMLInputElement).value.trim() ??
|
||||
undefined,
|
||||
}
|
||||
: iterSelection)
|
||||
);
|
||||
}}"
|
||||
value="${selection.label ?? ""}"
|
||||
placeholder="Customize name"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</lens-controls-shell>
|
||||
</form>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
import { type AttrFilter } from "./selections.ts";
|
||||
|
||||
@customElement("selection-filters")
|
||||
export class SelectionFilters extends LitElement {
|
||||
@property({ attribute: "selection-id" })
|
||||
selectionId = "";
|
||||
|
||||
@property({ attribute: true, type: Array })
|
||||
filters: AttrFilter[] = [];
|
||||
|
||||
static override styles = css`
|
||||
#selection-filters {
|
||||
display: flex;
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
protected override render() {
|
||||
return html`
|
||||
<ol id="selection-filters">
|
||||
${this.filters.map((filter) =>
|
||||
html`
|
||||
<li class="selection-filter">
|
||||
${"NameEq" in filter
|
||||
? html`
|
||||
<name-eq-filter>${filter.NameEq}</name-eq-filter>
|
||||
`
|
||||
: undefined} ${"NameMatches" in filter
|
||||
? html`
|
||||
<name-matches-filter>${filter.NameMatches}</name-matches-filter>
|
||||
`
|
||||
: undefined} ${"TypeEq" in filter
|
||||
? html`
|
||||
<type-eq-filter>${filter.TypeEq}</type-eq-filter>
|
||||
`
|
||||
: undefined}
|
||||
</li>
|
||||
`
|
||||
)}
|
||||
</ol>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement("name-eq-filter")
|
||||
export class NameEqFilter extends LitElement {
|
||||
protected override render() {
|
||||
return html`
|
||||
<div><slot></slot></div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
export type Selection = {
|
||||
id: string;
|
||||
attr_filters: AttrFilter[];
|
||||
display_type?: SelectionDisplayType;
|
||||
label?: string;
|
||||
visible: boolean;
|
||||
};
|
||||
|
||||
export type AttrFilter = {
|
||||
NameEq: string;
|
||||
} | {
|
||||
NameMatches: string;
|
||||
} | {
|
||||
TypeEq: string;
|
||||
};
|
||||
|
||||
export type SelectionDisplayType = "Text" | "InterimUser" | {
|
||||
Timestamp: {
|
||||
format: string;
|
||||
};
|
||||
};
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["deno.ns", "dom"],
|
||||
"experimentalDecorators": true,
|
||||
"useDefineForClassFields": false
|
||||
}
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
import { defineConfig } from "vite";
|
||||
import * as path from "jsr:@std/path";
|
||||
|
||||
import "lit";
|
||||
|
||||
const entrypointsDir = path.join(import.meta.dirname, "src/entrypoints");
|
||||
const entry = [...Deno.readDirSync(entrypointsDir)].map(({ name }) =>
|
||||
path.join(entrypointsDir, name)
|
||||
);
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
build: {
|
||||
lib: {
|
||||
entry,
|
||||
formats: ["es"],
|
||||
},
|
||||
outDir: "../js_dist",
|
||||
emptyOutDir: true,
|
||||
rollupOptions: {
|
||||
// external: /^lit/,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -49,26 +49,26 @@ impl Field {
|
|||
vec![]
|
||||
}
|
||||
|
||||
pub fn render(&self, value: &Encodable) -> String {
|
||||
match (self.field_type.0.clone(), value) {
|
||||
(FieldType::Integer, Encodable::Integer(Some(value))) => value.to_string(),
|
||||
(FieldType::Integer, Encodable::Integer(None)) => "".to_owned(),
|
||||
(FieldType::Integer, _) => "###".to_owned(),
|
||||
(FieldType::InterimUser, Encodable::Text(value)) => todo!(),
|
||||
(FieldType::InterimUser, _) => "###".to_owned(),
|
||||
(FieldType::Text, Encodable::Text(Some(value))) => value.clone(),
|
||||
(FieldType::Text, Encodable::Text(None)) => "".to_owned(),
|
||||
(FieldType::Text, _) => "###".to_owned(),
|
||||
(FieldType::Timestamp { format }, Encodable::Timestamptz(value)) => value
|
||||
.map(|value| value.format(&format).to_string())
|
||||
.unwrap_or("".to_owned()),
|
||||
(FieldType::Timestamp { .. }, _) => "###".to_owned(),
|
||||
(FieldType::Uuid, Encodable::Uuid(Some(value))) => value.hyphenated().to_string(),
|
||||
(FieldType::Uuid, Encodable::Uuid(None)) => "".to_owned(),
|
||||
(FieldType::Uuid, _) => "###".to_owned(),
|
||||
(FieldType::Unknown, _) => "###".to_owned(),
|
||||
}
|
||||
}
|
||||
// pub fn render(&self, value: &Encodable) -> String {
|
||||
// match (self.field_type.0.clone(), value) {
|
||||
// (FieldType::Integer, Encodable::Integer(Some(value))) => value.to_string(),
|
||||
// (FieldType::Integer, Encodable::Integer(None)) => "".to_owned(),
|
||||
// (FieldType::Integer, _) => "###".to_owned(),
|
||||
// (FieldType::InterimUser, Encodable::Text(value)) => todo!(),
|
||||
// (FieldType::InterimUser, _) => "###".to_owned(),
|
||||
// (FieldType::Text, Encodable::Text(Some(value))) => value.clone(),
|
||||
// (FieldType::Text, Encodable::Text(None)) => "".to_owned(),
|
||||
// (FieldType::Text, _) => "###".to_owned(),
|
||||
// (FieldType::Timestamp { format }, Encodable::Timestamptz(value)) => value
|
||||
// .map(|value| value.format(&format).to_string())
|
||||
// .unwrap_or("".to_owned()),
|
||||
// (FieldType::Timestamp { .. }, _) => "###".to_owned(),
|
||||
// (FieldType::Uuid, Encodable::Uuid(Some(value))) => value.hyphenated().to_string(),
|
||||
// (FieldType::Uuid, Encodable::Uuid(None)) => "".to_owned(),
|
||||
// (FieldType::Uuid, _) => "###".to_owned(),
|
||||
// (FieldType::Unknown, _) => "###".to_owned(),
|
||||
// }
|
||||
// }
|
||||
|
||||
pub fn get_value_encodable(&self, row: &PgRow) -> Result<Encodable, ParseError> {
|
||||
let value_ref = row
|
||||
|
|
@ -221,4 +221,15 @@ impl Encodable {
|
|||
Self::Uuid(value) => query.bind(value),
|
||||
}
|
||||
}
|
||||
|
||||
/// Transform the contained value into a serde_json::Value.
|
||||
pub fn inner_as_value(&self) -> serde_json::Value {
|
||||
let serialized = serde_json::to_value(self).unwrap();
|
||||
#[derive(Deserialize)]
|
||||
struct Tagged {
|
||||
c: serde_json::Value,
|
||||
}
|
||||
let deserialized: Tagged = serde_json::from_value(serialized).unwrap();
|
||||
deserialized.c
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,9 +9,12 @@
|
|||
</head>
|
||||
<body>
|
||||
{% block main %}{% endblock main %}
|
||||
<script type="module" src="{{ settings.root_path }}/js_dist/dev-reloader.mjs"></script>
|
||||
{% if settings.dev != 0 %}
|
||||
<dev-reloader ws="ws://127.0.0.1:8080{{ settings.root_path }}/__dev-healthz" auto=""></dev-reloader>
|
||||
<script type="module" src="{{ settings.root_path }}/dev_reloader.mjs"></script>
|
||||
<script type="module">
|
||||
import { initDevReloader } from "{{ settings.root_path }}/dev_reloader.mjs";
|
||||
initDevReloader("ws://127.0.0.1:8080{{ settings.root_path }}/__dev-healthz");
|
||||
</script>
|
||||
{% endif %}
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,8 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block main %}
|
||||
<script type="module" src="{{ settings.root_path }}/js_dist/field_adder_component.mjs"></script>
|
||||
<script type="module" src="{{ settings.root_path }}/js_dist/viewer_controller_component.mjs"></script>
|
||||
<script type="module" src="{{ settings.root_path }}/js_dist/cell_text_component.mjs"></script>
|
||||
<link rel="stylesheet" href="{{ settings.root_path }}/viewer.css">
|
||||
<viewer-controller root-path="{{ settings.root_path }}">
|
||||
<link rel="stylesheet" href="{{ settings.root_path }}/css_dist/viewer.css">
|
||||
<viewer-controller root-path="{{ settings.root_path }}" n-rows="{{ rows.len() }}" n-columns="{{ fields.len() }}">
|
||||
<table class="viewer">
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
@ -20,21 +17,24 @@
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in rows %}
|
||||
{% for (i, row) in rows.iter().enumerate() %}
|
||||
{# TODO: store primary keys in a Vec separate from rows #}
|
||||
<tr data-pkey="{{ row.pkeys | json }}">
|
||||
{% for field in fields %}
|
||||
<td>
|
||||
{% for (j, field) in fields.iter().enumerate() %}
|
||||
{# Setting max-width is required for overflow to work properly. #}
|
||||
<td style="width: {{ field.width_px }}px; max-width: {{ field.width_px }}px;">
|
||||
{% match field.get_value_encodable(row.data) %}
|
||||
{% when Ok with (encodable) %}
|
||||
<{{ field.webc_tag() | safe }}
|
||||
{% for (k, v) in field.webc_custom_attrs() %}
|
||||
{{ k }}="{{ v }}"
|
||||
{% endfor %}
|
||||
column="{{ field.name }}"
|
||||
value="{{ encodable | json }}"
|
||||
row="{{ i }}"
|
||||
column="{{ j }}"
|
||||
value="{{ encodable.inner_as_value() | json }}"
|
||||
class="cell"
|
||||
>
|
||||
{{ field.render(encodable) | safe }}
|
||||
{{ encodable.inner_as_value() | json }}
|
||||
</{{ field.webc_tag() | safe }}
|
||||
{% when Err with (err) %}
|
||||
<span class="pg-value-error">{{ err }}</span>
|
||||
|
|
@ -45,5 +45,11 @@
|
|||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<viewer-hoverbar root-path="{{ settings.root_path }}"></viewer-hoverbar>
|
||||
</viewer-controller>
|
||||
<script type="module" src="{{ settings.root_path }}/js_dist/field_adder_component.mjs"></script>
|
||||
<script type="module" src="{{ settings.root_path }}/js_dist/viewer_controller_component.mjs"></script>
|
||||
<script type="module" src="{{ settings.root_path }}/js_dist/cell_text_component.mjs"></script>
|
||||
<script type="module" src="{{ settings.root_path }}/js_dist/cell_uuid_component.mjs"></script>
|
||||
<script type="module" src="{{ settings.root_path }}/js_dist/viewer_hoverbar_component.mjs"></script>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,11 @@ $button-primary-color: #fff;
|
|||
$default-border: solid 1px #ccc;
|
||||
$font-family-default: 'Averia Serif Libre', 'Open Sans', 'Helvetica Neue', Arial, sans-serif;
|
||||
$font-family-data: 'Funnel Sans', 'Open Sans', 'Helvetica Neue', Arial, sans-serif;
|
||||
$font-family-mono: Menlo, 'Courier New', Courier, mono;
|
||||
$popover-border: $default-border;
|
||||
$popover-shadow: 0 0.5rem 0.5rem #3333;
|
||||
$border-radius-rounded-sm: 0.25rem;
|
||||
$border-radius-rounded: 0.5rem;
|
||||
|
||||
@mixin reset-button {
|
||||
appearance: none;
|
||||
|
|
@ -45,10 +48,20 @@ $popover-shadow: 0 0.5rem 0.5rem #3333;
|
|||
}
|
||||
}
|
||||
|
||||
@mixin reset-input {
|
||||
appearance: none;
|
||||
background: none;
|
||||
border: none;
|
||||
box-sizing: border-box;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
@mixin rounded-sm {
|
||||
border-radius: 0.25rem;
|
||||
border-radius: $border-radius-rounded-sm;
|
||||
}
|
||||
|
||||
@mixin rounded {
|
||||
border-radius: 0.5rem;
|
||||
border-radius: $border-radius-rounded;
|
||||
}
|
||||
|
|
|
|||
121
sass/_hoverbar.scss
Normal file
121
sass/_hoverbar.scss
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
@use 'globals';
|
||||
|
||||
$background-color: #fff;
|
||||
$tab-button-color-active: #0001;
|
||||
|
||||
.container-positioner {
|
||||
position: fixed;
|
||||
bottom: 2rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-end;
|
||||
overflow: visible;
|
||||
width: 100%;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: grid;
|
||||
grid-template-columns: max-content max-content 1rem max-content;
|
||||
filter: drop-shadow(globals.$popover-shadow);
|
||||
}
|
||||
|
||||
.tab-box {
|
||||
@include globals.rounded;
|
||||
height: 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
list-style-type: none;
|
||||
padding: 0.5rem;
|
||||
margin: 0;
|
||||
border: globals.$popover-border;
|
||||
border-right: none;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
background: $background-color;
|
||||
grid-row: 2;
|
||||
|
||||
&__button {
|
||||
@include globals.reset-button;
|
||||
@include globals.rounded-sm;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.25rem 0.5rem;
|
||||
cursor: pointer;
|
||||
height: 2rem;
|
||||
|
||||
&--active {
|
||||
background: $tab-button-color-active;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.control-bar {
|
||||
@include globals.rounded;
|
||||
height: 3rem;
|
||||
flex-shrink: 0;
|
||||
border: globals.$popover-border;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
overflow: hidden;
|
||||
width: 40rem;
|
||||
grid-row: 2;
|
||||
background: $background-color;
|
||||
|
||||
&--open {
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.control-buttons {
|
||||
height: 3rem;
|
||||
gird-row: 2;
|
||||
grid-column: 4;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.control-panel-positioner {
|
||||
grid-template-columns: subgrid;
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
/* Flexbox positioning is required for Safari */
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
anchor-name: --control-bar;
|
||||
}
|
||||
|
||||
.control-panel__container:popover-open {
|
||||
@include globals.rounded;
|
||||
inset: unset;
|
||||
border: globals.$popover-border;
|
||||
border-bottom: none;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
margin: 0;
|
||||
position: fixed;
|
||||
display: block;
|
||||
width: 40rem;
|
||||
/* Anchor positioning is required for Chromium */
|
||||
position-anchor: --control-bar;
|
||||
position-area: top;
|
||||
padding: 0;
|
||||
background: $background-color;
|
||||
box-shadow: globals.$popover-shadow;
|
||||
/* Clip drop shadow */
|
||||
clip-path: polygon(
|
||||
-100% -100%,
|
||||
200% -100%,
|
||||
200% 200%,
|
||||
100% 200%,
|
||||
100% 100%,
|
||||
-100% 100%
|
||||
);
|
||||
}
|
||||
|
||||
.control-panel {
|
||||
padding: 0.5rem;
|
||||
overflow: auto;
|
||||
max-height: 8rem;
|
||||
}
|
||||
34
sass/cells.scss
Normal file
34
sass/cells.scss
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
@use 'globals';
|
||||
|
||||
.cell__container {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: solid 2px transparent;
|
||||
|
||||
&--selected {
|
||||
border-color: #07f;
|
||||
}
|
||||
}
|
||||
|
||||
.cell__content {
|
||||
font-family: globals.$font-family-data;
|
||||
max-width: 100%;
|
||||
|
||||
&--padded {
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
&--null {
|
||||
opacity: 0.5;
|
||||
font-style: oblique;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&--uuid {
|
||||
font-family: globals.$font-family-mono;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
38
sass/viewer.scss
Normal file
38
sass/viewer.scss
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
table.viewer {
|
||||
border-collapse: collapse;
|
||||
height: 1px; /* css hack to make percentage based cell heights work */
|
||||
}
|
||||
|
||||
table.viewer > thead > tr > th {
|
||||
border: solid 1px #ccc;
|
||||
border-top: none;
|
||||
font-family: "Funnel Sans";
|
||||
background: #0001;
|
||||
height: 100%; /* css hack to make percentage based cell heights work */
|
||||
padding: 0 0.5rem;
|
||||
text-align: left;
|
||||
|
||||
&:first-child {
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
&.column-adder {
|
||||
border: none;
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
table.viewer .padded-cell {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
table.viewer > tbody > tr > td {
|
||||
border: solid 1px #ccc;
|
||||
height: 100%; /* css hack to make percentage based cell heights work */
|
||||
padding: 0;
|
||||
|
||||
&:first-child {
|
||||
border-left: none;
|
||||
}
|
||||
}
|
||||
10
sass/viewer_hoverbar.scss
Normal file
10
sass/viewer_hoverbar.scss
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
@use 'globals';
|
||||
@use 'hoverbar';
|
||||
|
||||
.control-bar__input {
|
||||
@include globals.reset-input;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0.5rem;
|
||||
font-family: globals.$font-family-data;
|
||||
}
|
||||
81
static/dev_reloader.mjs
Normal file
81
static/dev_reloader.mjs
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
export function initDevReloader(wsAddr, pollIntervalMs = 500) {
|
||||
// State model is implemented with variables and closures.
|
||||
let auto = true;
|
||||
let connected = false;
|
||||
let socket = undefined;
|
||||
let initialized = false;
|
||||
|
||||
const button = document.createElement("button");
|
||||
const indicator = document.createElement("div");
|
||||
const label = document.createElement("div");
|
||||
|
||||
// Rolling our own reactivity: call this after state change to initialize or
|
||||
// update DOM view.
|
||||
function render() {
|
||||
button.style.appearance = "none";
|
||||
button.style.background = "#fff";
|
||||
button.style.border = "solid 1px #0002";
|
||||
button.style.borderRadius = "999px";
|
||||
button.style.padding = "1rem";
|
||||
button.style.position = "fixed";
|
||||
button.style.zIndex = "999";
|
||||
button.style.bottom = "1rem";
|
||||
button.style.left = "1rem";
|
||||
button.style.opacity = "0.5";
|
||||
button.style.boxShadow = "0 0.5rem 1rem #0002";
|
||||
button.style.display = "flex";
|
||||
button.style.cursor = "pointer";
|
||||
button.style.alignItems = "center";
|
||||
button.style.fontFamily = "sans-serif";
|
||||
|
||||
indicator.style.width = "8px";
|
||||
indicator.style.height = "8px";
|
||||
indicator.style.borderRadius = "999px";
|
||||
indicator.style.background = connected ? "#06f" : "#f60";
|
||||
|
||||
label.style.marginLeft = "1rem";
|
||||
label.innerText = auto ? "Disable auto-reload" : "Enable auto-reload";
|
||||
}
|
||||
|
||||
function toggleAuto() {
|
||||
auto = !auto;
|
||||
render();
|
||||
}
|
||||
|
||||
function handleDisconnect() {
|
||||
if (connected || !initialized) {
|
||||
console.log("dev-reloader: disconnected");
|
||||
connected = false;
|
||||
socket = undefined;
|
||||
render();
|
||||
const intvl = setInterval(function () {
|
||||
try {
|
||||
socket = new WebSocket(wsAddr);
|
||||
socket.addEventListener("open", function () {
|
||||
console.log("dev-reloader: connected");
|
||||
clearInterval(intvl);
|
||||
if (auto && initialized) {
|
||||
globalThis.location.reload();
|
||||
}
|
||||
connected = true;
|
||||
initialized = true;
|
||||
render();
|
||||
});
|
||||
socket.addEventListener("close", handleDisconnect);
|
||||
socket.addEventListener("error", handleDisconnect);
|
||||
} catch { /* no-op */ }
|
||||
}, pollIntervalMs);
|
||||
}
|
||||
}
|
||||
|
||||
render();
|
||||
|
||||
button.setAttribute("type", "button");
|
||||
button.addEventListener("click", toggleAuto);
|
||||
button.appendChild(indicator);
|
||||
button.appendChild(label);
|
||||
document.body.appendChild(button);
|
||||
|
||||
// Simulate disconnect event to initialize.
|
||||
handleDisconnect();
|
||||
}
|
||||
|
|
@ -18,6 +18,8 @@ gleam_stdlib = ">= 0.44.0 and < 2.0.0"
|
|||
lustre = ">= 5.2.1 and < 6.0.0"
|
||||
gleam_json = ">= 3.0.2 and < 4.0.0"
|
||||
gleam_regexp = ">= 1.1.1 and < 2.0.0"
|
||||
plinth = ">= 0.7.1 and < 1.0.0"
|
||||
gleam_javascript = ">= 1.0.0 and < 2.0.0"
|
||||
|
||||
[dev-dependencies]
|
||||
gleeunit = ">= 1.0.0 and < 2.0.0"
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ packages = [
|
|||
{ name = "gleam_erlang", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "F91CE62A2D011FA13341F3723DB7DB118541AAA5FE7311BD2716D018F01EF9E3" },
|
||||
{ name = "gleam_http", version = "4.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "DB25DFC8530B64B77105405B80686541A0D96F7E2D83D807D6B2155FB9A8B1B8" },
|
||||
{ name = "gleam_httpc", version = "4.1.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "C670EBD46FC1472AD5F1F74F1D3938D1D0AC1C7531895ED1D4DDCB6F07279F43" },
|
||||
{ name = "gleam_javascript", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_javascript", source = "hex", outer_checksum = "EF6C77A506F026C6FB37941889477CD5E4234FCD4337FF0E9384E297CB8F97EB" },
|
||||
{ name = "gleam_json", version = "3.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "874FA3C3BB6E22DD2BB111966BD40B3759E9094E05257899A7C08F5DE77EC049" },
|
||||
{ name = "gleam_otp", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "7020E652D18F9ABAC9C877270B14160519FA0856EE80126231C505D719AD68DA" },
|
||||
{ name = "gleam_package_interface", version = "3.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_package_interface", source = "hex", outer_checksum = "8F2D19DE9876D9401BB0626260958A6B1580BB233489C32831FE74CE0ACAE8B4" },
|
||||
|
|
@ -33,6 +34,7 @@ packages = [
|
|||
{ name = "marceau", version = "1.3.0", build_tools = ["gleam"], requirements = [], otp_app = "marceau", source = "hex", outer_checksum = "2D1C27504BEF45005F5DFB18591F8610FB4BFA91744878210BDC464412EC44E9" },
|
||||
{ name = "mist", version = "5.0.2", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "gleam_yielder", "glisten", "gramps", "hpack_erl", "logging"], otp_app = "mist", source = "hex", outer_checksum = "0716CE491EA13E1AA1EFEC4B427593F8EB2B953B6EBDEBE41F15BE3D06A22918" },
|
||||
{ name = "platform", version = "1.0.0", build_tools = ["gleam"], requirements = [], otp_app = "platform", source = "hex", outer_checksum = "8339420A95AD89AAC0F82F4C3DB8DD401041742D6C3F46132A8739F6AEB75391" },
|
||||
{ name = "plinth", version = "0.7.1", build_tools = ["gleam"], requirements = ["gleam_javascript", "gleam_json", "gleam_stdlib"], otp_app = "plinth", source = "hex", outer_checksum = "63BB36AACCCCB82FBE46A862CF85CB88EBE4EF280ECDBAC4B6CB042340B9E1D8" },
|
||||
{ name = "repeatedly", version = "2.1.2", build_tools = ["gleam"], requirements = [], otp_app = "repeatedly", source = "hex", outer_checksum = "93AE1938DDE0DC0F7034F32C1BF0D4E89ACEBA82198A1FE21F604E849DA5F589" },
|
||||
{ name = "simplifile", version = "2.3.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "0A868DAC6063D9E983477981839810DC2E553285AB4588B87E3E9C96A7FB4CB4" },
|
||||
{ name = "snag", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "7E9F06390040EB5FAB392CE642771484136F2EC103A92AE11BA898C8167E6E17" },
|
||||
|
|
@ -43,9 +45,11 @@ packages = [
|
|||
]
|
||||
|
||||
[requirements]
|
||||
gleam_javascript = { version = ">= 1.0.0 and < 2.0.0" }
|
||||
gleam_json = { version = ">= 3.0.2 and < 4.0.0" }
|
||||
gleam_regexp = { version = ">= 1.1.1 and < 2.0.0" }
|
||||
gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" }
|
||||
gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
|
||||
lustre = { version = ">= 5.2.1 and < 6.0.0" }
|
||||
lustre_dev_tools = { version = ">= 1.9.0 and < 2.0.0" }
|
||||
plinth = { version = ">= 0.7.1 and < 1.0.0" }
|
||||
|
|
|
|||
136
webc/src/cell_common.gleam
Normal file
136
webc/src/cell_common.gleam
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
import gleam/dynamic
|
||||
import gleam/dynamic/decode
|
||||
import gleam/int
|
||||
import gleam/json
|
||||
import gleam/result
|
||||
import lustre/attribute as attr
|
||||
import lustre/component
|
||||
import lustre/effect.{type Effect}
|
||||
import lustre/element.{type Element}
|
||||
import lustre/element/html
|
||||
import lustre/event
|
||||
|
||||
import context
|
||||
|
||||
pub fn options_common(
|
||||
map_msg: fn(CommonMsg) -> msg,
|
||||
) -> List(component.Option(msg)) {
|
||||
[
|
||||
component.on_attribute_change("row", fn(value) {
|
||||
use value <- result.map(int.parse(value))
|
||||
ParentChangedRow(value) |> CommonMsg |> map_msg
|
||||
}),
|
||||
component.on_attribute_change("column", fn(value) {
|
||||
use value <- result.map(int.parse(value))
|
||||
ParentChangedColumn(value) |> CommonMsg |> map_msg
|
||||
}),
|
||||
component.on_attribute_change("selected", fn(value) {
|
||||
ParentChangedSelected(value != "") |> CommonMsg |> map_msg |> Ok
|
||||
}),
|
||||
component.on_attribute_change("editing", fn(value) {
|
||||
ParentChangedEditing(value != "") |> CommonMsg |> map_msg |> Ok
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
pub type ModelCommon {
|
||||
ModelCommon(
|
||||
root_path: String,
|
||||
row: Int,
|
||||
column: Int,
|
||||
selected: Bool,
|
||||
editing: Bool,
|
||||
)
|
||||
}
|
||||
|
||||
pub type Msg {
|
||||
AncestorChangedRootPath(String)
|
||||
ParentChangedEditing(Bool)
|
||||
ParentChangedRow(Int)
|
||||
ParentChangedColumn(Int)
|
||||
ParentChangedSelected(Bool)
|
||||
UserClickedCell
|
||||
}
|
||||
|
||||
pub fn init(_) -> #(ModelCommon, Effect(CommonMsg)) {
|
||||
#(
|
||||
ModelCommon(
|
||||
root_path: "",
|
||||
selected: False,
|
||||
editing: False,
|
||||
row: -1,
|
||||
column: -1,
|
||||
),
|
||||
context.request_context(
|
||||
context: dynamic.string("root_path"),
|
||||
subscribe: False,
|
||||
decoder: decode.string
|
||||
|> decode.map(fn(value) {
|
||||
value |> AncestorChangedRootPath |> CommonMsg
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
pub type CommonMsg {
|
||||
CommonMsg(msg: Msg)
|
||||
}
|
||||
|
||||
pub fn update(
|
||||
model: ModelCommon,
|
||||
msg_common: CommonMsg,
|
||||
) -> #(ModelCommon, Effect(msg)) {
|
||||
case msg_common.msg {
|
||||
AncestorChangedRootPath(root_path) -> #(
|
||||
ModelCommon(..model, root_path:),
|
||||
effect.none(),
|
||||
)
|
||||
ParentChangedRow(row) -> #(ModelCommon(..model, row:), effect.none())
|
||||
ParentChangedColumn(column) -> #(
|
||||
ModelCommon(..model, column:),
|
||||
effect.none(),
|
||||
)
|
||||
ParentChangedEditing(editing) -> #(
|
||||
ModelCommon(..model, editing:),
|
||||
effect.none(),
|
||||
)
|
||||
ParentChangedSelected(selected) -> #(
|
||||
ModelCommon(..model, selected:),
|
||||
effect.none(),
|
||||
)
|
||||
UserClickedCell -> #(
|
||||
model,
|
||||
event.emit(
|
||||
"cell-click",
|
||||
json.object([
|
||||
#("row", json.int(model.row)),
|
||||
#("column", json.int(model.column)),
|
||||
]),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn view(
|
||||
model model: ModelCommon,
|
||||
map_msg map_msg: fn(CommonMsg) -> msg,
|
||||
inner inner: fn() -> Element(msg),
|
||||
) -> Element(msg) {
|
||||
element.fragment([
|
||||
html.link([
|
||||
attr.rel("stylesheet"),
|
||||
attr.href(model.root_path <> "/css_dist/cells.css"),
|
||||
]),
|
||||
html.div(
|
||||
[
|
||||
attr.class("cell__container"),
|
||||
case model.selected {
|
||||
True -> attr.class("cell__container--selected")
|
||||
False -> attr.none()
|
||||
},
|
||||
event.on_click(CommonMsg(UserClickedCell) |> map_msg),
|
||||
],
|
||||
[inner()],
|
||||
),
|
||||
])
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
import gleam/dynamic
|
||||
import gleam/dynamic/decode
|
||||
import gleam/json
|
||||
import gleam/option.{type Option}
|
||||
import gleam/result
|
||||
import lustre.{type App}
|
||||
import lustre/attribute as attr
|
||||
import lustre/component
|
||||
|
|
@ -7,7 +9,7 @@ import lustre/effect.{type Effect}
|
|||
import lustre/element.{type Element}
|
||||
import lustre/element/html
|
||||
|
||||
import context
|
||||
import cell_common.{type CommonMsg, type ModelCommon}
|
||||
|
||||
pub const name: String = "cell-text"
|
||||
|
||||
|
|
@ -16,73 +18,56 @@ pub fn component() -> App(Nil, Model, Msg) {
|
|||
component.on_attribute_change("value", fn(value) {
|
||||
ParentChangedValue(value) |> Ok
|
||||
}),
|
||||
component.on_attribute_change("column", fn(value) {
|
||||
ParentChangedColumn(value) |> Ok
|
||||
}),
|
||||
component.on_attribute_change("selected", fn(value) {
|
||||
ParentChangedSelected(value != "") |> Ok
|
||||
}),
|
||||
component.on_attribute_change("editing", fn(value) {
|
||||
ParentChangedEditing(value != "") |> Ok
|
||||
}),
|
||||
component.on_property_change("pkeys", {
|
||||
decode.list(of: decode.string) |> decode.map(ParentChangedPkeys)
|
||||
}),
|
||||
..cell_common.options_common(Common)
|
||||
])
|
||||
}
|
||||
|
||||
pub type Model {
|
||||
Model(
|
||||
root_path: String,
|
||||
column: String,
|
||||
pkeys: List(String),
|
||||
selected: Bool,
|
||||
editing: Bool,
|
||||
)
|
||||
Model(common: ModelCommon, value: Option(String))
|
||||
}
|
||||
|
||||
fn init(_) -> #(Model, Effect(Msg)) {
|
||||
let #(model_common, effect_common) = cell_common.init(Nil)
|
||||
#(
|
||||
Model(
|
||||
root_path: "",
|
||||
column: "",
|
||||
pkeys: [""],
|
||||
selected: False,
|
||||
editing: False,
|
||||
),
|
||||
context.request_context(
|
||||
context: dynamic.string("root_path"),
|
||||
subscribe: False,
|
||||
decoder: decode.string |> decode.map(AncestorChangedRootPath),
|
||||
),
|
||||
Model(common: model_common, value: option.None),
|
||||
effect_common
|
||||
|> effect.map(fn(effect_common) { Common(effect_common) }),
|
||||
)
|
||||
}
|
||||
|
||||
pub type Msg {
|
||||
AncestorChangedRootPath(String)
|
||||
ParentChangedColumn(String)
|
||||
ParentChangedEditing(Bool)
|
||||
Common(CommonMsg)
|
||||
ParentChangedValue(String)
|
||||
ParentChangedSelected(Bool)
|
||||
ParentChangedPkeys(List(String))
|
||||
}
|
||||
|
||||
fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
|
||||
case msg {
|
||||
AncestorChangedRootPath(root_path) -> #(
|
||||
Model(..model, root_path:),
|
||||
Common(sub_msg) -> {
|
||||
let #(common, effect) = cell_common.update(model.common, sub_msg)
|
||||
#(Model(..model, common:), effect)
|
||||
}
|
||||
ParentChangedValue(value) -> #(
|
||||
Model(
|
||||
..model,
|
||||
value: json.parse(value, decode.optional(decode.string))
|
||||
|> result.unwrap(option.None),
|
||||
),
|
||||
effect.none(),
|
||||
)
|
||||
_ -> #(model, effect.none())
|
||||
}
|
||||
}
|
||||
|
||||
fn view(model: Model) -> Element(Msg) {
|
||||
element.fragment([
|
||||
html.link([
|
||||
attr.rel("stylesheet"),
|
||||
attr.href(model.root_path <> "/css_dist/cell_text/index.css"),
|
||||
]),
|
||||
html.div([], [html.text(model.root_path)]),
|
||||
])
|
||||
cell_common.view(model: model.common, map_msg: Common, inner: fn() {
|
||||
html.div(
|
||||
[
|
||||
attr.class("cell__content cell__content--padded"),
|
||||
case option.is_none(model.value) {
|
||||
True -> attr.class("cell__content--null")
|
||||
False -> attr.none()
|
||||
},
|
||||
],
|
||||
[html.text(model.value |> option.unwrap("Null"))],
|
||||
)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
73
webc/src/cell_uuid_component.gleam
Normal file
73
webc/src/cell_uuid_component.gleam
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import gleam/dynamic/decode
|
||||
import gleam/json
|
||||
import gleam/option.{type Option}
|
||||
import gleam/result
|
||||
import lustre.{type App}
|
||||
import lustre/attribute as attr
|
||||
import lustre/component
|
||||
import lustre/effect.{type Effect}
|
||||
import lustre/element.{type Element}
|
||||
import lustre/element/html
|
||||
|
||||
import cell_common.{type CommonMsg, type ModelCommon}
|
||||
|
||||
pub const name: String = "cell-uuid"
|
||||
|
||||
pub fn component() -> App(Nil, Model, Msg) {
|
||||
lustre.component(init, update, view, [
|
||||
component.on_attribute_change("value", fn(value) {
|
||||
ParentChangedValue(value) |> Ok
|
||||
}),
|
||||
..cell_common.options_common(Common)
|
||||
])
|
||||
}
|
||||
|
||||
pub type Model {
|
||||
Model(common: ModelCommon, value: Option(String))
|
||||
}
|
||||
|
||||
fn init(_) -> #(Model, Effect(Msg)) {
|
||||
let #(model_common, effect_common) = cell_common.init(Nil)
|
||||
#(
|
||||
Model(common: model_common, value: option.None),
|
||||
effect_common
|
||||
|> effect.map(fn(effect_common) { Common(effect_common) }),
|
||||
)
|
||||
}
|
||||
|
||||
pub type Msg {
|
||||
Common(CommonMsg)
|
||||
ParentChangedValue(String)
|
||||
}
|
||||
|
||||
fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
|
||||
case msg {
|
||||
Common(sub_msg) -> {
|
||||
let #(common, effect) = cell_common.update(model.common, sub_msg)
|
||||
#(Model(..model, common:), effect)
|
||||
}
|
||||
ParentChangedValue(value) -> #(
|
||||
Model(
|
||||
..model,
|
||||
value: json.parse(value, decode.optional(decode.string))
|
||||
|> result.unwrap(option.None),
|
||||
),
|
||||
effect.none(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn view(model: Model) -> Element(Msg) {
|
||||
cell_common.view(model: model.common, map_msg: Common, inner: fn() {
|
||||
html.div(
|
||||
[
|
||||
attr.class("cell__content cell__content--padded"),
|
||||
case option.is_none(model.value) {
|
||||
True -> attr.class("cell__content--null")
|
||||
False -> attr.class("cell__content--uuid")
|
||||
},
|
||||
],
|
||||
[html.text(model.value |> option.unwrap("Null"))],
|
||||
)
|
||||
})
|
||||
}
|
||||
|
|
@ -17,11 +17,10 @@ pub const name: String = "field-adder"
|
|||
pub fn component() -> App(Nil, Model, Msg) {
|
||||
lustre.component(init, update, view, [
|
||||
component.on_attribute_change("columns", fn(value) {
|
||||
Ok(
|
||||
json.parse(from: value, using: decode.list(of: decode.string))
|
||||
|> result.unwrap([])
|
||||
|> ParentChangedColumns,
|
||||
)
|
||||
|> ParentChangedColumns
|
||||
|> Ok
|
||||
}),
|
||||
component.on_attribute_change("root-path", fn(value) {
|
||||
ParentChangedRootPath(value) |> Ok
|
||||
|
|
@ -118,7 +117,7 @@ fn focus_element(selector: String) -> Effect(Msg) {
|
|||
do_focus_element(selector:, in: shadow_root) |> result.unwrap(Nil)
|
||||
}
|
||||
|
||||
@external(javascript, "./field_adder.ffi.mjs", "focusElement")
|
||||
@external(javascript, "./field_adder_component.ffi.mjs", "focusElement")
|
||||
fn do_focus_element(
|
||||
selector _selector: String,
|
||||
in _root: Dynamic,
|
||||
|
|
|
|||
6
webc/src/hoverbar.ffi.mjs
Normal file
6
webc/src/hoverbar.ffi.mjs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export function showPopover(root) {
|
||||
const controlPanel = root.querySelector(".control-panel__container");
|
||||
if (controlPanel) {
|
||||
controlPanel.showPopover();
|
||||
}
|
||||
}
|
||||
136
webc/src/hoverbar.gleam
Normal file
136
webc/src/hoverbar.gleam
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
import gleam/dynamic.{type Dynamic}
|
||||
import gleam/dynamic/decode
|
||||
import gleam/list
|
||||
import gleam/option.{type Option, None, Some}
|
||||
import lustre/attribute as attr
|
||||
import lustre/effect.{type Effect}
|
||||
import lustre/element.{type Element}
|
||||
import lustre/element/html
|
||||
import lustre/event
|
||||
|
||||
pub type Model {
|
||||
Model(tab_key: String, control_panel_open: Bool)
|
||||
}
|
||||
|
||||
pub type TabItem {
|
||||
TabItem(icon: String, label: String, key: String)
|
||||
}
|
||||
|
||||
pub fn init(_) -> #(Model, Effect(Msg)) {
|
||||
#(Model(tab_key: "", control_panel_open: False), effect.none())
|
||||
}
|
||||
|
||||
pub type Msg {
|
||||
UserClickedControlBar
|
||||
UserClickedTabBox
|
||||
UserClickedTab(String)
|
||||
SomeoneToggledControlPanel(Bool)
|
||||
}
|
||||
|
||||
pub fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
|
||||
case msg {
|
||||
UserClickedControlBar | UserClickedTabBox -> #(model, show_popover())
|
||||
UserClickedTab(tab_key) -> #(Model(..model, tab_key:), show_popover())
|
||||
SomeoneToggledControlPanel(control_panel_open) -> #(
|
||||
Model(..model, control_panel_open:),
|
||||
effect.none(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn show_popover() -> Effect(Msg) {
|
||||
use _, root <- effect.before_paint
|
||||
do_show_popover(root)
|
||||
}
|
||||
|
||||
@external(javascript, "./hoverbar.ffi.mjs", "showPopover")
|
||||
fn do_show_popover(in _root: Dynamic) -> Nil {
|
||||
Nil
|
||||
}
|
||||
|
||||
pub fn view(
|
||||
model model: Model,
|
||||
map_msg map_msg: fn(Msg) -> msg,
|
||||
tabs tabs: List(TabItem),
|
||||
control_bar control_bar: fn(String) -> Element(msg),
|
||||
control_panel control_panel: fn(String) -> Option(Element(msg)),
|
||||
) -> Element(msg) {
|
||||
html.div([attr.class("container-positioner")], [
|
||||
html.div([attr.class("container")], [
|
||||
tab_box(model, map_msg, tabs),
|
||||
view_control_bar(model, map_msg, control_bar(model.tab_key)),
|
||||
view_control_panel(model, map_msg, control_panel(model.tab_key)),
|
||||
]),
|
||||
])
|
||||
}
|
||||
|
||||
fn tab_box(
|
||||
model: Model,
|
||||
map_msg: fn(Msg) -> msg,
|
||||
tabs: List(TabItem),
|
||||
) -> Element(msg) {
|
||||
html.ul(
|
||||
[attr.class("tab-box"), event.on_click(UserClickedTabBox |> map_msg)],
|
||||
tabs
|
||||
|> list.map(fn(tab) {
|
||||
html.li([], [
|
||||
html.button(
|
||||
[
|
||||
attr.type_("button"),
|
||||
attr.class("tab-box__button"),
|
||||
case model.tab_key == tab.key {
|
||||
True -> attr.class("tab-box__button--active")
|
||||
False -> attr.none()
|
||||
},
|
||||
event.on_click(UserClickedTab(tab.key) |> map_msg),
|
||||
],
|
||||
[html.img([attr.src(tab.icon), attr.alt(tab.label)])],
|
||||
),
|
||||
])
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn view_control_bar(
|
||||
model: Model,
|
||||
map_msg: fn(Msg) -> msg,
|
||||
inner: Element(msg),
|
||||
) -> Element(msg) {
|
||||
html.div(
|
||||
[
|
||||
attr.class("control-bar"),
|
||||
case model.control_panel_open {
|
||||
True -> attr.class("control-bar--open")
|
||||
False -> attr.none()
|
||||
},
|
||||
event.on_click(UserClickedControlBar |> map_msg),
|
||||
],
|
||||
[inner],
|
||||
)
|
||||
}
|
||||
|
||||
fn view_control_panel(
|
||||
_: Model,
|
||||
map_msg: fn(Msg) -> msg,
|
||||
inner: Option(Element(msg)),
|
||||
) -> Element(msg) {
|
||||
case inner {
|
||||
Some(inner_element) ->
|
||||
html.div([attr.class("control-panel-positioner")], [
|
||||
html.div(
|
||||
[
|
||||
attr.class("control-panel__container"),
|
||||
attr.popover("auto"),
|
||||
event.on("toggle", {
|
||||
use new_state <- decode.field("newState", decode.string)
|
||||
decode.success(
|
||||
SomeoneToggledControlPanel(new_state == "open") |> map_msg,
|
||||
)
|
||||
}),
|
||||
],
|
||||
[html.div([attr.class("control-panel")], [inner_element])],
|
||||
),
|
||||
])
|
||||
None -> element.none()
|
||||
}
|
||||
}
|
||||
16
webc/src/viewer_controller_component.ffi.mjs
Normal file
16
webc/src/viewer_controller_component.ffi.mjs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
export function clearSelectedAttrs() {
|
||||
document.querySelectorAll(
|
||||
"table.viewer > tbody > tr > td > [selected='true']",
|
||||
)
|
||||
.forEach((element) => element.setAttribute("selected", ""));
|
||||
}
|
||||
|
||||
export function setSelectedAttr(row, column) {
|
||||
const tr = document.querySelectorAll("table.viewer > tbody > tr")[row];
|
||||
if (tr) {
|
||||
const cell = [...tr.querySelectorAll(":scope > td > *")][column];
|
||||
if (cell) {
|
||||
cell.setAttribute("selected", "true");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,18 @@
|
|||
import gleam/dynamic.{type Dynamic}
|
||||
import gleam/dynamic/decode
|
||||
import gleam/int
|
||||
import gleam/io
|
||||
import gleam/list
|
||||
import gleam/option.{type Option, Some}
|
||||
import gleam/result
|
||||
import lustre.{type App}
|
||||
import lustre/component
|
||||
import lustre/effect.{type Effect}
|
||||
import lustre/element.{type Element}
|
||||
import lustre/element/html
|
||||
import lustre/event
|
||||
import plinth/browser/document
|
||||
import plinth/browser/event as plinth_event
|
||||
|
||||
import context
|
||||
|
||||
|
|
@ -14,20 +23,61 @@ pub fn component() -> App(Nil, Model, Msg) {
|
|||
component.on_attribute_change("root-path", fn(value) {
|
||||
ParentChangedRootPath(value) |> Ok
|
||||
}),
|
||||
component.on_attribute_change("n-rows", fn(value) {
|
||||
int.parse(value) |> result.map(ParentChangedNRows)
|
||||
}),
|
||||
component.on_attribute_change("n-columns", fn(value) {
|
||||
int.parse(value) |> result.map(ParentChangedNColumns)
|
||||
}),
|
||||
component.on_attribute_change("root-path", fn(value) {
|
||||
ParentChangedRootPath(value) |> Ok
|
||||
}),
|
||||
])
|
||||
}
|
||||
|
||||
pub type Model {
|
||||
Model(root_path: String, root_path_consumers: List(Dynamic))
|
||||
Model(
|
||||
root_path: String,
|
||||
root_path_consumers: List(Dynamic),
|
||||
selected_row: Option(Int),
|
||||
selected_column: Option(Int),
|
||||
editing: Bool,
|
||||
n_rows: Int,
|
||||
n_columns: Int,
|
||||
)
|
||||
}
|
||||
|
||||
fn init(_) -> #(Model, Effect(Msg)) {
|
||||
#(Model(root_path: "", root_path_consumers: []), effect.none())
|
||||
#(
|
||||
Model(
|
||||
root_path: "",
|
||||
root_path_consumers: [],
|
||||
selected_row: option.None,
|
||||
selected_column: option.None,
|
||||
editing: False,
|
||||
n_rows: -1,
|
||||
n_columns: -1,
|
||||
),
|
||||
effect.before_paint(fn(dispatch, _) -> Nil {
|
||||
document.add_event_listener("keydown", fn(ev) -> Nil {
|
||||
let key = plinth_event.key(ev)
|
||||
case key {
|
||||
"ArrowLeft" | "ArrowRight" | "ArrowUp" | "ArrowDown" ->
|
||||
dispatch(UserPressedDirectionalKey(key))
|
||||
_ -> Nil
|
||||
}
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
pub type Msg {
|
||||
ParentChangedRootPath(String)
|
||||
ParentChangedNRows(Int)
|
||||
ParentChangedNColumns(Int)
|
||||
ChildRequestedRootPath(Dynamic, Bool)
|
||||
UserClickedCell(Int, Int)
|
||||
UserPressedDirectionalKey(String)
|
||||
}
|
||||
|
||||
fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
|
||||
|
|
@ -39,6 +89,11 @@ fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
|
|||
dynamic.string(root_path),
|
||||
),
|
||||
)
|
||||
ParentChangedNRows(n_rows) -> #(Model(..model, n_rows:), effect.none())
|
||||
ParentChangedNColumns(n_columns) -> #(
|
||||
Model(..model, n_columns:),
|
||||
effect.none(),
|
||||
)
|
||||
ChildRequestedRootPath(callback, subscribe) -> #(
|
||||
case subscribe {
|
||||
True ->
|
||||
|
|
@ -50,8 +105,86 @@ fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
|
|||
},
|
||||
context.update_consumers([callback], dynamic.string(model.root_path)),
|
||||
)
|
||||
UserClickedCell(row, column) -> #(
|
||||
Model(
|
||||
..model,
|
||||
selected_row: option.Some(row),
|
||||
selected_column: option.Some(column),
|
||||
),
|
||||
move_selection(to_row: option.Some(row), to_column: Some(column)),
|
||||
)
|
||||
UserPressedDirectionalKey(key) -> {
|
||||
case model.editing, model.selected_row, model.selected_column {
|
||||
False, Some(selected_row), Some(selected_column) -> {
|
||||
let first_row_selected = selected_row == 0
|
||||
let last_row_selected = selected_row == model.n_rows - 1
|
||||
let first_col_selected = selected_column == 0
|
||||
let last_col_selected = selected_column == model.n_columns - 1
|
||||
case
|
||||
key,
|
||||
first_row_selected,
|
||||
last_row_selected,
|
||||
first_col_selected,
|
||||
last_col_selected
|
||||
{
|
||||
"ArrowLeft", _, _, False, _ -> #(
|
||||
Model(..model, selected_column: option.Some(selected_column - 1)),
|
||||
move_selection(
|
||||
to_row: model.selected_row,
|
||||
to_column: Some(selected_column - 1),
|
||||
),
|
||||
)
|
||||
"ArrowRight", _, _, _, False -> #(
|
||||
Model(..model, selected_column: option.Some(selected_column + 1)),
|
||||
move_selection(
|
||||
to_row: model.selected_row,
|
||||
to_column: Some(selected_column + 1),
|
||||
),
|
||||
)
|
||||
"ArrowUp", False, _, _, _ -> #(
|
||||
Model(..model, selected_row: option.Some(selected_row - 1)),
|
||||
move_selection(
|
||||
to_row: Some(selected_row - 1),
|
||||
to_column: model.selected_column,
|
||||
),
|
||||
)
|
||||
"ArrowDown", _, False, _, _ -> #(
|
||||
Model(..model, selected_row: option.Some(selected_row + 1)),
|
||||
move_selection(
|
||||
to_row: Some(selected_row + 1),
|
||||
to_column: model.selected_column,
|
||||
),
|
||||
)
|
||||
_, _, _, _, _ -> #(model, effect.none())
|
||||
}
|
||||
}
|
||||
_, _, _ -> #(model, effect.none())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn move_selection(
|
||||
to_row to_row: Option(Int),
|
||||
to_column to_column: Option(Int),
|
||||
) -> Effect(msg) {
|
||||
use _dispatch, _root <- effect.before_paint()
|
||||
do_clear_selected_attrs()
|
||||
case to_row, to_column {
|
||||
option.Some(row), option.Some(column) -> do_set_selected_attr(row:, column:)
|
||||
_, _ -> Nil
|
||||
}
|
||||
}
|
||||
|
||||
@external(javascript, "./viewer_controller_component.ffi.mjs", "clearSelectedAttrs")
|
||||
fn do_clear_selected_attrs() -> Nil {
|
||||
Nil
|
||||
}
|
||||
|
||||
@external(javascript, "./viewer_controller_component.ffi.mjs", "setSelectedAttr")
|
||||
fn do_set_selected_attr(row _row: Int, column _column: Int) -> Nil {
|
||||
Nil
|
||||
}
|
||||
|
||||
fn view(_: Model) -> Element(Msg) {
|
||||
html.div(
|
||||
|
|
@ -60,6 +193,14 @@ fn view(_: Model) -> Element(Msg) {
|
|||
dynamic.string("root_path"),
|
||||
ChildRequestedRootPath,
|
||||
),
|
||||
event.on("cell-click", {
|
||||
use msg <- decode.field("detail", {
|
||||
use row <- decode.field("row", decode.int)
|
||||
use column <- decode.field("column", decode.int)
|
||||
decode.success(UserClickedCell(row, column))
|
||||
})
|
||||
decode.success(msg)
|
||||
}),
|
||||
],
|
||||
[component.default_slot([], [])],
|
||||
)
|
||||
|
|
|
|||
112
webc/src/viewer_hoverbar_component.gleam
Normal file
112
webc/src/viewer_hoverbar_component.gleam
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
import gleam/option.{None, Some}
|
||||
import lustre.{type App}
|
||||
import lustre/attribute as attr
|
||||
import lustre/component
|
||||
import lustre/effect.{type Effect}
|
||||
import lustre/element.{type Element}
|
||||
import lustre/element/html
|
||||
|
||||
import hoverbar
|
||||
|
||||
pub const name: String = "viewer-hoverbar"
|
||||
|
||||
pub fn component() -> App(Nil, Model, Msg) {
|
||||
lustre.component(init, update, view, [
|
||||
component.on_attribute_change("root-path", fn(value) {
|
||||
ParentChangedRootPath(value) |> Ok
|
||||
}),
|
||||
component.on_attribute_change("field-type", fn(value) {
|
||||
ParentChangedFieldType(value) |> Ok
|
||||
}),
|
||||
component.on_attribute_change("value", fn(value) {
|
||||
ParentChangedValue(value) |> Ok
|
||||
}),
|
||||
])
|
||||
}
|
||||
|
||||
pub type Model {
|
||||
Model(
|
||||
hoverbar_model: hoverbar.Model,
|
||||
root_path: String,
|
||||
field_type: String,
|
||||
value: String,
|
||||
)
|
||||
}
|
||||
|
||||
fn init(_) -> #(Model, Effect(Msg)) {
|
||||
let #(hoverbar_model, hoverbar_effect) = hoverbar.init(Nil)
|
||||
#(
|
||||
Model(
|
||||
hoverbar_model: hoverbar.Model(..hoverbar_model, tab_key: "editor"),
|
||||
root_path: "",
|
||||
field_type: "",
|
||||
value: "",
|
||||
),
|
||||
hoverbar_effect |> effect.map(HoverbarMsg),
|
||||
)
|
||||
}
|
||||
|
||||
pub type Msg {
|
||||
HoverbarMsg(hoverbar.Msg)
|
||||
ParentChangedRootPath(String)
|
||||
ParentChangedFieldType(String)
|
||||
ParentChangedValue(String)
|
||||
}
|
||||
|
||||
fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
|
||||
case msg {
|
||||
HoverbarMsg(hoverbar_msg) -> {
|
||||
let #(hoverbar_model, hoverbar_effect) =
|
||||
hoverbar.update(model.hoverbar_model, hoverbar_msg)
|
||||
#(
|
||||
Model(..model, hoverbar_model:),
|
||||
hoverbar_effect |> effect.map(HoverbarMsg),
|
||||
)
|
||||
}
|
||||
ParentChangedRootPath(root_path) -> #(
|
||||
Model(..model, root_path:),
|
||||
effect.none(),
|
||||
)
|
||||
ParentChangedFieldType(field_type) -> #(
|
||||
Model(..model, field_type:),
|
||||
effect.none(),
|
||||
)
|
||||
ParentChangedValue(value) -> #(Model(..model, value:), effect.none())
|
||||
}
|
||||
}
|
||||
|
||||
fn view(model: Model) -> Element(Msg) {
|
||||
element.fragment([
|
||||
html.link([
|
||||
attr.rel("stylesheet"),
|
||||
attr.href(model.root_path <> "/css_dist/viewer_hoverbar.css"),
|
||||
]),
|
||||
hoverbar.view(
|
||||
model: model.hoverbar_model,
|
||||
map_msg: HoverbarMsg,
|
||||
tabs: [
|
||||
hoverbar.TabItem(
|
||||
icon: model.root_path <> "/heroicons/16/solid/chart-bar.svg",
|
||||
label: "Editor",
|
||||
key: "editor",
|
||||
),
|
||||
],
|
||||
control_bar: fn(tab_key) {
|
||||
case tab_key {
|
||||
"editor" -> editor_control_bar(model)
|
||||
_ -> element.none()
|
||||
}
|
||||
},
|
||||
control_panel: fn(tab_key) {
|
||||
case tab_key {
|
||||
"editor" -> None
|
||||
_ -> None
|
||||
}
|
||||
},
|
||||
),
|
||||
])
|
||||
}
|
||||
|
||||
fn editor_control_bar(_model: Model) -> Element(Msg) {
|
||||
html.input([attr.type_("text"), attr.class("control-bar__input")])
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue