@necrolab/dashboard 0.5.30 → 0.5.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.html +5 -6
- package/package.json +5 -5
- package/postinstall.js +86 -101
- package/public/manifest.json +4 -4
- package/scripts/build.mjs +40 -0
- package/scripts/generate-sw.mjs +43 -0
- package/src/components/Editors/Account/AccountView.vue +17 -1
- package/src/components/Editors/Profile/ProfileView.vue +17 -1
- package/src/components/Tasks/TaskView.vue +26 -10
- package/src/composables/useDynamicTableHeight.js +23 -6
- package/src/registerServiceWorker.js +40 -27
- package/workbox-config.cjs +27 -22
- /package/public/{android-chrome-192x192.png → img/pwa/android-chrome-192x192.png} +0 -0
- /package/public/{android-chrome-512x512.png → img/pwa/android-chrome-512x512.png} +0 -0
- /package/public/{apple-touch-icon.png → img/pwa/apple-touch-icon.png} +0 -0
package/index.html
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<link rel="icon" href="/favicon.ico" />
|
|
6
|
+
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
|
7
|
+
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
|
6
8
|
<meta
|
|
7
9
|
name="viewport"
|
|
8
10
|
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
|
@@ -97,14 +99,11 @@
|
|
|
97
99
|
<link rel="prefetch" as="image" href="/flags/ae.svg" />
|
|
98
100
|
|
|
99
101
|
<!-- Prefetch other logo variants -->
|
|
100
|
-
<link rel="prefetch" as="image" href="/android-chrome-192x192.png" />
|
|
102
|
+
<link rel="prefetch" as="image" href="/img/pwa/android-chrome-192x192.png" />
|
|
101
103
|
|
|
102
|
-
|
|
103
|
-
<link rel="prefetch" as="script" href="/sw.js" />
|
|
104
|
-
|
|
105
|
-
<link rel="manifest" href="/manifest.json?v=3" crossorigin />
|
|
104
|
+
<link rel="manifest" href="/manifest.json" crossorigin />
|
|
106
105
|
<meta name="theme-color" content="#1a1b1e" />
|
|
107
|
-
<link rel="apple-touch-icon" href="/apple-touch-icon.png
|
|
106
|
+
<link rel="apple-touch-icon" href="/img/pwa/apple-touch-icon.png" />
|
|
108
107
|
|
|
109
108
|
<!-- Prism.js for syntax highlighting -->
|
|
110
109
|
<link
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@necrolab/dashboard",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.32",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
|
-
"build": "
|
|
6
|
+
"build": "node scripts/build.mjs",
|
|
7
7
|
"dev": "node dev-server.js",
|
|
8
8
|
"bot": "node dev-server.js",
|
|
9
9
|
"expose": "node dev-server.js",
|
|
@@ -38,7 +38,8 @@
|
|
|
38
38
|
"vue-virtual-scroller": "^2.0.0-beta.8",
|
|
39
39
|
"vue3-toastify": "^0.0.4",
|
|
40
40
|
"vuedraggable": "^4.1.0",
|
|
41
|
-
"websocket-heartbeat-js": "^1.1.3"
|
|
41
|
+
"websocket-heartbeat-js": "^1.1.3",
|
|
42
|
+
"workbox-build": "^7.3.0"
|
|
42
43
|
},
|
|
43
44
|
"main": "index.js",
|
|
44
45
|
"devDependencies": {
|
|
@@ -46,8 +47,7 @@
|
|
|
46
47
|
"@eslint/js": "^9.39.2",
|
|
47
48
|
"eslint": "^9.17.0",
|
|
48
49
|
"eslint-plugin-vue": "^9.32.0",
|
|
49
|
-
"globals": "^17.3.0"
|
|
50
|
-
"workbox-cli": "^7.3.0"
|
|
50
|
+
"globals": "^17.3.0"
|
|
51
51
|
},
|
|
52
52
|
"overrides": {
|
|
53
53
|
"glob": "^11.0.0",
|
package/postinstall.js
CHANGED
|
@@ -1,124 +1,109 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
-
import { execSync } from "node:child_process";
|
|
3
2
|
import path from "node:path";
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
|
|
6
|
-
const
|
|
6
|
+
const packageRoot = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const buildScriptPath = path.join(packageRoot, "scripts", "build.mjs");
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
const packageJsonPath = path.join(
|
|
10
|
-
let currentVersion = "0.0.0";
|
|
11
|
-
try {
|
|
9
|
+
function readPackageVersion() {
|
|
10
|
+
const packageJsonPath = path.join(packageRoot, "package.json");
|
|
12
11
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
13
|
-
|
|
14
|
-
} catch (e) {
|
|
15
|
-
console.error(`Failed to read package.json version: ${e.message}`);
|
|
12
|
+
return packageJson.version || "0.0.0";
|
|
16
13
|
}
|
|
17
14
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
} else {
|
|
29
|
-
// Running as a dependency - build to parent project's dashboard/dist
|
|
30
|
-
const projectRoot = path.resolve(__dirname, "../../..");
|
|
31
|
-
const dashboardDir = path.join(projectRoot, "dashboard");
|
|
32
|
-
distPath = path.join(dashboardDir, "dist");
|
|
33
|
-
versionFilePath = path.join(distPath, "version.txt");
|
|
15
|
+
function isDependencyInstall() {
|
|
16
|
+
return packageRoot.includes(`${path.sep}node_modules${path.sep}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getHostProjectRoot() {
|
|
20
|
+
const marker = `${path.sep}node_modules${path.sep}`;
|
|
21
|
+
const markerIndex = packageRoot.lastIndexOf(marker);
|
|
22
|
+
|
|
23
|
+
if (markerIndex === -1) {
|
|
24
|
+
return packageRoot;
|
|
34
25
|
}
|
|
35
26
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
console.log(`Version ${currentVersion} is already installed at ${distPath}. Skipping build.`);
|
|
43
|
-
process.exit(0);
|
|
44
|
-
} else {
|
|
45
|
-
console.log(
|
|
46
|
-
`Version mismatch (Installed: ${installedVersion}, Target: ${currentVersion}). Proceeding with build.`
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
} catch {
|
|
50
|
-
console.log("Could not read version file. Proceeding with build.");
|
|
51
|
-
}
|
|
52
|
-
} else {
|
|
53
|
-
console.log("No version file found. Proceeding with build.");
|
|
27
|
+
return packageRoot.slice(0, markerIndex);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function resolveTargetDistPath() {
|
|
31
|
+
if (!isDependencyInstall()) {
|
|
32
|
+
return path.join(packageRoot, "dist");
|
|
54
33
|
}
|
|
55
34
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
cwd: __dirname,
|
|
63
|
-
stdio: "inherit"
|
|
64
|
-
});
|
|
65
|
-
} catch (error) {
|
|
66
|
-
console.error("Failed to generate service worker.");
|
|
67
|
-
throw error;
|
|
35
|
+
return path.join(getHostProjectRoot(), "dashboard", "dist");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function readInstalledVersion(versionFilePath) {
|
|
39
|
+
if (!fs.existsSync(versionFilePath)) {
|
|
40
|
+
return null;
|
|
68
41
|
}
|
|
69
42
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
|
|
43
|
+
return fs.readFileSync(versionFilePath, "utf-8").trim() || null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function runBuildPipeline() {
|
|
47
|
+
execFileSync(process.execPath, [buildScriptPath], {
|
|
48
|
+
cwd: packageRoot,
|
|
49
|
+
stdio: "inherit"
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function ensureDirectory(directoryPath) {
|
|
54
|
+
if (!fs.existsSync(directoryPath)) {
|
|
55
|
+
fs.mkdirSync(directoryPath, { recursive: true });
|
|
80
56
|
}
|
|
57
|
+
}
|
|
81
58
|
|
|
82
|
-
|
|
59
|
+
function copyBuiltDistToTarget(targetDistPath) {
|
|
60
|
+
const sourceDistPath = path.join(packageRoot, "dist");
|
|
61
|
+
if (!fs.existsSync(sourceDistPath)) {
|
|
62
|
+
throw new Error(`Build failed: ${sourceDistPath} does not exist.`);
|
|
63
|
+
}
|
|
83
64
|
|
|
84
|
-
if (
|
|
85
|
-
|
|
65
|
+
if (path.resolve(sourceDistPath) === path.resolve(targetDistPath)) {
|
|
66
|
+
return;
|
|
86
67
|
}
|
|
87
68
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
// Running as dependency - move dist to parent project
|
|
95
|
-
console.log("Moving build artifacts...");
|
|
96
|
-
|
|
97
|
-
// Ensure target directory exists
|
|
98
|
-
const dashboardDir = path.dirname(distPath);
|
|
99
|
-
if (!fs.existsSync(dashboardDir)) {
|
|
100
|
-
console.log(`Creating target directory: ${dashboardDir}`);
|
|
101
|
-
fs.mkdirSync(dashboardDir, { recursive: true });
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Clean destination
|
|
105
|
-
if (fs.existsSync(distPath)) {
|
|
106
|
-
fs.rmSync(distPath, { recursive: true, force: true });
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Copy
|
|
110
|
-
fs.cpSync(buildPath, distPath, { recursive: true });
|
|
111
|
-
|
|
112
|
-
// Write version file
|
|
113
|
-
fs.writeFileSync(versionFilePath, currentVersion);
|
|
114
|
-
console.log(`Written version ${currentVersion} to ${versionFilePath}`);
|
|
115
|
-
|
|
116
|
-
// Cleanup source dist
|
|
117
|
-
fs.rmSync(buildPath, { recursive: true, force: true });
|
|
69
|
+
ensureDirectory(path.dirname(targetDistPath));
|
|
70
|
+
fs.rmSync(targetDistPath, { recursive: true, force: true });
|
|
71
|
+
fs.cpSync(sourceDistPath, targetDistPath, { recursive: true });
|
|
72
|
+
|
|
73
|
+
if (isDependencyInstall()) {
|
|
74
|
+
fs.rmSync(sourceDistPath, { recursive: true, force: true });
|
|
118
75
|
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function writeVersionFile(versionFilePath, version) {
|
|
79
|
+
ensureDirectory(path.dirname(versionFilePath));
|
|
80
|
+
fs.writeFileSync(versionFilePath, version);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function runPostinstall() {
|
|
84
|
+
const targetDistPath = resolveTargetDistPath();
|
|
85
|
+
const versionFilePath = path.join(targetDistPath, "version.txt");
|
|
86
|
+
const currentVersion = readPackageVersion();
|
|
87
|
+
const installedVersion = readInstalledVersion(versionFilePath);
|
|
88
|
+
|
|
89
|
+
console.log(`[postinstall] Target dist: ${targetDistPath}`);
|
|
90
|
+
console.log(`[postinstall] Package version: ${currentVersion}`);
|
|
119
91
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
92
|
+
if (installedVersion === currentVersion) {
|
|
93
|
+
console.log("[postinstall] Dashboard bundle is already up to date. Skipping build.");
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
runBuildPipeline();
|
|
98
|
+
copyBuiltDistToTarget(targetDistPath);
|
|
99
|
+
writeVersionFile(versionFilePath, currentVersion);
|
|
100
|
+
|
|
101
|
+
console.log("[postinstall] Dashboard bundle is ready.");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
runPostinstall();
|
|
106
|
+
} catch (error) {
|
|
107
|
+
console.error(`[postinstall] Failed: ${error.message}`);
|
|
123
108
|
process.exit(1);
|
|
124
109
|
}
|
package/public/manifest.json
CHANGED
|
@@ -3,24 +3,24 @@
|
|
|
3
3
|
"name": "Necro Dashboard",
|
|
4
4
|
"icons": [
|
|
5
5
|
{
|
|
6
|
-
"src": "/android-chrome-192x192.png
|
|
6
|
+
"src": "/img/pwa/android-chrome-192x192.png",
|
|
7
7
|
"type": "image/png",
|
|
8
8
|
"sizes": "192x192",
|
|
9
9
|
"purpose": "any"
|
|
10
10
|
},
|
|
11
11
|
{
|
|
12
|
-
"src": "/android-chrome-512x512.png
|
|
12
|
+
"src": "/img/pwa/android-chrome-512x512.png",
|
|
13
13
|
"type": "image/png",
|
|
14
14
|
"sizes": "512x512",
|
|
15
15
|
"purpose": "maskable"
|
|
16
16
|
},
|
|
17
17
|
{
|
|
18
|
-
"src": "/android-chrome-512x512.png
|
|
18
|
+
"src": "/img/pwa/android-chrome-512x512.png",
|
|
19
19
|
"type": "image/png",
|
|
20
20
|
"sizes": "512x512"
|
|
21
21
|
},
|
|
22
22
|
{
|
|
23
|
-
"src": "/apple-touch-icon.png
|
|
23
|
+
"src": "/img/pwa/apple-touch-icon.png",
|
|
24
24
|
"type": "image/png",
|
|
25
25
|
"sizes": "180x180"
|
|
26
26
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { generateServiceWorker } from "./generate-sw.mjs";
|
|
7
|
+
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
const scriptsDirectory = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const projectRoot = path.resolve(scriptsDirectory, "..");
|
|
11
|
+
const distDirectory = path.join(projectRoot, "dist");
|
|
12
|
+
|
|
13
|
+
function runNodeScript(scriptPath, args = []) {
|
|
14
|
+
const commandResult = spawnSync(process.execPath, [scriptPath, ...args], {
|
|
15
|
+
cwd: projectRoot,
|
|
16
|
+
stdio: "inherit"
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
if (commandResult.status !== 0) {
|
|
20
|
+
const exitCode = commandResult.status ?? 1;
|
|
21
|
+
throw new Error(`Command failed (${path.basename(scriptPath)}) with exit code ${exitCode}.`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function runViteBuild() {
|
|
26
|
+
const vitePackageJsonPath = require.resolve("vite/package.json");
|
|
27
|
+
const viteBinPath = path.join(path.dirname(vitePackageJsonPath), "bin", "vite.js");
|
|
28
|
+
runNodeScript(viteBinPath, ["build"]);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function runBuild() {
|
|
32
|
+
fs.rmSync(distDirectory, { recursive: true, force: true });
|
|
33
|
+
runViteBuild();
|
|
34
|
+
await generateServiceWorker(projectRoot);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
runBuild().catch((error) => {
|
|
38
|
+
console.error(`[build] Failed: ${error.message}`);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
const require = createRequire(import.meta.url);
|
|
7
|
+
const currentFilePath = fileURLToPath(import.meta.url);
|
|
8
|
+
|
|
9
|
+
export async function generateServiceWorker(projectRoot) {
|
|
10
|
+
const rootPath = projectRoot ?? path.resolve(path.dirname(currentFilePath), "..");
|
|
11
|
+
const configPath = path.join(rootPath, "workbox-config.cjs");
|
|
12
|
+
|
|
13
|
+
if (!fs.existsSync(configPath)) {
|
|
14
|
+
throw new Error(`Missing Workbox config: ${configPath}`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const baseConfig = require(configPath);
|
|
18
|
+
const { generateSW } = require("workbox-build");
|
|
19
|
+
|
|
20
|
+
const normalizedConfig = {
|
|
21
|
+
...baseConfig,
|
|
22
|
+
globDirectory: path.resolve(rootPath, baseConfig.globDirectory ?? "dist"),
|
|
23
|
+
swDest: path.resolve(rootPath, baseConfig.swDest ?? "dist/sw.js")
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const { count, size, warnings } = await generateSW(normalizedConfig);
|
|
27
|
+
|
|
28
|
+
if (warnings?.length) {
|
|
29
|
+
for (const warning of warnings) {
|
|
30
|
+
console.warn(`[workbox] ${warning}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const totalKilobytes = (size / 1024).toFixed(2);
|
|
35
|
+
console.log(`[workbox] Generated service worker with ${count} entries (${totalKilobytes} KiB).`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === currentFilePath) {
|
|
39
|
+
generateServiceWorker().catch((error) => {
|
|
40
|
+
console.error(`[workbox] Failed: ${error.message}`);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
</div>
|
|
31
31
|
</Header>
|
|
32
32
|
<RecycleScroller
|
|
33
|
-
v-if="props.accounts.length !== 0"
|
|
33
|
+
v-if="props.accounts.length !== 0 && useVirtualScroller"
|
|
34
34
|
class="hidden-scrollbars touch-pan-y overflow-y-auto overflow-x-hidden transition-colors duration-150 table-scroll scrollable"
|
|
35
35
|
:style="{ height: dynamicTableHeight, maxHeight: dynamicTableHeight }"
|
|
36
36
|
:items="virtualAccounts"
|
|
@@ -46,6 +46,20 @@
|
|
|
46
46
|
</div>
|
|
47
47
|
</template>
|
|
48
48
|
</RecycleScroller>
|
|
49
|
+
<div
|
|
50
|
+
v-else-if="props.accounts.length !== 0"
|
|
51
|
+
class="hidden-scrollbars touch-pan-y overflow-y-auto overflow-x-hidden transition-colors duration-150 table-scroll scrollable"
|
|
52
|
+
:style="{ maxHeight: dynamicTableHeight }">
|
|
53
|
+
<div
|
|
54
|
+
v-for="(item, index) in virtualAccounts"
|
|
55
|
+
:key="item.virtualKey"
|
|
56
|
+
class="min-h-16 flex-shrink-0 hover:bg-dark-550">
|
|
57
|
+
<Account
|
|
58
|
+
:class="getRowClass(index)"
|
|
59
|
+
:account="item.account"
|
|
60
|
+
:privacy="props.privacy" />
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
49
63
|
<EmptyState v-else :icon="MailIcon" message="No accounts found" subtitle="Create accounts to get started" />
|
|
50
64
|
</div>
|
|
51
65
|
</template>
|
|
@@ -89,6 +103,8 @@ const virtualAccounts = computed(() =>
|
|
|
89
103
|
virtualKey: String(account.id ?? account.email ?? "account")
|
|
90
104
|
}))
|
|
91
105
|
);
|
|
106
|
+
const ACCOUNT_VIRTUALIZE_THRESHOLD = 80;
|
|
107
|
+
const useVirtualScroller = computed(() => props.accounts.length > ACCOUNT_VIRTUALIZE_THRESHOLD);
|
|
92
108
|
|
|
93
109
|
const handleVirtualWheel = (event) => {
|
|
94
110
|
const target = event.currentTarget;
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
</div>
|
|
35
35
|
</Header>
|
|
36
36
|
<RecycleScroller
|
|
37
|
-
v-if="props.profiles.length !== 0"
|
|
37
|
+
v-if="props.profiles.length !== 0 && useVirtualScroller"
|
|
38
38
|
class="hidden-scrollbars touch-pan-y overflow-y-auto overflow-x-hidden transition-colors duration-150 table-scroll scrollable"
|
|
39
39
|
:style="{ height: dynamicTableHeight, maxHeight: dynamicTableHeight }"
|
|
40
40
|
:items="virtualProfiles"
|
|
@@ -50,6 +50,20 @@
|
|
|
50
50
|
</div>
|
|
51
51
|
</template>
|
|
52
52
|
</RecycleScroller>
|
|
53
|
+
<div
|
|
54
|
+
v-else-if="props.profiles.length !== 0"
|
|
55
|
+
class="hidden-scrollbars touch-pan-y overflow-y-auto overflow-x-hidden transition-colors duration-150 table-scroll scrollable"
|
|
56
|
+
:style="{ maxHeight: dynamicTableHeight }">
|
|
57
|
+
<div
|
|
58
|
+
v-for="(item, index) in virtualProfiles"
|
|
59
|
+
:key="item.virtualKey"
|
|
60
|
+
class="min-h-16 flex-shrink-0 hover:bg-dark-550">
|
|
61
|
+
<Profile
|
|
62
|
+
:class="getRowClass(index)"
|
|
63
|
+
:profile="item.profile"
|
|
64
|
+
:privacy="props.privacy" />
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
53
67
|
<EmptyState v-else :icon="ProfileIcon" message="No profiles found" subtitle="Create profiles to get started" />
|
|
54
68
|
</div>
|
|
55
69
|
</template>
|
|
@@ -88,6 +102,8 @@ const virtualProfiles = computed(() =>
|
|
|
88
102
|
virtualKey: String(profile.id ?? `${profile.profileName ?? "profile"}-${profile.cardNumber ?? ""}`)
|
|
89
103
|
}))
|
|
90
104
|
);
|
|
105
|
+
const PROFILE_VIRTUALIZE_THRESHOLD = 80;
|
|
106
|
+
const useVirtualScroller = computed(() => props.profiles.length > PROFILE_VIRTUALIZE_THRESHOLD);
|
|
91
107
|
|
|
92
108
|
const handleVirtualWheel = (event) => {
|
|
93
109
|
const target = event.currentTarget;
|
|
@@ -35,9 +35,9 @@
|
|
|
35
35
|
</div>
|
|
36
36
|
</Header>
|
|
37
37
|
<DynamicScroller
|
|
38
|
-
v-if="virtualTaskItems.length"
|
|
38
|
+
v-if="virtualTaskItems.length && useVirtualScroller"
|
|
39
39
|
class="hidden-scrollbars touch-pan-y min-h-0 overflow-y-auto overflow-x-hidden scrollable"
|
|
40
|
-
:style="{ height:
|
|
40
|
+
:style="{ height: maxTableHeight, maxHeight: maxTableHeight }"
|
|
41
41
|
:items="virtualTaskItems"
|
|
42
42
|
:min-item-size="virtualMinItemSize"
|
|
43
43
|
:buffer="virtualBuffer"
|
|
@@ -66,10 +66,25 @@
|
|
|
66
66
|
</DynamicScrollerItem>
|
|
67
67
|
</template>
|
|
68
68
|
</DynamicScroller>
|
|
69
|
+
<div
|
|
70
|
+
v-else-if="virtualTaskItems.length"
|
|
71
|
+
class="hidden-scrollbars touch-pan-y min-h-0 overflow-y-auto overflow-x-hidden scrollable"
|
|
72
|
+
:style="{ maxHeight: maxTableHeight }">
|
|
73
|
+
<div
|
|
74
|
+
v-for="(item, index) in virtualTaskItems"
|
|
75
|
+
:key="item.taskId"
|
|
76
|
+
class="shrink-0 border-b border-dark-650 min-h-14.5 md:min-h-17.25 has-[.event-details]:min-h-18.75 mobile-portrait:min-h-12.5 transition-colors duration-150 ease-in-out hover:!bg-dark-550">
|
|
77
|
+
<Task
|
|
78
|
+
v-if="props.tasks[item.taskId]"
|
|
79
|
+
:task="props.tasks[item.taskId]"
|
|
80
|
+
:preferEventName="props.preferEventName"
|
|
81
|
+
:class="getRowClass(index)" />
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
69
84
|
<div
|
|
70
85
|
v-else
|
|
71
86
|
class="empty-state flex flex-col items-center justify-center py-8 text-center bg-dark-400 text-light-500 text-sm font-medium"
|
|
72
|
-
:style="{ minHeight:
|
|
87
|
+
:style="{ minHeight: emptyStateHeight }">
|
|
73
88
|
<div
|
|
74
89
|
v-if="
|
|
75
90
|
!ui.queueStats.queued && !ui.queueStats.sleeping && ui.queueStats.nextQueuePasses.length === 0
|
|
@@ -237,6 +252,9 @@ onUnmounted(() => {
|
|
|
237
252
|
|
|
238
253
|
const virtualMinItemSize = computed(() => (windowWidth.value <= 768 ? 58 : 69));
|
|
239
254
|
const virtualBuffer = computed(() => virtualMinItemSize.value * 8);
|
|
255
|
+
const TASK_VIRTUALIZE_THRESHOLD = 80;
|
|
256
|
+
const useVirtualScroller = computed(() => virtualTaskItems.value.length > TASK_VIRTUALIZE_THRESHOLD);
|
|
257
|
+
const emptyStateHeight = computed(() => `${virtualMinItemSize.value * 2}px`);
|
|
240
258
|
|
|
241
259
|
const handleVirtualWheel = (event) => {
|
|
242
260
|
const target = event.currentTarget;
|
|
@@ -244,7 +262,7 @@ const handleVirtualWheel = (event) => {
|
|
|
244
262
|
target.scrollTop += event.deltaY;
|
|
245
263
|
};
|
|
246
264
|
|
|
247
|
-
const
|
|
265
|
+
const maxTableHeight = computed(() => {
|
|
248
266
|
// Detect PWA mode (standalone display)
|
|
249
267
|
const isPWA = window.matchMedia('(display-mode: standalone)').matches;
|
|
250
268
|
|
|
@@ -280,12 +298,10 @@ const dynamicTableHeight = computed(() => {
|
|
|
280
298
|
|
|
281
299
|
// Calculate row height based on screen size
|
|
282
300
|
const rowHeight = windowWidth.value <= 768 ? 58 : 69; // Mobile vs desktop row height
|
|
283
|
-
const
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
const maxCompleteRows = Math.floor(Math.max(availableHeight, minHeight) / rowHeight) + 1;
|
|
288
|
-
const exactHeight = maxCompleteRows * rowHeight;
|
|
301
|
+
const safeAvailableHeight = Math.max(availableHeight, rowHeight * 2);
|
|
302
|
+
const desktopBonusRows = windowWidth.value >= 1024 ? 1 : 0;
|
|
303
|
+
const maxRowsThatFit = Math.max(1, Math.floor(safeAvailableHeight / rowHeight) + desktopBonusRows);
|
|
304
|
+
const exactHeight = maxRowsThatFit * rowHeight;
|
|
289
305
|
|
|
290
306
|
return exactHeight + "px";
|
|
291
307
|
});
|
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import { computed } from "vue";
|
|
1
|
+
import { computed, unref } from "vue";
|
|
2
2
|
import { useWindowDimensions } from "./useWindowDimensions";
|
|
3
3
|
|
|
4
|
-
export function useDynamicTableHeight(options = {}) {
|
|
4
|
+
export function useDynamicTableHeight(options = {}, itemCount = null) {
|
|
5
5
|
const { windowHeight, windowWidth } = useWindowDimensions();
|
|
6
6
|
|
|
7
7
|
const {
|
|
8
8
|
topReservedSpace = options.TOP_RESERVED_SPACE ?? 243,
|
|
9
9
|
bottomBuffer = options.BOTTOM_BUFFER ?? 16,
|
|
10
10
|
rowHeight = options.ROW_HEIGHT ?? 64,
|
|
11
|
-
minRowsToShow = options.MIN_ROWS_TO_SHOW ?? 2
|
|
11
|
+
minRowsToShow = options.MIN_ROWS_TO_SHOW ?? 2,
|
|
12
|
+
minRowsWhenEmpty = options.MIN_ROWS_WHEN_EMPTY ?? minRowsToShow
|
|
12
13
|
} = options;
|
|
13
14
|
|
|
14
15
|
const dynamicTableHeight = computed(() => {
|
|
@@ -22,9 +23,25 @@ export function useDynamicTableHeight(options = {}) {
|
|
|
22
23
|
const extraBuffer = isPWA && isMobile ? (isIPhonePortrait ? 24 : 40) : 0;
|
|
23
24
|
|
|
24
25
|
const availableHeight = windowHeight.value - topReservedSpace - bottomBuffer - extraBuffer;
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
|
|
26
|
+
const safeAvailableHeight = Math.max(availableHeight, rowHeight);
|
|
27
|
+
const maxRowsThatFit = Math.max(1, Math.floor(safeAvailableHeight / rowHeight));
|
|
28
|
+
|
|
29
|
+
if (itemCount === null || itemCount === undefined) {
|
|
30
|
+
const minRows = Math.max(1, minRowsToShow);
|
|
31
|
+
const exactHeight = Math.max(minRows, maxRowsThatFit) * rowHeight;
|
|
32
|
+
return exactHeight + "px";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const count = Number(unref(itemCount) || 0);
|
|
36
|
+
let targetRows = Math.max(1, minRowsWhenEmpty);
|
|
37
|
+
|
|
38
|
+
if (count > 0) {
|
|
39
|
+
targetRows = Math.max(1, Math.min(count, maxRowsThatFit));
|
|
40
|
+
} else {
|
|
41
|
+
targetRows = Math.min(maxRowsThatFit, Math.max(1, minRowsWhenEmpty));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const exactHeight = targetRows * rowHeight;
|
|
28
45
|
|
|
29
46
|
return exactHeight + "px";
|
|
30
47
|
});
|
|
@@ -2,31 +2,44 @@
|
|
|
2
2
|
|
|
3
3
|
import { register } from "register-service-worker";
|
|
4
4
|
|
|
5
|
-
if (
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
5
|
+
if ("serviceWorker" in navigator) {
|
|
6
|
+
if (import.meta.env.PROD) {
|
|
7
|
+
register("/sw.js", {
|
|
8
|
+
ready() {
|
|
9
|
+
console.log(
|
|
10
|
+
"App is being served from cache by a service worker.\n" +
|
|
11
|
+
"For more details, visit https://goo.gl/AFskqB"
|
|
12
|
+
);
|
|
13
|
+
},
|
|
14
|
+
registered() {
|
|
15
|
+
console.log("Service worker has been registered.");
|
|
16
|
+
},
|
|
17
|
+
cached() {
|
|
18
|
+
console.log("Content has been cached for offline use.");
|
|
19
|
+
},
|
|
20
|
+
updatefound() {
|
|
21
|
+
console.log("New content is downloading.");
|
|
22
|
+
},
|
|
23
|
+
updated() {
|
|
24
|
+
console.log("New content is available; please refresh.");
|
|
25
|
+
},
|
|
26
|
+
offline() {
|
|
27
|
+
console.log("No internet connection found. App is running in offline mode.");
|
|
28
|
+
},
|
|
29
|
+
error(error) {
|
|
30
|
+
console.error("Error during service worker registration:", error);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
} else {
|
|
34
|
+
window.addEventListener("load", () => {
|
|
35
|
+
navigator.serviceWorker
|
|
36
|
+
.getRegistrations()
|
|
37
|
+
.then((registrations) => {
|
|
38
|
+
for (const registration of registrations) {
|
|
39
|
+
registration.unregister();
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
.catch(() => {});
|
|
43
|
+
});
|
|
44
|
+
}
|
|
32
45
|
}
|
package/workbox-config.cjs
CHANGED
|
@@ -1,20 +1,25 @@
|
|
|
1
1
|
module.exports = {
|
|
2
2
|
globDirectory: "dist/",
|
|
3
3
|
globPatterns: [
|
|
4
|
-
//
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
"
|
|
10
|
-
// Manifest and other static files
|
|
4
|
+
// App shell + bundled assets
|
|
5
|
+
"index.html",
|
|
6
|
+
"assets/**/*.{js,css,woff2,woff,ttf,eot,png,ico,svg,jpg,jpeg,webp,gif}",
|
|
7
|
+
// Public static assets
|
|
8
|
+
"img/**/*.{png,ico,svg,jpg,jpeg,webp,gif}",
|
|
9
|
+
"flags/**/*.svg",
|
|
11
10
|
"manifest.json",
|
|
12
|
-
"
|
|
11
|
+
"favicon.ico",
|
|
12
|
+
"favicon-16x16.png",
|
|
13
|
+
"favicon-32x32.png",
|
|
14
|
+
"robots.txt"
|
|
13
15
|
],
|
|
16
|
+
globIgnores: ["sw.js", "workbox-*.js"],
|
|
14
17
|
swDest: "dist/sw.js",
|
|
18
|
+
mode: "production",
|
|
15
19
|
sourcemap: false,
|
|
16
20
|
skipWaiting: true,
|
|
17
21
|
clientsClaim: true,
|
|
22
|
+
navigateFallback: "/index.html",
|
|
18
23
|
ignoreURLParametersMatching: [/^utm_/, /^fbclid$/],
|
|
19
24
|
cleanupOutdatedCaches: true,
|
|
20
25
|
// Increase max size to handle larger bundles
|
|
@@ -33,6 +38,18 @@ module.exports = {
|
|
|
33
38
|
}
|
|
34
39
|
}
|
|
35
40
|
},
|
|
41
|
+
// Country flags
|
|
42
|
+
{
|
|
43
|
+
urlPattern: /\/flags\//,
|
|
44
|
+
handler: "CacheFirst",
|
|
45
|
+
options: {
|
|
46
|
+
cacheName: "country-flags",
|
|
47
|
+
expiration: {
|
|
48
|
+
maxEntries: 50,
|
|
49
|
+
maxAgeSeconds: 60 * 60 * 24 * 90 // 90 days
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
},
|
|
36
53
|
// Images - Cache first for best performance
|
|
37
54
|
{
|
|
38
55
|
urlPattern: /\.(?:png|jpg|jpeg|svg|gif|webp|ico)$/,
|
|
@@ -52,8 +69,8 @@ module.exports = {
|
|
|
52
69
|
options: {
|
|
53
70
|
cacheName: "static-resources",
|
|
54
71
|
expiration: {
|
|
55
|
-
maxEntries: 100,
|
|
56
|
-
maxAgeSeconds: 60 * 60 * 24 * 30
|
|
72
|
+
maxEntries: 100,
|
|
73
|
+
maxAgeSeconds: 60 * 60 * 24 * 30
|
|
57
74
|
}
|
|
58
75
|
}
|
|
59
76
|
},
|
|
@@ -77,18 +94,6 @@ module.exports = {
|
|
|
77
94
|
}
|
|
78
95
|
}
|
|
79
96
|
},
|
|
80
|
-
// Country flags
|
|
81
|
-
{
|
|
82
|
-
urlPattern: /\/flags\//,
|
|
83
|
-
handler: "CacheFirst",
|
|
84
|
-
options: {
|
|
85
|
-
cacheName: "country-flags",
|
|
86
|
-
expiration: {
|
|
87
|
-
maxEntries: 50,
|
|
88
|
-
maxAgeSeconds: 60 * 60 * 24 * 90 // 90 days
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
},
|
|
92
97
|
// Prism.js CDN resources
|
|
93
98
|
{
|
|
94
99
|
urlPattern: /^https:\/\/cdnjs\.cloudflare\.com\/ajax\/libs\/prism\//,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|