@talex-touch/utils 1.0.29 → 1.0.31

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.
@@ -16,7 +16,7 @@
16
16
  * @module core-box/tuff-dsl
17
17
  */
18
18
 
19
- import { TalexTouch } from "packages/utils/types";
19
+ // import { TalexTouch } from "packages/utils/types";
20
20
 
21
21
  /**
22
22
  * 定义高亮范围
@@ -375,55 +375,15 @@ export interface TuffCustomRender {
375
375
  scripts?: string[];
376
376
  }
377
377
 
378
+ import type { ITuffIcon } from '../../types/icon'
379
+
378
380
  /**
379
- * 图标定义
381
+ * Icon definition
380
382
  *
381
383
  * @description
382
- * 支持多种图标类型,从简单的emoji到复杂的组件。
383
- * 可以是简单字符串或包含详细配置的对象。
384
+ * Unified icon type supporting only ITuffIcon object format
384
385
  */
385
- export type TuffIcon =
386
- | string // 简单字符串:emoji、URL、组件名
387
- | {
388
- /**
389
- * 图标类型
390
- * @description 指定图标的数据格式和来源
391
- * @required
392
- */
393
- type: 'emoji' | 'url' | 'base64' | 'fluent' | 'component';
394
-
395
- /**
396
- * 图标值
397
- * @description 根据type不同,可能是emoji字符、URL地址、Base64编码或组件名
398
- * @required
399
- */
400
- value: string;
401
-
402
- /**
403
- * 备用图标
404
- * @description 当主图标无法加载时显示的替代图标
405
- */
406
- fallback?: string;
407
-
408
- /**
409
- * 动态加载函数
410
- * @description 用于异步加载图标资源的函数
411
- */
412
- loader?: () => Promise<string>;
413
-
414
- /**
415
- * 样式配置
416
- * @description 控制图标的视觉效果
417
- */
418
- style?: {
419
- /** 图标尺寸 */
420
- size?: number;
421
- /** 图标颜色 */
422
- color?: string;
423
- /** 动画效果 */
424
- animation?: 'spin' | 'pulse' | 'bounce';
425
- };
426
- };
386
+ export type TuffIcon = ITuffIcon
427
387
 
428
388
  /**
429
389
  * 标签定义
@@ -1081,18 +1041,88 @@ export interface TuffDisplayAction extends Omit<TuffAction, 'payload'> {
1081
1041
 
1082
1042
  // ==================== 工具类型 ====================
1083
1043
 
1044
+ /**
1045
+ * 查询输入类型枚举
1046
+ *
1047
+ * @description
1048
+ * 定义系统支持的所有输入类型。
1049
+ * 用于插件、providers 和 features 声明其支持的输入类型。
1050
+ */
1051
+ export enum TuffInputType {
1052
+ /** 纯文本输入 */
1053
+ Text = 'text',
1054
+ /** 图像输入(data URL 格式) */
1055
+ Image = 'image',
1056
+ /** 文件输入(文件路径数组) */
1057
+ Files = 'files',
1058
+ /** 富文本输入(HTML 格式) */
1059
+ Html = 'html'
1060
+ }
1061
+
1062
+ /**
1063
+ * 查询输入项
1064
+ *
1065
+ * @description
1066
+ * 定义查询中除文本外的多种类型输入(如剪贴板数据)。
1067
+ * 用于支持图像、文件、富文本等多媒体输入。
1068
+ */
1069
+ export interface TuffQueryInput {
1070
+ /**
1071
+ * 输入类型
1072
+ * @description 定义输入数据的类型
1073
+ * @required
1074
+ */
1075
+ type: TuffInputType;
1076
+
1077
+ /**
1078
+ * 输入内容
1079
+ * @description 根据类型不同包含不同格式的数据:
1080
+ * - text: 纯文本字符串
1081
+ * - image: data URL 格式的图像数据
1082
+ * - files: JSON 序列化的文件路径数组
1083
+ * - html: HTML 格式的富文本
1084
+ * @required
1085
+ */
1086
+ content: string;
1087
+
1088
+ /**
1089
+ * 原始内容
1090
+ * @description 可选的原始格式内容,如富文本的 HTML 源码
1091
+ */
1092
+ rawContent?: string;
1093
+
1094
+ /**
1095
+ * 缩略图
1096
+ * @description 图像的缩略图 data URL(用于预览)
1097
+ */
1098
+ thumbnail?: string;
1099
+
1100
+ /**
1101
+ * 元数据
1102
+ * @description 附加的元数据信息
1103
+ */
1104
+ metadata?: Record<string, any>;
1105
+ }
1106
+
1084
1107
  /**
1085
1108
  * 搜索查询结构
1086
1109
  *
1087
1110
  * @description
1088
1111
  * 定义搜索请求的参数和过滤条件。
1089
1112
  * 系统根据这些参数执行搜索并返回匹配结果。
1113
+ *
1114
+ * **重要区分**:
1115
+ * - `text`: 用户在输入框中主动输入的查询文本
1116
+ * - `inputs`: 来自剪贴板或其他来源的附加输入数据(图像、文件、富文本等)
1090
1117
  */
1091
1118
  export interface TuffQuery {
1092
1119
  /**
1093
- * 查询文本
1094
- * @description 用户输入的搜索文本
1120
+ * 用户输入的查询文本
1121
+ * @description 这是用户在搜索框中主动输入的文本,不包括剪贴板内容
1095
1122
  * @required
1123
+ *
1124
+ * @example
1125
+ * 用户输入 "translate" → text = "translate"
1096
1126
  */
1097
1127
  text: string;
1098
1128
 
@@ -1102,6 +1132,31 @@ export interface TuffQuery {
1102
1132
  */
1103
1133
  type?: 'text' | 'voice' | 'image';
1104
1134
 
1135
+ /**
1136
+ * 多类型输入(附加数据)
1137
+ * @description 除了用户输入的文本外的其他输入数据(如剪贴板中的图像、文件、富文本等)
1138
+ *
1139
+ * **与 text 的区别**:
1140
+ * - `text`: 用户主动输入,总是存在
1141
+ * - `inputs`: 系统自动检测的附加数据,可能为空
1142
+ *
1143
+ * 当用户触发 feature 时,系统会自动检测剪贴板并填充此字段。
1144
+ *
1145
+ * @example
1146
+ * 场景 1: 用户输入 "translate" + 剪贴板有图片
1147
+ * text: "translate"
1148
+ * inputs: [{ type: 'image', content: 'data:image/png;base64,...' }]
1149
+ *
1150
+ * 场景 2: 用户输入 "compress" + 剪贴板有文件
1151
+ * text: "compress"
1152
+ * inputs: [{ type: 'files', content: '["/path/to/file"]' }]
1153
+ *
1154
+ * 场景 3: 用户输入 "format" + 剪贴板有富文本
1155
+ * text: "format"
1156
+ * inputs: [{ type: 'html', content: 'plain text', rawContent: '<p>html</p>' }]
1157
+ */
1158
+ inputs?: TuffQueryInput[];
1159
+
1105
1160
  /**
1106
1161
  * 过滤条件
1107
1162
  * @description 限制搜索范围的过滤器
@@ -1277,6 +1332,15 @@ export interface ISearchProvider<C> {
1277
1332
  */
1278
1333
  readonly icon?: any
1279
1334
 
1335
+ /**
1336
+ * Supported input types
1337
+ * @description Declares which types of inputs this provider can handle.
1338
+ * If not specified, defaults to [TuffInputType.Text] only.
1339
+ * When query contains non-text inputs, only providers supporting those types will be searched.
1340
+ * @example [TuffInputType.Text, TuffInputType.Image, TuffInputType.Files]
1341
+ */
1342
+ readonly supportedInputTypes?: TuffInputType[]
1343
+
1280
1344
  /**
1281
1345
  * Core search method (PULL mode).
1282
1346
  * The engine calls this method to get results from the provider.
package/index.ts CHANGED
@@ -1,8 +1,12 @@
1
+ // export * from './auth' // Renderer-only, use @talex-touch/utils/renderer instead
1
2
  export * from './base'
2
3
  export * from './common'
3
4
  export * from './plugin'
4
5
  export * from './core-box'
5
6
  export * from './channel'
6
7
  export * from './types'
8
+ export * from './types/icon'
7
9
  export * from './eventbus'
8
10
  export * from './preload'
11
+ export * from './types/download'
12
+ export * from './types/update'
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "module": "./index.ts",
6
6
  "license": "MPL-2.0",
7
7
  "private": false,
8
- "version": "1.0.29",
8
+ "version": "1.0.31",
9
9
  "scripts": {
10
10
  "publish": "npm publish --access public"
11
11
  },
package/plugin/index.ts CHANGED
@@ -14,6 +14,9 @@ export enum PluginStatus {
14
14
  LOADING,
15
15
  LOADED,
16
16
  LOAD_FAILED,
17
+
18
+ DEV_DISCONNECTED, // Dev Server 断连
19
+ DEV_RECONNECTING, // 正在重连
17
20
  }
18
21
 
19
22
  export interface PluginIssue {
@@ -26,14 +29,16 @@ export interface PluginIssue {
26
29
  timestamp?: number
27
30
  }
28
31
 
29
- export interface IPluginIcon {
30
- type: string | 'remixicon' | 'class'
31
- value: any
32
- _value?: string
33
-
34
- init(): Promise<void>
32
+ export interface DevServerHealthCheckResult {
33
+ healthy: boolean
34
+ version?: string
35
+ timestamp: number
36
+ error?: string
35
37
  }
36
38
 
39
+ import type { ITuffIcon } from '../types/icon'
40
+
41
+
37
42
  export interface IPlatformInfo {
38
43
  enable: boolean
39
44
  arch: Arch[]
@@ -51,7 +56,7 @@ export interface IPluginBaseInfo {
51
56
  readme: string
52
57
  version: string
53
58
  desc: string
54
- icon: IPluginIcon
59
+ icon: ITuffIcon
55
60
  platforms: IPlatform
56
61
  _uniqueChannelKey: string
57
62
  }
@@ -134,7 +139,7 @@ export interface IPluginFeature {
134
139
  id: string
135
140
  name: string
136
141
  desc: string
137
- icon: IPluginIcon
142
+ icon: ITuffIcon
138
143
  push: boolean
139
144
  platform: IPlatform
140
145
  commands: IFeatureCommand[]
@@ -145,6 +150,15 @@ export interface IPluginFeature {
145
150
  * Default is 0
146
151
  */
147
152
  priority?: number
153
+ /**
154
+ * Accepted input types for this feature
155
+ * @description Declares which types of inputs this feature can accept and process.
156
+ * If not specified, defaults to ['text'] only (backward compatible).
157
+ * When query contains inputs, only features accepting those input types will be shown.
158
+ * @example ['text', 'image'] - Feature accepts both text and images
159
+ * @example ['image', 'files'] - Feature only accepts images and files (no text-only queries)
160
+ */
161
+ acceptedInputTypes?: Array<'text' | 'image' | 'files' | 'html'>
148
162
  }
149
163
 
150
164
  export type IFeatureInteraction = {
@@ -182,11 +196,14 @@ export interface IFeatureLifeCycle {
182
196
  /**
183
197
  * Called when a feature is triggered via a matching command.
184
198
  * @param id - Feature ID
185
- * @param data - The triggering payload
199
+ * @param data - The triggering payload. Can be:
200
+ * - string: Plain text query (backward compatible)
201
+ * - TuffQuery object: Complete query with text and optional inputs array containing clipboard data (images, files, HTML)
186
202
  * @param feature - The full feature definition
187
203
  * @param signal - An AbortSignal to cancel the operation
204
+ * @returns If returns false, the feature will not enter activation state (e.g., just opens browser and exits)
188
205
  */
189
- onFeatureTriggered(id: string, data: any, feature: IPluginFeature, signal?: AbortSignal): void
206
+ onFeatureTriggered(id: string, data: any, feature: IPluginFeature, signal?: AbortSignal): boolean | void
190
207
 
191
208
  /**
192
209
  * Called when user input changes within this feature’s input box.
@@ -248,10 +265,13 @@ export interface ITargetFeatureLifeCycle {
248
265
 
249
266
  /**
250
267
  * Called when the feature is triggered via a matching command.
251
- * @param data - The triggering payload
268
+ * @param data - The triggering payload. Can be:
269
+ * - string: Plain text query (backward compatible)
270
+ * - TuffQuery object: Complete query with text and optional inputs array containing clipboard data (images, files, HTML)
252
271
  * @param feature - The full feature definition
272
+ * @returns If returns false, the feature will not enter activation state (e.g., just opens browser and exits)
253
273
  */
254
- onFeatureTriggered(data: any, feature: IPluginFeature): void
274
+ onFeatureTriggered(data: any, feature: IPluginFeature): boolean | void
255
275
 
256
276
  /**
257
277
  * Called when user input changes within this feature’s input box.
@@ -323,11 +343,14 @@ export interface IPluginManager {
323
343
  pluginPath: string
324
344
  watcher: FSWatcher | null
325
345
  devWatcher: any // Temporarily any, as DevPluginWatcher is internal to core-app
346
+ healthMonitor: any | null // DevServerHealthMonitor instance, set by PluginModule
326
347
 
327
348
  getPluginList(): Array<object>
328
349
  setActivePlugin(pluginName: string): boolean
329
350
  hasPlugin(name: string): boolean
330
351
  getPluginByName(name: string): ITouchPlugin | undefined
352
+ enablePlugin(pluginName: string): Promise<boolean>
353
+ disablePlugin(pluginName: string): Promise<boolean>
331
354
  reloadPlugin(pluginName: string): Promise<void>
332
355
  persistEnabledPlugins(): Promise<void>
333
356
  listPlugins(): Promise<Array<string>>
package/plugin/preload.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  import type { ITouchClientChannel } from '../channel'
2
+ import type { ITouchSDK } from './sdk/index'
3
+ // Import SDK for side effects (initializes hooks)
2
4
  import './sdk/index'
3
5
 
4
- // window type
6
+ // window type - includes both plugin preload types and SDK types
5
7
  declare global {
6
- export interface Window {
8
+ interface Window {
7
9
  $plugin: {
8
10
  name: string
9
11
  path: Object
@@ -13,6 +15,7 @@ declare global {
13
15
  $config: {
14
16
  themeStyle: any
15
17
  }
18
+ $touchSDK: ITouchSDK
16
19
  }
17
20
  }
18
21
 
@@ -121,7 +121,7 @@ export interface IFeaturesManager {
121
121
  * ```
122
122
  */
123
123
  export function createFeaturesManager(
124
- pluginName: string,
124
+ _pluginName: string,
125
125
  utils: any
126
126
  ): IFeaturesManager {
127
127
  return {
@@ -5,12 +5,7 @@ export interface ITouchSDK {
5
5
  __hooks: {}
6
6
  }
7
7
 
8
- // window type
9
- declare global {
10
- export interface Window {
11
- $touchSDK: ITouchSDK
12
- }
13
- }
8
+ // Note: Window.$touchSDK is declared in ../preload.ts to avoid duplicate declarations
14
9
 
15
10
  export * from './types'
16
11
  export * from './window/index'
@@ -22,4 +17,4 @@ export * from './clipboard'
22
17
  export * from './core-box'
23
18
  export * from './storage'
24
19
  export * from './system'
25
- export * from './features'
20
+ export { createFeaturesManager, useFeatures } from './features'
@@ -1,3 +1,5 @@
1
+ import type { StorageStats, StorageTreeNode, FileDetails } from '../../types/storage'
2
+
1
3
  /**
2
4
  * Get the storage for the current plugin.
3
5
  * It provides simple file-based storage that is persisted across application launches.
@@ -50,6 +52,68 @@ export function usePluginStorage() {
50
52
  */
51
53
  listFiles: async (): Promise<string[]> => {
52
54
  return channel.send('plugin:storage:list-files', { pluginName })
55
+ },
56
+
57
+ /**
58
+ * Gets storage statistics for the current plugin.
59
+ * @returns A promise that resolves with storage statistics.
60
+ */
61
+ getStats: async (): Promise<StorageStats> => {
62
+ return channel.send('plugin:storage:get-stats', { pluginName })
63
+ },
64
+
65
+ /**
66
+ * Gets the directory tree structure of plugin storage.
67
+ * @returns A promise that resolves with the tree structure.
68
+ */
69
+ getTree: async (): Promise<StorageTreeNode[]> => {
70
+ return channel.send('plugin:storage:get-tree', { pluginName })
71
+ },
72
+
73
+ /**
74
+ * Gets detailed information about a specific file.
75
+ * @param fileName The name of the file to get details for.
76
+ * @returns A promise that resolves with file details.
77
+ */
78
+ getFileDetails: async (fileName: string): Promise<FileDetails | null> => {
79
+ return channel.send('plugin:storage:get-file-details', { pluginName, fileName })
80
+ },
81
+
82
+ /**
83
+ * Clears all storage for the current plugin.
84
+ * @returns A promise that resolves with the operation result.
85
+ */
86
+ clearAll: async (): Promise<{ success: boolean, error?: string }> => {
87
+ return channel.send('plugin:storage:clear', { pluginName })
88
+ },
89
+
90
+ /**
91
+ * Opens the plugin storage folder in the system file manager.
92
+ * @returns A promise that resolves when the folder is opened.
93
+ */
94
+ openFolder: async (): Promise<void> => {
95
+ await channel.send('plugin:storage:open-folder', { pluginName })
96
+ },
97
+
98
+ /**
99
+ * Listens for changes to the storage.
100
+ * @param fileName The file name to listen for changes
101
+ * @param callback The function to call when the storage changes for the current plugin.
102
+ * @returns A function to unsubscribe from the listener.
103
+ */
104
+ onDidChange: (fileName: string, callback: (newConfig: any) => void) => {
105
+ const listener = (data: { name: string, fileName?: string }) => {
106
+ if (data.name === pluginName &&
107
+ (data.fileName === fileName || data.fileName === undefined)) {
108
+ callback(data)
109
+ }
110
+ }
111
+
112
+ channel.regChannel('plugin:storage:update', listener)
113
+
114
+ return () => {
115
+ channel.unRegChannel('plugin:storage:update', listener)
116
+ }
53
117
  }
54
118
  }
55
119
  }
@@ -518,9 +518,29 @@ export interface IPluginLifecycle {
518
518
  /**
519
519
  * Called when a plugin feature is triggered
520
520
  * @param featureId - The ID of the triggered feature
521
- * @param query - The search query or input data
521
+ * @param query - The search query or input data. Can be:
522
+ * - string: Plain text query (backward compatible)
523
+ * - TuffQuery object: Complete query with text and optional inputs array
524
+ * - query.text: The text query string
525
+ * - query.inputs: Array of TuffQueryInput objects (images, files, HTML)
522
526
  * @param feature - The feature configuration object
523
527
  * @returns Promise or void
528
+ * @example
529
+ * ```typescript
530
+ * onFeatureTriggered(featureId, query, feature) {
531
+ * if (typeof query === 'string') {
532
+ * // Backward compatible: plain text query
533
+ * console.log('Text query:', query)
534
+ * } else {
535
+ * // New: complete query object
536
+ * console.log('Text:', query.text)
537
+ * const imageInput = query.inputs?.find(i => i.type === 'image')
538
+ * if (imageInput) {
539
+ * console.log('Image data:', imageInput.content)
540
+ * }
541
+ * }
542
+ * }
543
+ * ```
524
544
  */
525
545
  onFeatureTriggered(featureId: string, query: any, feature: any): Promise<void> | void;
526
546
 
@@ -842,3 +862,78 @@ export interface IPluginInfoManager {
842
862
  */
843
863
  getPlatforms(): any
844
864
  }
865
+
866
+ /**
867
+ * Plugin state change event types
868
+ *
869
+ * @description
870
+ * Represents different types of plugin state changes for incremental updates
871
+ */
872
+ export type PluginStateEvent =
873
+ | { type: 'added'; plugin: any }
874
+ | { type: 'removed'; name: string }
875
+ | { type: 'updated'; name: string; changes: Partial<any> }
876
+ | { type: 'status-changed'; name: string; status: number }
877
+ | { type: 'readme-updated'; name: string; readme: string }
878
+
879
+ /**
880
+ * Plugin filter options for list queries
881
+ */
882
+ export interface PluginFilters {
883
+ /** Filter by plugin status */
884
+ status?: number
885
+ /** Filter by enabled state */
886
+ enabled?: boolean
887
+ /** Filter by development mode */
888
+ dev?: boolean
889
+ }
890
+
891
+ /**
892
+ * Trigger feature request payload
893
+ */
894
+ export interface TriggerFeatureRequest {
895
+ /** Plugin name */
896
+ plugin: string
897
+ /** Feature ID */
898
+ feature: string
899
+ /** Search query or trigger input */
900
+ query?: string
901
+ }
902
+
903
+ /**
904
+ * Input changed event payload
905
+ */
906
+ export interface InputChangedRequest {
907
+ /** Plugin name */
908
+ plugin: string
909
+ /** Feature ID */
910
+ feature: string
911
+ /** Current input value */
912
+ query: string
913
+ }
914
+
915
+ /**
916
+ * Plugin install request payload
917
+ */
918
+ export interface InstallRequest {
919
+ /** Install source (URL, file path, etc) */
920
+ source: string
921
+ /** Hint type for installer */
922
+ hintType?: string
923
+ /** Additional metadata */
924
+ metadata?: Record<string, any>
925
+ /** Client metadata */
926
+ clientMetadata?: Record<string, any>
927
+ }
928
+
929
+ /**
930
+ * Plugin install response
931
+ */
932
+ export interface InstallResponse {
933
+ /** Whether installation was successful */
934
+ success: boolean
935
+ /** Error message if failed */
936
+ error?: string
937
+ /** Installed plugin name */
938
+ pluginName?: string
939
+ }
@@ -2,7 +2,7 @@ import type { LoadingEvent, LoadingMode, LoadingState, PreloadAPI } from './load
2
2
 
3
3
  function getPreloadApi(): PreloadAPI | null {
4
4
  if (typeof window === 'undefined') return null
5
- return window.api ?? null
5
+ return (window as any).api ?? null
6
6
  }
7
7
 
8
8
  export function sendPreloadEvent(event: LoadingEvent): void {
@@ -1,2 +1,3 @@
1
1
  export * from './arg-mapper'
2
2
  export * from './initialize'
3
+ export * from './performance'
@@ -196,3 +196,8 @@ export function refreshPerformanceInfo(): IInitializationInfo {
196
196
  }
197
197
  return window.$initInfo
198
198
  }
199
+
200
+ /**
201
+ * Development mode flag
202
+ */
203
+ export const isDev = import.meta.env.MODE === 'development'
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Renderer Process Performance Monitoring
3
+ *
4
+ * Collects detailed performance metrics in the renderer process
5
+ */
6
+
7
+ /**
8
+ * Renderer performance metrics
9
+ */
10
+ export interface RendererPerformanceMetrics {
11
+ /** Performance timing origin */
12
+ timeOrigin: number
13
+ /** Navigation start time */
14
+ navigationStart: number
15
+ /** DOM content loaded event end */
16
+ domContentLoadedEventEnd?: number
17
+ /** Load event end */
18
+ loadEventEnd?: number
19
+ /** DOM interactive time */
20
+ domInteractive?: number
21
+ /** First paint time */
22
+ firstPaint?: number
23
+ /** First contentful paint time */
24
+ firstContentfulPaint?: number
25
+ }
26
+
27
+ /**
28
+ * Get current renderer performance metrics
29
+ */
30
+ export function getRendererPerformanceMetrics(): RendererPerformanceMetrics {
31
+ const navEntry = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming
32
+ const paintEntries = performance.getEntriesByType('paint')
33
+
34
+ const firstPaint = paintEntries.find(e => e.name === 'first-paint')
35
+ const firstContentfulPaint = paintEntries.find(e => e.name === 'first-contentful-paint')
36
+
37
+ return {
38
+ timeOrigin: performance.timeOrigin,
39
+ navigationStart: performance.timeOrigin,
40
+ domContentLoadedEventEnd: navEntry?.domContentLoadedEventEnd,
41
+ loadEventEnd: navEntry?.loadEventEnd,
42
+ domInteractive: navEntry?.domInteractive,
43
+ firstPaint: firstPaint?.startTime,
44
+ firstContentfulPaint: firstContentfulPaint?.startTime
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Wait for page load and collect metrics
50
+ */
51
+ export function collectPerformanceMetricsOnLoad(): Promise<RendererPerformanceMetrics> {
52
+ return new Promise((resolve) => {
53
+ if (document.readyState === 'complete') {
54
+ // Page already loaded
55
+ setTimeout(() => {
56
+ resolve(getRendererPerformanceMetrics())
57
+ }, 0)
58
+ } else {
59
+ // Wait for load event
60
+ window.addEventListener('load', () => {
61
+ setTimeout(() => {
62
+ resolve(getRendererPerformanceMetrics())
63
+ }, 0)
64
+ }, { once: true })
65
+ }
66
+ })
67
+ }
68
+
69
+ /**
70
+ * Get performance summary for display
71
+ */
72
+ export function getPerformanceSummary(): {
73
+ domReady: number
74
+ pageLoad: number
75
+ firstPaint: number | undefined
76
+ firstContentfulPaint: number | undefined
77
+ } {
78
+ const metrics = getRendererPerformanceMetrics()
79
+
80
+ return {
81
+ domReady: metrics.domContentLoadedEventEnd || 0,
82
+ pageLoad: metrics.loadEventEnd || 0,
83
+ firstPaint: metrics.firstPaint,
84
+ firstContentfulPaint: metrics.firstContentfulPaint
85
+ }
86
+ }
87
+