juxscript 1.0.102 → 1.0.103

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "juxscript",
3
- "version": "1.0.102",
3
+ "version": "1.0.103",
4
4
  "type": "module",
5
5
  "description": "A JavaScript UX authorship platform",
6
6
  "main": "./index.js",
@@ -22,7 +22,12 @@
22
22
  "index.js",
23
23
  "bin",
24
24
  "create",
25
- "lib",
25
+ "lib/**/*.js",
26
+ "lib/**/*.js.map",
27
+ "lib/**/*.d.ts",
28
+ "lib/**/*.d.ts.map",
29
+ "lib/**/*.css",
30
+ "lib/**/stubs/*.stub",
26
31
  "machinery",
27
32
  "types",
28
33
  "juxconfig.example.js",
@@ -1,332 +0,0 @@
1
- import { State } from './State.js';
2
- import { GlobalBus } from './GlobalBus.js';
3
- import { validateOptions, OptionsContractSchema, ValidationResult } from './OptionsContract.js';
4
-
5
-
6
- export interface JuxServiceContract<TEngine = any> {
7
- name: string;
8
- version?: string;
9
- targetEnv?: 'client' | 'server' | 'universal'; // Environment Constraint
10
- install: (engine: TEngine) => void;
11
- uninstall?: (engine: TEngine) => void;
12
- }
13
-
14
- export interface BaseState {
15
- id: string;
16
- classes: string[];
17
- visible: boolean;
18
- disabled: boolean;
19
- loading: boolean;
20
- attributes: Record<string, string>;
21
- }
22
-
23
- type EventListener<T = any> = (data: T) => void;
24
-
25
- /**
26
- * THE ENGINE AGREEMENT
27
- *
28
- * 1. Must define State and Options types.
29
- * 2. Must implement `prepareState` to map Options -> Initial State.
30
- * 3. Provides reactivity, eventing, and plugin systems out of the box.
31
- */
32
- export abstract class BaseEngine<TState extends BaseState, TOptions = any> {
33
- #state: State<TState>;
34
- #listeners = new Map<string, Set<EventListener>>();
35
- #cleanupListeners: Array<() => void> = [];
36
- #plugins = new Map<string, JuxServiceContract>();
37
- #emitHistory: string[] = [];
38
- #debugMode = false;
39
- #validationResult: ValidationResult | null = null;
40
-
41
- constructor(id: string, options: TOptions) {
42
- // Validate options if schema is defined
43
- const schema = this.optionsSchema;
44
- if (schema) {
45
- this.#validationResult = validateOptions(
46
- this.constructor.name,
47
- options as Record<string, any>,
48
- schema,
49
- this.#debugMode
50
- );
51
- options = this.#validationResult.normalized as TOptions;
52
- }
53
-
54
- // Enforce the Contract: Child must define how state is born from options
55
- const initialState = this.prepareState(id, options);
56
- this.#state = new State(initialState);
57
- }
58
-
59
- /**
60
- * CONTRACT: Override to define the valid options schema for this engine.
61
- * Enables option validation with helpful error messages.
62
- */
63
- protected get optionsSchema(): OptionsContractSchema | null {
64
- return null; // Override in child classes
65
- }
66
-
67
- /**
68
- * Access validation results (warnings, errors, normalized options)
69
- */
70
- get validation(): ValidationResult | null {
71
- return this.#validationResult;
72
- }
73
-
74
- /**
75
- * CONTRACT: Must transform input options into the initial State.
76
- * This separates "mapping logic" from "construction/startup logic".
77
- */
78
- protected abstract prepareState(id: string, options: TOptions): TState;
79
-
80
- get state(): TState {
81
- return this.#state.value;
82
- }
83
-
84
- subscribe(callback: (value: TState) => void) {
85
- return this.#state.subscribe(callback);
86
- }
87
-
88
- /**
89
- * Enable/Disable Active Console Logging
90
- */
91
- debug(enabled: boolean = true): this {
92
- this.#debugMode = enabled;
93
- if (enabled) {
94
- console.info(`%c[JUX DEBUG] Mode Enabled for %c${this.state.id}`, 'color: #ff00ff; font-weight: bold;', 'font-weight: bold;');
95
- }
96
- return this;
97
- }
98
-
99
- protected updateState(patch: Partial<TState>): void {
100
- if (this.#debugMode) {
101
- console.groupCollapsed(`%c 💾 STATE %c ${this.state.id}`, 'color: #ff9900; font-weight: bold;', 'color: #888');
102
- console.log('Patch:', patch);
103
- console.log('Result:', { ...this.#state.value, ...patch });
104
- console.groupEnd();
105
- }
106
- this.#state.set({ ...this.#state.value, ...patch });
107
- }
108
-
109
- /**
110
- * Revert the state to the previous snapshot
111
- */
112
- rollback(): this {
113
- this.#state.rollback();
114
- this.emit('rollback', { index: this.#state.history.length }); // ✅ Added Emission
115
- return this; // ✅ Fluent Return
116
- }
117
-
118
- /**
119
- * Redo the previously rolled-back state
120
- */
121
- rollforward(): this {
122
- this.#state.rollforward();
123
- this.emit('rollforward', { index: this.#state.history.length }); // ✅ Added Emission
124
- return this; // ✅ Fluent Return
125
- }
126
-
127
- // --- Interaction System ---
128
-
129
- /**
130
- * Local Subscription: Subscribe to a specific named event on THIS instance.
131
- */
132
- on<T = any>(event: string, callback: EventListener<T>): this {
133
- if (!this.#listeners.has(event)) {
134
- this.#listeners.set(event, new Set());
135
- }
136
- this.#listeners.get(event)!.add(callback);
137
- return this;
138
- }
139
-
140
- /**
141
- * Unsubscribe from a local event.
142
- */
143
- off<T = any>(event: string, callback: EventListener<T>): this {
144
- this.#listeners.get(event)?.delete(callback);
145
- return this;
146
- }
147
-
148
- /**
149
- * THE LISTENER (Neighborhood Watch)
150
- * Listen for events broadcasting on the global frequency.
151
- * @param channel The global channel name (e.g. 'demo-list-v2:move')
152
- * @param callback Reaction logic
153
- */
154
- listenTo<T = any>(channel: string, callback: EventListener<T>): this {
155
- GlobalBus.on(channel, callback);
156
- // Track for cleanup
157
- this.#cleanupListeners.push(() => GlobalBus.off(channel, callback));
158
- return this;
159
- }
160
-
161
- /**
162
- * Cleanup all global listeners attached by this engine.
163
- */
164
- dispose(): void {
165
- // 1. Uninstall Plugins
166
- this.#plugins.forEach(p => {
167
- try {
168
- if (p.uninstall) p.uninstall(this);
169
- } catch (e) {
170
- console.error(`Jux: Error uninstalling plugin ${p.name}`, e);
171
- }
172
- });
173
- this.#plugins.clear();
174
-
175
- // 2. Clear Listeners
176
- this.#cleanupListeners.forEach(cleanup => cleanup());
177
- this.#cleanupListeners = [];
178
-
179
- if (this.#debugMode) console.log(`%c[JUX] Disposed ${this.state.id}`, 'color: #888');
180
- }
181
-
182
- /**
183
- * BROADCASTER
184
- * Emits locally AND to the global neighborhood.
185
- */
186
- protected emit(event: string, data: any): void {
187
- const timestamp = new Date().toISOString();
188
-
189
- // 0. Record History
190
- this.#emitHistory.push(JSON.stringify({ timestamp, event, data }));
191
-
192
- if (this.#debugMode) {
193
- console.log(`%c 📡 EMIT %c ${this.state.id}:${event}`, 'color: #00ccff; font-weight: bold;', 'color: #333; font-weight: bold;', data);
194
- }
195
-
196
- // 1. Notify Local Listeners
197
- this.#listeners.get(event)?.forEach(cb => cb(data));
198
-
199
- // 2. Notify The Neighborhood (Global Bus)
200
- // Format: "ComponentID:EventName"
201
- const globalChannel = `${this.state.id}:${event}`;
202
-
203
- GlobalBus.emit(globalChannel, {
204
- ...data,
205
- _event: event,
206
- _source: this.state.id
207
- });
208
- }
209
-
210
- /**
211
- * DEBUG: Access the Global Event Bus registry to see active channels and listener identities.
212
- * Useful for debugging communication topology from the console.
213
- */
214
- get eventRegistry(): Record<string, string[]> {
215
- return GlobalBus.registry;
216
- }
217
-
218
- /**
219
- * DEBUG: Access the Internal State Ledger (History).
220
- * Returns an array of JSON strings representing past states.
221
- */
222
- get stateHistory(): string[] {
223
- return this.#state.history;
224
- }
225
-
226
- /**
227
- * DEBUG: Access the Event Emission Ledger.
228
- */
229
- get emitHistory(): string[] {
230
- return [...this.#emitHistory];
231
- }
232
-
233
- /* ═════════════════════════════════════════════════════════════════
234
- * EXTENSION SYSTEM (DI / Plugins)
235
- * ═════════════════════════════════════════════════════════════════ */
236
-
237
- /**
238
- * Inject a Service/Plugin into this Engine.
239
- * @param plugin A contract object { name, install, uninstall? }
240
- */
241
- addPlugin(plugin: JuxServiceContract<this>): this {
242
- if (this.#plugins.has(plugin.name)) {
243
- console.warn(`Jux: Plugin "${plugin.name}" already registered on ${this.state.id}.`);
244
- return this;
245
- }
246
-
247
- if (this.#debugMode) {
248
- console.log(`%c 🔌 PLUGIN %c Installed: ${plugin.name} (v${plugin.version || '?'})`, 'color: #cc00ff; font-weight: bold;', 'color: #333');
249
- }
250
-
251
- this.#plugins.set(plugin.name, plugin);
252
-
253
- try {
254
- plugin.install(this);
255
- this.emit('plugin:added', { name: plugin.name });
256
- } catch (err) {
257
- console.error(`Jux: Failed to install plugin "${plugin.name}"`, err);
258
- // Rollback registration on failure
259
- this.#plugins.delete(plugin.name);
260
- }
261
-
262
- return this;
263
- }
264
-
265
- /**
266
- * Remove a plugin and run its teardown logic.
267
- */
268
- removePlugin(name: string): this {
269
- const plugin = this.#plugins.get(name);
270
- if (plugin) {
271
- if (plugin.uninstall) {
272
- try {
273
- plugin.uninstall(this);
274
- } catch (err) {
275
- console.error(`Jux: Error uninstalling plugin "${name}"`, err);
276
- }
277
- }
278
- this.#plugins.delete(name);
279
- this.emit('plugin:removed', { name });
280
- }
281
- return this;
282
- }
283
-
284
- /* ═════════════════════════════════════════════════════════════════
285
- * COMMON FLUENT API (Universal Capabilities)
286
- * ═════════════════════════════════════════════════════════════════ */
287
-
288
- addClass(className: string): this {
289
- const current = this.state.classes;
290
- if (!current.includes(className)) {
291
- this.updateState({ classes: [...current, className] } as Partial<TState>);
292
- this.emit('classAdd', { className });
293
- }
294
- return this;
295
- }
296
-
297
- removeClass(className: string): this {
298
- const current = this.state.classes;
299
- this.updateState({
300
- classes: current.filter(c => c !== className)
301
- } as Partial<TState>);
302
- this.emit('classRemove', { className });
303
- return this;
304
- }
305
-
306
- visible(isVisible: boolean): this {
307
- this.updateState({ visible: isVisible } as Partial<TState>);
308
- this.emit('visible', { visible: isVisible });
309
- return this;
310
- }
311
-
312
- disable(isDisabled: boolean = true): this {
313
- this.updateState({ disabled: isDisabled } as Partial<TState>);
314
- this.emit('disabled', { disabled: isDisabled });
315
- return this;
316
- }
317
-
318
- loading(isLoading: boolean = true): this {
319
- this.updateState({ loading: isLoading } as Partial<TState>);
320
- this.emit('loading', { loading: isLoading });
321
- return this;
322
- }
323
-
324
- attr(key: string, value: string): this {
325
- const current = this.state.attributes;
326
- this.updateState({
327
- attributes: { ...current, [key]: value }
328
- } as Partial<TState>);
329
- this.emit('attribute', { key, value });
330
- return this;
331
- }
332
- }
@@ -1,124 +0,0 @@
1
- import { BaseEngine, BaseState } from './BaseEngine.js';
2
-
3
- /**
4
- * THE SKIN AGREEMENT
5
- *
6
- * A Skin must:
7
- * 1. Hold a reference to its Engine.
8
- * 2. Implement `structureCss` to provide layout styles.
9
- * 3. Implement hooks for creation (`createRoot`) and event binding (`bindEvents`).
10
- * 4. Implement `updateSkin(state)` to react to State changes.
11
- */
12
- export abstract class BaseSkin<TState extends BaseState, TEngine extends BaseEngine<TState>> {
13
- protected engine: TEngine;
14
- protected root: HTMLElement | null = null;
15
-
16
- constructor(engine: TEngine) {
17
- this.engine = engine;
18
- }
19
-
20
- /**
21
- * CONTRACT: Provide the URL/Path to the structural CSS file.
22
- *
23
- * 💡 WHY `import.meta.url`?
24
- * Standard `import` statements load JS modules. To load ASSETS (like CSS) at runtime,
25
- * we need their full URL. `import.meta.url` tells the browser "Start looking from THIS file".
26
- *
27
- * Usage: return new URL('./structure.css', import.meta.url).href;
28
- */
29
- protected abstract get structureCss(): string;
30
-
31
-
32
- /**
33
- * Utility: Inject or Update a CSS Link tag in the document head.
34
- * Allows for external CSS files ('references') and runtime Theme Swapping.
35
- * @param id Unique ID for the link tag
36
- * @param href The URL to the CSS file
37
- */
38
- public injectCSSLink(id: string, href: string): void {
39
- if (typeof document === 'undefined') return;
40
-
41
- let link = document.getElementById(id) as HTMLLinkElement;
42
- if (!link) {
43
- link = document.createElement('link');
44
- link.id = id;
45
- link.rel = 'stylesheet';
46
- document.head.appendChild(link);
47
- }
48
-
49
- if (link.href !== href) {
50
- link.href = href;
51
- }
52
- }
53
-
54
- /**
55
- * Utility: Inject CSS into the document head if not already present.
56
- * Useful for Skins to seamlessly load their required aesthetics.
57
- */
58
- /**
59
- * API: Hot-Swap the Aesthetic Skin.
60
- * Replaces the component-specific CSS styles at runtime.
61
- */
62
- public setTheme(cssContent: string): void {
63
- const id = `jux-skin-${this.engine.state.id}-theme`;
64
- const oldStyle = document.getElementById(id);
65
- if (oldStyle) oldStyle.remove();
66
- }
67
-
68
- /**
69
- * Template Method: Render
70
- * Orchestrates the standard lifecycle: Create -> Bind -> Mount -> Subscribe.
71
- */
72
- public renderSkin(targetElement: HTMLElement, mode: 'append' | 'prepend' = 'append'): void {
73
- // 0. Inject Immutable Structure (Layout logic)
74
- // We use constructor.name to deduplicate style tags across multiple instances of the same component type.
75
-
76
- // 1. Create Root (Abstract Hook)
77
- this.root = this.createRoot();
78
-
79
- // 2. Bind Events (Hook)
80
- this.bindEvents(this.root);
81
-
82
- // 3. Mount
83
- if (mode === 'prepend') {
84
- targetElement.prepend(this.root);
85
- } else {
86
- targetElement.appendChild(this.root);
87
- }
88
-
89
- // 4. Subscribe (Automatic)
90
- this.engine.subscribe((state) => this.updateSkin(state));
91
- }
92
-
93
- protected createRoot(): HTMLElement {
94
- return document.createElement('div');
95
- }
96
-
97
- protected abstract bindEvents(root: HTMLElement): void;
98
-
99
- protected abstract updateSkin(state: TState): void;
100
-
101
- /**
102
- * HELPER: Applies universal Jux behavior (visibility, ID, disabled)
103
- */
104
- protected applySkinAttributes(element: HTMLElement, state: TState): void {
105
- element.style.display = state.visible ? '' : 'none';
106
- element.dataset.id = state.id;
107
-
108
- if (state.disabled) {
109
- element.setAttribute('aria-disabled', 'true');
110
- } else {
111
- element.removeAttribute('aria-disabled');
112
- }
113
-
114
- if (state.loading) {
115
- element.setAttribute('aria-busy', 'true');
116
- } else {
117
- element.removeAttribute('aria-busy');
118
- }
119
-
120
- Object.entries(state.attributes).forEach(([k, v]) => {
121
- element.setAttribute(k, v);
122
- });
123
- }
124
- }
@@ -1,60 +0,0 @@
1
- type GlobalListener = (data: any) => void;
2
-
3
- /**
4
- * THE NEIGHBORHOOD (Global Event Mediator)
5
- *
6
- * Acts as the certal nervous system where all components post updates.
7
- * Other components can tune in to specific frequencies (channels) here.
8
- */
9
- export class JuxGlobalBus {
10
- private listeners = new Map<string, Set<GlobalListener>>();
11
-
12
- on(channel: string, callback: GlobalListener): void {
13
- if (!this.listeners.has(channel)) {
14
- this.listeners.set(channel, new Set());
15
- }
16
- this.listeners.get(channel)!.add(callback);
17
- }
18
-
19
- off(channel: string, callback: GlobalListener): void {
20
- const set = this.listeners.get(channel);
21
- if (set) {
22
- set.delete(callback);
23
- if (set.size === 0) {
24
- this.listeners.delete(channel);
25
- }
26
- }
27
- }
28
-
29
- emit(channel: string, data: any): void {
30
- const set = this.listeners.get(channel);
31
- if (set) {
32
- set.forEach(callback => {
33
- try {
34
- callback(data);
35
- } catch (e) {
36
- console.error(`Jux GlobalBus Error [${channel}]:`, e);
37
- }
38
- });
39
- }
40
-
41
- // Optional: We could have a wildcard '*' listener here for debuggers/loggers
42
- }
43
-
44
- /**
45
- * DEBUG: Read-only Snapshot of active channels and listener identities.
46
- * usage: juxV2.events.registry
47
- * Returns: { "channelName": ["functionName", "(anonymous)"] }
48
- */
49
- get registry(): Record<string, string[]> {
50
- const snapshot: Record<string, string[]> = {};
51
- this.listeners.forEach((set, channel) => {
52
- // Map the Set of functions to their names for easier debugging
53
- snapshot[channel] = Array.from(set).map(fn => fn.name || '(anonymous)');
54
- });
55
- return snapshot;
56
- }
57
- }
58
-
59
- // Singleton Instance
60
- export const GlobalBus = new JuxGlobalBus();
@@ -1,139 +0,0 @@
1
- export interface OptionDefinition {
2
- type: 'string' | 'number' | 'boolean' | 'array' | 'object' | 'function';
3
- required?: boolean;
4
- default?: any;
5
- description?: string;
6
- aliases?: string[]; // Common misspellings or alternate names
7
- }
8
-
9
- export type OptionsContractSchema = Record<string, OptionDefinition>;
10
-
11
- export interface ValidationResult {
12
- valid: boolean;
13
- warnings: string[];
14
- errors: string[];
15
- normalized: Record<string, any>;
16
- }
17
-
18
- /**
19
- * Validates user-provided options against a defined contract schema.
20
- * Emits warnings for unknown/misnamed options and errors for type mismatches.
21
- */
22
- export function validateOptions<T extends Record<string, any>>(
23
- componentName: string,
24
- userOptions: Record<string, any>,
25
- schema: OptionsContractSchema,
26
- debugMode: boolean = false
27
- ): ValidationResult {
28
- const result: ValidationResult = {
29
- valid: true,
30
- warnings: [],
31
- errors: [],
32
- normalized: {}
33
- };
34
-
35
- const validKeys = new Set(Object.keys(schema));
36
- const aliasMap = new Map<string, string>();
37
-
38
- // Build alias lookup
39
- for (const [key, def] of Object.entries(schema)) {
40
- if (def.aliases) {
41
- def.aliases.forEach(alias => aliasMap.set(alias.toLowerCase(), key));
42
- }
43
- }
44
-
45
- // Check for unknown or aliased options
46
- for (const [key, value] of Object.entries(userOptions)) {
47
- const lowerKey = key.toLowerCase();
48
-
49
- if (validKeys.has(key)) {
50
- // Valid key - validate type
51
- const def = schema[key];
52
- if (!validateType(value, def.type)) {
53
- result.errors.push(
54
- `[${componentName}] Option "${key}" expects type "${def.type}", got "${typeof value}".`
55
- );
56
- result.valid = false;
57
- } else {
58
- result.normalized[key] = value;
59
- }
60
- } else if (aliasMap.has(lowerKey)) {
61
- // Found an alias - warn and normalize
62
- const correctKey = aliasMap.get(lowerKey)!;
63
- result.warnings.push(
64
- `[${componentName}] Option "${key}" is not valid. Did you mean "${correctKey}"?`
65
- );
66
- result.normalized[correctKey] = value;
67
- } else {
68
- // Unknown option
69
- const suggestion = findClosestMatch(key, Array.from(validKeys));
70
- const hint = suggestion ? ` Did you mean "${suggestion}"?` : '';
71
- result.warnings.push(
72
- `[${componentName}] Unknown option "${key}".${hint} Valid options: ${Array.from(validKeys).join(', ')}`
73
- );
74
- }
75
- }
76
-
77
- // Apply defaults for missing required options
78
- for (const [key, def] of Object.entries(schema)) {
79
- if (!(key in result.normalized)) {
80
- if (def.required && def.default === undefined) {
81
- result.errors.push(`[${componentName}] Required option "${key}" is missing.`);
82
- result.valid = false;
83
- } else if (def.default !== undefined) {
84
- result.normalized[key] = def.default;
85
- }
86
- }
87
- }
88
-
89
- // Log warnings/errors in debug mode
90
- if (debugMode || result.warnings.length || result.errors.length) {
91
- result.warnings.forEach(w => console.warn(w));
92
- result.errors.forEach(e => console.error(e));
93
- }
94
-
95
- return result;
96
- }
97
-
98
- function validateType(value: any, expectedType: string): boolean {
99
- if (value === undefined || value === null) return true; // Allow optional
100
- switch (expectedType) {
101
- case 'string': return typeof value === 'string';
102
- case 'number': return typeof value === 'number';
103
- case 'boolean': return typeof value === 'boolean';
104
- case 'array': return Array.isArray(value);
105
- case 'object': return typeof value === 'object' && !Array.isArray(value);
106
- case 'function': return typeof value === 'function';
107
- default: return true;
108
- }
109
- }
110
-
111
- function findClosestMatch(input: string, candidates: string[]): string | null {
112
- const lower = input.toLowerCase();
113
- let best: string | null = null;
114
- let bestScore = Infinity;
115
-
116
- for (const candidate of candidates) {
117
- const score = levenshtein(lower, candidate.toLowerCase());
118
- if (score < bestScore && score <= 3) { // Max 3 edits
119
- bestScore = score;
120
- best = candidate;
121
- }
122
- }
123
- return best;
124
- }
125
-
126
- function levenshtein(a: string, b: string): number {
127
- const matrix: number[][] = [];
128
- for (let i = 0; i <= b.length; i++) matrix[i] = [i];
129
- for (let j = 0; j <= a.length; j++) matrix[0][j] = j;
130
-
131
- for (let i = 1; i <= b.length; i++) {
132
- for (let j = 1; j <= a.length; j++) {
133
- matrix[i][j] = b[i - 1] === a[j - 1]
134
- ? matrix[i - 1][j - 1]
135
- : Math.min(matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1);
136
- }
137
- }
138
- return matrix[b.length][a.length];
139
- }