@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.
Files changed (47) hide show
  1. package/README.md +80 -0
  2. package/dist/adapters.d.ts +11 -0
  3. package/dist/adapters.d.ts.map +1 -0
  4. package/dist/adapters.js +11 -0
  5. package/dist/adapters.js.map +1 -0
  6. package/dist/contracts.d.ts +11 -0
  7. package/dist/contracts.d.ts.map +1 -0
  8. package/dist/contracts.js +11 -0
  9. package/dist/contracts.js.map +1 -0
  10. package/dist/index.d.ts +5 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +5 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/input.d.ts +37 -0
  15. package/dist/input.d.ts.map +1 -0
  16. package/dist/input.js +137 -0
  17. package/dist/input.js.map +1 -0
  18. package/dist/loading.d.ts +35 -0
  19. package/dist/loading.d.ts.map +1 -0
  20. package/dist/loading.js +123 -0
  21. package/dist/loading.js.map +1 -0
  22. package/dist/pointer.d.ts +43 -0
  23. package/dist/pointer.d.ts.map +1 -0
  24. package/dist/pointer.js +135 -0
  25. package/dist/pointer.js.map +1 -0
  26. package/dist/storage.d.ts +24 -0
  27. package/dist/storage.d.ts.map +1 -0
  28. package/dist/storage.js +125 -0
  29. package/dist/storage.js.map +1 -0
  30. package/dist/three.d.ts +28 -0
  31. package/dist/three.d.ts.map +1 -0
  32. package/dist/three.js +131 -0
  33. package/dist/three.js.map +1 -0
  34. package/dist/ui-widgets.d.ts +23 -0
  35. package/dist/ui-widgets.d.ts.map +1 -0
  36. package/dist/ui-widgets.js +92 -0
  37. package/dist/ui-widgets.js.map +1 -0
  38. package/package.json +35 -0
  39. package/src/adapters.ts +11 -0
  40. package/src/contracts.ts +15 -0
  41. package/src/index.ts +5 -0
  42. package/src/input.ts +207 -0
  43. package/src/loading.ts +174 -0
  44. package/src/pointer.ts +203 -0
  45. package/src/storage.ts +194 -0
  46. package/src/three.ts +178 -0
  47. package/src/ui-widgets.ts +120 -0
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Company: EonHive Inc.
3
+ * Title: Adapter Contracts
4
+ * Purpose: Re-export the canonical kernel adapter contracts from @urk/core.
5
+ * Author: Stan Nesi
6
+ * Created: 2026-04-12
7
+ * Updated: 2026-04-15
8
+ * Notes: Vibe coded with Codex.
9
+ */
10
+
11
+ export type {
12
+ AdapterRegistration,
13
+ KernelEvent,
14
+ RuntimeContext,
15
+ } from '@urk/core';
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ /**
2
+ * URK Adapters - Contract-first capability adapters.
3
+ */
4
+
5
+ export * from './adapters';
package/src/input.ts ADDED
@@ -0,0 +1,207 @@
1
+ /**
2
+ * Company: EonHive Inc.
3
+ * Title: Input Adapter
4
+ * Purpose: Normalize keyboard input into a small reusable runtime capability.
5
+ * Author: Stan Nesi
6
+ * Created: 2026-04-20
7
+ * Updated: 2026-04-20
8
+ * Notes: Vibe coded with Codex.
9
+ */
10
+
11
+ import type { AdapterRegistration, RuntimeContext } from '@urk/core';
12
+
13
+ export type InputEventPhase = 'down' | 'up';
14
+
15
+ export interface InputKeyEvent {
16
+ code: string;
17
+ key: string;
18
+ phase: InputEventPhase;
19
+ repeat: boolean;
20
+ altKey: boolean;
21
+ ctrlKey: boolean;
22
+ metaKey: boolean;
23
+ shiftKey: boolean;
24
+ nativeEvent: KeyboardEvent;
25
+ }
26
+
27
+ export interface InputBinding {
28
+ code: string;
29
+ phase?: InputEventPhase;
30
+ allowRepeat?: boolean;
31
+ handler: (event: InputKeyEvent) => void;
32
+ }
33
+
34
+ export type InputListener = (event: InputKeyEvent) => void;
35
+
36
+ export interface InputAdapterApi {
37
+ isPressed(code: string): boolean;
38
+ bindKey(binding: InputBinding): () => void;
39
+ subscribe(listener: InputListener): () => void;
40
+ clear(): void;
41
+ }
42
+
43
+ type InputTarget = Window | Document | HTMLElement;
44
+
45
+ type BoundInputBinding = {
46
+ code: string;
47
+ phase: InputEventPhase;
48
+ allowRepeat: boolean;
49
+ handler: (event: InputKeyEvent) => void;
50
+ };
51
+
52
+ function resolveInputTarget(ctx: RuntimeContext): InputTarget {
53
+ const serviceTarget = ctx.services.get<unknown>('input:target');
54
+
55
+ if (serviceTarget === undefined) {
56
+ return window;
57
+ }
58
+
59
+ if (serviceTarget === window) {
60
+ return window;
61
+ }
62
+
63
+ if (typeof Document !== 'undefined' && serviceTarget instanceof Document) {
64
+ return serviceTarget;
65
+ }
66
+
67
+ if (typeof HTMLElement !== 'undefined' && serviceTarget instanceof HTMLElement) {
68
+ return serviceTarget;
69
+ }
70
+
71
+ throw new Error('Service input:target must be a Window, Document, or HTMLElement.');
72
+ }
73
+
74
+ function createInputEvent(
75
+ phase: InputEventPhase,
76
+ nativeEvent: KeyboardEvent,
77
+ ): InputKeyEvent {
78
+ return {
79
+ code: nativeEvent.code || nativeEvent.key,
80
+ key: nativeEvent.key,
81
+ phase,
82
+ repeat: nativeEvent.repeat,
83
+ altKey: nativeEvent.altKey,
84
+ ctrlKey: nativeEvent.ctrlKey,
85
+ metaKey: nativeEvent.metaKey,
86
+ shiftKey: nativeEvent.shiftKey,
87
+ nativeEvent,
88
+ };
89
+ }
90
+
91
+ export function createInputAdapter(
92
+ id = 'input-adapter',
93
+ ): AdapterRegistration<InputAdapterApi> {
94
+ let disposeTargetListeners: (() => void) | null = null;
95
+
96
+ return {
97
+ id,
98
+ capability: 'input',
99
+ isSupported() {
100
+ return typeof window !== 'undefined' && typeof document !== 'undefined';
101
+ },
102
+ setup(ctx) {
103
+ const target = resolveInputTarget(ctx);
104
+ const resetTarget = window;
105
+ const pressedCodes = new Set<string>();
106
+ const bindings = new Set<BoundInputBinding>();
107
+ const listeners = new Set<InputListener>();
108
+
109
+ const publish = (event: InputKeyEvent): void => {
110
+ ctx.events.emit({
111
+ type: event.phase === 'down' ? 'input:key-down' : 'input:key-up',
112
+ source: id,
113
+ payload: event,
114
+ timestamp: Date.now(),
115
+ });
116
+
117
+ for (const listener of [...listeners]) {
118
+ listener(event);
119
+ }
120
+
121
+ for (const binding of [...bindings]) {
122
+ if (binding.code !== event.code || binding.phase !== event.phase) {
123
+ continue;
124
+ }
125
+
126
+ if (event.repeat && !binding.allowRepeat) {
127
+ continue;
128
+ }
129
+
130
+ binding.handler(event);
131
+ }
132
+ };
133
+
134
+ const onKeyDown = (nativeEvent: Event): void => {
135
+ if (!(nativeEvent instanceof KeyboardEvent)) {
136
+ return;
137
+ }
138
+
139
+ const event = createInputEvent('down', nativeEvent);
140
+ pressedCodes.add(event.code);
141
+ publish(event);
142
+ };
143
+
144
+ const onKeyUp = (nativeEvent: Event): void => {
145
+ if (!(nativeEvent instanceof KeyboardEvent)) {
146
+ return;
147
+ }
148
+
149
+ const event = createInputEvent('up', nativeEvent);
150
+ pressedCodes.delete(event.code);
151
+ publish(event);
152
+ };
153
+
154
+ const onBlur = (): void => {
155
+ pressedCodes.clear();
156
+ };
157
+
158
+ target.addEventListener('keydown', onKeyDown);
159
+ target.addEventListener('keyup', onKeyUp);
160
+ resetTarget.addEventListener('blur', onBlur);
161
+
162
+ // Keep the public API minimal while still ensuring dispose can tear down DOM listeners.
163
+ disposeTargetListeners = () => {
164
+ target.removeEventListener('keydown', onKeyDown);
165
+ target.removeEventListener('keyup', onKeyUp);
166
+ resetTarget.removeEventListener('blur', onBlur);
167
+ };
168
+
169
+ return {
170
+ isPressed(code) {
171
+ return pressedCodes.has(code);
172
+ },
173
+ bindKey(binding) {
174
+ const normalizedBinding: BoundInputBinding = {
175
+ code: binding.code,
176
+ phase: binding.phase ?? 'down',
177
+ allowRepeat: binding.allowRepeat ?? false,
178
+ handler: binding.handler,
179
+ };
180
+
181
+ bindings.add(normalizedBinding);
182
+
183
+ return () => {
184
+ bindings.delete(normalizedBinding);
185
+ };
186
+ },
187
+ subscribe(listener) {
188
+ listeners.add(listener);
189
+
190
+ return () => {
191
+ listeners.delete(listener);
192
+ };
193
+ },
194
+ clear() {
195
+ pressedCodes.clear();
196
+ bindings.clear();
197
+ listeners.clear();
198
+ },
199
+ };
200
+ },
201
+ dispose(_ctx, api) {
202
+ disposeTargetListeners?.();
203
+ disposeTargetListeners = null;
204
+ api.clear();
205
+ },
206
+ };
207
+ }
package/src/loading.ts ADDED
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Company: EonHive Inc.
3
+ * Title: Loading Adapter
4
+ * Purpose: Track staged loading progress and expose a small observable API.
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 LoadingStage {
14
+ id: string;
15
+ label: string;
16
+ weight?: number;
17
+ }
18
+
19
+ export interface LoadingSnapshot {
20
+ active: boolean;
21
+ complete: boolean;
22
+ progress: number;
23
+ message: string;
24
+ stageId: string | null;
25
+ stageLabel: string | null;
26
+ stages: LoadingStage[];
27
+ updatedAt: number;
28
+ }
29
+
30
+ export type LoadingListener = (snapshot: LoadingSnapshot) => void;
31
+
32
+ export interface LoadingAdapterApi {
33
+ begin(stages: LoadingStage[], message?: string): LoadingSnapshot;
34
+ setStage(stageId: string, progressWithinStage: number, message?: string): LoadingSnapshot;
35
+ complete(message?: string): LoadingSnapshot;
36
+ getSnapshot(): LoadingSnapshot;
37
+ subscribe(listener: LoadingListener): () => void;
38
+ }
39
+
40
+ function normalizeStages(stages: LoadingStage[]): LoadingStage[] {
41
+ if (stages.length === 0) {
42
+ throw new Error('Loading adapter requires at least one stage.');
43
+ }
44
+
45
+ return stages.map((stage) => ({
46
+ ...stage,
47
+ weight: stage.weight && stage.weight > 0 ? stage.weight : 1,
48
+ }));
49
+ }
50
+
51
+ function clampProgress(progress: number): number {
52
+ return Math.max(0, Math.min(progress, 1));
53
+ }
54
+
55
+ function createEmptySnapshot(): LoadingSnapshot {
56
+ return {
57
+ active: false,
58
+ complete: false,
59
+ progress: 0,
60
+ message: 'Waiting to start',
61
+ stageId: null,
62
+ stageLabel: null,
63
+ stages: [],
64
+ updatedAt: Date.now(),
65
+ };
66
+ }
67
+
68
+ export function createLoadingAdapter(
69
+ id = 'loading-adapter',
70
+ ): AdapterRegistration<LoadingAdapterApi> {
71
+ return {
72
+ id,
73
+ capability: 'loading',
74
+ setup(ctx) {
75
+ let snapshot = createEmptySnapshot();
76
+ const listeners = new Set<LoadingListener>();
77
+
78
+ const publish = (): LoadingSnapshot => {
79
+ const next = { ...snapshot, stages: [...snapshot.stages] };
80
+
81
+ ctx.events.emit({
82
+ type: 'loading:changed',
83
+ source: id,
84
+ payload: next,
85
+ timestamp: Date.now(),
86
+ });
87
+
88
+ for (const listener of [...listeners]) {
89
+ listener(next);
90
+ }
91
+
92
+ return next;
93
+ };
94
+
95
+ const getTotalWeight = (): number => {
96
+ return snapshot.stages.reduce((total, stage) => total + (stage.weight ?? 1), 0);
97
+ };
98
+
99
+ return {
100
+ begin(stages, message = 'Starting load') {
101
+ const nextStages = normalizeStages(stages);
102
+ const firstStage = nextStages[0];
103
+
104
+ snapshot = {
105
+ active: true,
106
+ complete: false,
107
+ progress: 0,
108
+ message,
109
+ stageId: firstStage.id,
110
+ stageLabel: firstStage.label,
111
+ stages: nextStages,
112
+ updatedAt: Date.now(),
113
+ };
114
+
115
+ return publish();
116
+ },
117
+ setStage(stageId, progressWithinStage, message) {
118
+ const stageIndex = snapshot.stages.findIndex((stage) => stage.id === stageId);
119
+
120
+ if (stageIndex === -1) {
121
+ throw new Error(`Unknown loading stage: ${stageId}`);
122
+ }
123
+
124
+ const previousWeight = snapshot.stages
125
+ .slice(0, stageIndex)
126
+ .reduce((total, stage) => total + (stage.weight ?? 1), 0);
127
+ const currentStage = snapshot.stages[stageIndex];
128
+ const totalWeight = getTotalWeight();
129
+ const progress =
130
+ (previousWeight + (currentStage.weight ?? 1) * clampProgress(progressWithinStage)) /
131
+ totalWeight;
132
+
133
+ snapshot = {
134
+ ...snapshot,
135
+ active: true,
136
+ progress,
137
+ message: message ?? snapshot.message,
138
+ stageId: currentStage.id,
139
+ stageLabel: currentStage.label,
140
+ updatedAt: Date.now(),
141
+ };
142
+
143
+ return publish();
144
+ },
145
+ complete(message = 'Loading complete') {
146
+ const lastStage = snapshot.stages[snapshot.stages.length - 1] ?? null;
147
+
148
+ snapshot = {
149
+ ...snapshot,
150
+ active: false,
151
+ complete: true,
152
+ progress: 1,
153
+ message,
154
+ stageId: lastStage?.id ?? null,
155
+ stageLabel: lastStage?.label ?? null,
156
+ updatedAt: Date.now(),
157
+ };
158
+
159
+ return publish();
160
+ },
161
+ getSnapshot() {
162
+ return { ...snapshot, stages: [...snapshot.stages] };
163
+ },
164
+ subscribe(listener) {
165
+ listeners.add(listener);
166
+
167
+ return () => {
168
+ listeners.delete(listener);
169
+ };
170
+ },
171
+ };
172
+ },
173
+ };
174
+ }
package/src/pointer.ts ADDED
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Company: EonHive Inc.
3
+ * Title: Pointer Adapter
4
+ * Purpose: Normalize pointer target binding and emit interaction events.
5
+ * Author: Stan Nesi
6
+ * Created: 2026-04-12
7
+ * Updated: 2026-04-22
8
+ * Notes: Vibe coded with Codex.
9
+ */
10
+
11
+ import type { AdapterRegistration } from '@urk/core';
12
+
13
+ export interface PointerTargetDefinition {
14
+ id: string;
15
+ element: HTMLElement;
16
+ meta?: Record<string, unknown>;
17
+ }
18
+
19
+ export interface PointerTargetEventPayload {
20
+ targetId: string;
21
+ element: HTMLElement;
22
+ meta?: Record<string, unknown>;
23
+ nativeEvent: MouseEvent | PointerEvent;
24
+ }
25
+
26
+ export interface PointerSurfaceDefinition {
27
+ id: string;
28
+ element: HTMLElement;
29
+ meta?: Record<string, unknown>;
30
+ }
31
+
32
+ export interface PointerSurfaceEventPayload {
33
+ surfaceId: string;
34
+ element: HTMLElement;
35
+ meta?: Record<string, unknown>;
36
+ clientX: number;
37
+ clientY: number;
38
+ localX: number;
39
+ localY: number;
40
+ nativeEvent: MouseEvent | PointerEvent;
41
+ }
42
+
43
+ export interface PointerAdapterApi {
44
+ bindTarget(target: PointerTargetDefinition): () => void;
45
+ bindSurface(surface: PointerSurfaceDefinition): () => void;
46
+ clear(): void;
47
+ }
48
+
49
+ function getSurfaceCoordinates(
50
+ element: HTMLElement,
51
+ nativeEvent: MouseEvent | PointerEvent,
52
+ ): {
53
+ clientX: number;
54
+ clientY: number;
55
+ localX: number;
56
+ localY: number;
57
+ } {
58
+ const bounds = element.getBoundingClientRect();
59
+
60
+ return {
61
+ clientX: nativeEvent.clientX,
62
+ clientY: nativeEvent.clientY,
63
+ localX: nativeEvent.clientX - bounds.left,
64
+ localY: nativeEvent.clientY - bounds.top,
65
+ };
66
+ }
67
+
68
+ export function createPointerAdapter(
69
+ id = 'pointer-adapter',
70
+ ): AdapterRegistration<PointerAdapterApi> {
71
+ return {
72
+ id,
73
+ capability: 'pointer',
74
+ setup(ctx) {
75
+ const cleanups = new Map<string, () => void>();
76
+
77
+ const emit = <TPayload>(type: string, payload: TPayload): void => {
78
+ ctx.events.emit({
79
+ type,
80
+ source: id,
81
+ payload,
82
+ timestamp: Date.now(),
83
+ });
84
+ };
85
+
86
+ return {
87
+ bindTarget(target) {
88
+ if (cleanups.has(target.id)) {
89
+ throw new Error(`Pointer target already bound: ${target.id}`);
90
+ }
91
+
92
+ const onEnter = (nativeEvent: PointerEvent): void => {
93
+ emit('pointer:hover', {
94
+ targetId: target.id,
95
+ element: target.element,
96
+ meta: target.meta,
97
+ nativeEvent,
98
+ });
99
+ };
100
+
101
+ const onLeave = (nativeEvent: PointerEvent): void => {
102
+ emit('pointer:leave', {
103
+ targetId: target.id,
104
+ element: target.element,
105
+ meta: target.meta,
106
+ nativeEvent,
107
+ });
108
+ };
109
+
110
+ const onSelect = (nativeEvent: MouseEvent): void => {
111
+ emit('pointer:select', {
112
+ targetId: target.id,
113
+ element: target.element,
114
+ meta: target.meta,
115
+ nativeEvent,
116
+ });
117
+ };
118
+
119
+ target.element.addEventListener('pointerenter', onEnter);
120
+ target.element.addEventListener('pointerleave', onLeave);
121
+ target.element.addEventListener('click', onSelect);
122
+
123
+ const cleanup = (): void => {
124
+ target.element.removeEventListener('pointerenter', onEnter);
125
+ target.element.removeEventListener('pointerleave', onLeave);
126
+ target.element.removeEventListener('click', onSelect);
127
+ };
128
+
129
+ cleanups.set(target.id, cleanup);
130
+
131
+ return () => {
132
+ cleanup();
133
+ cleanups.delete(target.id);
134
+ };
135
+ },
136
+ bindSurface(surface) {
137
+ const cleanupKey = `surface:${surface.id}`;
138
+
139
+ if (cleanups.has(cleanupKey)) {
140
+ throw new Error(`Pointer surface already bound: ${surface.id}`);
141
+ }
142
+
143
+ const onMove = (nativeEvent: PointerEvent): void => {
144
+ emit('pointer:surface-move', {
145
+ surfaceId: surface.id,
146
+ element: surface.element,
147
+ meta: surface.meta,
148
+ ...getSurfaceCoordinates(surface.element, nativeEvent),
149
+ nativeEvent,
150
+ } satisfies PointerSurfaceEventPayload);
151
+ };
152
+
153
+ const onLeave = (nativeEvent: PointerEvent): void => {
154
+ emit('pointer:surface-leave', {
155
+ surfaceId: surface.id,
156
+ element: surface.element,
157
+ meta: surface.meta,
158
+ ...getSurfaceCoordinates(surface.element, nativeEvent),
159
+ nativeEvent,
160
+ } satisfies PointerSurfaceEventPayload);
161
+ };
162
+
163
+ const onSelect = (nativeEvent: MouseEvent): void => {
164
+ emit('pointer:surface-select', {
165
+ surfaceId: surface.id,
166
+ element: surface.element,
167
+ meta: surface.meta,
168
+ ...getSurfaceCoordinates(surface.element, nativeEvent),
169
+ nativeEvent,
170
+ } satisfies PointerSurfaceEventPayload);
171
+ };
172
+
173
+ surface.element.addEventListener('pointermove', onMove);
174
+ surface.element.addEventListener('pointerleave', onLeave);
175
+ surface.element.addEventListener('click', onSelect);
176
+
177
+ const cleanup = (): void => {
178
+ surface.element.removeEventListener('pointermove', onMove);
179
+ surface.element.removeEventListener('pointerleave', onLeave);
180
+ surface.element.removeEventListener('click', onSelect);
181
+ };
182
+
183
+ cleanups.set(cleanupKey, cleanup);
184
+
185
+ return () => {
186
+ cleanup();
187
+ cleanups.delete(cleanupKey);
188
+ };
189
+ },
190
+ clear() {
191
+ for (const cleanup of cleanups.values()) {
192
+ cleanup();
193
+ }
194
+
195
+ cleanups.clear();
196
+ },
197
+ };
198
+ },
199
+ dispose(_ctx, api) {
200
+ api.clear();
201
+ },
202
+ };
203
+ }