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,200 @@
|
|
|
1
|
+
import { Reactive, getOrCreateContainer } from './reactivity.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Node component options
|
|
5
|
+
*/
|
|
6
|
+
export interface NodeOptions {
|
|
7
|
+
tagType?: string;
|
|
8
|
+
className?: string;
|
|
9
|
+
textContent?: string;
|
|
10
|
+
innerHTML?: string;
|
|
11
|
+
attributes?: Record<string, string>;
|
|
12
|
+
styles?: Record<string, string>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Node component state
|
|
17
|
+
*/
|
|
18
|
+
type NodeState = {
|
|
19
|
+
tagType: string;
|
|
20
|
+
className: string;
|
|
21
|
+
textContent: string;
|
|
22
|
+
innerHTML: string;
|
|
23
|
+
attributes: Record<string, string>;
|
|
24
|
+
styles: Record<string, string>;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Node component - Create arbitrary HTML elements
|
|
29
|
+
*
|
|
30
|
+
* Usage:
|
|
31
|
+
* const div = jux.node('myDiv', { tagType: 'div', className: 'container' });
|
|
32
|
+
* div.render('#target');
|
|
33
|
+
*
|
|
34
|
+
* const span = jux.node('mySpan')
|
|
35
|
+
* .tagType('span')
|
|
36
|
+
* .className('highlight')
|
|
37
|
+
* .textContent('Hello World')
|
|
38
|
+
* .render();
|
|
39
|
+
*/
|
|
40
|
+
export class Node extends Reactive {
|
|
41
|
+
state!: NodeState;
|
|
42
|
+
container: HTMLElement | null = null;
|
|
43
|
+
|
|
44
|
+
constructor(componentId: string, options: NodeOptions = {}) {
|
|
45
|
+
super();
|
|
46
|
+
this._setComponentId(componentId);
|
|
47
|
+
|
|
48
|
+
this.state = this._createReactiveState({
|
|
49
|
+
tagType: options.tagType ?? 'div',
|
|
50
|
+
className: options.className ?? '',
|
|
51
|
+
textContent: options.textContent ?? '',
|
|
52
|
+
innerHTML: options.innerHTML ?? '',
|
|
53
|
+
attributes: options.attributes ?? {},
|
|
54
|
+
styles: options.styles ?? {}
|
|
55
|
+
}) as NodeState;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* -------------------------
|
|
59
|
+
* Fluent API
|
|
60
|
+
* ------------------------- */
|
|
61
|
+
|
|
62
|
+
tagType(value: string): this {
|
|
63
|
+
this.state.tagType = value;
|
|
64
|
+
return this;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
className(value: string): this {
|
|
68
|
+
this.state.className = value;
|
|
69
|
+
return this;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
addClass(value: string): this {
|
|
73
|
+
const classes = this.state.className.split(' ').filter(c => c);
|
|
74
|
+
if (!classes.includes(value)) {
|
|
75
|
+
classes.push(value);
|
|
76
|
+
this.state.className = classes.join(' ');
|
|
77
|
+
}
|
|
78
|
+
return this;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
removeClass(value: string): this {
|
|
82
|
+
const classes = this.state.className.split(' ').filter(c => c && c !== value);
|
|
83
|
+
this.state.className = classes.join(' ');
|
|
84
|
+
return this;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
textContent(value: string): this {
|
|
88
|
+
this.state.textContent = value;
|
|
89
|
+
this.state.innerHTML = ''; // Clear innerHTML when setting textContent
|
|
90
|
+
return this;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
innerHTML(value: string): this {
|
|
94
|
+
this.state.innerHTML = value;
|
|
95
|
+
this.state.textContent = ''; // Clear textContent when setting innerHTML
|
|
96
|
+
return this;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
attr(name: string, value: string): this {
|
|
100
|
+
this.state.attributes[name] = value;
|
|
101
|
+
return this;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
attrs(attributes: Record<string, string>): this {
|
|
105
|
+
this.state.attributes = { ...this.state.attributes, ...attributes };
|
|
106
|
+
return this;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
style(property: string, value: string): this {
|
|
110
|
+
this.state.styles[property] = value;
|
|
111
|
+
return this;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
styles(styles: Record<string, string>): this {
|
|
115
|
+
this.state.styles = { ...this.state.styles, ...styles };
|
|
116
|
+
return this;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* -------------------------
|
|
120
|
+
* Render
|
|
121
|
+
* ------------------------- */
|
|
122
|
+
|
|
123
|
+
render(targetId?: string): this {
|
|
124
|
+
let container: HTMLElement;
|
|
125
|
+
|
|
126
|
+
if (targetId) {
|
|
127
|
+
// Use provided targetId - must exist
|
|
128
|
+
const target = document.querySelector(targetId);
|
|
129
|
+
if (!target || !(target instanceof HTMLElement)) {
|
|
130
|
+
throw new Error(`Node: Target element "${targetId}" not found`);
|
|
131
|
+
}
|
|
132
|
+
container = target;
|
|
133
|
+
} else {
|
|
134
|
+
// Create or get container with component ID
|
|
135
|
+
container = getOrCreateContainer(this._componentId) as HTMLElement;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
this.container = container;
|
|
139
|
+
const { tagType, className, textContent, innerHTML, attributes, styles } = this.state;
|
|
140
|
+
|
|
141
|
+
// Create the element
|
|
142
|
+
const element = document.createElement(tagType);
|
|
143
|
+
|
|
144
|
+
// Set ID to component ID
|
|
145
|
+
element.id = this._componentId;
|
|
146
|
+
|
|
147
|
+
// Set className
|
|
148
|
+
if (className) {
|
|
149
|
+
element.className = className;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Set content (innerHTML takes precedence over textContent)
|
|
153
|
+
if (innerHTML) {
|
|
154
|
+
element.innerHTML = innerHTML;
|
|
155
|
+
} else if (textContent) {
|
|
156
|
+
element.textContent = textContent;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Set attributes
|
|
160
|
+
Object.entries(attributes).forEach(([name, value]) => {
|
|
161
|
+
element.setAttribute(name, value);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Set styles
|
|
165
|
+
Object.entries(styles).forEach(([property, value]) => {
|
|
166
|
+
element.style.setProperty(property, value);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
container.appendChild(element);
|
|
170
|
+
return this;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Render to another Jux component's container
|
|
175
|
+
*
|
|
176
|
+
* Usage:
|
|
177
|
+
* const container = jux.node('myContainer');
|
|
178
|
+
* const child = jux.node('myChild').renderTo(container);
|
|
179
|
+
*/
|
|
180
|
+
renderTo(juxComponent: any): this {
|
|
181
|
+
// Verify it's a Jux component (has _componentId from Reactive base)
|
|
182
|
+
if (!juxComponent || typeof juxComponent !== 'object') {
|
|
183
|
+
throw new Error('Node.renderTo: Invalid component - not an object');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (!juxComponent._componentId || typeof juxComponent._componentId !== 'string') {
|
|
187
|
+
throw new Error('Node.renderTo: Invalid component - missing _componentId (not a Jux component)');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Render to the component's ID as a selector
|
|
191
|
+
return this.render(`#${juxComponent._componentId}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Factory helper
|
|
197
|
+
*/
|
|
198
|
+
export function node(componentId: string, options: NodeOptions = {}): Node {
|
|
199
|
+
return new Node(componentId, options);
|
|
200
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ultra-light reactivity system for Jux components
|
|
3
|
+
* Provides event-based state management with emit/on pattern
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
function getOrCreateContainer(componentId) {
|
|
7
|
+
if (typeof document === 'undefined') return null;
|
|
8
|
+
|
|
9
|
+
let container = document.getElementById(componentId);
|
|
10
|
+
|
|
11
|
+
// Container already exists, return it
|
|
12
|
+
if (container) {
|
|
13
|
+
return container;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Auto-create container if it doesn't exist
|
|
17
|
+
container = document.createElement('div');
|
|
18
|
+
container.id = componentId;
|
|
19
|
+
|
|
20
|
+
// Find appropriate parent
|
|
21
|
+
let parent;
|
|
22
|
+
// Page components go inside #appmain (or [data-jux-page] if no layout)
|
|
23
|
+
const appmain = document.getElementById('appmain');
|
|
24
|
+
const dataJuxPage = document.querySelector('[data-jux-page]');
|
|
25
|
+
parent = appmain || dataJuxPage || document.body;
|
|
26
|
+
parent.appendChild(container);
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
return container;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
class Reactive {
|
|
33
|
+
constructor() {
|
|
34
|
+
this._listeners = new Map();
|
|
35
|
+
this._state = {};
|
|
36
|
+
this._componentId = null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Set the component ID and auto-wire the container getter
|
|
41
|
+
* Call this in component constructors: instance._setComponentId(componentId)
|
|
42
|
+
*/
|
|
43
|
+
_setComponentId(componentId) {
|
|
44
|
+
this._componentId = componentId;
|
|
45
|
+
this.id = componentId;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
_createReactiveState(initialState = {}) {
|
|
49
|
+
const self = this;
|
|
50
|
+
return new Proxy(initialState, {
|
|
51
|
+
set(target, property, value) {
|
|
52
|
+
const oldValue = target[property];
|
|
53
|
+
target[property] = value;
|
|
54
|
+
|
|
55
|
+
if (oldValue !== value) {
|
|
56
|
+
self.emit('stateChange', { property, value, oldValue });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
on(event, handler) {
|
|
65
|
+
if (!this._listeners.has(event)) {
|
|
66
|
+
this._listeners.set(event, []);
|
|
67
|
+
}
|
|
68
|
+
this._listeners.get(event).push(handler);
|
|
69
|
+
|
|
70
|
+
return this;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
emit(event, data) {
|
|
74
|
+
if (this._listeners.has(event)) {
|
|
75
|
+
this._listeners.get(event).forEach((handler, index) => {
|
|
76
|
+
try {
|
|
77
|
+
handler(data);
|
|
78
|
+
} catch (err) {
|
|
79
|
+
console.error(`[Reactive] Error in handler for '${event}':`, err);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return this;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
off(event, handler) {
|
|
88
|
+
if (this._listeners.has(event)) {
|
|
89
|
+
const callbacks = this._listeners.get(event);
|
|
90
|
+
const index = callbacks.indexOf(handler);
|
|
91
|
+
if (index > -1) {
|
|
92
|
+
callbacks.splice(index, 1);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return this;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
hasListeners(event) {
|
|
100
|
+
return this._listeners.has(event) && this._listeners.get(event).length > 0;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export { Reactive, getOrCreateContainer };
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { ErrorHandler } from './error-handler.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Script - Inject JavaScript into the document
|
|
5
|
+
* Auto-renders when created or modified
|
|
6
|
+
*/
|
|
7
|
+
export class Script {
|
|
8
|
+
private _content: string = '';
|
|
9
|
+
private _src: string = '';
|
|
10
|
+
private _isSrc: boolean = false;
|
|
11
|
+
private _async: boolean = false;
|
|
12
|
+
private _defer: boolean = false;
|
|
13
|
+
private _type: string = '';
|
|
14
|
+
private _element: HTMLScriptElement | null = null;
|
|
15
|
+
|
|
16
|
+
constructor(contentOrSrc: string = '') {
|
|
17
|
+
// Detect if it's a URL or inline script
|
|
18
|
+
if (contentOrSrc.trim().startsWith('http') ||
|
|
19
|
+
contentOrSrc.endsWith('.js') ||
|
|
20
|
+
contentOrSrc.includes('/')) {
|
|
21
|
+
this._src = contentOrSrc;
|
|
22
|
+
this._isSrc = true;
|
|
23
|
+
} else {
|
|
24
|
+
this._content = contentOrSrc;
|
|
25
|
+
this._isSrc = false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Auto-render if content/src provided
|
|
29
|
+
if (contentOrSrc) {
|
|
30
|
+
this.render();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Set inline JavaScript content
|
|
36
|
+
*/
|
|
37
|
+
content(js: string): this {
|
|
38
|
+
this._content = js;
|
|
39
|
+
this._isSrc = false;
|
|
40
|
+
this._src = '';
|
|
41
|
+
this.render();
|
|
42
|
+
return this;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Set external script URL
|
|
47
|
+
*/
|
|
48
|
+
src(url: string): this {
|
|
49
|
+
this._src = url;
|
|
50
|
+
this._isSrc = true;
|
|
51
|
+
this._content = '';
|
|
52
|
+
this.render();
|
|
53
|
+
return this;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Enable async loading (for external scripts)
|
|
58
|
+
*/
|
|
59
|
+
async(value: boolean = true): this {
|
|
60
|
+
this._async = value;
|
|
61
|
+
this.render();
|
|
62
|
+
return this;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Enable defer loading (for external scripts)
|
|
67
|
+
*/
|
|
68
|
+
defer(value: boolean = true): this {
|
|
69
|
+
this._defer = value;
|
|
70
|
+
this.render();
|
|
71
|
+
return this;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Set script type (e.g., 'module', 'text/javascript')
|
|
76
|
+
*/
|
|
77
|
+
type(value: string): this {
|
|
78
|
+
this._type = value;
|
|
79
|
+
this.render();
|
|
80
|
+
return this;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Render the script element
|
|
85
|
+
*/
|
|
86
|
+
render(): this {
|
|
87
|
+
if (typeof document === 'undefined') {
|
|
88
|
+
return this;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
// Remove existing element if it exists
|
|
93
|
+
this.remove();
|
|
94
|
+
|
|
95
|
+
const script = document.createElement('script');
|
|
96
|
+
|
|
97
|
+
if (this._isSrc) {
|
|
98
|
+
script.src = this._src;
|
|
99
|
+
|
|
100
|
+
// Add error handler for failed loads
|
|
101
|
+
script.onerror = () => {
|
|
102
|
+
ErrorHandler.captureError({
|
|
103
|
+
component: 'Script',
|
|
104
|
+
method: 'render',
|
|
105
|
+
message: `Failed to load script: ${this._src}`,
|
|
106
|
+
timestamp: new Date(),
|
|
107
|
+
context: { src: this._src, type: 'external', error: 'load_failed' }
|
|
108
|
+
});
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
script.onload = () => {
|
|
112
|
+
console.log(`✓ Script loaded: ${this._src}`);
|
|
113
|
+
};
|
|
114
|
+
} else {
|
|
115
|
+
script.textContent = this._content;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (this._async) script.async = true;
|
|
119
|
+
if (this._defer) script.defer = true;
|
|
120
|
+
if (this._type) script.type = this._type;
|
|
121
|
+
|
|
122
|
+
document.head.appendChild(script);
|
|
123
|
+
this._element = script;
|
|
124
|
+
} catch (error: any) {
|
|
125
|
+
ErrorHandler.captureError({
|
|
126
|
+
component: 'Script',
|
|
127
|
+
method: 'render',
|
|
128
|
+
message: error.message,
|
|
129
|
+
stack: error.stack,
|
|
130
|
+
timestamp: new Date(),
|
|
131
|
+
context: {
|
|
132
|
+
isSrc: this._isSrc,
|
|
133
|
+
src: this._src,
|
|
134
|
+
error: 'runtime_exception'
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return this;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Remove the script from the document
|
|
144
|
+
*/
|
|
145
|
+
remove(): this {
|
|
146
|
+
if (this._element && this._element.parentNode) {
|
|
147
|
+
this._element.parentNode.removeChild(this._element);
|
|
148
|
+
this._element = null;
|
|
149
|
+
}
|
|
150
|
+
return this;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { Reactive, getOrCreateContainer } from './reactivity.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sidebar component options
|
|
5
|
+
*/
|
|
6
|
+
export interface SidebarOptions {
|
|
7
|
+
title?: string;
|
|
8
|
+
width?: string;
|
|
9
|
+
position?: 'left' | 'right';
|
|
10
|
+
collapsible?: boolean;
|
|
11
|
+
collapsed?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Sidebar component state
|
|
16
|
+
*/
|
|
17
|
+
type SidebarState = {
|
|
18
|
+
title: string;
|
|
19
|
+
width: string;
|
|
20
|
+
position: string;
|
|
21
|
+
collapsible: boolean;
|
|
22
|
+
collapsed: boolean;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Sidebar component
|
|
27
|
+
*
|
|
28
|
+
* Usage:
|
|
29
|
+
* const sidebar = jux.sidebar('mySidebar', {
|
|
30
|
+
* title: 'Navigation',
|
|
31
|
+
* width: '250px',
|
|
32
|
+
* position: 'left'
|
|
33
|
+
* });
|
|
34
|
+
* sidebar.render('#appsidebar');
|
|
35
|
+
*/
|
|
36
|
+
export class Sidebar extends Reactive {
|
|
37
|
+
state!: SidebarState;
|
|
38
|
+
container: HTMLElement | null = null;
|
|
39
|
+
|
|
40
|
+
constructor(componentId: string, options: SidebarOptions = {}) {
|
|
41
|
+
super();
|
|
42
|
+
this._setComponentId(componentId);
|
|
43
|
+
|
|
44
|
+
this.state = this._createReactiveState({
|
|
45
|
+
title: options.title ?? '',
|
|
46
|
+
width: options.width ?? '250px',
|
|
47
|
+
position: options.position ?? 'left',
|
|
48
|
+
collapsible: options.collapsible ?? false,
|
|
49
|
+
collapsed: options.collapsed ?? false
|
|
50
|
+
}) as SidebarState;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/* -------------------------
|
|
54
|
+
* Fluent API
|
|
55
|
+
* ------------------------- */
|
|
56
|
+
|
|
57
|
+
title(value: string): this {
|
|
58
|
+
this.state.title = value;
|
|
59
|
+
return this;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
width(value: string): this {
|
|
63
|
+
this.state.width = value;
|
|
64
|
+
return this;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
position(value: 'left' | 'right'): this {
|
|
68
|
+
this.state.position = value;
|
|
69
|
+
return this;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
collapsible(value: boolean): this {
|
|
73
|
+
this.state.collapsible = value;
|
|
74
|
+
return this;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
collapsed(value: boolean): this {
|
|
78
|
+
this.state.collapsed = value;
|
|
79
|
+
return this;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
toggle(): this {
|
|
83
|
+
this.state.collapsed = !this.state.collapsed;
|
|
84
|
+
return this;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/* -------------------------
|
|
88
|
+
* Render
|
|
89
|
+
* ------------------------- */
|
|
90
|
+
|
|
91
|
+
render(targetId?: string): this {
|
|
92
|
+
let container: HTMLElement;
|
|
93
|
+
|
|
94
|
+
if (targetId) {
|
|
95
|
+
const target = document.querySelector(targetId);
|
|
96
|
+
if (!target || !(target instanceof HTMLElement)) {
|
|
97
|
+
throw new Error(`Sidebar: Target element "${targetId}" not found`);
|
|
98
|
+
}
|
|
99
|
+
container = target;
|
|
100
|
+
} else {
|
|
101
|
+
container = getOrCreateContainer(this._componentId) as HTMLElement;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
this.container = container;
|
|
105
|
+
const { title, width, position, collapsible, collapsed } = this.state;
|
|
106
|
+
|
|
107
|
+
const sidebar = document.createElement('aside');
|
|
108
|
+
sidebar.className = `jux-sidebar jux-sidebar-${position}`;
|
|
109
|
+
sidebar.id = this._componentId;
|
|
110
|
+
sidebar.style.width = collapsed ? '0' : width;
|
|
111
|
+
|
|
112
|
+
if (collapsed) {
|
|
113
|
+
sidebar.classList.add('jux-sidebar-collapsed');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (title) {
|
|
117
|
+
const titleEl = document.createElement('div');
|
|
118
|
+
titleEl.className = 'jux-sidebar-title';
|
|
119
|
+
titleEl.textContent = title;
|
|
120
|
+
sidebar.appendChild(titleEl);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const content = document.createElement('div');
|
|
124
|
+
content.className = 'jux-sidebar-content';
|
|
125
|
+
sidebar.appendChild(content);
|
|
126
|
+
|
|
127
|
+
container.appendChild(sidebar);
|
|
128
|
+
|
|
129
|
+
// Event binding - toggle button
|
|
130
|
+
if (collapsible) {
|
|
131
|
+
const toggleBtn = document.createElement('button');
|
|
132
|
+
toggleBtn.className = 'jux-sidebar-toggle';
|
|
133
|
+
toggleBtn.textContent = collapsed ? '>' : '<';
|
|
134
|
+
sidebar.appendChild(toggleBtn);
|
|
135
|
+
|
|
136
|
+
toggleBtn.addEventListener('click', () => {
|
|
137
|
+
this.toggle();
|
|
138
|
+
sidebar.classList.toggle('jux-sidebar-collapsed');
|
|
139
|
+
sidebar.style.width = this.state.collapsed ? '0' : width;
|
|
140
|
+
toggleBtn.textContent = this.state.collapsed ? '>' : '<';
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return this;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Render to another Jux component's container
|
|
149
|
+
*/
|
|
150
|
+
renderTo(juxComponent: any): this {
|
|
151
|
+
if (!juxComponent || typeof juxComponent !== 'object') {
|
|
152
|
+
throw new Error('Sidebar.renderTo: Invalid component - not an object');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (!juxComponent._componentId || typeof juxComponent._componentId !== 'string') {
|
|
156
|
+
throw new Error('Sidebar.renderTo: Invalid component - missing _componentId (not a Jux component)');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return this.render(`#${juxComponent._componentId}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Factory helper
|
|
165
|
+
*/
|
|
166
|
+
export function sidebar(componentId: string, options: SidebarOptions = {}): Sidebar {
|
|
167
|
+
return new Sidebar(componentId, options);
|
|
168
|
+
}
|