@hyperfixi/behaviors 2.0.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/dist/all.browser.global.js +116 -0
- package/dist/all.browser.global.js.map +1 -0
- package/dist/behaviors/draggable.cjs +109 -0
- package/dist/behaviors/draggable.cjs.map +1 -0
- package/dist/behaviors/draggable.d.cts +15 -0
- package/dist/behaviors/draggable.d.ts +15 -0
- package/dist/behaviors/draggable.js +14 -0
- package/dist/behaviors/draggable.js.map +1 -0
- package/dist/behaviors/removable.cjs +120 -0
- package/dist/behaviors/removable.cjs.map +1 -0
- package/dist/behaviors/removable.d.cts +15 -0
- package/dist/behaviors/removable.d.ts +15 -0
- package/dist/behaviors/removable.js +14 -0
- package/dist/behaviors/removable.js.map +1 -0
- package/dist/behaviors/resizable.cjs +146 -0
- package/dist/behaviors/resizable.cjs.map +1 -0
- package/dist/behaviors/resizable.d.cts +15 -0
- package/dist/behaviors/resizable.d.ts +15 -0
- package/dist/behaviors/resizable.js +14 -0
- package/dist/behaviors/resizable.js.map +1 -0
- package/dist/behaviors/sortable.cjs +119 -0
- package/dist/behaviors/sortable.cjs.map +1 -0
- package/dist/behaviors/sortable.d.cts +15 -0
- package/dist/behaviors/sortable.d.ts +15 -0
- package/dist/behaviors/sortable.js +14 -0
- package/dist/behaviors/sortable.js.map +1 -0
- package/dist/behaviors/toggleable.cjs +105 -0
- package/dist/behaviors/toggleable.cjs.map +1 -0
- package/dist/behaviors/toggleable.d.cts +15 -0
- package/dist/behaviors/toggleable.d.ts +15 -0
- package/dist/behaviors/toggleable.js +14 -0
- package/dist/behaviors/toggleable.js.map +1 -0
- package/dist/chunk-272NKUJ3.js +63 -0
- package/dist/chunk-272NKUJ3.js.map +1 -0
- package/dist/chunk-2NQHIHD5.js +37 -0
- package/dist/chunk-2NQHIHD5.js.map +1 -0
- package/dist/chunk-3DVDS7CT.js +170 -0
- package/dist/chunk-3DVDS7CT.js.map +1 -0
- package/dist/chunk-3Q652JQB.js +37 -0
- package/dist/chunk-3Q652JQB.js.map +1 -0
- package/dist/chunk-BWZ6EGII.js +62 -0
- package/dist/chunk-BWZ6EGII.js.map +1 -0
- package/dist/chunk-G6WR2AYR.js +52 -0
- package/dist/chunk-G6WR2AYR.js.map +1 -0
- package/dist/chunk-HMRPQWQU.js +37 -0
- package/dist/chunk-HMRPQWQU.js.map +1 -0
- package/dist/chunk-MEP6B7XW.js +37 -0
- package/dist/chunk-MEP6B7XW.js.map +1 -0
- package/dist/chunk-URBOGGW6.js +32 -0
- package/dist/chunk-URBOGGW6.js.map +1 -0
- package/dist/chunk-V5IWDPDP.js +89 -0
- package/dist/chunk-V5IWDPDP.js.map +1 -0
- package/dist/chunk-WU37DFAL.js +37 -0
- package/dist/chunk-WU37DFAL.js.map +1 -0
- package/dist/chunk-WUHHMDAA.js +48 -0
- package/dist/chunk-WUHHMDAA.js.map +1 -0
- package/dist/core.browser.global.js +39 -0
- package/dist/core.browser.global.js.map +1 -0
- package/dist/draggable.browser.global.js +25 -0
- package/dist/draggable.browser.global.js.map +1 -0
- package/dist/index.cjs +907 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +134 -0
- package/dist/index.d.ts +134 -0
- package/dist/index.js +193 -0
- package/dist/index.js.map +1 -0
- package/dist/loaders.cjs +562 -0
- package/dist/loaders.cjs.map +1 -0
- package/dist/loaders.d.cts +2 -0
- package/dist/loaders.d.ts +2 -0
- package/dist/loaders.js +8 -0
- package/dist/loaders.js.map +1 -0
- package/dist/registry.cjs +214 -0
- package/dist/registry.cjs.map +1 -0
- package/dist/registry.d.cts +116 -0
- package/dist/registry.d.ts +116 -0
- package/dist/registry.js +47 -0
- package/dist/registry.js.map +1 -0
- package/dist/removable.browser.global.js +23 -0
- package/dist/removable.browser.global.js.map +1 -0
- package/dist/resizable.browser.global.js +35 -0
- package/dist/resizable.browser.global.js.map +1 -0
- package/dist/schemas/index.cjs +353 -0
- package/dist/schemas/index.cjs.map +1 -0
- package/dist/schemas/index.d.cts +75 -0
- package/dist/schemas/index.d.ts +75 -0
- package/dist/schemas/index.js +41 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/sortable.browser.global.js +25 -0
- package/dist/sortable.browser.global.js.map +1 -0
- package/dist/toggleable.browser.global.js +16 -0
- package/dist/toggleable.browser.global.js.map +1 -0
- package/dist/types-CCc_2wAb.d.cts +100 -0
- package/dist/types-CCc_2wAb.d.ts +100 -0
- package/package.json +109 -0
- package/src/behaviors/draggable.ts +60 -0
- package/src/behaviors/index.ts +40 -0
- package/src/behaviors/integration.test.ts +80 -0
- package/src/behaviors/removable.ts +55 -0
- package/src/behaviors/resizable.ts +55 -0
- package/src/behaviors/sortable.ts +56 -0
- package/src/behaviors/toggleable.ts +55 -0
- package/src/core-bundle.ts +35 -0
- package/src/generated/index.ts +9 -0
- package/src/generated/metadata.ts +78 -0
- package/src/generated/types.ts +51 -0
- package/src/index.test.ts +180 -0
- package/src/index.ts +222 -0
- package/src/loaders.ts +39 -0
- package/src/registry.test.ts +207 -0
- package/src/registry.ts +304 -0
- package/src/schemas/draggable.schema.ts +54 -0
- package/src/schemas/index.ts +60 -0
- package/src/schemas/removable.schema.ts +69 -0
- package/src/schemas/resizable.schema.ts +91 -0
- package/src/schemas/sortable.schema.ts +68 -0
- package/src/schemas/toggleable.schema.ts +50 -0
- package/src/schemas/types.ts +107 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import type { BehaviorModule, BehaviorSchema } from './schemas/types';
|
|
3
|
+
|
|
4
|
+
// Initialize the registry with schemas and loaders (same side-effect as index.ts)
|
|
5
|
+
import './loaders';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
registerBehavior,
|
|
9
|
+
registerSchema,
|
|
10
|
+
registerLoader,
|
|
11
|
+
getBehavior,
|
|
12
|
+
tryGetBehavior,
|
|
13
|
+
getSchema,
|
|
14
|
+
tryGetSchema,
|
|
15
|
+
isRegistered,
|
|
16
|
+
hasLoader,
|
|
17
|
+
getRegisteredBehaviors,
|
|
18
|
+
getAvailableBehaviors,
|
|
19
|
+
getBehaviorsByCategory,
|
|
20
|
+
getBehaviorsByTier,
|
|
21
|
+
getAllSchemas,
|
|
22
|
+
getAllSchemasRecord,
|
|
23
|
+
loadBehavior,
|
|
24
|
+
preloadTier,
|
|
25
|
+
preloadCategory,
|
|
26
|
+
loadAll,
|
|
27
|
+
registerWithRuntime,
|
|
28
|
+
registerAllWithRuntime,
|
|
29
|
+
} from './registry';
|
|
30
|
+
|
|
31
|
+
// Create test fixtures
|
|
32
|
+
function createMockSchema(overrides: Partial<BehaviorSchema> = {}): BehaviorSchema {
|
|
33
|
+
return {
|
|
34
|
+
name: 'TestBehavior',
|
|
35
|
+
category: 'ui',
|
|
36
|
+
tier: 'core',
|
|
37
|
+
version: '1.0.0',
|
|
38
|
+
description: 'Test behavior',
|
|
39
|
+
parameters: [],
|
|
40
|
+
events: [],
|
|
41
|
+
source: 'behavior TestBehavior\nend',
|
|
42
|
+
...overrides,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function createMockModule(overrides: Partial<BehaviorModule> = {}): BehaviorModule {
|
|
47
|
+
const schema = createMockSchema(overrides.metadata ? { ...overrides.metadata } : undefined);
|
|
48
|
+
return {
|
|
49
|
+
source: schema.source,
|
|
50
|
+
metadata: schema,
|
|
51
|
+
register: vi.fn().mockResolvedValue(undefined),
|
|
52
|
+
...overrides,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
describe('registry', () => {
|
|
57
|
+
// Note: The registry is a module-level singleton, so loaders.ts has already
|
|
58
|
+
// populated it with the 5 real behavior schemas. Tests build on this state.
|
|
59
|
+
|
|
60
|
+
describe('registerBehavior', () => {
|
|
61
|
+
it('should register a behavior and its schema', () => {
|
|
62
|
+
const module = createMockModule({
|
|
63
|
+
metadata: createMockSchema({ name: 'RegTest' }),
|
|
64
|
+
});
|
|
65
|
+
registerBehavior('RegTest', module);
|
|
66
|
+
expect(getBehavior('RegTest')).toBe(module);
|
|
67
|
+
expect(getSchema('RegTest').name).toBe('RegTest');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('getBehavior / tryGetBehavior', () => {
|
|
72
|
+
it('should throw for unregistered behavior', () => {
|
|
73
|
+
expect(() => getBehavior('NonExistent')).toThrowError(/not registered/);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should suggest lazy loading in error message', () => {
|
|
77
|
+
expect(() => getBehavior('NonExistent')).toThrowError(/loadBehavior/);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('tryGetBehavior should return undefined for unregistered', () => {
|
|
81
|
+
expect(tryGetBehavior('NonExistent')).toBeUndefined();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('getSchema / tryGetSchema', () => {
|
|
86
|
+
it('should throw for unregistered schema', () => {
|
|
87
|
+
expect(() => getSchema('NonExistent')).toThrowError(/not registered/);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should suggest tryGetSchema in error message', () => {
|
|
91
|
+
expect(() => getSchema('NonExistent')).toThrowError(/tryGetSchema/);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('tryGetSchema should return undefined for unregistered', () => {
|
|
95
|
+
expect(tryGetSchema('NonExistent')).toBeUndefined();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should return schema for registered behavior', () => {
|
|
99
|
+
const schema = getSchema('Draggable');
|
|
100
|
+
expect(schema.name).toBe('Draggable');
|
|
101
|
+
expect(schema.category).toBe('ui');
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('isRegistered / hasLoader', () => {
|
|
106
|
+
it('hasLoader should return true for registered loaders', () => {
|
|
107
|
+
expect(hasLoader('Draggable')).toBe(true);
|
|
108
|
+
expect(hasLoader('NonExistent')).toBe(false);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe('metadata queries', () => {
|
|
113
|
+
it('getAllSchemasRecord should return record keyed by name', () => {
|
|
114
|
+
const record = getAllSchemasRecord();
|
|
115
|
+
expect(record.Draggable).toBeDefined();
|
|
116
|
+
expect(record.Draggable.category).toBe('ui');
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe('loadBehavior', () => {
|
|
121
|
+
it('should load via registered loader', async () => {
|
|
122
|
+
const module = await loadBehavior('Toggleable');
|
|
123
|
+
expect(module).toBeDefined();
|
|
124
|
+
expect(module.metadata.name).toBe('Toggleable');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should return cached module on second load', async () => {
|
|
128
|
+
const first = await loadBehavior('Toggleable');
|
|
129
|
+
const second = await loadBehavior('Toggleable');
|
|
130
|
+
expect(first).toBe(second);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should throw for behavior with no loader', async () => {
|
|
134
|
+
await expect(loadBehavior('NonExistent')).rejects.toThrowError(/No loader/);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe('preloadTier / preloadCategory', () => {
|
|
139
|
+
it('preloadTier should load all behaviors in a tier', async () => {
|
|
140
|
+
await preloadTier('core');
|
|
141
|
+
expect(isRegistered('Draggable')).toBe(true);
|
|
142
|
+
expect(isRegistered('Toggleable')).toBe(true);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('preloadCategory should load all behaviors in a category', async () => {
|
|
146
|
+
await preloadCategory('ui');
|
|
147
|
+
expect(isRegistered('Draggable')).toBe(true);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('preloadTier should warn on load failure', async () => {
|
|
151
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
152
|
+
// Register a loader that fails
|
|
153
|
+
registerLoader('FailBehavior', () => Promise.reject(new Error('test fail')));
|
|
154
|
+
registerSchema(createMockSchema({ name: 'FailBehavior', tier: 'optional' }));
|
|
155
|
+
|
|
156
|
+
await preloadTier('optional');
|
|
157
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
158
|
+
expect.stringContaining('FailBehavior'),
|
|
159
|
+
expect.any(Error)
|
|
160
|
+
);
|
|
161
|
+
warnSpy.mockRestore();
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe('loadAll', () => {
|
|
166
|
+
it('should load all available behaviors', async () => {
|
|
167
|
+
await loadAll();
|
|
168
|
+
const registered = getRegisteredBehaviors();
|
|
169
|
+
expect(registered).toContain('Draggable');
|
|
170
|
+
expect(registered).toContain('Toggleable');
|
|
171
|
+
expect(registered).toContain('Removable');
|
|
172
|
+
expect(registered).toContain('Sortable');
|
|
173
|
+
expect(registered).toContain('Resizable');
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe('registerWithRuntime', () => {
|
|
178
|
+
it('should load and call module.register', async () => {
|
|
179
|
+
const mockInstance = {
|
|
180
|
+
compileSync: vi.fn().mockReturnValue({ ok: true, ast: {} }),
|
|
181
|
+
execute: vi.fn().mockResolvedValue(undefined),
|
|
182
|
+
createContext: vi.fn().mockReturnValue({ locals: new Map(), globals: new Map() }),
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
// Load first so module is available
|
|
186
|
+
await loadBehavior('Draggable');
|
|
187
|
+
|
|
188
|
+
await registerWithRuntime('Draggable', mockInstance);
|
|
189
|
+
expect(mockInstance.compileSync).toHaveBeenCalled();
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe('registerAllWithRuntime', () => {
|
|
194
|
+
it('should register all loaded behaviors', async () => {
|
|
195
|
+
const mockInstance = {
|
|
196
|
+
compileSync: vi.fn().mockReturnValue({ ok: true, ast: {} }),
|
|
197
|
+
execute: vi.fn().mockResolvedValue(undefined),
|
|
198
|
+
createContext: vi.fn().mockReturnValue({ locals: new Map(), globals: new Map() }),
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
await loadAll();
|
|
202
|
+
await registerAllWithRuntime(mockInstance);
|
|
203
|
+
// Each behavior calls compileSync once
|
|
204
|
+
expect(mockInstance.compileSync.mock.calls.length).toBeGreaterThanOrEqual(5);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
});
|
package/src/registry.ts
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Behavior Registry
|
|
3
|
+
*
|
|
4
|
+
* Central registration point for behaviors. Modeled on semantic/registry.ts.
|
|
5
|
+
* Supports lazy loading, category/tier queries, and tree-shaking.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* // Register a behavior
|
|
10
|
+
* registerBehavior('Draggable', DraggableModule);
|
|
11
|
+
*
|
|
12
|
+
* // Query behaviors
|
|
13
|
+
* const uiBehaviors = getBehaviorsByCategory('ui');
|
|
14
|
+
* const coreBehaviors = getBehaviorsByTier('core');
|
|
15
|
+
*
|
|
16
|
+
* // Lazy load on demand
|
|
17
|
+
* const module = await loadBehavior('Resizable');
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type {
|
|
22
|
+
BehaviorModule,
|
|
23
|
+
BehaviorSchema,
|
|
24
|
+
BehaviorCategory,
|
|
25
|
+
BehaviorTier,
|
|
26
|
+
LokaScriptInstance,
|
|
27
|
+
} from './schemas/types';
|
|
28
|
+
|
|
29
|
+
// =============================================================================
|
|
30
|
+
// Registry State
|
|
31
|
+
// =============================================================================
|
|
32
|
+
|
|
33
|
+
const behaviors = new Map<string, BehaviorModule>();
|
|
34
|
+
const schemas = new Map<string, BehaviorSchema>();
|
|
35
|
+
const loaders = new Map<string, () => Promise<{ default: BehaviorModule }>>();
|
|
36
|
+
const loadingPromises = new Map<string, Promise<BehaviorModule>>();
|
|
37
|
+
|
|
38
|
+
// =============================================================================
|
|
39
|
+
// Registration Functions
|
|
40
|
+
// =============================================================================
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Register a behavior module.
|
|
44
|
+
*/
|
|
45
|
+
export function registerBehavior(name: string, module: BehaviorModule): void {
|
|
46
|
+
behaviors.set(name, module);
|
|
47
|
+
if (module.metadata) {
|
|
48
|
+
schemas.set(name, module.metadata);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Register a behavior schema (without the module).
|
|
54
|
+
*/
|
|
55
|
+
export function registerSchema(schema: BehaviorSchema): void {
|
|
56
|
+
schemas.set(schema.name, schema);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Register a lazy loader for a behavior.
|
|
61
|
+
* The loader should return a module with a default export.
|
|
62
|
+
*/
|
|
63
|
+
export function registerLoader(
|
|
64
|
+
name: string,
|
|
65
|
+
loader: () => Promise<{ default: BehaviorModule }>
|
|
66
|
+
): void {
|
|
67
|
+
loaders.set(name, loader);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// =============================================================================
|
|
71
|
+
// Query Functions (Sync)
|
|
72
|
+
// =============================================================================
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Get a behavior module by name.
|
|
76
|
+
* @throws Error if behavior is not registered
|
|
77
|
+
*/
|
|
78
|
+
export function getBehavior(name: string): BehaviorModule {
|
|
79
|
+
const behavior = behaviors.get(name);
|
|
80
|
+
if (!behavior) {
|
|
81
|
+
const registered = Array.from(behaviors.keys()).join(', ');
|
|
82
|
+
throw new Error(
|
|
83
|
+
`Behavior '${name}' is not registered. ` +
|
|
84
|
+
`Registered behaviors: ${registered || 'none'}. ` +
|
|
85
|
+
`Use loadBehavior('${name}') to lazy load it.`
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
return behavior;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Try to get a behavior, returning undefined if not registered.
|
|
93
|
+
*/
|
|
94
|
+
export function tryGetBehavior(name: string): BehaviorModule | undefined {
|
|
95
|
+
return behaviors.get(name);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Get a behavior schema by name.
|
|
100
|
+
* @throws Error if schema is not registered
|
|
101
|
+
*/
|
|
102
|
+
export function getSchema(name: string): BehaviorSchema {
|
|
103
|
+
const schema = schemas.get(name);
|
|
104
|
+
if (!schema) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
`Schema for behavior '${name}' is not registered. ` +
|
|
107
|
+
`Use tryGetSchema('${name}') for a non-throwing alternative.`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
return schema;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Try to get a schema, returning undefined if not registered.
|
|
115
|
+
*/
|
|
116
|
+
export function tryGetSchema(name: string): BehaviorSchema | undefined {
|
|
117
|
+
return schemas.get(name);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Check if a behavior is registered.
|
|
122
|
+
*/
|
|
123
|
+
export function isRegistered(name: string): boolean {
|
|
124
|
+
return behaviors.has(name);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Check if a loader is registered for a behavior.
|
|
129
|
+
*/
|
|
130
|
+
export function hasLoader(name: string): boolean {
|
|
131
|
+
return loaders.has(name);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get all registered behavior names.
|
|
136
|
+
*/
|
|
137
|
+
export function getRegisteredBehaviors(): string[] {
|
|
138
|
+
return Array.from(behaviors.keys());
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Get all behavior names that have loaders (including unloaded).
|
|
143
|
+
*/
|
|
144
|
+
export function getAvailableBehaviors(): string[] {
|
|
145
|
+
const names = new Set([...behaviors.keys(), ...loaders.keys()]);
|
|
146
|
+
return Array.from(names);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// =============================================================================
|
|
150
|
+
// Query by Metadata
|
|
151
|
+
// =============================================================================
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Get behavior names by category.
|
|
155
|
+
*/
|
|
156
|
+
export function getBehaviorsByCategory(category: BehaviorCategory): string[] {
|
|
157
|
+
const result: string[] = [];
|
|
158
|
+
for (const [name, schema] of schemas) {
|
|
159
|
+
if (schema.category === category) {
|
|
160
|
+
result.push(name);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return result;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Get behavior names by tier.
|
|
168
|
+
*/
|
|
169
|
+
export function getBehaviorsByTier(tier: BehaviorTier): string[] {
|
|
170
|
+
const result: string[] = [];
|
|
171
|
+
for (const [name, schema] of schemas) {
|
|
172
|
+
if (schema.tier === tier) {
|
|
173
|
+
result.push(name);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return result;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Get all schemas.
|
|
181
|
+
*/
|
|
182
|
+
export function getAllSchemas(): BehaviorSchema[] {
|
|
183
|
+
return Array.from(schemas.values());
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Get all schemas as a record.
|
|
188
|
+
*/
|
|
189
|
+
export function getAllSchemasRecord(): Record<string, BehaviorSchema> {
|
|
190
|
+
const result: Record<string, BehaviorSchema> = {};
|
|
191
|
+
for (const [name, schema] of schemas) {
|
|
192
|
+
result[name] = schema;
|
|
193
|
+
}
|
|
194
|
+
return result;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// =============================================================================
|
|
198
|
+
// Lazy Loading (Async)
|
|
199
|
+
// =============================================================================
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Load a behavior by name.
|
|
203
|
+
* If already loaded, returns the cached module.
|
|
204
|
+
* If a loader is registered, loads and caches the module.
|
|
205
|
+
*/
|
|
206
|
+
export async function loadBehavior(name: string): Promise<BehaviorModule> {
|
|
207
|
+
// Return cached if available
|
|
208
|
+
const cached = behaviors.get(name);
|
|
209
|
+
if (cached) {
|
|
210
|
+
return cached;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Check for in-flight load
|
|
214
|
+
const inFlight = loadingPromises.get(name);
|
|
215
|
+
if (inFlight) {
|
|
216
|
+
return inFlight;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Check for loader
|
|
220
|
+
const loader = loaders.get(name);
|
|
221
|
+
if (!loader) {
|
|
222
|
+
throw new Error(
|
|
223
|
+
`No loader registered for behavior '${name}'. ` +
|
|
224
|
+
`Available loaders: ${Array.from(loaders.keys()).join(', ') || 'none'}`
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Load and cache
|
|
229
|
+
const promise = loader().then(mod => {
|
|
230
|
+
const module = mod.default;
|
|
231
|
+
registerBehavior(name, module);
|
|
232
|
+
loadingPromises.delete(name);
|
|
233
|
+
return module;
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
loadingPromises.set(name, promise);
|
|
237
|
+
return promise;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Preload all behaviors in a tier.
|
|
242
|
+
*/
|
|
243
|
+
export async function preloadTier(tier: BehaviorTier): Promise<void> {
|
|
244
|
+
const names = getBehaviorsByTier(tier);
|
|
245
|
+
await Promise.all(
|
|
246
|
+
names.map(name =>
|
|
247
|
+
loadBehavior(name).catch(err => {
|
|
248
|
+
console.warn(`[behaviors] Failed to preload '${name}' (tier: ${tier}):`, err);
|
|
249
|
+
})
|
|
250
|
+
)
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Preload all behaviors in a category.
|
|
256
|
+
*/
|
|
257
|
+
export async function preloadCategory(category: BehaviorCategory): Promise<void> {
|
|
258
|
+
const names = getBehaviorsByCategory(category);
|
|
259
|
+
await Promise.all(
|
|
260
|
+
names.map(name =>
|
|
261
|
+
loadBehavior(name).catch(err => {
|
|
262
|
+
console.warn(`[behaviors] Failed to preload '${name}' (category: ${category}):`, err);
|
|
263
|
+
})
|
|
264
|
+
)
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Load and register all available behaviors.
|
|
270
|
+
*/
|
|
271
|
+
export async function loadAll(): Promise<void> {
|
|
272
|
+
const names = getAvailableBehaviors();
|
|
273
|
+
await Promise.all(
|
|
274
|
+
names.map(name =>
|
|
275
|
+
loadBehavior(name).catch(err => {
|
|
276
|
+
console.warn(`[behaviors] Failed to load '${name}':`, err);
|
|
277
|
+
})
|
|
278
|
+
)
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// =============================================================================
|
|
283
|
+
// Runtime Registration
|
|
284
|
+
// =============================================================================
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Register a behavior with HyperFixi runtime.
|
|
288
|
+
* Compiles and executes the behavior source.
|
|
289
|
+
*/
|
|
290
|
+
export async function registerWithRuntime(
|
|
291
|
+
name: string,
|
|
292
|
+
hyperfixi?: LokaScriptInstance
|
|
293
|
+
): Promise<void> {
|
|
294
|
+
const module = await loadBehavior(name);
|
|
295
|
+
await module.register(hyperfixi);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Register all loaded behaviors with HyperFixi runtime.
|
|
300
|
+
*/
|
|
301
|
+
export async function registerAllWithRuntime(hyperfixi?: LokaScriptInstance): Promise<void> {
|
|
302
|
+
const registrations = Array.from(behaviors.values()).map(module => module.register(hyperfixi));
|
|
303
|
+
await Promise.all(registrations);
|
|
304
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { BehaviorSchema } from './types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Draggable Behavior Schema
|
|
5
|
+
*
|
|
6
|
+
* Makes elements draggable with pointer events.
|
|
7
|
+
* Supports custom drag handles and lifecycle events.
|
|
8
|
+
*/
|
|
9
|
+
export const draggableSchema: BehaviorSchema = {
|
|
10
|
+
name: 'Draggable',
|
|
11
|
+
category: 'ui',
|
|
12
|
+
tier: 'core',
|
|
13
|
+
version: '1.0.0',
|
|
14
|
+
description: 'Makes elements draggable with pointer events',
|
|
15
|
+
parameters: [
|
|
16
|
+
{
|
|
17
|
+
name: 'dragHandle',
|
|
18
|
+
type: 'selector',
|
|
19
|
+
optional: true,
|
|
20
|
+
default: 'me',
|
|
21
|
+
description: 'CSS selector for the drag handle element',
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
events: [
|
|
25
|
+
{ name: 'draggable:start', description: 'Fired when drag begins' },
|
|
26
|
+
{ name: 'draggable:move', description: 'Fired during each movement' },
|
|
27
|
+
{ name: 'draggable:end', description: 'Fired when drag completes' },
|
|
28
|
+
],
|
|
29
|
+
requirements: ['position: absolute or position: fixed on the element'],
|
|
30
|
+
source: `
|
|
31
|
+
behavior Draggable(dragHandle)
|
|
32
|
+
init
|
|
33
|
+
if no dragHandle set dragHandle to me
|
|
34
|
+
end
|
|
35
|
+
on pointerdown(clientX, clientY) from dragHandle
|
|
36
|
+
halt the event
|
|
37
|
+
trigger draggable:start
|
|
38
|
+
measure x
|
|
39
|
+
set startX to it
|
|
40
|
+
measure y
|
|
41
|
+
set startY to it
|
|
42
|
+
set xoff to clientX - startX
|
|
43
|
+
set yoff to clientY - startY
|
|
44
|
+
repeat until event pointerup from document
|
|
45
|
+
wait for pointermove(clientX, clientY) or
|
|
46
|
+
pointerup(clientX, clientY) from document
|
|
47
|
+
add { left: \${clientX - xoff}px; top: \${clientY - yoff}px; }
|
|
48
|
+
trigger draggable:move
|
|
49
|
+
end
|
|
50
|
+
trigger draggable:end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
`.trim(),
|
|
54
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Behavior Schemas
|
|
3
|
+
*
|
|
4
|
+
* Central export point for all behavior schemas.
|
|
5
|
+
* Used by the registry and code generation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Types
|
|
9
|
+
export type {
|
|
10
|
+
BehaviorSchema,
|
|
11
|
+
BehaviorCategory,
|
|
12
|
+
BehaviorTier,
|
|
13
|
+
ParameterSchema,
|
|
14
|
+
EventSchema,
|
|
15
|
+
BehaviorModule,
|
|
16
|
+
LokaScriptInstance,
|
|
17
|
+
} from './types';
|
|
18
|
+
|
|
19
|
+
// Individual schemas
|
|
20
|
+
export { draggableSchema } from './draggable.schema';
|
|
21
|
+
export { sortableSchema } from './sortable.schema';
|
|
22
|
+
export { resizableSchema } from './resizable.schema';
|
|
23
|
+
export { removableSchema } from './removable.schema';
|
|
24
|
+
export { toggleableSchema } from './toggleable.schema';
|
|
25
|
+
|
|
26
|
+
// All schemas as a collection
|
|
27
|
+
import { draggableSchema } from './draggable.schema';
|
|
28
|
+
import { sortableSchema } from './sortable.schema';
|
|
29
|
+
import { resizableSchema } from './resizable.schema';
|
|
30
|
+
import { removableSchema } from './removable.schema';
|
|
31
|
+
import { toggleableSchema } from './toggleable.schema';
|
|
32
|
+
import type { BehaviorSchema } from './types';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* All behavior schemas indexed by name.
|
|
36
|
+
*/
|
|
37
|
+
export const ALL_SCHEMAS: Record<string, BehaviorSchema> = {
|
|
38
|
+
Draggable: draggableSchema,
|
|
39
|
+
Sortable: sortableSchema,
|
|
40
|
+
Resizable: resizableSchema,
|
|
41
|
+
Removable: removableSchema,
|
|
42
|
+
Toggleable: toggleableSchema,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get all schema names.
|
|
47
|
+
*/
|
|
48
|
+
export function getSchemaNames(): string[] {
|
|
49
|
+
return Object.keys(ALL_SCHEMAS);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Find a schema by name. Returns undefined if not found.
|
|
54
|
+
*
|
|
55
|
+
* Note: For the throwing version, use `getSchema()` from the registry
|
|
56
|
+
* (exported from '@hyperfixi/behaviors').
|
|
57
|
+
*/
|
|
58
|
+
export function findSchema(name: string): BehaviorSchema | undefined {
|
|
59
|
+
return ALL_SCHEMAS[name];
|
|
60
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { BehaviorSchema } from './types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Removable Behavior Schema
|
|
5
|
+
*
|
|
6
|
+
* Removes an element when a trigger is clicked.
|
|
7
|
+
* Supports optional confirmation and transition effects.
|
|
8
|
+
*
|
|
9
|
+
* @since 1.2.0 — `trigger` parameter renamed to `triggerEl` to avoid
|
|
10
|
+
* shadowing the hyperscript `trigger` command keyword. Users migrating
|
|
11
|
+
* from v1.x should update: `install Removable(triggerEl: .close-btn)`.
|
|
12
|
+
*/
|
|
13
|
+
export const removableSchema: BehaviorSchema = {
|
|
14
|
+
name: 'Removable',
|
|
15
|
+
category: 'data',
|
|
16
|
+
tier: 'common',
|
|
17
|
+
version: '1.2.0',
|
|
18
|
+
description: 'Makes elements removable on click',
|
|
19
|
+
parameters: [
|
|
20
|
+
{
|
|
21
|
+
name: 'triggerEl',
|
|
22
|
+
type: 'selector',
|
|
23
|
+
optional: true,
|
|
24
|
+
default: 'me',
|
|
25
|
+
description: 'Element that triggers removal (e.g., a close button)',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: 'confirm',
|
|
29
|
+
type: 'boolean',
|
|
30
|
+
optional: true,
|
|
31
|
+
default: false,
|
|
32
|
+
description: 'Show confirmation dialog before removal',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'effect',
|
|
36
|
+
type: 'string',
|
|
37
|
+
optional: true,
|
|
38
|
+
default: 'none',
|
|
39
|
+
enum: ['fade', 'none'],
|
|
40
|
+
description: 'Transition effect: "fade" or "none"',
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
events: [
|
|
44
|
+
{ name: 'removable:before', description: 'Fired before removal (cancelable)' },
|
|
45
|
+
{ name: 'removable:removed', description: 'Fired after removal' },
|
|
46
|
+
],
|
|
47
|
+
source: `
|
|
48
|
+
behavior Removable(triggerEl, confirm, effect)
|
|
49
|
+
init
|
|
50
|
+
if triggerEl is undefined
|
|
51
|
+
set triggerEl to me
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
on click from triggerEl
|
|
55
|
+
if confirm
|
|
56
|
+
if not window.confirm("Are you sure?")
|
|
57
|
+
halt
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
trigger removable:before
|
|
61
|
+
if effect is "fade"
|
|
62
|
+
transition opacity to 0 over 300ms
|
|
63
|
+
end
|
|
64
|
+
trigger removable:removed
|
|
65
|
+
remove me
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
`.trim(),
|
|
69
|
+
};
|