@kbve/droid 0.0.1 → 0.0.3

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 (38) hide show
  1. package/LICENSE +1 -0
  2. package/README.md +5 -1
  3. package/assets/canvas-worker-DbsoY2Xv.js +5 -0
  4. package/assets/db-worker-CjXS52DQ.js +14 -0
  5. package/assets/ws-worker-FssAQgaD.js +5 -0
  6. package/comlink-CC72iIUO.js +253 -0
  7. package/droid.es.js +74 -12
  8. package/package.json +13 -3
  9. package/reference-Dk_1njEH.js +259 -0
  10. package/src/index.ts +21 -0
  11. package/src/lib/droid.spec.ts +26 -0
  12. package/src/lib/droid.ts +7 -0
  13. package/src/lib/mod/mod-manager.ts +73 -0
  14. package/src/lib/mod/mod-urls.ts +3 -0
  15. package/src/lib/mod/module/bento/mod-bento.worker.ts +128 -0
  16. package/src/lib/mod/module/phaser/mod-phaser.ts +0 -0
  17. package/src/lib/mod/module/supabase/mod-supabase.spec.ts +30 -0
  18. package/src/lib/mod/module/supabase/mod-supabase.worker.ts +59 -0
  19. package/src/lib/types/bento.ts +80 -0
  20. package/src/lib/types/discord.ts +40 -0
  21. package/src/lib/types/event-types.ts +35 -0
  22. package/src/lib/types/jedi.ts +66 -0
  23. package/src/lib/types/modules.ts +46 -0
  24. package/src/lib/types/panel-types.ts +18 -0
  25. package/src/lib/types/supabase-esm.d.ts +4 -0
  26. package/src/lib/workers/canvas-worker.ts +107 -0
  27. package/src/lib/workers/data.ts +215 -0
  28. package/src/lib/workers/db-worker.ts +203 -0
  29. package/src/lib/workers/events.ts +52 -0
  30. package/src/lib/workers/flexbuilder.ts +6 -0
  31. package/src/lib/workers/init.ts +40 -0
  32. package/src/lib/workers/main.ts +531 -0
  33. package/src/lib/workers/tools.ts +60 -0
  34. package/src/lib/workers/ws-worker.ts +73 -0
  35. package/src/setup-vitest.ts +2 -0
  36. package/src/types.d.ts +31 -0
  37. package/workers/main.js +1091 -0
  38. package/droid.cjs.js +0 -1
@@ -0,0 +1,128 @@
1
+ import { expose } from 'comlink';
2
+ import type { BaseModAPI, VirtualNode } from '../../../types/modules';
3
+ import {
4
+ BentoTileSchema,
5
+ type BentoTile,
6
+ BENTO_BADGE_CLASS_MAP,
7
+ BENTO_ANIMATION_CLASS_MAP,
8
+ BENTO_VARIANT_CLASS_MAP,
9
+ } from '../../../types/bento';
10
+
11
+ let emitToMain: ((msg: any) => void) | undefined;
12
+
13
+ function createVNodeFromTile(tile: BentoTile): VirtualNode {
14
+ const variant = tile.variant ?? 'default';
15
+ const animationClass = tile.animation
16
+ ? BENTO_ANIMATION_CLASS_MAP[tile.animation]
17
+ : '';
18
+ const variantBase = BENTO_VARIANT_CLASS_MAP[variant]?.base ?? '';
19
+ const variantHover = BENTO_VARIANT_CLASS_MAP[variant]?.hover ?? '';
20
+
21
+ const rootClasses = [
22
+ 'bento-item',
23
+ 'col-span-1',
24
+ 'row-span-1',
25
+ tile.span ?? 'md:col-span-2 md:row-span-1',
26
+ 'bg-gradient-to-br',
27
+ tile.primaryColor ? `from-${tile.primaryColor}` : '',
28
+ tile.secondaryColor ? `to-${tile.secondaryColor}` : '',
29
+ 'rounded-2xl overflow-hidden shadow-2xl',
30
+ variantBase,
31
+ variantHover ? `hover:${variantHover}` : '',
32
+ animationClass,
33
+ tile.className ?? '',
34
+ ]
35
+ .filter(Boolean)
36
+ .join(' ');
37
+
38
+ const datasetAttrs = tile.dataset
39
+ ? Object.fromEntries(
40
+ Object.entries(tile.dataset).map(([k, v]) => [`data-${k}`, v]),
41
+ )
42
+ : {};
43
+
44
+ const wrapper: VirtualNode = {
45
+ tag: tile.href ? 'a' : 'div',
46
+ class: rootClasses,
47
+ attrs: {
48
+ ...(tile.href
49
+ ? { href: tile.href, target: tile.target ?? '_self' }
50
+ : {}),
51
+ ...(tile.onclick ? { onclick: tile.onclick } : {}),
52
+ ...(tile.ariaLabel ? { 'aria-label': tile.ariaLabel } : {}),
53
+ ...(tile.role ? { role: tile.role } : {}),
54
+ ...datasetAttrs,
55
+ },
56
+ children: [
57
+ tile.badge && {
58
+ tag: 'span',
59
+ class: `absolute top-2 right-2 px-2 py-1 text-xs rounded z-10 ${BENTO_BADGE_CLASS_MAP[tile.badgeType ?? 'default']}`,
60
+ children: [tile.badge],
61
+ },
62
+ {
63
+ tag: 'div',
64
+ class: 'p-4 flex flex-col justify-between h-full',
65
+ children: [
66
+ {
67
+ tag: 'div',
68
+ class: 'flex items-center gap-3',
69
+ children: [
70
+ tile.icon && {
71
+ tag: 'div',
72
+ class: 'text-2xl bg-white/20 rounded-full p-2 text-white',
73
+ children: [tile.icon],
74
+ },
75
+ {
76
+ tag: 'h3',
77
+ class: 'text-white text-lg font-bold',
78
+ children: [tile.title],
79
+ },
80
+ ].filter(Boolean) as VirtualNode[],
81
+ },
82
+ tile.subtitle && {
83
+ tag: 'p',
84
+ class: 'text-sm text-white/80 mt-2',
85
+ children: [tile.subtitle],
86
+ },
87
+ tile.description && {
88
+ tag: 'p',
89
+ class: 'text-xs text-white/60 mt-1',
90
+ children: [tile.description],
91
+ },
92
+ ].filter(Boolean) as VirtualNode[],
93
+ },
94
+ ].filter(Boolean) as VirtualNode[],
95
+ };
96
+
97
+ return wrapper;
98
+ }
99
+
100
+ const api: BaseModAPI = {
101
+ async getMeta() {
102
+ return {
103
+ name: 'bento',
104
+ version: '0.1.0',
105
+ };
106
+ },
107
+
108
+ async init(context) {
109
+ emitToMain = context?.emitFromWorker;
110
+ },
111
+
112
+ async run(data: unknown) {
113
+ if (!emitToMain) {
114
+ console.warn('[mod-bento] emitFromWorker not defined');
115
+ return;
116
+ }
117
+
118
+ try {
119
+ const tile = BentoTileSchema.parse(data);
120
+ const vnode = createVNodeFromTile(tile);
121
+ emitToMain({ type: 'injectVNode', vnode });
122
+ } catch (err) {
123
+ console.error('[mod-bento] Invalid BentoTile data:', err);
124
+ }
125
+ },
126
+ };
127
+
128
+ expose(api);
File without changes
@@ -0,0 +1,30 @@
1
+ import { describe, it, expect, beforeAll } from 'vitest';
2
+ import { droid } from '../../../droid';
3
+ import type { Remote } from 'comlink';
4
+ import type { ModHandle, SupabaseModAPI } from '../../../types/modules';
5
+
6
+ console.log('[spec] Starting mod-supabase test file...');
7
+
8
+
9
+ let mod: Remote<SupabaseModAPI>;
10
+ let meta: { name: string; version: string };
11
+ describe('mod-supabase', () => {
12
+ it('should have meta info', async () => {
13
+ await droid();
14
+ if (!window.kbve?.mod) {
15
+ throw new Error('Mod manager not available');
16
+ }
17
+
18
+ console.log('[test] Loading Supabase mod...');
19
+ const modURL = new URL('./mod-supabase.worker.ts', import.meta.url);
20
+ console.log('Resolved worker URL:', modURL.href);
21
+
22
+ const handle = await window.kbve.mod.load(modURL.href);
23
+ const mod: Remote<SupabaseModAPI> = handle.instance;
24
+ const meta = handle.meta;
25
+
26
+ console.log('[test] Supabase mod loaded:', meta);
27
+ expect(meta.name).toBe('supabase');
28
+ expect(meta.version).toBeDefined();
29
+ }, 30_000);
30
+ });
@@ -0,0 +1,59 @@
1
+ import { expose } from 'comlink';
2
+ import type { SupabaseClient } from '@supabase/supabase-js';
3
+
4
+ let supabase: SupabaseClient | null = null;
5
+ let initialized = false;
6
+
7
+ const state = {
8
+ ctx: null as any,
9
+ url: '',
10
+ key: '',
11
+ };
12
+
13
+ const mod = {
14
+ getMeta: () => ({
15
+ name: 'supabase',
16
+ version: '1.0.0',
17
+ description: 'Supabase mod for async DB access',
18
+ author: 'kbve',
19
+ }),
20
+
21
+ init(ctx: any) {
22
+ state.ctx = ctx;
23
+ },
24
+
25
+ async loadSupabaseClient() {
26
+ try {
27
+ const supabaseModule = await import('https://cdn.jsdelivr.net/npm/@supabase/supabase-js/+esm');
28
+ console.log('[mod-supabase] Supabase module loaded');
29
+ return supabaseModule.createClient;
30
+ } catch (err) {
31
+ console.error('[mod-supabase] Failed to load Supabase:', err);
32
+ throw err;
33
+ }
34
+ },
35
+
36
+ async configure(url: string, key: string) {
37
+ if (!initialized) {
38
+ const createClient = await this.loadSupabaseClient();
39
+ supabase = createClient(url, key);
40
+ state.url = url;
41
+ state.key = key;
42
+ initialized = true;
43
+ }
44
+ },
45
+
46
+ async queryTestTable() {
47
+ if (!supabase) throw new Error('Supabase not configured. Call `configure()` first.');
48
+ const { data, error } = await supabase.from('test').select('*');
49
+ return { data, error };
50
+ },
51
+
52
+ async insertTest(payload: Record<string, any>) {
53
+ if (!supabase) throw new Error('Supabase not configured. Call `configure()` first.');
54
+ const { data, error } = await supabase.from('test').insert(payload).select();
55
+ return { data, error };
56
+ }
57
+ };
58
+
59
+ expose(mod);
@@ -0,0 +1,80 @@
1
+ import { z } from 'zod';
2
+
3
+ // Enums
4
+ export const BENTO_VARIANTS = ['default', 'minimal', 'image-heavy'] as const;
5
+ export const BENTO_ANIMATIONS = ['fade-in', 'slide-up', 'none'] as const;
6
+ export const BENTO_TARGETS = ['_self', '_blank', '_parent', '_top'] as const;
7
+ export const BENTO_ROLES = ['button', 'link', 'region', 'article'] as const;
8
+ export const BENTO_BADGE_TYPES = ['default', 'success', 'warning', 'error'] as const;
9
+
10
+ // Regex validators
11
+ const tailwindColorRegex = /^[a-z]+-(100|200|300|400|500|600|700|800|900)$/;
12
+ const spanRegex = /^(?:(?:[a-z]+:)?(col|row)-span-\d+\s*)+$/;
13
+ const ulidRegex = /^01[0-9A-HJKMNP-TV-Z]{24}$/;
14
+
15
+ // Schema
16
+ export const BentoTileSchema = z.object({
17
+ id: z.string().regex(ulidRegex, 'Invalid ULID format').optional(),
18
+
19
+ title: z.string().min(1, 'Title is required'),
20
+ subtitle: z.string().optional(),
21
+ description: z.string().optional(),
22
+ span: z.string().regex(spanRegex, 'Invalid span format (e.g., col-span-2, row-span-1)').optional(),
23
+
24
+ primaryColor: z.string().regex(tailwindColorRegex, 'Invalid Tailwind color format (e.g., blue-500)'),
25
+ secondaryColor: z.string().regex(tailwindColorRegex, 'Invalid Tailwind color format (e.g., blue-500)'),
26
+
27
+ icon: z.string().optional(),
28
+ backgroundImage: z.string().url().optional(),
29
+
30
+ onclick: z.string().optional(),
31
+ href: z.string().url().optional(),
32
+ target: z.enum(BENTO_TARGETS).optional(),
33
+
34
+ ariaLabel: z.string().optional(),
35
+ role: z.enum(BENTO_ROLES).optional(),
36
+
37
+ variant: z.enum(BENTO_VARIANTS).optional(),
38
+ animation: z.enum(BENTO_ANIMATIONS).optional(),
39
+
40
+ badge: z.string().optional(),
41
+ badgeType: z.enum(BENTO_BADGE_TYPES).optional(),
42
+
43
+ priority: z.number().int().min(0).optional(),
44
+ tooltip: z.string().optional(),
45
+ disabled: z.boolean().optional(),
46
+
47
+ meta: z.record(z.any()).optional(),
48
+ dataset: z.record(z.string()).optional(),
49
+ className: z.string().optional(),
50
+ }).refine(
51
+ (data) => !data.href || (data.href && data.target),
52
+ { message: 'target is required when href is provided', path: ['target'] }
53
+ ).refine(
54
+ (data) => !data.badgeType || (data.badge && data.badge.length > 0),
55
+ { message: 'badge must be provided if badgeType is set', path: ['badge'] }
56
+ );
57
+
58
+ // Types
59
+ export type BentoTile = z.infer<typeof BentoTileSchema>;
60
+ export type BentoVariantClass = { base: string; hover?: string };
61
+
62
+ // Mappings
63
+ export const BENTO_VARIANT_CLASS_MAP: Record<(typeof BENTO_VARIANTS)[number], BentoVariantClass> = {
64
+ default: { base: 'p-4 flex flex-col justify-between' },
65
+ minimal: { base: 'p-2 bg-opacity-30 backdrop-blur-sm', hover: 'bg-opacity-50' },
66
+ 'image-heavy': { base: 'p-0 overflow-hidden', hover: 'scale-105' },
67
+ };
68
+
69
+ export const BENTO_ANIMATION_CLASS_MAP: Record<(typeof BENTO_ANIMATIONS)[number], string> = {
70
+ 'fade-in': 'animate-fade-in',
71
+ 'slide-up': 'animate-slide-up',
72
+ none: '',
73
+ };
74
+
75
+ export const BENTO_BADGE_CLASS_MAP: Record<(typeof BENTO_BADGE_TYPES)[number], string> = {
76
+ default: 'bg-white/30 text-white',
77
+ success: 'bg-green-500/80 text-white',
78
+ warning: 'bg-yellow-500/80 text-black',
79
+ error: 'bg-red-500/80 text-white',
80
+ };
@@ -0,0 +1,40 @@
1
+ import { z } from 'zod';
2
+
3
+ export const DiscordServerSchema = z.object({
4
+ server_id: z.string(),
5
+ owner_id: z.string(),
6
+ lang: z.number(),
7
+ status: z.number(),
8
+ invite: z.string(),
9
+ name: z.string(),
10
+ summary: z.string(),
11
+ description: z.string().nullable().optional(),
12
+ website: z.string().nullable().optional(),
13
+ logo: z.string().nullable().optional(),
14
+ banner: z.string().nullable().optional(),
15
+ video: z.string().nullable().optional(),
16
+ categories: z.number(),
17
+ updated_at: z.string(),
18
+ });
19
+
20
+ export type DiscordServer = z.infer<typeof DiscordServerSchema>;
21
+
22
+ export const DiscordTagSchema = z.object({
23
+ tag_id: z.string(),
24
+ name: z.string(),
25
+ status: z.number(),
26
+ });
27
+
28
+ export type DiscordTag = z.infer<typeof DiscordTagSchema>;
29
+
30
+ export const ProfileSchema = z.object({
31
+ profile_id: z.string(),
32
+ user_id: z.string(),
33
+ username: z.string(),
34
+ avatar: z.string().url().optional(),
35
+ bio: z.string().optional(),
36
+ joined_at: z.string(),
37
+ status: z.number(),
38
+ });
39
+
40
+ export type Profile = z.infer<typeof ProfileSchema>;
@@ -0,0 +1,35 @@
1
+ import { z } from 'zod';
2
+ import { PanelIdSchema } from './panel-types';
3
+
4
+ export const ModMetaSchema = z.object({
5
+ name: z.string(),
6
+ version: z.string().optional(),
7
+ });
8
+
9
+ export const DroidModReadySchema = z.object({
10
+ meta: ModMetaSchema.optional(),
11
+ timestamp: z.number(),
12
+ });
13
+
14
+ export const PanelEventSchema = z.object({
15
+ id: PanelIdSchema,
16
+ payload: z.any().optional(),
17
+ });
18
+
19
+ export const DroidReadySchema = z.object({
20
+ timestamp: z.number(),
21
+ });
22
+
23
+ export const DroidEventSchemas = {
24
+ 'droid-ready': DroidReadySchema,
25
+ 'droid-mod-ready': DroidModReadySchema,
26
+ 'panel-open': PanelEventSchema,
27
+ 'panel-close': PanelEventSchema,
28
+ };
29
+
30
+ export type DroidEventMap = {
31
+ [K in keyof typeof DroidEventSchemas]: z.infer<(typeof DroidEventSchemas)[K]>;
32
+ };
33
+
34
+ export type EventKey = keyof DroidEventMap;
35
+ export type EventHandler<K extends EventKey> = (payload: DroidEventMap[K]) => void;
@@ -0,0 +1,66 @@
1
+
2
+ export type FieldMap = Record<string, string>;
3
+ export type StreamRequest = { stream: string; id: string };
4
+
5
+ export enum PayloadFormat {
6
+ PAYLOAD_UNKNOWN = 0,
7
+ JSON = 1,
8
+ FLEX = 2,
9
+ PROTOBUF = 3,
10
+ FLATBUFFER = 4,
11
+ }
12
+
13
+ export interface JediEnvelopeFlex {
14
+ version: number;
15
+ kind: number;
16
+ format: number;
17
+ payload: Uint8Array;
18
+ metadata?: Uint8Array;
19
+ }
20
+
21
+ export enum MessageKind {
22
+ // Verbs (Bits 0–7)
23
+ UNKNOWN = 0,
24
+ ADD = 1 << 0,
25
+ READ = 1 << 1,
26
+ GET = 1 << 2,
27
+ SET = 1 << 3,
28
+ DEL = 1 << 4,
29
+ STREAM = 1 << 5,
30
+ GROUP = 1 << 6,
31
+ LIST = 1 << 7,
32
+
33
+ // Intent (Bits 8–15)
34
+ ACTION = 1 << 8,
35
+ MESSAGE = 1 << 9,
36
+ INFO = 1 << 10,
37
+ DEBUG = 1 << 11,
38
+ ERROR = 1 << 12,
39
+ AUTH = 1 << 13,
40
+ HEARTBEAT = 1 << 14,
41
+
42
+ // Targets (Bits 16–23)
43
+ CONFIG_UPDATE = 1 << 15,
44
+ REDIS = 1 << 16,
45
+ SUPABASE = 1 << 17,
46
+ FILESYSTEM = 1 << 18,
47
+ WEBSOCKET = 1 << 19,
48
+ HTTP_API = 1 << 20,
49
+ LOCAL_CACHE = 1 << 21,
50
+ AI = 1 << 22,
51
+ }
52
+
53
+ export const MultiMessageKind = {
54
+ RGET: MessageKind.REDIS | MessageKind.GET,
55
+ RSET: MessageKind.REDIS | MessageKind.SET,
56
+ RDEL: MessageKind.REDIS | MessageKind.DEL,
57
+ XADD: MessageKind.REDIS | MessageKind.STREAM | MessageKind.ADD,
58
+ XREAD: MessageKind.REDIS | MessageKind.STREAM | MessageKind.READ,
59
+ WATCH: MessageKind.REDIS | MessageKind.HEARTBEAT | MessageKind.READ | MessageKind.INFO,
60
+ UNWATCH: MessageKind.REDIS | MessageKind.HEARTBEAT | MessageKind.DEL | MessageKind.INFO,
61
+ PUBLISH: MessageKind.REDIS | MessageKind.MESSAGE | MessageKind.ACTION,
62
+ SUBSCRIBE: MessageKind.REDIS | MessageKind.MESSAGE | MessageKind.READ,
63
+ } as const;
64
+
65
+ export type MultiMessageKindKey = keyof typeof MultiMessageKind;
66
+
@@ -0,0 +1,46 @@
1
+ import type { Remote } from 'comlink';
2
+
3
+ export interface BaseModAPI {
4
+ getMeta(): Promise<ModMeta>;
5
+ init?(ctx: any): void | Promise<void>;
6
+ [key: string]: any;
7
+ }
8
+
9
+ export interface ModMeta {
10
+ name: string;
11
+ version: string;
12
+ description?: string;
13
+ author?: string;
14
+ }
15
+
16
+ export interface ModHandle {
17
+ id: string;
18
+ worker: Worker;
19
+ instance: Remote<BaseModAPI>;
20
+ meta: ModMeta;
21
+ url: string;
22
+ }
23
+
24
+ export interface ModManager {
25
+ registry: Record<string, ModHandle>;
26
+ load: (url: string) => Promise<ModHandle>;
27
+ unload: (id: string) => void;
28
+ list: () => ModMeta[];
29
+ reload: (id: string) => Promise<ModHandle>;
30
+ }
31
+
32
+ export interface SupabaseModAPI extends BaseModAPI {
33
+ configure(url: string, key: string): Promise<void>;
34
+ queryTestTable(): Promise<{ data: any; error: any }>;
35
+ insertTest(payload: Record<string, any>): Promise<{ data: any; error: any }>;
36
+ }
37
+
38
+ export type VirtualNode = {
39
+ tag: string;
40
+ id?: string;
41
+ key?: string;
42
+ class?: string;
43
+ attrs?: Record<string, any>;
44
+ style?: Partial<CSSStyleDeclaration>;
45
+ children?: (string | VirtualNode)[];
46
+ };
@@ -0,0 +1,18 @@
1
+ import { z } from 'zod';
2
+
3
+ export const PanelIdSchema = z.enum(['top', 'right', 'bottom', 'left']);
4
+ export type PanelId = z.infer<typeof PanelIdSchema>;
5
+
6
+ export const CanvasOptionsSchema = z.object({
7
+ width: z.number(),
8
+ height: z.number(),
9
+ mode: z.enum(['static', 'animated', 'dynamic']).optional(),
10
+ });
11
+
12
+ export const PanelPayloadSchema = z.object({
13
+ rawHtml: z.string().optional(),
14
+ needsCanvas: z.boolean().optional(),
15
+ canvasOptions: CanvasOptionsSchema.optional(),
16
+ });
17
+
18
+ export type PanelPayload = z.infer<typeof PanelPayloadSchema>;
@@ -0,0 +1,4 @@
1
+ declare module 'https://cdn.jsdelivr.net/npm/@supabase/supabase-js/+esm' {
2
+ import * as supabase from '@supabase/supabase-js';
3
+ export = supabase;
4
+ }
@@ -0,0 +1,107 @@
1
+ import { expose } from 'comlink';
2
+
3
+ export interface CanvasWorkerAPI {
4
+ bindCanvas(panelId: string, canvas: OffscreenCanvas, mode?: CanvasDrawMode): Promise<void>;
5
+ unbindCanvas(panelId: string): Promise<void>;
6
+ }
7
+
8
+ interface CanvasBinding {
9
+ ctx: OffscreenCanvasRenderingContext2D;
10
+ canvas: OffscreenCanvas;
11
+ panelId: string;
12
+ mode?: CanvasDrawMode;
13
+ animationFrame?: number;
14
+ }
15
+
16
+ export type CanvasDrawMode = 'static' | 'animated' | 'dynamic';
17
+
18
+ const CanvasManager = {
19
+ bindings: new Map<string, CanvasBinding>(),
20
+
21
+ async bindCanvas(panelId: string, canvas: OffscreenCanvas, mode: CanvasDrawMode = 'animated') {
22
+ const ctx = canvas.getContext('2d');
23
+
24
+ if (!ctx) {
25
+ console.error(`[CanvasWorker] Failed to get 2D context for panel ${panelId}`);
26
+ return;
27
+ }
28
+
29
+ console.log(`[CanvasWorker] Successfully bound canvas for panel ${panelId} with mode ${mode}`);
30
+
31
+ this.bindings.set(panelId, { ctx, canvas, panelId, mode });
32
+
33
+ this.startAnimation(panelId);
34
+ },
35
+
36
+ startAnimation(panelId: string) {
37
+ const binding = this.bindings.get(panelId);
38
+ if (!binding) return;
39
+
40
+ switch (binding.mode) {
41
+ case 'static':
42
+ this.drawStatic(binding);
43
+ break;
44
+ case 'animated':
45
+ this.drawAnimated(binding);
46
+ break;
47
+ case 'dynamic':
48
+ this.drawDynamic(binding);
49
+ break;
50
+ default:
51
+ console.warn(`[CanvasWorker] Unknown draw mode for panel ${panelId}`);
52
+ }
53
+ },
54
+
55
+ drawStatic(binding: CanvasBinding) {
56
+ binding.ctx.fillStyle = 'gray';
57
+ binding.ctx.fillRect(0, 0, binding.canvas.width, binding.canvas.height);
58
+ },
59
+
60
+ drawAnimated(binding: CanvasBinding) {
61
+ let hue = 0;
62
+
63
+ const drawFrame = () => {
64
+ hue = (hue + 1) % 360;
65
+ binding.ctx.fillStyle = `hsl(${hue}, 100%, 50%)`;
66
+ binding.ctx.fillRect(0, 0, binding.canvas.width, binding.canvas.height);
67
+
68
+ binding.animationFrame = requestAnimationFrame(drawFrame);
69
+ };
70
+
71
+ drawFrame();
72
+ },
73
+
74
+ drawDynamic(binding: CanvasBinding) {
75
+ let time = 0;
76
+
77
+ const drawFrame = () => {
78
+ time += 0.05;
79
+ binding.ctx.clearRect(0, 0, binding.canvas.width, binding.canvas.height);
80
+ binding.ctx.beginPath();
81
+ binding.ctx.arc(
82
+ binding.canvas.width / 2 + Math.sin(time) * 50,
83
+ binding.canvas.height / 2 + Math.cos(time) * 50,
84
+ 30,
85
+ 0,
86
+ Math.PI * 2
87
+ );
88
+ binding.ctx.fillStyle = 'orange';
89
+ binding.ctx.fill();
90
+
91
+ binding.animationFrame = requestAnimationFrame(drawFrame);
92
+ };
93
+
94
+ drawFrame();
95
+ },
96
+
97
+ async unbindCanvas(panelId: string) {
98
+ const binding = this.bindings.get(panelId);
99
+ if (binding?.animationFrame) {
100
+ cancelAnimationFrame(binding.animationFrame);
101
+ }
102
+ this.bindings.delete(panelId);
103
+ console.log(`[CanvasWorker] Unbound canvas for panel ${panelId}`);
104
+ },
105
+ };
106
+
107
+ expose(CanvasManager);