@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 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
- <!-- Prefetch PWA resources -->
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?v=3" />
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.30",
3
+ "version": "0.5.32",
4
4
  "type": "module",
5
5
  "scripts": {
6
- "build": "rm -rf dist && vite build && npx workbox-cli generateSW workbox-config.cjs",
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 __dirname = path.dirname(fileURLToPath(import.meta.url));
6
+ const packageRoot = path.dirname(fileURLToPath(import.meta.url));
7
+ const buildScriptPath = path.join(packageRoot, "scripts", "build.mjs");
7
8
 
8
- // Read package.json to get current version
9
- const packageJsonPath = path.join(__dirname, "package.json");
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
- currentVersion = packageJson.version;
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
- try {
19
- // Detect if we're running inside the project itself or as a dependency
20
- const isInsideProject = __dirname.includes("node_modules") === false;
21
-
22
- let distPath, versionFilePath;
23
-
24
- if (isInsideProject) {
25
- // Running inside the project itself - build to ./dist
26
- distPath = path.join(__dirname, "dist");
27
- versionFilePath = path.join(distPath, "version.txt");
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
- // Check if we can skip logic
37
- console.log(`Checking version at ${distPath}...`);
38
- if (fs.existsSync(versionFilePath)) {
39
- try {
40
- const installedVersion = fs.readFileSync(versionFilePath, "utf-8").trim();
41
- if (installedVersion === currentVersion) {
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
- console.log("Running postinstall build...");
57
-
58
- // Service Worker
59
- console.log("Generating service worker...");
60
- try {
61
- execSync(`npx workbox-cli generateSW workbox-config.cjs`, {
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
- // Build
71
- console.log("Building with Vite...");
72
- try {
73
- execSync(`npx vite build`, {
74
- cwd: __dirname,
75
- stdio: "inherit"
76
- });
77
- } catch (error) {
78
- console.error("Failed to build with Vite.");
79
- throw error;
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
- const buildPath = path.join(__dirname, "dist");
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 (!fs.existsSync(buildPath)) {
85
- throw new Error(`Build failed: ${buildPath} does not exist.`);
65
+ if (path.resolve(sourceDistPath) === path.resolve(targetDistPath)) {
66
+ return;
86
67
  }
87
68
 
88
- if (isInsideProject) {
89
- // Running inside project - dist is already in the right place, just write version
90
- console.log("Build artifacts already in correct location.");
91
- fs.writeFileSync(versionFilePath, currentVersion);
92
- console.log(`Written version ${currentVersion} to ${versionFilePath}`);
93
- } else {
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
- console.log("Postinstall completed successfully.");
121
- } catch (e) {
122
- console.error(`An error occurred: ${e.message}`);
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
  }
@@ -3,24 +3,24 @@
3
3
  "name": "Necro Dashboard",
4
4
  "icons": [
5
5
  {
6
- "src": "/android-chrome-192x192.png?v=3",
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?v=3",
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?v=3",
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?v=3",
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: dynamicTableHeight, maxHeight: dynamicTableHeight }"
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: dynamicTableHeight, maxHeight: dynamicTableHeight }">
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 dynamicTableHeight = computed(() => {
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 minRowsToShow = 2; // Always show at least 2 rows
284
- const minHeight = minRowsToShow * rowHeight;
285
-
286
- // Calculate how many complete rows can fit
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 minHeight = minRowsToShow * rowHeight;
26
- const maxCompleteRows = Math.floor(Math.max(availableHeight, minHeight) / rowHeight);
27
- const exactHeight = maxCompleteRows * rowHeight;
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 (process.env.NODE_ENV != "development") {
6
- register(`/sw.js`, {
7
- ready() {
8
- console.log(
9
- "App is being served from cache by a service worker.\n" +
10
- "For more details, visit https://goo.gl/AFskqB"
11
- );
12
- },
13
- registered() {
14
- console.log("Service worker has been registered.");
15
- },
16
- cached() {
17
- console.log("Content has been cached for offline use.");
18
- },
19
- updatefound() {
20
- console.log("New content is downloading.");
21
- },
22
- updated() {
23
- console.log("New content is available; please refresh.");
24
- },
25
- offline() {
26
- console.log("No internet connection found. App is running in offline mode.");
27
- },
28
- error(error) {
29
- console.error("Error during service worker registration:", error);
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
  }
@@ -1,20 +1,25 @@
1
1
  module.exports = {
2
2
  globDirectory: "dist/",
3
3
  globPatterns: [
4
- // HTML files
5
- "**/*.html",
6
- // All built assets (JS, CSS)
7
- "assets/**/*.{js,css}",
8
- // All images (including bundled ones)
9
- "**/*.{png,ico,svg,jpg,jpeg,webp,gif}",
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
- "*.{png,ico,webp}"
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, // Increased for more assets
56
- maxAgeSeconds: 60 * 60 * 24 * 30 // 30 days (longer)
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\//,