@urk/adapters 0.1.0
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/README.md +80 -0
- package/dist/adapters.d.ts +11 -0
- package/dist/adapters.d.ts.map +1 -0
- package/dist/adapters.js +11 -0
- package/dist/adapters.js.map +1 -0
- package/dist/contracts.d.ts +11 -0
- package/dist/contracts.d.ts.map +1 -0
- package/dist/contracts.js +11 -0
- package/dist/contracts.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/input.d.ts +37 -0
- package/dist/input.d.ts.map +1 -0
- package/dist/input.js +137 -0
- package/dist/input.js.map +1 -0
- package/dist/loading.d.ts +35 -0
- package/dist/loading.d.ts.map +1 -0
- package/dist/loading.js +123 -0
- package/dist/loading.js.map +1 -0
- package/dist/pointer.d.ts +43 -0
- package/dist/pointer.d.ts.map +1 -0
- package/dist/pointer.js +135 -0
- package/dist/pointer.js.map +1 -0
- package/dist/storage.d.ts +24 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +125 -0
- package/dist/storage.js.map +1 -0
- package/dist/three.d.ts +28 -0
- package/dist/three.d.ts.map +1 -0
- package/dist/three.js +131 -0
- package/dist/three.js.map +1 -0
- package/dist/ui-widgets.d.ts +23 -0
- package/dist/ui-widgets.d.ts.map +1 -0
- package/dist/ui-widgets.js +92 -0
- package/dist/ui-widgets.js.map +1 -0
- package/package.json +35 -0
- package/src/adapters.ts +11 -0
- package/src/contracts.ts +15 -0
- package/src/index.ts +5 -0
- package/src/input.ts +207 -0
- package/src/loading.ts +174 -0
- package/src/pointer.ts +203 -0
- package/src/storage.ts +194 -0
- package/src/three.ts +178 -0
- package/src/ui-widgets.ts +120 -0
package/src/storage.ts
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Company: EonHive Inc.
|
|
3
|
+
* Title: Storage Adapter
|
|
4
|
+
* Purpose: Expose a small namespaced storage capability for local and session persistence.
|
|
5
|
+
* Author: Stan Nesi
|
|
6
|
+
* Created: 2026-04-21
|
|
7
|
+
* Updated: 2026-04-21
|
|
8
|
+
* Notes: Vibe coded with Codex.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { AdapterRegistration, RuntimeContext } from '@urk/core';
|
|
12
|
+
|
|
13
|
+
export type StorageArea = 'local' | 'session';
|
|
14
|
+
|
|
15
|
+
export interface StorageAdapterOptions {
|
|
16
|
+
id?: string;
|
|
17
|
+
namespace?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface StorageAdapterApi {
|
|
21
|
+
getItem<T = unknown>(key: string, area?: StorageArea): T | null;
|
|
22
|
+
setItem<T>(key: string, value: T, area?: StorageArea): void;
|
|
23
|
+
removeItem(key: string, area?: StorageArea): void;
|
|
24
|
+
listKeys(area?: StorageArea): string[];
|
|
25
|
+
clear(area?: StorageArea): void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type StorageBackendLike = {
|
|
29
|
+
length: number;
|
|
30
|
+
key(index: number): string | null;
|
|
31
|
+
getItem(key: string): string | null;
|
|
32
|
+
setItem(key: string, value: string): void;
|
|
33
|
+
removeItem(key: string): void;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const DEFAULT_NAMESPACE = 'urk';
|
|
37
|
+
|
|
38
|
+
function isStorageBackendLike(value: unknown): value is StorageBackendLike {
|
|
39
|
+
if (typeof value !== 'object' || value === null) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const candidate = value as Partial<StorageBackendLike>;
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
typeof candidate.length === 'number' &&
|
|
47
|
+
typeof candidate.key === 'function' &&
|
|
48
|
+
typeof candidate.getItem === 'function' &&
|
|
49
|
+
typeof candidate.setItem === 'function' &&
|
|
50
|
+
typeof candidate.removeItem === 'function'
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function resolveServiceBackend(
|
|
55
|
+
ctx: RuntimeContext,
|
|
56
|
+
area: StorageArea,
|
|
57
|
+
): StorageBackendLike | null {
|
|
58
|
+
const serviceName = area === 'local' ? 'storage:local' : 'storage:session';
|
|
59
|
+
const value = ctx.services.get<unknown>(serviceName);
|
|
60
|
+
|
|
61
|
+
if (value === undefined) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!isStorageBackendLike(value)) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`Service ${serviceName} must provide a Storage-compatible backend.`,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return value;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function resolveWindowBackend(area: StorageArea): StorageBackendLike | null {
|
|
75
|
+
if (typeof window === 'undefined') {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
return area === 'local' ? window.localStorage : window.sessionStorage;
|
|
81
|
+
} catch (error) {
|
|
82
|
+
const message =
|
|
83
|
+
error instanceof Error ? error.message : `Unable to access ${area}Storage.`;
|
|
84
|
+
|
|
85
|
+
throw new Error(`Storage adapter could not access ${area}Storage: ${message}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function resolveBackend(ctx: RuntimeContext, area: StorageArea): StorageBackendLike {
|
|
90
|
+
const serviceBackend = resolveServiceBackend(ctx, area);
|
|
91
|
+
|
|
92
|
+
if (serviceBackend) {
|
|
93
|
+
return serviceBackend;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const windowBackend = resolveWindowBackend(area);
|
|
97
|
+
|
|
98
|
+
if (windowBackend) {
|
|
99
|
+
return windowBackend;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
throw new Error(`Storage adapter requires a ${area} storage backend.`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function getStoragePrefix(namespace: string): string {
|
|
106
|
+
return `${namespace}:`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function getNamespacedKey(namespace: string, key: string): string {
|
|
110
|
+
return `${getStoragePrefix(namespace)}${key}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function collectNamespacedKeys(
|
|
114
|
+
backend: StorageBackendLike,
|
|
115
|
+
namespace: string,
|
|
116
|
+
): string[] {
|
|
117
|
+
const keys: string[] = [];
|
|
118
|
+
const prefix = getStoragePrefix(namespace);
|
|
119
|
+
|
|
120
|
+
for (let index = 0; index < backend.length; index += 1) {
|
|
121
|
+
const key = backend.key(index);
|
|
122
|
+
|
|
123
|
+
if (!key || !key.startsWith(prefix)) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
keys.push(key.slice(prefix.length));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return keys;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function createStorageAdapter(
|
|
134
|
+
options: StorageAdapterOptions = {},
|
|
135
|
+
): AdapterRegistration<StorageAdapterApi> {
|
|
136
|
+
const id = options.id ?? 'storage-adapter';
|
|
137
|
+
const namespace = options.namespace?.trim() || DEFAULT_NAMESPACE;
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
id,
|
|
141
|
+
capability: 'storage',
|
|
142
|
+
setup(ctx) {
|
|
143
|
+
const backends: Record<StorageArea, StorageBackendLike> = {
|
|
144
|
+
local: resolveBackend(ctx, 'local'),
|
|
145
|
+
session: resolveBackend(ctx, 'session'),
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const getBackend = (area: StorageArea = 'local'): StorageBackendLike => {
|
|
149
|
+
return backends[area];
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
getItem<T = unknown>(key: string, area: StorageArea = 'local'): T | null {
|
|
154
|
+
const raw = getBackend(area).getItem(getNamespacedKey(namespace, key));
|
|
155
|
+
|
|
156
|
+
if (raw === null) {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
return JSON.parse(raw) as T;
|
|
162
|
+
} catch {
|
|
163
|
+
throw new Error(
|
|
164
|
+
`Stored value for key "${key}" in ${area} storage is not valid JSON.`,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
setItem<T>(key: string, value: T, area: StorageArea = 'local'): void {
|
|
169
|
+
const serialized = JSON.stringify(value);
|
|
170
|
+
|
|
171
|
+
if (serialized === undefined) {
|
|
172
|
+
throw new Error(`Storage value for key "${key}" is not JSON serializable.`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
getBackend(area).setItem(getNamespacedKey(namespace, key), serialized);
|
|
176
|
+
},
|
|
177
|
+
removeItem(key: string, area: StorageArea = 'local'): void {
|
|
178
|
+
getBackend(area).removeItem(getNamespacedKey(namespace, key));
|
|
179
|
+
},
|
|
180
|
+
listKeys(area: StorageArea = 'local'): string[] {
|
|
181
|
+
return collectNamespacedKeys(getBackend(area), namespace);
|
|
182
|
+
},
|
|
183
|
+
clear(area: StorageArea = 'local'): void {
|
|
184
|
+
const backend = getBackend(area);
|
|
185
|
+
|
|
186
|
+
// Only remove keys owned by this adapter namespace.
|
|
187
|
+
for (const key of collectNamespacedKeys(backend, namespace)) {
|
|
188
|
+
backend.removeItem(getNamespacedKey(namespace, key));
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
}
|
package/src/three.ts
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Company: EonHive Inc.
|
|
3
|
+
* Title: Three Adapter
|
|
4
|
+
* Purpose: Mount a small Three.js scene surface behind a stable URK capability contract.
|
|
5
|
+
* Author: Stan Nesi
|
|
6
|
+
* Created: 2026-04-22
|
|
7
|
+
* Updated: 2026-04-22
|
|
8
|
+
* Notes: Vibe coded with Codex.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { AdapterRegistration } from '@urk/core';
|
|
12
|
+
import * as THREE from 'three';
|
|
13
|
+
|
|
14
|
+
export interface ThreeAdapterSize {
|
|
15
|
+
width: number;
|
|
16
|
+
height: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ThreeAdapterApi {
|
|
20
|
+
getHost(): HTMLElement;
|
|
21
|
+
getCanvas(): HTMLCanvasElement;
|
|
22
|
+
getScene(): THREE.Scene;
|
|
23
|
+
getCamera(): THREE.PerspectiveCamera;
|
|
24
|
+
getRenderer(): THREE.WebGLRenderer;
|
|
25
|
+
getSize(): ThreeAdapterSize;
|
|
26
|
+
resize(): void;
|
|
27
|
+
render(): void;
|
|
28
|
+
raycast(
|
|
29
|
+
clientX: number,
|
|
30
|
+
clientY: number,
|
|
31
|
+
objects: THREE.Object3D[],
|
|
32
|
+
): THREE.Intersection<THREE.Object3D>[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function assertHtmlElement(value: unknown, serviceName: string): HTMLElement {
|
|
36
|
+
if (typeof HTMLElement === 'undefined' || !(value instanceof HTMLElement)) {
|
|
37
|
+
throw new Error(`Service ${serviceName} must be an HTMLElement.`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getHostSize(host: HTMLElement): ThreeAdapterSize {
|
|
44
|
+
return {
|
|
45
|
+
width: Math.max(host.clientWidth, 1),
|
|
46
|
+
height: Math.max(host.clientHeight, 1),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getPixelRatio(): number {
|
|
51
|
+
if (typeof window === 'undefined') {
|
|
52
|
+
return 1;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return Math.min(window.devicePixelRatio || 1, 2);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function createThreeAdapter(
|
|
59
|
+
id = 'three-adapter',
|
|
60
|
+
): AdapterRegistration<ThreeAdapterApi> {
|
|
61
|
+
let disposeResizeTracking: (() => void) | null = null;
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
id,
|
|
65
|
+
capability: 'three',
|
|
66
|
+
isSupported() {
|
|
67
|
+
return typeof window !== 'undefined' && typeof document !== 'undefined';
|
|
68
|
+
},
|
|
69
|
+
setup(ctx) {
|
|
70
|
+
const host = assertHtmlElement(ctx.services.require('three:host'), 'three:host');
|
|
71
|
+
const scene = new THREE.Scene();
|
|
72
|
+
const camera = new THREE.PerspectiveCamera(42, 1, 0.1, 100);
|
|
73
|
+
const renderer = new THREE.WebGLRenderer({
|
|
74
|
+
alpha: true,
|
|
75
|
+
antialias: true,
|
|
76
|
+
});
|
|
77
|
+
const raycaster = new THREE.Raycaster();
|
|
78
|
+
const raycastPointer = new THREE.Vector2();
|
|
79
|
+
|
|
80
|
+
camera.position.set(0, 0, 5.75);
|
|
81
|
+
camera.lookAt(0, 0, 0);
|
|
82
|
+
|
|
83
|
+
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
|
84
|
+
renderer.setClearColor(0x000000, 0);
|
|
85
|
+
renderer.domElement.style.width = '100%';
|
|
86
|
+
renderer.domElement.style.height = '100%';
|
|
87
|
+
renderer.domElement.style.display = 'block';
|
|
88
|
+
renderer.domElement.style.pointerEvents = 'none';
|
|
89
|
+
|
|
90
|
+
host.append(renderer.domElement);
|
|
91
|
+
|
|
92
|
+
let size = getHostSize(host);
|
|
93
|
+
|
|
94
|
+
const resize = (): void => {
|
|
95
|
+
size = getHostSize(host);
|
|
96
|
+
renderer.setPixelRatio(getPixelRatio());
|
|
97
|
+
renderer.setSize(size.width, size.height, false);
|
|
98
|
+
camera.aspect = size.width / size.height;
|
|
99
|
+
camera.updateProjectionMatrix();
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
resize();
|
|
103
|
+
|
|
104
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
105
|
+
const observer = new ResizeObserver(() => {
|
|
106
|
+
resize();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
observer.observe(host);
|
|
110
|
+
disposeResizeTracking = () => {
|
|
111
|
+
observer.disconnect();
|
|
112
|
+
};
|
|
113
|
+
} else if (typeof window !== 'undefined') {
|
|
114
|
+
const onResize = (): void => {
|
|
115
|
+
resize();
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
window.addEventListener('resize', onResize);
|
|
119
|
+
disposeResizeTracking = () => {
|
|
120
|
+
window.removeEventListener('resize', onResize);
|
|
121
|
+
};
|
|
122
|
+
} else {
|
|
123
|
+
disposeResizeTracking = null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
getHost() {
|
|
128
|
+
return host;
|
|
129
|
+
},
|
|
130
|
+
getCanvas() {
|
|
131
|
+
return renderer.domElement;
|
|
132
|
+
},
|
|
133
|
+
getScene() {
|
|
134
|
+
return scene;
|
|
135
|
+
},
|
|
136
|
+
getCamera() {
|
|
137
|
+
return camera;
|
|
138
|
+
},
|
|
139
|
+
getRenderer() {
|
|
140
|
+
return renderer;
|
|
141
|
+
},
|
|
142
|
+
getSize() {
|
|
143
|
+
return { ...size };
|
|
144
|
+
},
|
|
145
|
+
resize,
|
|
146
|
+
render() {
|
|
147
|
+
renderer.render(scene, camera);
|
|
148
|
+
},
|
|
149
|
+
raycast(clientX, clientY, objects) {
|
|
150
|
+
if (objects.length === 0) {
|
|
151
|
+
return [];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const bounds = renderer.domElement.getBoundingClientRect();
|
|
155
|
+
|
|
156
|
+
if (bounds.width <= 0 || bounds.height <= 0) {
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
raycastPointer.set(
|
|
161
|
+
((clientX - bounds.left) / bounds.width) * 2 - 1,
|
|
162
|
+
-((clientY - bounds.top) / bounds.height) * 2 + 1,
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
raycaster.setFromCamera(raycastPointer, camera);
|
|
166
|
+
|
|
167
|
+
return raycaster.intersectObjects(objects, true);
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
},
|
|
171
|
+
dispose(_ctx, api) {
|
|
172
|
+
disposeResizeTracking?.();
|
|
173
|
+
disposeResizeTracking = null;
|
|
174
|
+
api.getRenderer().dispose();
|
|
175
|
+
api.getCanvas().remove();
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Company: EonHive Inc.
|
|
3
|
+
* Title: UI Widgets Adapter
|
|
4
|
+
* Purpose: Mount a small overlay shell with status and callout surfaces.
|
|
5
|
+
* Author: Stan Nesi
|
|
6
|
+
* Created: 2026-04-12
|
|
7
|
+
* Updated: 2026-04-15
|
|
8
|
+
* Notes: Vibe coded with Codex.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { AdapterRegistration } from '@urk/core';
|
|
12
|
+
|
|
13
|
+
export interface UiWidgetCallout {
|
|
14
|
+
title: string;
|
|
15
|
+
body: string;
|
|
16
|
+
tone?: 'neutral' | 'active' | 'selected';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface UiWidgetsAdapterApi {
|
|
20
|
+
setStatus(message: string): void;
|
|
21
|
+
showCallout(callout: UiWidgetCallout): void;
|
|
22
|
+
hideCallout(): void;
|
|
23
|
+
destroy(): void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function assertHtmlElement(value: unknown, serviceName: string): HTMLElement {
|
|
27
|
+
if (typeof HTMLElement === 'undefined' || !(value instanceof HTMLElement)) {
|
|
28
|
+
throw new Error(`Service ${serviceName} must be an HTMLElement.`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function createUiWidgetsAdapter(
|
|
35
|
+
id = 'ui-widgets-adapter',
|
|
36
|
+
): AdapterRegistration<UiWidgetsAdapterApi> {
|
|
37
|
+
return {
|
|
38
|
+
id,
|
|
39
|
+
capability: 'ui-widgets',
|
|
40
|
+
setup(ctx) {
|
|
41
|
+
const host = assertHtmlElement(ctx.services.require('ui:host'), 'ui:host');
|
|
42
|
+
const root = document.createElement('div');
|
|
43
|
+
const status = document.createElement('div');
|
|
44
|
+
const callout = document.createElement('div');
|
|
45
|
+
const title = document.createElement('div');
|
|
46
|
+
const body = document.createElement('div');
|
|
47
|
+
|
|
48
|
+
root.style.position = 'absolute';
|
|
49
|
+
root.style.inset = '0';
|
|
50
|
+
root.style.pointerEvents = 'none';
|
|
51
|
+
root.style.display = 'flex';
|
|
52
|
+
root.style.flexDirection = 'column';
|
|
53
|
+
root.style.justifyContent = 'space-between';
|
|
54
|
+
root.style.padding = '16px';
|
|
55
|
+
|
|
56
|
+
status.style.alignSelf = 'flex-start';
|
|
57
|
+
status.style.padding = '8px 12px';
|
|
58
|
+
status.style.borderRadius = '999px';
|
|
59
|
+
status.style.background = 'rgba(15, 23, 42, 0.82)';
|
|
60
|
+
status.style.color = '#f8fafc';
|
|
61
|
+
status.style.fontSize = '13px';
|
|
62
|
+
status.style.fontWeight = '600';
|
|
63
|
+
status.style.letterSpacing = '0.02em';
|
|
64
|
+
status.style.backdropFilter = 'blur(12px)';
|
|
65
|
+
|
|
66
|
+
callout.style.alignSelf = 'flex-end';
|
|
67
|
+
callout.style.maxWidth = '280px';
|
|
68
|
+
callout.style.padding = '14px 16px';
|
|
69
|
+
callout.style.borderRadius = '18px';
|
|
70
|
+
callout.style.background = 'rgba(15, 23, 42, 0.92)';
|
|
71
|
+
callout.style.color = '#f8fafc';
|
|
72
|
+
callout.style.boxShadow = '0 20px 45px rgba(15, 23, 42, 0.22)';
|
|
73
|
+
callout.style.backdropFilter = 'blur(12px)';
|
|
74
|
+
callout.style.display = 'none';
|
|
75
|
+
|
|
76
|
+
title.style.fontSize = '14px';
|
|
77
|
+
title.style.fontWeight = '700';
|
|
78
|
+
title.style.marginBottom = '6px';
|
|
79
|
+
|
|
80
|
+
body.style.fontSize = '13px';
|
|
81
|
+
body.style.lineHeight = '1.5';
|
|
82
|
+
body.style.opacity = '0.92';
|
|
83
|
+
|
|
84
|
+
callout.append(title, body);
|
|
85
|
+
root.append(status, callout);
|
|
86
|
+
host.append(root);
|
|
87
|
+
|
|
88
|
+
const applyTone = (tone: UiWidgetCallout['tone'] = 'neutral'): void => {
|
|
89
|
+
const palette = {
|
|
90
|
+
neutral: 'rgba(15, 23, 42, 0.92)',
|
|
91
|
+
active: 'rgba(14, 116, 144, 0.92)',
|
|
92
|
+
selected: 'rgba(22, 101, 52, 0.94)',
|
|
93
|
+
} as const;
|
|
94
|
+
|
|
95
|
+
callout.style.background = palette[tone];
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
setStatus(message) {
|
|
100
|
+
status.textContent = message;
|
|
101
|
+
},
|
|
102
|
+
showCallout(config) {
|
|
103
|
+
title.textContent = config.title;
|
|
104
|
+
body.textContent = config.body;
|
|
105
|
+
callout.style.display = 'block';
|
|
106
|
+
applyTone(config.tone);
|
|
107
|
+
},
|
|
108
|
+
hideCallout() {
|
|
109
|
+
callout.style.display = 'none';
|
|
110
|
+
},
|
|
111
|
+
destroy() {
|
|
112
|
+
root.remove();
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
},
|
|
116
|
+
dispose(_ctx, api) {
|
|
117
|
+
api.destroy();
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|