@composy/log-viewer-core 0.0.1 → 0.0.2

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.
Files changed (108) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +175 -37
  3. package/dist/index.js +614 -299
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.min.js +548 -1
  6. package/dist/index.min.js.map +1 -0
  7. package/es/.dts-output-manifest.json +1 -0
  8. package/es/.dts.tsbuildinfo +1 -0
  9. package/es/core/EventEmitter.d.ts +0 -1
  10. package/es/core/EventEmitter.js +13 -9
  11. package/es/core/EventEmitter.js.map +1 -1
  12. package/es/core/LogViewer.d.ts +14 -11
  13. package/es/core/LogViewer.js +243 -64
  14. package/es/core/LogViewer.js.map +1 -1
  15. package/es/filters/FilterEngine.d.ts +0 -1
  16. package/es/filters/FilterEngine.js +31 -26
  17. package/es/filters/FilterEngine.js.map +1 -1
  18. package/es/index.d.ts +7 -7
  19. package/es/index.js +3 -3
  20. package/es/index.js.map +1 -1
  21. package/es/renderers/DiffRenderer.d.ts +0 -1
  22. package/es/renderers/DiffRenderer.js +13 -3
  23. package/es/renderers/DiffRenderer.js.map +1 -1
  24. package/es/renderers/TimelineRenderer.d.ts +7 -3
  25. package/es/renderers/TimelineRenderer.js +56 -22
  26. package/es/renderers/TimelineRenderer.js.map +1 -1
  27. package/es/styles/index.d.ts +1 -5
  28. package/es/styles/index.js +167 -131
  29. package/es/styles/index.js.map +1 -1
  30. package/es/types/index.d.ts +4 -1
  31. package/es/types/index.js +1 -1
  32. package/es/types/index.js.map +1 -1
  33. package/es/utils/index.d.ts +1 -2
  34. package/es/utils/index.js +54 -6
  35. package/es/utils/index.js.map +1 -1
  36. package/esm/core/EventEmitter.js +13 -9
  37. package/esm/core/EventEmitter.js.map +1 -1
  38. package/esm/core/LogViewer.js +243 -64
  39. package/esm/core/LogViewer.js.map +1 -1
  40. package/esm/filters/FilterEngine.js +31 -26
  41. package/esm/filters/FilterEngine.js.map +1 -1
  42. package/esm/index.js +2 -2
  43. package/esm/index.js.map +1 -1
  44. package/esm/renderers/DiffRenderer.js +13 -3
  45. package/esm/renderers/DiffRenderer.js.map +1 -1
  46. package/esm/renderers/TimelineRenderer.js +56 -22
  47. package/esm/renderers/TimelineRenderer.js.map +1 -1
  48. package/esm/styles/index.js +167 -131
  49. package/esm/styles/index.js.map +1 -1
  50. package/esm/types/index.js +1 -1
  51. package/esm/types/index.js.map +1 -1
  52. package/esm/utils/index.js +54 -6
  53. package/esm/utils/index.js.map +1 -1
  54. package/lib/core/EventEmitter.cjs +13 -9
  55. package/lib/core/EventEmitter.cjs.map +1 -1
  56. package/{esm/core/EventEmitter.d.ts → lib/core/EventEmitter.d.cts} +0 -1
  57. package/lib/core/LogViewer.cjs +247 -68
  58. package/lib/core/LogViewer.cjs.map +1 -1
  59. package/{esm/core/LogViewer.d.ts → lib/core/LogViewer.d.cts} +14 -11
  60. package/lib/filters/FilterEngine.cjs +31 -26
  61. package/lib/filters/FilterEngine.cjs.map +1 -1
  62. package/lib/filters/{FilterEngine.d.ts → FilterEngine.d.cts} +0 -1
  63. package/lib/index.cjs +3 -3
  64. package/lib/index.cjs.map +1 -1
  65. package/{esm/index.d.ts → lib/index.d.cts} +7 -7
  66. package/lib/renderers/DiffRenderer.cjs +13 -3
  67. package/lib/renderers/DiffRenderer.cjs.map +1 -1
  68. package/lib/renderers/{DiffRenderer.d.ts → DiffRenderer.d.cts} +0 -1
  69. package/lib/renderers/TimelineRenderer.cjs +55 -21
  70. package/lib/renderers/TimelineRenderer.cjs.map +1 -1
  71. package/lib/renderers/{TimelineRenderer.d.ts → TimelineRenderer.d.cts} +7 -3
  72. package/lib/styles/index.cjs +167 -131
  73. package/lib/styles/index.cjs.map +1 -1
  74. package/{esm/styles/index.d.ts → lib/styles/index.d.cts} +1 -5
  75. package/lib/types/index.cjs +1 -1
  76. package/lib/types/index.cjs.map +1 -1
  77. package/{esm/types/index.d.ts → lib/types/index.d.cts} +4 -1
  78. package/lib/utils/index.cjs +54 -6
  79. package/lib/utils/index.cjs.map +1 -1
  80. package/{esm/utils/index.d.ts → lib/utils/index.d.cts} +1 -2
  81. package/package.json +15 -9
  82. package/es/core/EventEmitter.d.ts.map +0 -1
  83. package/es/core/LogViewer.d.ts.map +0 -1
  84. package/es/filters/FilterEngine.d.ts.map +0 -1
  85. package/es/index.d.ts.map +0 -1
  86. package/es/renderers/DiffRenderer.d.ts.map +0 -1
  87. package/es/renderers/TimelineRenderer.d.ts.map +0 -1
  88. package/es/styles/index.d.ts.map +0 -1
  89. package/es/types/index.d.ts.map +0 -1
  90. package/es/utils/index.d.ts.map +0 -1
  91. package/esm/core/EventEmitter.d.ts.map +0 -1
  92. package/esm/core/LogViewer.d.ts.map +0 -1
  93. package/esm/filters/FilterEngine.d.ts +0 -26
  94. package/esm/filters/FilterEngine.d.ts.map +0 -1
  95. package/esm/index.d.ts.map +0 -1
  96. package/esm/renderers/DiffRenderer.d.ts +0 -18
  97. package/esm/renderers/DiffRenderer.d.ts.map +0 -1
  98. package/esm/renderers/TimelineRenderer.d.ts +0 -22
  99. package/esm/renderers/TimelineRenderer.d.ts.map +0 -1
  100. package/esm/styles/index.d.ts.map +0 -1
  101. package/esm/types/index.d.ts.map +0 -1
  102. package/esm/utils/index.d.ts.map +0 -1
  103. package/lib/core/EventEmitter.d.ts +0 -11
  104. package/lib/core/LogViewer.d.ts +0 -46
  105. package/lib/index.d.ts +0 -26
  106. package/lib/styles/index.d.ts +0 -9
  107. package/lib/types/index.d.ts +0 -113
  108. package/lib/utils/index.d.ts +0 -18
@@ -2,7 +2,7 @@
2
2
  * ***********************************
3
3
  * @composy/log-viewer-core v0.0.1 *
4
4
  * Built with rollup *
5
- * Build time: 2024-08-17 11:33:32 *
5
+ * Build time: 2024-10-12 17:29:20 *
6
6
  * Build mode: production *
7
7
  * Minified: No *
8
8
  * ***********************************
@@ -10,11 +10,12 @@
10
10
  //#region src/utils/index.ts
11
11
  let _uid = 0;
12
12
  function uid(prefix = "lv") {
13
- return `${prefix}-${++_uid}-${Math.random().toString(36).slice(2, 8)}`;
13
+ const random = typeof crypto !== "undefined" && typeof crypto.randomUUID === "function" ? crypto.randomUUID().slice(0, 8) : Math.random().toString(36).slice(2, 8);
14
+ return `${prefix}-${++_uid}-${random}`;
14
15
  }
15
16
  function formatDate(value, fmt = "YYYY-MM-DD HH:mm:ss") {
16
17
  const d = value instanceof Date ? value : new Date(value);
17
- if (isNaN(d.getTime())) return String(value);
18
+ if (Number.isNaN(d.getTime())) return String(value);
18
19
  const pad = (n) => String(n).padStart(2, "0");
19
20
  const year = String(d.getFullYear());
20
21
  const month = pad(d.getMonth() + 1);
@@ -27,6 +28,7 @@ function formatDate(value, fmt = "YYYY-MM-DD HH:mm:ss") {
27
28
  function formatRelativeTime(value) {
28
29
  const d = value instanceof Date ? value : new Date(value);
29
30
  const diff = Date.now() - d.getTime();
31
+ if (!Number.isFinite(diff) || diff < 0) return formatDate(d);
30
32
  if (diff < 6e4) return "刚刚";
31
33
  if (diff < 36e5) return `${Math.floor(diff / 6e4)} \u5206\u949F\u524D`;
32
34
  if (diff < 864e5) return `${Math.floor(diff / 36e5)} \u5C0F\u65F6\u524D`;
@@ -45,15 +47,58 @@ function escapeHtml(str) {
45
47
  }
46
48
  function debounce(fn, delay) {
47
49
  let timer = null;
50
+ const wait = Number.isFinite(delay) && delay > 0 ? delay : 0;
48
51
  return ((...args) => {
49
52
  if (timer) clearTimeout(timer);
50
- timer = setTimeout(() => fn(...args), delay);
53
+ timer = setTimeout(() => {
54
+ timer = null;
55
+ fn(...args);
56
+ }, wait);
51
57
  });
52
58
  }
53
59
  function deepClone(obj) {
54
60
  if (obj === null || typeof obj !== "object") return obj;
61
+ if (typeof structuredClone === "function") try {
62
+ return structuredClone(obj);
63
+ } catch {}
64
+ const seen = /* @__PURE__ */ new WeakMap();
65
+ const clone = (value) => {
66
+ if (value === null || typeof value !== "object") return value;
67
+ if (seen.has(value)) return seen.get(value);
68
+ if (value instanceof Date) return new Date(value.getTime());
69
+ if (value instanceof RegExp) return new RegExp(value.source, value.flags);
70
+ if (value instanceof Map) {
71
+ const result2 = /* @__PURE__ */ new Map();
72
+ seen.set(value, result2);
73
+ value.forEach((mapValue, key) => result2.set(clone(key), clone(mapValue)));
74
+ return result2;
75
+ }
76
+ if (value instanceof Set) {
77
+ const result2 = /* @__PURE__ */ new Set();
78
+ seen.set(value, result2);
79
+ value.forEach((setValue) => result2.add(clone(setValue)));
80
+ return result2;
81
+ }
82
+ if (Array.isArray(value)) {
83
+ const result2 = [];
84
+ seen.set(value, result2);
85
+ value.forEach((item) => result2.push(clone(item)));
86
+ return result2;
87
+ }
88
+ const result = Object.create(Object.getPrototypeOf(value));
89
+ seen.set(value, result);
90
+ for (const key of Reflect.ownKeys(value)) {
91
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
92
+ if (!descriptor || !("value" in descriptor)) continue;
93
+ Object.defineProperty(result, key, {
94
+ ...descriptor,
95
+ value: clone(descriptor.value)
96
+ });
97
+ }
98
+ return result;
99
+ };
55
100
  try {
56
- return JSON.parse(JSON.stringify(obj));
101
+ return clone(obj);
57
102
  } catch {
58
103
  return obj;
59
104
  }
@@ -61,7 +106,10 @@ function deepClone(obj) {
61
106
  function formatValue(val) {
62
107
  if (val === null || val === void 0) return "-";
63
108
  if (typeof val === "object") try {
64
- return JSON.stringify(val, null, 2);
109
+ return JSON.stringify(val, (_key, value) => {
110
+ if (typeof value === "bigint") return `${value}n`;
111
+ return value;
112
+ }, 2);
65
113
  } catch {
66
114
  return String(val);
67
115
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/utils/index.ts"],"sourcesContent":["/**\n * 工具函数\n */\n\nlet _uid = 0\n\n/** 生成唯一 ID */\nexport function uid(prefix = 'lv'): string {\n return `${prefix}-${++_uid}-${Math.random().toString(36).slice(2, 8)}`\n}\n\n/** 格式化日期 */\nexport function formatDate(value: string | number | Date, fmt = 'YYYY-MM-DD HH:mm:ss'): string {\n const d = value instanceof Date ? value : new Date(value)\n if (isNaN(d.getTime())) return String(value)\n\n const pad = (n: number) => String(n).padStart(2, '0')\n const year = String(d.getFullYear())\n const month = pad(d.getMonth() + 1)\n const day = pad(d.getDate())\n const hours = pad(d.getHours())\n const minutes = pad(d.getMinutes())\n const seconds = pad(d.getSeconds())\n\n return fmt\n .replace('YYYY', year)\n .replace('MM', month)\n .replace('DD', day)\n .replace('HH', hours)\n .replace('mm', minutes)\n .replace('ss', seconds)\n}\n\n/** 格式化相对时间 */\nexport function formatRelativeTime(value: string | number | Date): string {\n const d = value instanceof Date ? value : new Date(value)\n const now = Date.now()\n const diff = now - d.getTime()\n\n if (diff < 60_000) return '刚刚'\n if (diff < 3_600_000) return `${Math.floor(diff / 60_000)} 分钟前`\n if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)} 小时前`\n if (diff < 604_800_000) return `${Math.floor(diff / 86_400_000)} 天前`\n return formatDate(d)\n}\n\n/** HTML 转义 */\nexport function escapeHtml(str: string): string {\n const map: Record<string, string> = {\n '&': '&amp;',\n '<': '&lt;',\n '>': '&gt;',\n '\"': '&quot;',\n \"'\": '&#39;',\n }\n return str.replace(/[&<>\"']/g, (c) => map[c] || c)\n}\n\n/** 防抖 */\nexport function debounce<T extends (...args: unknown[]) => void>(fn: T, delay: number): T {\n let timer: ReturnType<typeof setTimeout> | null = null\n return ((...args: unknown[]) => {\n if (timer) clearTimeout(timer)\n timer = setTimeout(() => fn(...args), delay)\n }) as unknown as T\n}\n\n/** 深拷贝简易实现 */\nexport function deepClone<T>(obj: T): T {\n if (obj === null || typeof obj !== 'object') return obj\n try {\n return JSON.parse(JSON.stringify(obj))\n } catch {\n return obj\n }\n}\n\n/** 将值格式化为可展示的字符串 */\nexport function formatValue(val: unknown): string {\n if (val === null || val === undefined) return '-'\n if (typeof val === 'object') {\n try {\n return JSON.stringify(val, null, 2)\n } catch {\n return String(val)\n }\n }\n return String(val)\n}\n"],"mappings":";;;;;;;;;;AAIA,IAAI,OAAO;AAGJ,SAAS,IAAI,SAAS,MAAc;CACzC,OAAO,GAAG,OAAM,GAAI,EAAE,KAAI,GAAI,KAAK,OAAO,CAAA,CAAE,SAAS,EAAE,CAAA,CAAE,MAAM,GAAG,CAAC;AACrE;AAGO,SAAS,WAAW,OAA+B,MAAM,uBAA+B;CAC7F,MAAM,IAAI,iBAAiB,OAAO,QAAQ,IAAI,KAAK,KAAK;CACxD,IAAI,MAAM,EAAE,QAAQ,CAAC,GAAG,OAAO,OAAO,KAAK;CAE3C,MAAM,OAAO,MAAc,OAAO,CAAC,CAAA,CAAE,SAAS,GAAG,GAAG;CACpD,MAAM,OAAO,OAAO,EAAE,YAAY,CAAC;CACnC,MAAM,QAAQ,IAAI,EAAE,SAAS,IAAI,CAAC;CAClC,MAAM,MAAM,IAAI,EAAE,QAAQ,CAAC;CAC3B,MAAM,QAAQ,IAAI,EAAE,SAAS,CAAC;CAC9B,MAAM,UAAU,IAAI,EAAE,WAAW,CAAC;CAClC,MAAM,UAAU,IAAI,EAAE,WAAW,CAAC;CAElC,OAAO,IACJ,QAAQ,QAAQ,IAAI,CAAA,CACpB,QAAQ,MAAM,KAAK,CAAA,CACnB,QAAQ,MAAM,GAAG,CAAA,CACjB,QAAQ,MAAM,KAAK,CAAA,CACnB,QAAQ,MAAM,OAAO,CAAA,CACrB,QAAQ,MAAM,OAAO;AAC1B;AAGO,SAAS,mBAAmB,OAAuC;CACxE,MAAM,IAAI,iBAAiB,OAAO,QAAQ,IAAI,KAAK,KAAK;CAExD,MAAM,OADM,KAAK,IACJ,IAAM,EAAE,QAAQ;CAE7B,IAAI,OAAO,KAAQ,OAAO;CAC1B,IAAI,OAAO,MAAW,OAAO,GAAG,KAAK,MAAM,OAAO,GAAM,EAAC;CACzD,IAAI,OAAO,OAAY,OAAO,GAAG,KAAK,MAAM,OAAO,IAAS,EAAC;CAC7D,IAAI,OAAO,QAAa,OAAO,GAAG,KAAK,MAAM,OAAO,KAAU,EAAC;CAC/D,OAAO,WAAW,CAAC;AACrB;AAGO,SAAS,WAAW,KAAqB;CAC9C,MAAM,MAA8B;EAClC,KAAK;EACL,KAAK;EACL,KAAK;EACL,MAAK;EACL,KAAK;CACP;CACA,OAAO,IAAI,QAAQ,aAAa,MAAM,IAAI,MAAM,CAAC;AACnD;AAGO,SAAS,SAAiD,IAAO,OAAkB;CACxF,IAAI,QAA8C;CAClD,SAAQ,GAAI,SAAoB;EAC9B,IAAI,OAAO,aAAa,KAAK;EAC7B,QAAQ,iBAAiB,GAAG,GAAG,IAAI,GAAG,KAAK;CAC7C;AACF;AAGO,SAAS,UAAa,KAAW;CACtC,IAAI,QAAQ,QAAQ,OAAO,QAAQ,UAAU,OAAO;CACpD,IAAI;EACF,OAAO,KAAK,MAAM,KAAK,UAAU,GAAG,CAAC;CACvC,QAAQ;EACN,OAAO;CACT;AACF;AAGO,SAAS,YAAY,KAAsB;CAChD,IAAI,QAAQ,QAAQ,QAAQ,KAAA,GAAW,OAAO;CAC9C,IAAI,OAAO,QAAQ,UACjB,IAAI;EACF,OAAO,KAAK,UAAU,KAAK,MAAM,CAAC;CACpC,QAAQ;EACN,OAAO,OAAO,GAAG;CACnB;CAEF,OAAO,OAAO,GAAG;AACnB"}
1
+ {"version":3,"file":"index.js","names":["result"],"sources":["../../src/utils/index.ts"],"sourcesContent":["/**\r\n * 工具函数\r\n */\r\n\r\nlet _uid = 0\r\n\r\n/** 生成唯一 ID */\r\nexport function uid(prefix = 'lv'): string {\r\n const random =\r\n typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'\r\n ? crypto.randomUUID().slice(0, 8)\r\n : Math.random().toString(36).slice(2, 8)\r\n return `${prefix}-${++_uid}-${random}`\r\n}\r\n\r\n/** 格式化日期 */\r\nexport function formatDate(\r\n value: string | number | Date,\r\n fmt = 'YYYY-MM-DD HH:mm:ss'\r\n): string {\r\n const d = value instanceof Date ? value : new Date(value)\r\n if (Number.isNaN(d.getTime())) return String(value)\r\n\r\n const pad = (n: number) => String(n).padStart(2, '0')\r\n const year = String(d.getFullYear())\r\n const month = pad(d.getMonth() + 1)\r\n const day = pad(d.getDate())\r\n const hours = pad(d.getHours())\r\n const minutes = pad(d.getMinutes())\r\n const seconds = pad(d.getSeconds())\r\n\r\n return fmt\r\n .replace('YYYY', year)\r\n .replace('MM', month)\r\n .replace('DD', day)\r\n .replace('HH', hours)\r\n .replace('mm', minutes)\r\n .replace('ss', seconds)\r\n}\r\n\r\n/** 格式化相对时间 */\r\nexport function formatRelativeTime(value: string | number | Date): string {\r\n const d = value instanceof Date ? value : new Date(value)\r\n const now = Date.now()\r\n const diff = now - d.getTime()\r\n\r\n if (!Number.isFinite(diff) || diff < 0) return formatDate(d)\r\n if (diff < 60_000) return '刚刚'\r\n if (diff < 3_600_000) return `${Math.floor(diff / 60_000)} 分钟前`\r\n if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)} 小时前`\r\n if (diff < 604_800_000) return `${Math.floor(diff / 86_400_000)} 天前`\r\n return formatDate(d)\r\n}\r\n\r\n/** HTML 转义 */\r\nexport function escapeHtml(str: string): string {\r\n const map: Record<string, string> = {\r\n '&': '&amp;',\r\n '<': '&lt;',\r\n '>': '&gt;',\r\n '\"': '&quot;',\r\n \"'\": '&#39;',\r\n }\r\n return str.replace(/[&<>\"']/g, c => map[c] || c)\r\n}\r\n\r\n/** 防抖 */\r\nexport function debounce<T extends (...args: unknown[]) => void>(\r\n fn: T,\r\n delay: number\r\n): T {\r\n let timer: ReturnType<typeof setTimeout> | null = null\r\n const wait = Number.isFinite(delay) && delay > 0 ? delay : 0\r\n return ((...args: unknown[]) => {\r\n if (timer) clearTimeout(timer)\r\n timer = setTimeout(() => {\r\n timer = null\r\n fn(...args)\r\n }, wait)\r\n }) as unknown as T\r\n}\r\n\r\n/** 深拷贝值,同时保留 Date、Map、Set 和循环引用。 */\r\nexport function deepClone<T>(obj: T): T {\r\n if (obj === null || typeof obj !== 'object') return obj\r\n\r\n if (typeof structuredClone === 'function') {\r\n try {\r\n return structuredClone(obj)\r\n } catch {\r\n // structuredClone rejects functions and a few host objects; use the\r\n // compatibility clone below so configuration callbacks stay intact.\r\n }\r\n }\r\n\r\n const seen = new WeakMap<object, unknown>()\r\n\r\n const clone = (value: unknown): unknown => {\r\n if (value === null || typeof value !== 'object') return value\r\n if (seen.has(value)) return seen.get(value)\r\n\r\n if (value instanceof Date) return new Date(value.getTime())\r\n if (value instanceof RegExp) return new RegExp(value.source, value.flags)\r\n\r\n if (value instanceof Map) {\r\n const result = new Map<unknown, unknown>()\r\n seen.set(value, result)\r\n value.forEach((mapValue, key) => result.set(clone(key), clone(mapValue)))\r\n return result\r\n }\r\n\r\n if (value instanceof Set) {\r\n const result = new Set<unknown>()\r\n seen.set(value, result)\r\n value.forEach(setValue => result.add(clone(setValue)))\r\n return result\r\n }\r\n\r\n if (Array.isArray(value)) {\r\n const result: unknown[] = []\r\n seen.set(value, result)\r\n value.forEach(item => result.push(clone(item)))\r\n return result\r\n }\r\n\r\n const result = Object.create(Object.getPrototypeOf(value)) as Record<\r\n string,\r\n unknown\r\n >\r\n seen.set(value, result)\r\n for (const key of Reflect.ownKeys(value)) {\r\n const descriptor = Object.getOwnPropertyDescriptor(value, key)\r\n if (!descriptor || !('value' in descriptor)) continue\r\n Object.defineProperty(result, key, {\r\n ...descriptor,\r\n value: clone(descriptor.value),\r\n })\r\n }\r\n return result\r\n }\r\n\r\n try {\r\n return clone(obj) as T\r\n } catch {\r\n return obj\r\n }\r\n}\r\n\r\n/** 将值格式化为可展示的字符串 */\r\nexport function formatValue(val: unknown): string {\r\n if (val === null || val === undefined) return '-'\r\n if (typeof val === 'object') {\r\n try {\r\n return JSON.stringify(\r\n val,\r\n (_key, value) => {\r\n if (typeof value === 'bigint') return `${value}n`\r\n return value\r\n },\r\n 2\r\n )\r\n } catch {\r\n return String(val)\r\n }\r\n }\r\n return String(val)\r\n}\r\n"],"mappings":"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"}
@@ -2,7 +2,7 @@
2
2
  * ***********************************
3
3
  * @composy/log-viewer-core v0.0.1 *
4
4
  * Built with rollup *
5
- * Build time: 2024-08-17 11:33:32 *
5
+ * Build time: 2024-10-12 17:29:20 *
6
6
  * Build mode: production *
7
7
  * Minified: No *
8
8
  * ***********************************
@@ -14,21 +14,25 @@ var EventEmitter = class {
14
14
  this.listeners = /* @__PURE__ */ new Map();
15
15
  }
16
16
  on(event, callback) {
17
+ if (!event || typeof callback !== "function") return () => void 0;
17
18
  if (!this.listeners.has(event)) this.listeners.set(event, /* @__PURE__ */ new Set());
18
19
  this.listeners.get(event).add(callback);
19
20
  return () => this.off(event, callback);
20
21
  }
21
22
  off(event, callback) {
22
- this.listeners.get(event)?.delete(callback);
23
+ const listeners = this.listeners.get(event);
24
+ if (!listeners) return;
25
+ listeners.delete(callback);
26
+ if (listeners.size === 0) this.listeners.delete(event);
23
27
  }
24
28
  emit(event, ...args) {
25
- this.listeners.get(event)?.forEach((cb) => {
26
- try {
27
- cb(...args);
28
- } catch (e) {
29
- console.error(`[LogViewer] Event handler error (${event}):`, e);
30
- }
31
- });
29
+ const listeners = this.listeners.get(event);
30
+ if (!listeners) return;
31
+ for (const cb of [...listeners]) try {
32
+ cb(...args);
33
+ } catch (e) {
34
+ console.error(`[LogViewer] Event handler error (${event}):`, e);
35
+ }
32
36
  }
33
37
  removeAllListeners(event) {
34
38
  if (event) this.listeners.delete(event);
@@ -1 +1 @@
1
- {"version":3,"file":"EventEmitter.cjs","names":[],"sources":["../../src/core/EventEmitter.ts"],"sourcesContent":["/**\n * 简单的事件发射器\n */\nexport class EventEmitter {\n private listeners = new Map<string, Set<(...args: unknown[]) => void>>()\n\n on(event: string, callback: (...args: unknown[]) => void): () => void {\n if (!this.listeners.has(event)) {\n this.listeners.set(event, new Set())\n }\n this.listeners.get(event)!.add(callback)\n return () => this.off(event, callback)\n }\n\n off(event: string, callback: (...args: unknown[]) => void): void {\n this.listeners.get(event)?.delete(callback)\n }\n\n emit(event: string, ...args: unknown[]): void {\n this.listeners.get(event)?.forEach((cb) => {\n try {\n cb(...args)\n } catch (e) {\n console.error(`[LogViewer] Event handler error (${event}):`, e)\n }\n })\n }\n\n removeAllListeners(event?: string): void {\n if (event) {\n this.listeners.delete(event)\n } else {\n this.listeners.clear()\n }\n }\n}\n"],"mappings":";;;;;;;;;;;AAGO,IAAM,eAAN,MAAmB;CAAnB,cAAA;EACL,KAAQ,4BAAY,IAAI,IAA+C;CAAA;CAEvE,GAAG,OAAe,UAAoD;EACpE,IAAI,CAAC,KAAK,UAAU,IAAI,KAAK,GAC3B,KAAK,UAAU,IAAI,uBAAO,IAAI,IAAI,CAAC;EAErC,KAAK,UAAU,IAAI,KAAK,CAAA,CAAG,IAAI,QAAQ;EACvC,aAAa,KAAK,IAAI,OAAO,QAAQ;CACvC;CAEA,IAAI,OAAe,UAA8C;EAC/D,KAAK,UAAU,IAAI,KAAK,CAAA,EAAG,OAAO,QAAQ;CAC5C;CAEA,KAAK,OAAA,GAAkB,MAAuB;EAC5C,KAAK,UAAU,IAAI,KAAK,CAAA,EAAG,SAAS,OAAO;GACzC,IAAI;IACF,GAAG,GAAG,IAAI;GACZ,SAAS,GAAG;IACV,QAAQ,MAAM,oCAAoC,MAAK,KAAM,CAAC;GAChE;EACF,CAAC;CACH;CAEA,mBAAmB,OAAsB;EACvC,IAAI,OACF,KAAK,UAAU,OAAO,KAAK;OAE3B,KAAK,UAAU,MAAM;CAEzB;AACF"}
1
+ {"version":3,"file":"EventEmitter.cjs","names":[],"sources":["../../src/core/EventEmitter.ts"],"sourcesContent":["/**\r\n * 简单的事件发射器\r\n */\r\nexport class EventEmitter {\r\n private listeners = new Map<string, Set<(...args: unknown[]) => void>>()\r\n\r\n on(event: string, callback: (...args: unknown[]) => void): () => void {\r\n if (!event || typeof callback !== 'function') {\r\n return () => undefined\r\n }\r\n if (!this.listeners.has(event)) {\r\n this.listeners.set(event, new Set())\r\n }\r\n const listeners = this.listeners.get(event)!\r\n listeners.add(callback)\r\n return () => this.off(event, callback)\r\n }\r\n\r\n off(event: string, callback: (...args: unknown[]) => void): void {\r\n const listeners = this.listeners.get(event)\r\n if (!listeners) return\r\n listeners.delete(callback)\r\n if (listeners.size === 0) this.listeners.delete(event)\r\n }\r\n\r\n emit(event: string, ...args: unknown[]): void {\r\n const listeners = this.listeners.get(event)\r\n if (!listeners) return\r\n\r\n // Snapshot the set so listeners can unsubscribe or subscribe while the\r\n // current event is being delivered without changing this dispatch cycle.\r\n for (const cb of [...listeners]) {\r\n try {\r\n cb(...args)\r\n } catch (e) {\r\n console.error(`[LogViewer] Event handler error (${event}):`, e)\r\n }\r\n }\r\n }\r\n\r\n removeAllListeners(event?: string): void {\r\n if (event) {\r\n this.listeners.delete(event)\r\n } else {\r\n this.listeners.clear()\r\n }\r\n }\r\n}\r\n"],"mappings":";;;;;;;;;;;AAGO,IAAM,eAAN,MAAmB;CAAnB,cAAA;EACL,KAAQ,4BAAY,IAAI,IAA+C;CAAA;CAEvE,GAAG,OAAe,UAAoD;EACpE,IAAI,CAAC,SAAS,OAAO,aAAa,YAChC,aAAa,KAAA;EAEf,IAAI,CAAC,KAAK,UAAU,IAAI,KAAK,GAC3B,KAAK,UAAU,IAAI,uBAAO,IAAI,IAAI,CAAC;EAGrC,KADuB,UAAU,IAAI,KACrC,CAAA,CAAU,IAAI,QAAQ;EACtB,aAAa,KAAK,IAAI,OAAO,QAAQ;CACvC;CAEA,IAAI,OAAe,UAA8C;EAC/D,MAAM,YAAY,KAAK,UAAU,IAAI,KAAK;EAC1C,IAAI,CAAC,WAAW;EAChB,UAAU,OAAO,QAAQ;EACzB,IAAI,UAAU,SAAS,GAAG,KAAK,UAAU,OAAO,KAAK;CACvD;CAEA,KAAK,OAAA,GAAkB,MAAuB;EAC5C,MAAM,YAAY,KAAK,UAAU,IAAI,KAAK;EAC1C,IAAI,CAAC,WAAW;EAIhB,KAAA,MAAW,MAAM,CAAC,GAAG,SAAS,GAC5B,IAAI;GACF,GAAG,GAAG,IAAI;EACZ,SAAS,GAAG;GACV,QAAQ,MAAM,oCAAoC,MAAK,KAAM,CAAC;EAChE;CAEJ;CAEA,mBAAmB,OAAsB;EACvC,IAAI,OACF,KAAK,UAAU,OAAO,KAAK;OAE3B,KAAK,UAAU,MAAM;CAEzB;AACF"}
@@ -8,4 +8,3 @@ export declare class EventEmitter {
8
8
  emit(event: string, ...args: unknown[]): void;
9
9
  removeAllListeners(event?: string): void;
10
10
  }
11
- //# sourceMappingURL=EventEmitter.d.ts.map
@@ -2,19 +2,64 @@
2
2
  * ***********************************
3
3
  * @composy/log-viewer-core v0.0.1 *
4
4
  * Built with rollup *
5
- * Build time: 2024-08-17 11:33:32 *
5
+ * Build time: 2024-10-12 17:29:20 *
6
6
  * Build mode: production *
7
7
  * Minified: No *
8
8
  * ***********************************
9
9
  */
10
10
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
11
11
  const require_EventEmitter = require("./EventEmitter.cjs");
12
+ const require_index = require("../types/index.cjs");
12
13
  const require_FilterEngine = require("../filters/FilterEngine.cjs");
13
- const require_index = require("../utils/index.cjs");
14
- const require_TimelineRenderer = require("../renderers/TimelineRenderer.cjs");
14
+ const require_index$1 = require("../utils/index.cjs");
15
15
  const require_DiffRenderer = require("../renderers/DiffRenderer.cjs");
16
- const require_index$1 = require("../styles/index.cjs");
16
+ const require_TimelineRenderer = require("../renderers/TimelineRenderer.cjs");
17
+ const require_index$2 = require("../styles/index.cjs");
17
18
  //#region src/core/LogViewer.ts
19
+ const DEFAULT_PAGE_SIZE = 20;
20
+ const MAX_PAGE_SIZE = 1e3;
21
+ function normalizePageSize(value, fallback = DEFAULT_PAGE_SIZE) {
22
+ const pageSize = Number(value);
23
+ if (!Number.isFinite(pageSize) || pageSize <= 0) return fallback;
24
+ return Math.min(MAX_PAGE_SIZE, Math.max(1, Math.floor(pageSize)));
25
+ }
26
+ function normalizePage(value) {
27
+ const page = Number(value);
28
+ if (!Number.isFinite(page) || page < 1) return 1;
29
+ return Math.floor(page);
30
+ }
31
+ function cloneEntries(entries) {
32
+ if (entries === void 0) return [];
33
+ if (!Array.isArray(entries)) throw new TypeError("[LogViewer] data must be an array");
34
+ return entries.map((entry, index) => {
35
+ if (!entry || typeof entry !== "object") throw new TypeError(`[LogViewer] data[${index}] must be an object`);
36
+ const cloned = require_index$1.deepClone(entry);
37
+ const id = String(cloned.id ?? "").trim();
38
+ if (!id) throw new TypeError(`[LogViewer] data[${index}].id must be a non-empty string`);
39
+ return {
40
+ ...cloned,
41
+ id,
42
+ user: String(cloned.user ?? ""),
43
+ module: String(cloned.module ?? ""),
44
+ changes: Array.isArray(cloned.changes) ? cloned.changes : void 0
45
+ };
46
+ });
47
+ }
48
+ function isActionType(value) {
49
+ return Object.keys(require_index.ACTION_LABELS).includes(value);
50
+ }
51
+ function sourceEntries(payload) {
52
+ if (Array.isArray(payload)) return payload;
53
+ if (!payload || typeof payload !== "object") throw new TypeError("[LogViewer] dataSource response must be an array");
54
+ const record = payload;
55
+ for (const key of [
56
+ "data",
57
+ "logs",
58
+ "items",
59
+ "entries"
60
+ ]) if (Array.isArray(record[key])) return record[key];
61
+ throw new TypeError("[LogViewer] dataSource response must contain an array");
62
+ }
18
63
  var LogViewer = class {
19
64
  constructor(config) {
20
65
  this.emitter = new require_EventEmitter.EventEmitter();
@@ -26,89 +71,107 @@ var LogViewer = class {
26
71
  this.filter = {};
27
72
  this.pagination = {
28
73
  page: 1,
29
- pageSize: 20
74
+ pageSize: DEFAULT_PAGE_SIZE
30
75
  };
31
76
  this.theme = "light";
32
77
  this.expandedIds = /* @__PURE__ */ new Set();
78
+ this.filterEl = null;
33
79
  this.timelineEl = null;
34
80
  this.paginationEl = null;
35
81
  this.destroyed = false;
36
- this.config = config;
82
+ this.requestId = 0;
83
+ this.abortController = null;
84
+ if (typeof document === "undefined") throw new Error("[LogViewer] A browser document is required");
37
85
  const container = typeof config.container === "string" ? document.querySelector(config.container) : config.container;
38
- if (!container) throw new Error(`[LogViewer] Container not found: ${config.container}`);
39
- require_index$1.injectStyles();
86
+ if (!container) throw new Error(`[LogViewer] Container not found: ${String(config.container)}`);
87
+ this.config = { ...config };
88
+ this.theme = config.theme === "dark" ? "dark" : "light";
89
+ this.pagination = {
90
+ page: normalizePage(config.pagination?.page),
91
+ pageSize: normalizePageSize(config.pageSize ?? config.pagination?.pageSize)
92
+ };
93
+ this.filter = require_index$1.deepClone(config.filter || {});
94
+ this.data = cloneEntries(config.data);
95
+ require_index$2.injectStyles();
40
96
  this.root = document.createElement("div");
41
97
  this.root.className = "lv-root";
42
- container.appendChild(this.root);
43
- this.theme = config.theme || "light";
44
98
  this.root.setAttribute("data-theme", this.theme);
45
- if (config.pageSize) this.pagination.pageSize = config.pageSize;
46
- if (config.pagination) Object.assign(this.pagination, config.pagination);
47
- if (config.filter) this.filter = require_index.deepClone(config.filter);
48
- if (config.data) this.data = [...config.data];
49
- this.applyFilterAndRender();
99
+ container.appendChild(this.root);
50
100
  this.bindEvents();
101
+ this.applyFilterAndRender();
102
+ if (config.dataSource) this.loadData().catch(() => void 0);
51
103
  }
52
104
  getData() {
53
- return [...this.data];
105
+ return require_index$1.deepClone(this.data);
54
106
  }
55
107
  setData(entries) {
56
- this.data = [...entries];
108
+ if (this.destroyed) return;
109
+ this.data = cloneEntries(entries);
57
110
  this.pagination.page = 1;
111
+ this.expandedIds.clear();
58
112
  this.applyFilterAndRender();
59
- this.emitter.emit("data:change", this.data);
113
+ this.emitter.emit("data:change", this.getData());
60
114
  }
61
115
  appendData(entries) {
62
- this.data.push(...entries);
116
+ if (this.destroyed) return;
117
+ this.data.push(...cloneEntries(entries));
63
118
  this.applyFilterAndRender();
64
- this.emitter.emit("data:change", this.data);
119
+ this.emitter.emit("data:change", this.getData());
65
120
  }
66
121
  getFilter() {
67
- return require_index.deepClone(this.filter);
122
+ return require_index$1.deepClone(this.filter);
68
123
  }
69
124
  setFilter(filter) {
70
- this.filter = require_index.deepClone(filter);
125
+ if (this.destroyed) return;
126
+ this.filter = require_index$1.deepClone(filter || {});
71
127
  this.pagination.page = 1;
72
128
  this.applyFilterAndRender();
73
- this.emitter.emit("filter:change", this.filter);
74
- this.config.onFilterChange?.(this.filter);
129
+ const current = this.getFilter();
130
+ this.emitter.emit("filter:change", current);
131
+ this.config.onFilterChange?.(current);
75
132
  }
76
133
  resetFilter() {
77
- this.filter = {};
78
- this.pagination.page = 1;
79
- this.applyFilterAndRender();
80
- this.emitter.emit("filter:change", this.filter);
134
+ if (this.destroyed) return;
135
+ this.setFilter({});
81
136
  }
82
137
  getTheme() {
83
138
  return this.theme;
84
139
  }
85
140
  setTheme(theme) {
141
+ if (this.destroyed) return;
142
+ if (theme !== "light" && theme !== "dark") throw new TypeError("[LogViewer] theme must be light or dark");
143
+ if (this.theme === theme) return;
86
144
  this.theme = theme;
87
145
  this.root.setAttribute("data-theme", theme);
88
146
  this.emitter.emit("theme:change", theme);
89
147
  this.config.onThemeChange?.(theme);
90
148
  }
91
149
  expandEntry(id) {
150
+ if (this.destroyed || this.expandedIds.has(id)) return;
92
151
  this.expandedIds.add(id);
93
- const entry = this.data.find((e) => e.id === id);
94
- if (entry) this.emitter.emit("entry:expand", entry);
152
+ const entry = this.data.find((item) => item.id === id);
153
+ if (entry) this.emitter.emit("entry:expand", require_index$1.deepClone(entry));
95
154
  this.renderTimeline();
96
155
  }
97
156
  collapseEntry(id) {
157
+ if (this.destroyed || !this.expandedIds.has(id)) return;
98
158
  this.expandedIds.delete(id);
99
- const entry = this.data.find((e) => e.id === id);
100
- if (entry) this.emitter.emit("entry:collapse", entry);
159
+ const entry = this.data.find((item) => item.id === id);
160
+ if (entry) this.emitter.emit("entry:collapse", require_index$1.deepClone(entry));
101
161
  this.renderTimeline();
102
162
  }
103
163
  collapseAll() {
164
+ if (this.destroyed || this.expandedIds.size === 0) return;
104
165
  this.expandedIds.clear();
105
166
  this.renderTimeline();
106
167
  }
107
168
  goToPage(page) {
108
- const totalPages = Math.ceil(this.filteredData.length / this.pagination.pageSize);
109
- if (page < 1 || page > totalPages) return;
110
- this.pagination.page = page;
111
- this.emitter.emit("page:change", page);
169
+ if (this.destroyed) return;
170
+ const totalPages = Math.max(1, Math.ceil(this.filteredData.length / this.pagination.pageSize));
171
+ const nextPage = Math.min(totalPages, Math.max(1, normalizePage(page)));
172
+ if (nextPage === this.pagination.page) return;
173
+ this.pagination.page = nextPage;
174
+ this.emitter.emit("page:change", nextPage);
112
175
  this.renderTimeline();
113
176
  }
114
177
  getPagination() {
@@ -121,17 +184,55 @@ var LogViewer = class {
121
184
  return this.emitter.on(event, callback);
122
185
  }
123
186
  refresh() {
124
- this.applyFilterAndRender();
187
+ if (!this.destroyed) this.applyFilterAndRender();
188
+ }
189
+ async loadData() {
190
+ if (this.destroyed || !this.config.dataSource) return;
191
+ const currentRequest = ++this.requestId;
192
+ this.abortController?.abort();
193
+ this.abortController = typeof AbortController !== "undefined" ? new AbortController() : null;
194
+ try {
195
+ let payload;
196
+ if (typeof this.config.dataSource === "function") payload = await this.config.dataSource();
197
+ else {
198
+ if (typeof fetch !== "function") throw new Error("[LogViewer] fetch is unavailable");
199
+ const response = await fetch(this.config.dataSource, { signal: this.abortController?.signal });
200
+ if (!response.ok) throw new Error(`[LogViewer] dataSource request failed: ${response.status}`);
201
+ payload = await response.json();
202
+ }
203
+ if (this.destroyed || currentRequest !== this.requestId) return;
204
+ this.setData(sourceEntries(payload));
205
+ } catch (error) {
206
+ if (this.destroyed || typeof DOMException !== "undefined" && error instanceof DOMException && error.name === "AbortError") return;
207
+ this.config.onError?.(error);
208
+ this.emitter.emit("error", error);
209
+ throw error;
210
+ }
125
211
  }
126
212
  destroy() {
127
213
  if (this.destroyed) return;
128
214
  this.destroyed = true;
215
+ this.requestId++;
216
+ this.abortController?.abort();
217
+ this.abortController = null;
129
218
  this.emitter.removeAllListeners();
219
+ this.expandedIds.clear();
220
+ this.root.replaceChildren();
130
221
  this.root.remove();
131
222
  }
132
223
  applyFilterAndRender() {
133
224
  this.filteredData = this.filterEngine.filter(this.data, this.filter);
134
- this.filteredData.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
225
+ this.filteredData = this.filteredData.map((entry, index) => ({
226
+ entry,
227
+ index
228
+ })).sort((a, b) => {
229
+ const left = new Date(a.entry.timestamp).getTime();
230
+ const right = new Date(b.entry.timestamp).getTime();
231
+ const leftTime = Number.isFinite(left) ? left : Number.MIN_SAFE_INTEGER;
232
+ return (Number.isFinite(right) ? right : Number.MIN_SAFE_INTEGER) - leftTime || a.index - b.index;
233
+ }).map(({ entry }) => entry);
234
+ const totalPages = Math.max(1, Math.ceil(this.filteredData.length / this.pagination.pageSize));
235
+ this.pagination.page = Math.min(this.pagination.page, totalPages);
135
236
  this.pagination.total = this.filteredData.length;
136
237
  this.renderTimeline();
137
238
  }
@@ -140,43 +241,121 @@ var LogViewer = class {
140
241
  const start = (page - 1) * pageSize;
141
242
  return this.filteredData.slice(start, start + pageSize);
142
243
  }
244
+ renderFilterBar() {
245
+ if (this.config.showFilter === false) return null;
246
+ const filter = document.createElement("div");
247
+ filter.className = "lv-filter";
248
+ filter.setAttribute("role", "search");
249
+ const addInput = (field, label) => {
250
+ const item = document.createElement("label");
251
+ item.className = "lv-filter-item";
252
+ const text = document.createElement("span");
253
+ text.className = "lv-filter-label";
254
+ text.textContent = label;
255
+ const input = document.createElement("input");
256
+ input.className = "lv-filter-input";
257
+ input.type = "search";
258
+ input.value = String(this.filter[field] || "");
259
+ input.setAttribute("data-filter-field", field);
260
+ input.setAttribute("aria-label", label);
261
+ item.append(text, input);
262
+ filter.appendChild(item);
263
+ };
264
+ addInput("keyword", this.config.locale === "en-US" ? "Keyword" : "关键词");
265
+ addInput("user", this.config.locale === "en-US" ? "User" : "用户");
266
+ addInput("module", this.config.locale === "en-US" ? "Module" : "模块");
267
+ addInput("ip", "IP");
268
+ const actionItem = document.createElement("label");
269
+ actionItem.className = "lv-filter-item";
270
+ const actionLabel = document.createElement("span");
271
+ actionLabel.className = "lv-filter-label";
272
+ actionLabel.textContent = this.config.locale === "en-US" ? "Action" : "操作";
273
+ const action = document.createElement("select");
274
+ action.className = "lv-filter-select";
275
+ action.setAttribute("data-filter-field", "action");
276
+ action.setAttribute("aria-label", actionLabel.textContent);
277
+ const allOption = document.createElement("option");
278
+ allOption.value = "";
279
+ allOption.textContent = this.config.locale === "en-US" ? "All" : "全部";
280
+ action.appendChild(allOption);
281
+ for (const [value, label] of Object.entries(require_index.ACTION_LABELS)) {
282
+ const option = document.createElement("option");
283
+ option.value = value;
284
+ option.textContent = label;
285
+ action.appendChild(option);
286
+ }
287
+ action.value = Array.isArray(this.filter.action) ? "" : this.filter.action || "";
288
+ actionItem.append(actionLabel, action);
289
+ filter.appendChild(actionItem);
290
+ const reset = document.createElement("button");
291
+ reset.type = "button";
292
+ reset.className = "lv-filter-btn";
293
+ reset.setAttribute("data-filter-reset", "true");
294
+ reset.textContent = this.config.locale === "en-US" ? "Reset" : "重置";
295
+ filter.appendChild(reset);
296
+ return filter;
297
+ }
298
+ updateFilterField(field, value) {
299
+ const next = this.getFilter();
300
+ if (!value.trim()) delete next[field];
301
+ else if (field === "action" && isActionType(value)) next.action = value;
302
+ else if (field !== "action") next[field] = value;
303
+ this.setFilter(next);
304
+ }
143
305
  renderTimeline() {
144
306
  if (this.destroyed) return;
145
307
  const pageData = this.getPageData();
146
- const newTimeline = this.timelineRenderer.render(pageData, this.expandedIds);
147
- for (const entry of pageData) if (this.expandedIds.has(entry.id)) {
148
- const itemEl = newTimeline.querySelector(`[data-id="${entry.id}"] .lv-timeline-card`);
149
- if (itemEl) itemEl.appendChild(this.diffRenderer.renderDetail(entry));
308
+ const timeline = this.timelineRenderer.render(pageData, this.expandedIds, {
309
+ showDiff: this.config.showDiff,
310
+ showAnomalyBadge: this.config.showAnomalyBadge,
311
+ locale: this.config.locale
312
+ });
313
+ if (this.config.showDiff !== false) for (const entry of pageData) {
314
+ if (!this.expandedIds.has(entry.id)) continue;
315
+ ([...timeline.children].find((child) => child.getAttribute("data-id") === entry.id)?.querySelector(".lv-timeline-card"))?.appendChild(this.diffRenderer.renderDetail(entry));
150
316
  }
151
- if (this.timelineEl) this.root.removeChild(this.timelineEl);
152
- this.timelineEl = newTimeline;
153
- this.root.appendChild(this.timelineEl);
154
- const newPagination = this.timelineRenderer.renderPagination(this.pagination.page, this.pagination.pageSize, this.filteredData.length);
155
- if (this.paginationEl) this.root.removeChild(this.paginationEl);
156
- this.paginationEl = newPagination;
157
- this.root.appendChild(this.paginationEl);
317
+ this.filterEl?.remove();
318
+ this.timelineEl?.remove();
319
+ this.paginationEl?.remove();
320
+ this.filterEl = this.renderFilterBar();
321
+ this.timelineEl = timeline;
322
+ this.paginationEl = this.timelineRenderer.renderPagination(this.pagination.page, this.pagination.pageSize, this.filteredData.length);
323
+ if (this.filterEl) this.root.appendChild(this.filterEl);
324
+ this.root.append(this.timelineEl, this.paginationEl);
158
325
  }
159
326
  bindEvents() {
160
- this.root.addEventListener("click", (e) => {
161
- const target = e.target;
162
- const card = target.closest(".lv-timeline-card");
163
- if (card) {
164
- const id = card.closest(".lv-timeline-item")?.getAttribute("data-id");
165
- if (!id) return;
166
- const entry = this.filteredData.find((en) => en.id === id);
167
- if (!entry) return;
168
- if (target.classList.contains("lv-timeline-expand-hint")) {
169
- if (this.expandedIds.has(id)) this.collapseEntry(id);
170
- else this.expandEntry(id);
171
- return;
172
- }
173
- this.emitter.emit("entry:click", entry);
174
- this.config.onEntryClick?.(entry);
327
+ this.root.addEventListener("change", (event) => {
328
+ const target = event.target;
329
+ if (!(target instanceof HTMLInputElement) && !(target instanceof HTMLSelectElement)) return;
330
+ const field = target.getAttribute("data-filter-field");
331
+ if (field) this.updateFilterField(field, target.value);
332
+ });
333
+ this.root.addEventListener("click", (event) => {
334
+ const target = event.target;
335
+ if (!(target instanceof Element)) return;
336
+ if (target.closest("[data-filter-reset]")) {
337
+ this.resetFilter();
338
+ return;
339
+ }
340
+ const hint = target.closest(".lv-timeline-expand-hint");
341
+ const id = (hint?.closest(".lv-timeline-item") || target.closest(".lv-timeline-item"))?.getAttribute("data-id");
342
+ if (hint && id) {
343
+ if (this.expandedIds.has(id)) this.collapseEntry(id);
344
+ else this.expandEntry(id);
345
+ return;
346
+ }
347
+ const pageButton = target.closest(".lv-pagination-btn");
348
+ if (pageButton && !pageButton.disabled) {
349
+ const page = Number(pageButton.getAttribute("data-page"));
350
+ if (Number.isFinite(page)) this.goToPage(page);
351
+ return;
175
352
  }
176
- const pageBtn = target.closest(".lv-pagination-btn");
177
- if (pageBtn && !pageBtn.disabled) {
178
- const page = parseInt(pageBtn.getAttribute("data-page") || "1", 10);
179
- this.goToPage(page);
353
+ if (!target.closest(".lv-timeline-card") || !id) return;
354
+ const entry = this.filteredData.find((candidate) => candidate.id === id);
355
+ if (entry) {
356
+ const snapshot = require_index$1.deepClone(entry);
357
+ this.emitter.emit("entry:click", snapshot);
358
+ this.config.onEntryClick?.(snapshot);
180
359
  }
181
360
  });
182
361
  }