juxscript 1.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/README.md +292 -0
- package/bin/cli.js +149 -0
- package/lib/adapters/base-adapter.js +35 -0
- package/lib/adapters/index.js +33 -0
- package/lib/adapters/mysql-adapter.js +65 -0
- package/lib/adapters/postgres-adapter.js +70 -0
- package/lib/adapters/sqlite-adapter.js +56 -0
- package/lib/components/app.ts +124 -0
- package/lib/components/button.ts +136 -0
- package/lib/components/card.ts +205 -0
- package/lib/components/chart.ts +125 -0
- package/lib/components/code.ts +242 -0
- package/lib/components/container.ts +282 -0
- package/lib/components/data.ts +105 -0
- package/lib/components/docs-data.json +1211 -0
- package/lib/components/error-handler.ts +285 -0
- package/lib/components/footer.ts +146 -0
- package/lib/components/header.ts +167 -0
- package/lib/components/hero.ts +170 -0
- package/lib/components/import.ts +430 -0
- package/lib/components/input.ts +175 -0
- package/lib/components/layout.ts +113 -0
- package/lib/components/list.ts +392 -0
- package/lib/components/main.ts +111 -0
- package/lib/components/menu.ts +170 -0
- package/lib/components/modal.ts +216 -0
- package/lib/components/nav.ts +136 -0
- package/lib/components/node.ts +200 -0
- package/lib/components/reactivity.js +104 -0
- package/lib/components/script.ts +152 -0
- package/lib/components/sidebar.ts +168 -0
- package/lib/components/style.ts +129 -0
- package/lib/components/table.ts +279 -0
- package/lib/components/tabs.ts +191 -0
- package/lib/components/theme.ts +97 -0
- package/lib/components/view.ts +174 -0
- package/lib/jux.ts +203 -0
- package/lib/layouts/default.css +260 -0
- package/lib/layouts/default.jux +8 -0
- package/lib/layouts/figma.css +334 -0
- package/lib/layouts/figma.jux +0 -0
- package/lib/layouts/notion.css +258 -0
- package/lib/styles/base-theme.css +186 -0
- package/lib/styles/dark-theme.css +144 -0
- package/lib/styles/global.css +1131 -0
- package/lib/styles/light-theme.css +144 -0
- package/lib/styles/tokens/dark.css +86 -0
- package/lib/styles/tokens/light.css +86 -0
- package/lib/themes/dark.css +86 -0
- package/lib/themes/light.css +86 -0
- package/lib/utils/path-resolver.js +23 -0
- package/machinery/compiler.js +262 -0
- package/machinery/doc-generator.js +160 -0
- package/machinery/generators/css.js +128 -0
- package/machinery/generators/html.js +108 -0
- package/machinery/imports.js +155 -0
- package/machinery/server.js +185 -0
- package/machinery/validators/file-validator.js +123 -0
- package/machinery/watcher.js +148 -0
- package/package.json +58 -0
- package/types/globals.d.ts +16 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { ErrorHandler } from './error-handler.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* App - Configure application-level settings
|
|
5
|
+
* Manages document metadata, theme, and global configuration
|
|
6
|
+
*/
|
|
7
|
+
export class App {
|
|
8
|
+
private _title: string = '';
|
|
9
|
+
private _theme: 'light' | 'dark' | 'auto' = 'auto';
|
|
10
|
+
private _favicon: string = '';
|
|
11
|
+
private _meta: Map<string, string> = new Map();
|
|
12
|
+
|
|
13
|
+
title(title: string): this {
|
|
14
|
+
this._title = title;
|
|
15
|
+
this.applyTitle();
|
|
16
|
+
return this;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
theme(theme: 'light' | 'dark' | 'auto'): this {
|
|
20
|
+
this._theme = theme;
|
|
21
|
+
this.applyTheme();
|
|
22
|
+
return this;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
favicon(url: string): this {
|
|
26
|
+
this._favicon = url;
|
|
27
|
+
this.applyFavicon();
|
|
28
|
+
return this;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
meta(name: string, content: string): this {
|
|
32
|
+
this._meta.set(name, content);
|
|
33
|
+
this.applyMeta(name, content);
|
|
34
|
+
return this;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private applyTitle(): void {
|
|
38
|
+
if (typeof document === 'undefined') return;
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
document.title = this._title;
|
|
42
|
+
} catch (error: any) {
|
|
43
|
+
ErrorHandler.captureError({
|
|
44
|
+
component: 'App',
|
|
45
|
+
method: 'applyTitle',
|
|
46
|
+
message: error.message,
|
|
47
|
+
stack: error.stack,
|
|
48
|
+
timestamp: new Date(),
|
|
49
|
+
context: { title: this._title }
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private applyTheme(): void {
|
|
55
|
+
if (typeof document === 'undefined') return;
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
document.body.setAttribute('data-theme', this._theme);
|
|
59
|
+
|
|
60
|
+
// Add theme class for styling
|
|
61
|
+
document.body.classList.remove('theme-light', 'theme-dark', 'theme-auto');
|
|
62
|
+
document.body.classList.add(`theme-${this._theme}`);
|
|
63
|
+
} catch (error: any) {
|
|
64
|
+
ErrorHandler.captureError({
|
|
65
|
+
component: 'App',
|
|
66
|
+
method: 'applyTheme',
|
|
67
|
+
message: error.message,
|
|
68
|
+
stack: error.stack,
|
|
69
|
+
timestamp: new Date(),
|
|
70
|
+
context: { theme: this._theme }
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private applyFavicon(): void {
|
|
76
|
+
if (typeof document === 'undefined') return;
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
let link = document.querySelector<HTMLLinkElement>('link[rel="icon"]');
|
|
80
|
+
|
|
81
|
+
if (!link) {
|
|
82
|
+
link = document.createElement('link');
|
|
83
|
+
link.rel = 'icon';
|
|
84
|
+
document.head.appendChild(link);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
link.href = this._favicon;
|
|
88
|
+
} catch (error: any) {
|
|
89
|
+
ErrorHandler.captureError({
|
|
90
|
+
component: 'App',
|
|
91
|
+
method: 'applyFavicon',
|
|
92
|
+
message: error.message,
|
|
93
|
+
stack: error.stack,
|
|
94
|
+
timestamp: new Date(),
|
|
95
|
+
context: { favicon: this._favicon }
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private applyMeta(name: string, content: string): void {
|
|
101
|
+
if (typeof document === 'undefined') return;
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
let meta = document.querySelector<HTMLMetaElement>(`meta[name="${name}"]`);
|
|
105
|
+
|
|
106
|
+
if (!meta) {
|
|
107
|
+
meta = document.createElement('meta');
|
|
108
|
+
meta.name = name;
|
|
109
|
+
document.head.appendChild(meta);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
meta.content = content;
|
|
113
|
+
} catch (error: any) {
|
|
114
|
+
ErrorHandler.captureError({
|
|
115
|
+
component: 'App',
|
|
116
|
+
method: 'applyMeta',
|
|
117
|
+
message: error.message,
|
|
118
|
+
stack: error.stack,
|
|
119
|
+
timestamp: new Date(),
|
|
120
|
+
context: { name, content }
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { Reactive, getOrCreateContainer } from './reactivity.js';
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Button component options
|
|
6
|
+
*/
|
|
7
|
+
export interface ButtonOptions {
|
|
8
|
+
label?: string;
|
|
9
|
+
variant?: 'primary' | 'secondary' | 'danger' | 'success' | string;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
onClick?: (e: Event) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Button component state
|
|
16
|
+
*/
|
|
17
|
+
type ButtonState = {
|
|
18
|
+
label: string;
|
|
19
|
+
variant: string;
|
|
20
|
+
disabled: boolean;
|
|
21
|
+
onClick: ((e: Event) => void) | null;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Button component
|
|
26
|
+
*
|
|
27
|
+
* Usage:
|
|
28
|
+
* const btn = jux.button('myButton', { label: 'Click Me', onClick: () => console.log('clicked') });
|
|
29
|
+
* btn.render();
|
|
30
|
+
*/
|
|
31
|
+
export class Button extends Reactive {
|
|
32
|
+
state!: ButtonState;
|
|
33
|
+
container: HTMLElement | null = null;
|
|
34
|
+
|
|
35
|
+
constructor(componentId: string, options: ButtonOptions = {}) {
|
|
36
|
+
super();
|
|
37
|
+
this._setComponentId(componentId);
|
|
38
|
+
|
|
39
|
+
this.state = this._createReactiveState({
|
|
40
|
+
label: options.label ?? 'Button',
|
|
41
|
+
variant: options.variant ?? 'primary',
|
|
42
|
+
disabled: options.disabled ?? false,
|
|
43
|
+
onClick: options.onClick ?? null
|
|
44
|
+
}) as ButtonState;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* -------------------------
|
|
48
|
+
* Fluent API
|
|
49
|
+
* ------------------------- */
|
|
50
|
+
|
|
51
|
+
label(value: string): this {
|
|
52
|
+
this.state.label = value;
|
|
53
|
+
return this;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
variant(value: string): this {
|
|
57
|
+
this.state.variant = value;
|
|
58
|
+
return this;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
disabled(value: boolean): this {
|
|
62
|
+
this.state.disabled = value;
|
|
63
|
+
return this;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
onClick(callback: (e: Event) => void): this {
|
|
67
|
+
this.state.onClick = callback;
|
|
68
|
+
return this;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/* -------------------------
|
|
72
|
+
* Render
|
|
73
|
+
* ------------------------- */
|
|
74
|
+
|
|
75
|
+
render(targetId?: string): this {
|
|
76
|
+
let container: HTMLElement;
|
|
77
|
+
|
|
78
|
+
if (targetId) {
|
|
79
|
+
// Use provided targetId - must exist
|
|
80
|
+
const target = document.querySelector(targetId);
|
|
81
|
+
if (!target || !(target instanceof HTMLElement)) {
|
|
82
|
+
throw new Error(`Button: Target element "${targetId}" not found`);
|
|
83
|
+
}
|
|
84
|
+
container = target;
|
|
85
|
+
} else {
|
|
86
|
+
// Create or get container with component ID
|
|
87
|
+
container = getOrCreateContainer(this._componentId) as HTMLElement;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
this.container = container;
|
|
91
|
+
const { label, variant, disabled, onClick } = this.state;
|
|
92
|
+
|
|
93
|
+
// Create button element
|
|
94
|
+
const button = document.createElement('button');
|
|
95
|
+
button.className = `jux-button jux-button-${variant}`;
|
|
96
|
+
button.id = this._componentId;
|
|
97
|
+
button.textContent = label;
|
|
98
|
+
button.disabled = disabled;
|
|
99
|
+
|
|
100
|
+
// Attach click handler if provided
|
|
101
|
+
if (onClick) {
|
|
102
|
+
button.addEventListener('click', onClick);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
container.appendChild(button);
|
|
106
|
+
return this;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Render to another Jux component's container
|
|
111
|
+
*
|
|
112
|
+
* Usage:
|
|
113
|
+
* const container = jux.node('myContainer');
|
|
114
|
+
* const btn = jux.button('myBtn').renderTo(container);
|
|
115
|
+
*/
|
|
116
|
+
renderTo(juxComponent: any): this {
|
|
117
|
+
// Verify it's a Jux component (has _componentId from Reactive base)
|
|
118
|
+
if (!juxComponent || typeof juxComponent !== 'object') {
|
|
119
|
+
throw new Error('Button.renderTo: Invalid component - not an object');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (!juxComponent._componentId || typeof juxComponent._componentId !== 'string') {
|
|
123
|
+
throw new Error('Button.renderTo: Invalid component - missing _componentId (not a Jux component)');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Render to the component's ID as a selector
|
|
127
|
+
return this.render(`#${juxComponent._componentId}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Factory helper
|
|
133
|
+
*/
|
|
134
|
+
export function button(componentId: string, options: ButtonOptions = {}): Button {
|
|
135
|
+
return new Button(componentId, options);
|
|
136
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { Reactive, getOrCreateContainer } from './reactivity.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Action configuration for card actions
|
|
5
|
+
*/
|
|
6
|
+
export interface CardAction {
|
|
7
|
+
label: string;
|
|
8
|
+
onClick: () => void;
|
|
9
|
+
variant?: 'primary' | 'secondary' | 'danger';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Card component options
|
|
14
|
+
*/
|
|
15
|
+
export interface CardOptions {
|
|
16
|
+
title?: string;
|
|
17
|
+
subtitle?: string;
|
|
18
|
+
content?: string;
|
|
19
|
+
image?: string;
|
|
20
|
+
actions?: CardAction[];
|
|
21
|
+
variant?: 'default' | 'elevated' | 'outlined';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Card component state
|
|
26
|
+
*/
|
|
27
|
+
type CardState = {
|
|
28
|
+
title: string;
|
|
29
|
+
subtitle: string;
|
|
30
|
+
content: string;
|
|
31
|
+
image: string;
|
|
32
|
+
actions: CardAction[];
|
|
33
|
+
variant: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Card component
|
|
38
|
+
*
|
|
39
|
+
* Usage:
|
|
40
|
+
* const card = jux.card('myCard', {
|
|
41
|
+
* title: 'Card Title',
|
|
42
|
+
* content: 'Card content here'
|
|
43
|
+
* });
|
|
44
|
+
* card.render();
|
|
45
|
+
*/
|
|
46
|
+
export class Card extends Reactive {
|
|
47
|
+
state!: CardState;
|
|
48
|
+
container: HTMLElement | null = null;
|
|
49
|
+
|
|
50
|
+
constructor(componentId: string, options: CardOptions = {}) {
|
|
51
|
+
super();
|
|
52
|
+
this._setComponentId(componentId);
|
|
53
|
+
|
|
54
|
+
this.state = this._createReactiveState({
|
|
55
|
+
title: options.title ?? '',
|
|
56
|
+
subtitle: options.subtitle ?? '',
|
|
57
|
+
content: options.content ?? '',
|
|
58
|
+
image: options.image ?? '',
|
|
59
|
+
actions: options.actions ?? [],
|
|
60
|
+
variant: options.variant ?? 'default'
|
|
61
|
+
}) as CardState;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* -------------------------
|
|
65
|
+
* Fluent API
|
|
66
|
+
* ------------------------- */
|
|
67
|
+
|
|
68
|
+
title(value: string): this {
|
|
69
|
+
this.state.title = value;
|
|
70
|
+
return this;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
subtitle(value: string): this {
|
|
74
|
+
this.state.subtitle = value;
|
|
75
|
+
return this;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
content(value: string): this {
|
|
79
|
+
this.state.content = value;
|
|
80
|
+
return this;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
image(value: string): this {
|
|
84
|
+
this.state.image = value;
|
|
85
|
+
return this;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
actions(value: CardAction[]): this {
|
|
89
|
+
this.state.actions = value;
|
|
90
|
+
return this;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
variant(value: string): this {
|
|
94
|
+
this.state.variant = value;
|
|
95
|
+
return this;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/* -------------------------
|
|
99
|
+
* Render
|
|
100
|
+
* ------------------------- */
|
|
101
|
+
|
|
102
|
+
render(targetId?: string): this {
|
|
103
|
+
let container: HTMLElement;
|
|
104
|
+
|
|
105
|
+
if (targetId) {
|
|
106
|
+
const target = document.querySelector(targetId);
|
|
107
|
+
if (!target || !(target instanceof HTMLElement)) {
|
|
108
|
+
throw new Error(`Card: Target element "${targetId}" not found`);
|
|
109
|
+
}
|
|
110
|
+
container = target;
|
|
111
|
+
} else {
|
|
112
|
+
container = getOrCreateContainer(this._componentId) as HTMLElement;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
this.container = container;
|
|
116
|
+
const { title, subtitle, content, image, actions, variant } = this.state;
|
|
117
|
+
|
|
118
|
+
// Create card element
|
|
119
|
+
const card = document.createElement('div');
|
|
120
|
+
card.className = `jux-card jux-card-${variant}`;
|
|
121
|
+
card.id = this._componentId;
|
|
122
|
+
|
|
123
|
+
// Image
|
|
124
|
+
if (image) {
|
|
125
|
+
const img = document.createElement('img');
|
|
126
|
+
img.className = 'jux-card-image';
|
|
127
|
+
img.src = image;
|
|
128
|
+
img.alt = title || 'Card image';
|
|
129
|
+
card.appendChild(img);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Content wrapper
|
|
133
|
+
const cardBody = document.createElement('div');
|
|
134
|
+
cardBody.className = 'jux-card-body';
|
|
135
|
+
|
|
136
|
+
// Title
|
|
137
|
+
if (title) {
|
|
138
|
+
const titleEl = document.createElement('h3');
|
|
139
|
+
titleEl.className = 'jux-card-title';
|
|
140
|
+
titleEl.textContent = title;
|
|
141
|
+
cardBody.appendChild(titleEl);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Subtitle
|
|
145
|
+
if (subtitle) {
|
|
146
|
+
const subtitleEl = document.createElement('p');
|
|
147
|
+
subtitleEl.className = 'jux-card-subtitle';
|
|
148
|
+
subtitleEl.textContent = subtitle;
|
|
149
|
+
cardBody.appendChild(subtitleEl);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Content
|
|
153
|
+
if (content) {
|
|
154
|
+
const contentEl = document.createElement('p');
|
|
155
|
+
contentEl.className = 'jux-card-content';
|
|
156
|
+
contentEl.textContent = content;
|
|
157
|
+
cardBody.appendChild(contentEl);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
card.appendChild(cardBody);
|
|
161
|
+
|
|
162
|
+
// Actions
|
|
163
|
+
if (actions.length > 0) {
|
|
164
|
+
const actionsEl = document.createElement('div');
|
|
165
|
+
actionsEl.className = 'jux-card-actions';
|
|
166
|
+
|
|
167
|
+
actions.forEach(action => {
|
|
168
|
+
const btn = document.createElement('button');
|
|
169
|
+
btn.className = `jux-button jux-button-${action.variant || 'primary'}`;
|
|
170
|
+
btn.textContent = action.label;
|
|
171
|
+
actionsEl.appendChild(btn);
|
|
172
|
+
|
|
173
|
+
// Event binding
|
|
174
|
+
btn.addEventListener('click', action.onClick);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
card.appendChild(actionsEl);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
container.appendChild(card);
|
|
181
|
+
return this;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Render to another Jux component's container
|
|
186
|
+
*/
|
|
187
|
+
renderTo(juxComponent: any): this {
|
|
188
|
+
if (!juxComponent || typeof juxComponent !== 'object') {
|
|
189
|
+
throw new Error('Card.renderTo: Invalid component - not an object');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (!juxComponent._componentId || typeof juxComponent._componentId !== 'string') {
|
|
193
|
+
throw new Error('Card.renderTo: Invalid component - missing _componentId (not a Jux component)');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return this.render(`#${juxComponent._componentId}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Factory helper
|
|
202
|
+
*/
|
|
203
|
+
export function card(componentId: string, options: CardOptions = {}): Card {
|
|
204
|
+
return new Card(componentId, options);
|
|
205
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { Reactive, getOrCreateContainer } from './reactivity.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Chart component options
|
|
5
|
+
*/
|
|
6
|
+
export interface ChartOptions {
|
|
7
|
+
type?: 'bar' | 'line' | 'pie' | 'doughnut';
|
|
8
|
+
data?: any;
|
|
9
|
+
options?: any;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Chart component state
|
|
14
|
+
*/
|
|
15
|
+
type ChartState = {
|
|
16
|
+
type: string;
|
|
17
|
+
data: any;
|
|
18
|
+
options: any;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Chart component - placeholder for chart integration
|
|
23
|
+
*
|
|
24
|
+
* Usage:
|
|
25
|
+
* const chart = jux.chart('myChart', {
|
|
26
|
+
* type: 'bar',
|
|
27
|
+
* data: {
|
|
28
|
+
* labels: ['A', 'B', 'C'],
|
|
29
|
+
* datasets: [{ data: [10, 20, 30] }]
|
|
30
|
+
* }
|
|
31
|
+
* });
|
|
32
|
+
* chart.render();
|
|
33
|
+
*/
|
|
34
|
+
export class Chart extends Reactive {
|
|
35
|
+
state!: ChartState;
|
|
36
|
+
container: HTMLElement | null = null;
|
|
37
|
+
|
|
38
|
+
constructor(componentId: string, options: ChartOptions = {}) {
|
|
39
|
+
super();
|
|
40
|
+
this._setComponentId(componentId);
|
|
41
|
+
|
|
42
|
+
this.state = this._createReactiveState({
|
|
43
|
+
type: options.type ?? 'bar',
|
|
44
|
+
data: options.data ?? {},
|
|
45
|
+
options: options.options ?? {}
|
|
46
|
+
}) as ChartState;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* -------------------------
|
|
50
|
+
* Fluent API
|
|
51
|
+
* ------------------------- */
|
|
52
|
+
|
|
53
|
+
type(value: string): this {
|
|
54
|
+
this.state.type = value;
|
|
55
|
+
return this;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
data(value: any): this {
|
|
59
|
+
this.state.data = value;
|
|
60
|
+
return this;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
options(value: any): this {
|
|
64
|
+
this.state.options = value;
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/* -------------------------
|
|
69
|
+
* Render
|
|
70
|
+
* ------------------------- */
|
|
71
|
+
|
|
72
|
+
render(targetId?: string): this {
|
|
73
|
+
let container: HTMLElement;
|
|
74
|
+
|
|
75
|
+
if (targetId) {
|
|
76
|
+
const target = document.querySelector(targetId);
|
|
77
|
+
if (!target || !(target instanceof HTMLElement)) {
|
|
78
|
+
throw new Error(`Chart: Target element "${targetId}" not found`);
|
|
79
|
+
}
|
|
80
|
+
container = target;
|
|
81
|
+
} else {
|
|
82
|
+
container = getOrCreateContainer(this._componentId) as HTMLElement;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
this.container = container;
|
|
86
|
+
const { type, data } = this.state;
|
|
87
|
+
|
|
88
|
+
const canvas = document.createElement('canvas');
|
|
89
|
+
canvas.id = this._componentId;
|
|
90
|
+
canvas.className = 'jux-chart';
|
|
91
|
+
|
|
92
|
+
// Placeholder rendering (would integrate with Chart.js or similar)
|
|
93
|
+
const placeholder = document.createElement('div');
|
|
94
|
+
placeholder.className = 'jux-chart-placeholder';
|
|
95
|
+
placeholder.textContent = `Chart (${type}) - Integration pending`;
|
|
96
|
+
placeholder.style.cssText = 'padding: 2rem; border: 2px dashed var(--border-color); text-align: center;';
|
|
97
|
+
|
|
98
|
+
container.appendChild(placeholder);
|
|
99
|
+
container.appendChild(canvas);
|
|
100
|
+
|
|
101
|
+
return this;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Render to another Jux component's container
|
|
106
|
+
*/
|
|
107
|
+
renderTo(juxComponent: any): this {
|
|
108
|
+
if (!juxComponent || typeof juxComponent !== 'object') {
|
|
109
|
+
throw new Error('Chart.renderTo: Invalid component - not an object');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!juxComponent._componentId || typeof juxComponent._componentId !== 'string') {
|
|
113
|
+
throw new Error('Chart.renderTo: Invalid component - missing _componentId (not a Jux component)');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return this.render(`#${juxComponent._componentId}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Factory helper
|
|
122
|
+
*/
|
|
123
|
+
export function chart(componentId: string, options: ChartOptions = {}): Chart {
|
|
124
|
+
return new Chart(componentId, options);
|
|
125
|
+
}
|