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,111 @@
|
|
|
1
|
+
import { Reactive, getOrCreateContainer } from './reactivity.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Main component options
|
|
5
|
+
*/
|
|
6
|
+
export interface MainOptions {
|
|
7
|
+
content?: string;
|
|
8
|
+
padding?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Main component state
|
|
13
|
+
*/
|
|
14
|
+
type MainState = {
|
|
15
|
+
content: string;
|
|
16
|
+
padding: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Main component - represents the main content area
|
|
21
|
+
*
|
|
22
|
+
* Usage:
|
|
23
|
+
* const main = jux.main('myMain', {
|
|
24
|
+
* content: 'Main content here',
|
|
25
|
+
* padding: '2rem'
|
|
26
|
+
* });
|
|
27
|
+
* main.render('#appmain');
|
|
28
|
+
*/
|
|
29
|
+
export class Main extends Reactive {
|
|
30
|
+
state!: MainState;
|
|
31
|
+
container: HTMLElement | null = null;
|
|
32
|
+
|
|
33
|
+
constructor(componentId: string, options: MainOptions = {}) {
|
|
34
|
+
super();
|
|
35
|
+
this._setComponentId(componentId);
|
|
36
|
+
|
|
37
|
+
this.state = this._createReactiveState({
|
|
38
|
+
content: options.content ?? '',
|
|
39
|
+
padding: options.padding ?? 'var(--space-xl)'
|
|
40
|
+
}) as MainState;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* -------------------------
|
|
44
|
+
* Fluent API
|
|
45
|
+
* ------------------------- */
|
|
46
|
+
|
|
47
|
+
content(value: string): this {
|
|
48
|
+
this.state.content = value;
|
|
49
|
+
return this;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
padding(value: string): this {
|
|
53
|
+
this.state.padding = value;
|
|
54
|
+
return this;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* -------------------------
|
|
58
|
+
* Render
|
|
59
|
+
* ------------------------- */
|
|
60
|
+
|
|
61
|
+
render(targetId?: string): this {
|
|
62
|
+
let container: HTMLElement;
|
|
63
|
+
|
|
64
|
+
if (targetId) {
|
|
65
|
+
const target = document.querySelector(targetId);
|
|
66
|
+
if (!target || !(target instanceof HTMLElement)) {
|
|
67
|
+
throw new Error(`Main: Target element "${targetId}" not found`);
|
|
68
|
+
}
|
|
69
|
+
container = target;
|
|
70
|
+
} else {
|
|
71
|
+
container = getOrCreateContainer(this._componentId) as HTMLElement;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
this.container = container;
|
|
75
|
+
const { content, padding } = this.state;
|
|
76
|
+
|
|
77
|
+
const main = document.createElement('main');
|
|
78
|
+
main.className = 'jux-main';
|
|
79
|
+
main.id = this._componentId;
|
|
80
|
+
main.style.padding = padding;
|
|
81
|
+
|
|
82
|
+
if (content) {
|
|
83
|
+
main.innerHTML = content;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
container.appendChild(main);
|
|
87
|
+
return this;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Render to another Jux component's container
|
|
92
|
+
*/
|
|
93
|
+
renderTo(juxComponent: any): this {
|
|
94
|
+
if (!juxComponent || typeof juxComponent !== 'object') {
|
|
95
|
+
throw new Error('Main.renderTo: Invalid component - not an object');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!juxComponent._componentId || typeof juxComponent._componentId !== 'string') {
|
|
99
|
+
throw new Error('Main.renderTo: Invalid component - missing _componentId (not a Jux component)');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return this.render(`#${juxComponent._componentId}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Factory helper
|
|
108
|
+
*/
|
|
109
|
+
export function main(componentId: string, options: MainOptions = {}): Main {
|
|
110
|
+
return new Main(componentId, options);
|
|
111
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { Reactive, getOrCreateContainer } from './reactivity.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Menu item configuration
|
|
5
|
+
*/
|
|
6
|
+
export interface MenuItem {
|
|
7
|
+
label: string;
|
|
8
|
+
href?: string;
|
|
9
|
+
onClick?: () => void;
|
|
10
|
+
icon?: string;
|
|
11
|
+
items?: MenuItem[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Menu component options
|
|
16
|
+
*/
|
|
17
|
+
export interface MenuOptions {
|
|
18
|
+
items?: MenuItem[];
|
|
19
|
+
orientation?: 'vertical' | 'horizontal';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Menu component state
|
|
24
|
+
*/
|
|
25
|
+
type MenuState = {
|
|
26
|
+
items: MenuItem[];
|
|
27
|
+
orientation: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Menu component
|
|
32
|
+
*
|
|
33
|
+
* Usage:
|
|
34
|
+
* const menu = jux.menu('myMenu', {
|
|
35
|
+
* orientation: 'vertical',
|
|
36
|
+
* items: [
|
|
37
|
+
* { label: 'Home', href: '/' },
|
|
38
|
+
* { label: 'About', href: '/about' }
|
|
39
|
+
* ]
|
|
40
|
+
* });
|
|
41
|
+
* menu.render();
|
|
42
|
+
*/
|
|
43
|
+
export class Menu extends Reactive {
|
|
44
|
+
state!: MenuState;
|
|
45
|
+
container: HTMLElement | null = null;
|
|
46
|
+
|
|
47
|
+
constructor(componentId: string, options: MenuOptions = {}) {
|
|
48
|
+
super();
|
|
49
|
+
this._setComponentId(componentId);
|
|
50
|
+
|
|
51
|
+
this.state = this._createReactiveState({
|
|
52
|
+
items: options.items ?? [],
|
|
53
|
+
orientation: options.orientation ?? 'vertical'
|
|
54
|
+
}) as MenuState;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* -------------------------
|
|
58
|
+
* Fluent API
|
|
59
|
+
* ------------------------- */
|
|
60
|
+
|
|
61
|
+
items(value: MenuItem[]): this {
|
|
62
|
+
this.state.items = value;
|
|
63
|
+
return this;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
addItem(item: MenuItem): this {
|
|
67
|
+
this.state.items.push(item);
|
|
68
|
+
return this;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
orientation(value: 'vertical' | 'horizontal'): this {
|
|
72
|
+
this.state.orientation = value;
|
|
73
|
+
return this;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* -------------------------
|
|
77
|
+
* Helpers
|
|
78
|
+
* ------------------------- */
|
|
79
|
+
|
|
80
|
+
private _renderMenuItem(item: MenuItem): HTMLElement {
|
|
81
|
+
const menuItem = document.createElement('div');
|
|
82
|
+
menuItem.className = 'jux-menu-item';
|
|
83
|
+
|
|
84
|
+
if (item.href) {
|
|
85
|
+
const link = document.createElement('a');
|
|
86
|
+
link.className = 'jux-menu-link';
|
|
87
|
+
link.href = item.href;
|
|
88
|
+
link.textContent = item.label;
|
|
89
|
+
menuItem.appendChild(link);
|
|
90
|
+
} else {
|
|
91
|
+
const button = document.createElement('button');
|
|
92
|
+
button.className = 'jux-menu-button';
|
|
93
|
+
button.textContent = item.label;
|
|
94
|
+
menuItem.appendChild(button);
|
|
95
|
+
|
|
96
|
+
// Event binding - onClick
|
|
97
|
+
if (item.onClick) {
|
|
98
|
+
button.addEventListener('click', item.onClick);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Nested items (submenu)
|
|
103
|
+
if (item.items && item.items.length > 0) {
|
|
104
|
+
const submenu = document.createElement('div');
|
|
105
|
+
submenu.className = 'jux-menu-submenu';
|
|
106
|
+
|
|
107
|
+
item.items.forEach(subItem => {
|
|
108
|
+
submenu.appendChild(this._renderMenuItem(subItem));
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
menuItem.appendChild(submenu);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return menuItem;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/* -------------------------
|
|
118
|
+
* Render
|
|
119
|
+
* ------------------------- */
|
|
120
|
+
|
|
121
|
+
render(targetId?: string): this {
|
|
122
|
+
let container: HTMLElement;
|
|
123
|
+
|
|
124
|
+
if (targetId) {
|
|
125
|
+
const target = document.querySelector(targetId);
|
|
126
|
+
if (!target || !(target instanceof HTMLElement)) {
|
|
127
|
+
throw new Error(`Menu: Target element "${targetId}" not found`);
|
|
128
|
+
}
|
|
129
|
+
container = target;
|
|
130
|
+
} else {
|
|
131
|
+
container = getOrCreateContainer(this._componentId) as HTMLElement;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
this.container = container;
|
|
135
|
+
const { items, orientation } = this.state;
|
|
136
|
+
|
|
137
|
+
const menu = document.createElement('nav');
|
|
138
|
+
menu.className = `jux-menu jux-menu-${orientation}`;
|
|
139
|
+
menu.id = this._componentId;
|
|
140
|
+
|
|
141
|
+
items.forEach(item => {
|
|
142
|
+
menu.appendChild(this._renderMenuItem(item));
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
container.appendChild(menu);
|
|
146
|
+
return this;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Render to another Jux component's container
|
|
151
|
+
*/
|
|
152
|
+
renderTo(juxComponent: any): this {
|
|
153
|
+
if (!juxComponent || typeof juxComponent !== 'object') {
|
|
154
|
+
throw new Error('Menu.renderTo: Invalid component - not an object');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (!juxComponent._componentId || typeof juxComponent._componentId !== 'string') {
|
|
158
|
+
throw new Error('Menu.renderTo: Invalid component - missing _componentId (not a Jux component)');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return this.render(`#${juxComponent._componentId}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Factory helper
|
|
167
|
+
*/
|
|
168
|
+
export function menu(componentId: string, options: MenuOptions = {}): Menu {
|
|
169
|
+
return new Menu(componentId, options);
|
|
170
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { Reactive, getOrCreateContainer } from './reactivity.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Modal component options
|
|
5
|
+
*/
|
|
6
|
+
export interface ModalOptions {
|
|
7
|
+
title?: string;
|
|
8
|
+
content?: string;
|
|
9
|
+
showCloseButton?: boolean;
|
|
10
|
+
closeOnBackdropClick?: boolean;
|
|
11
|
+
size?: 'small' | 'medium' | 'large';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Modal component state
|
|
16
|
+
*/
|
|
17
|
+
type ModalState = {
|
|
18
|
+
title: string;
|
|
19
|
+
content: string;
|
|
20
|
+
showCloseButton: boolean;
|
|
21
|
+
closeOnBackdropClick: boolean;
|
|
22
|
+
size: string;
|
|
23
|
+
isOpen: boolean;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Modal component
|
|
28
|
+
*
|
|
29
|
+
* Usage:
|
|
30
|
+
* const modal = jux.modal('myModal', {
|
|
31
|
+
* title: 'Confirmation',
|
|
32
|
+
* content: 'Are you sure?',
|
|
33
|
+
* size: 'medium'
|
|
34
|
+
* });
|
|
35
|
+
* modal.render();
|
|
36
|
+
* modal.open();
|
|
37
|
+
*/
|
|
38
|
+
export class Modal extends Reactive {
|
|
39
|
+
state!: ModalState;
|
|
40
|
+
container: HTMLElement | null = null;
|
|
41
|
+
|
|
42
|
+
constructor(componentId: string, options: ModalOptions = {}) {
|
|
43
|
+
super();
|
|
44
|
+
this._setComponentId(componentId);
|
|
45
|
+
|
|
46
|
+
this.state = this._createReactiveState({
|
|
47
|
+
title: options.title ?? '',
|
|
48
|
+
content: options.content ?? '',
|
|
49
|
+
showCloseButton: options.showCloseButton ?? true,
|
|
50
|
+
closeOnBackdropClick: options.closeOnBackdropClick ?? true,
|
|
51
|
+
size: options.size ?? 'medium',
|
|
52
|
+
isOpen: false
|
|
53
|
+
}) as ModalState;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* -------------------------
|
|
57
|
+
* Fluent API
|
|
58
|
+
* ------------------------- */
|
|
59
|
+
|
|
60
|
+
title(value: string): this {
|
|
61
|
+
this.state.title = value;
|
|
62
|
+
return this;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
content(value: string): this {
|
|
66
|
+
this.state.content = value;
|
|
67
|
+
return this;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
showCloseButton(value: boolean): this {
|
|
71
|
+
this.state.showCloseButton = value;
|
|
72
|
+
return this;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
closeOnBackdropClick(value: boolean): this {
|
|
76
|
+
this.state.closeOnBackdropClick = value;
|
|
77
|
+
return this;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
size(value: 'small' | 'medium' | 'large'): this {
|
|
81
|
+
this.state.size = value;
|
|
82
|
+
return this;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
open(): this {
|
|
86
|
+
this.state.isOpen = true;
|
|
87
|
+
if (this.container) {
|
|
88
|
+
const modalEl = this.container.querySelector('.jux-modal');
|
|
89
|
+
if (modalEl) {
|
|
90
|
+
modalEl.classList.add('jux-modal-open');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
this.emit('open');
|
|
94
|
+
return this;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
close(): this {
|
|
98
|
+
this.state.isOpen = false;
|
|
99
|
+
if (this.container) {
|
|
100
|
+
const modalEl = this.container.querySelector('.jux-modal');
|
|
101
|
+
if (modalEl) {
|
|
102
|
+
modalEl.classList.remove('jux-modal-open');
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
this.emit('close');
|
|
106
|
+
return this;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* -------------------------
|
|
110
|
+
* Render
|
|
111
|
+
* ------------------------- */
|
|
112
|
+
|
|
113
|
+
render(targetId?: string): this {
|
|
114
|
+
let container: HTMLElement;
|
|
115
|
+
|
|
116
|
+
if (targetId) {
|
|
117
|
+
const target = document.querySelector(targetId);
|
|
118
|
+
if (!target || !(target instanceof HTMLElement)) {
|
|
119
|
+
throw new Error(`Modal: Target element "${targetId}" not found`);
|
|
120
|
+
}
|
|
121
|
+
container = target;
|
|
122
|
+
} else {
|
|
123
|
+
container = getOrCreateContainer(this._componentId) as HTMLElement;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
this.container = container;
|
|
127
|
+
const { title, content, showCloseButton, closeOnBackdropClick, size, isOpen } = this.state;
|
|
128
|
+
|
|
129
|
+
// Modal backdrop
|
|
130
|
+
const modal = document.createElement('div');
|
|
131
|
+
modal.className = `jux-modal jux-modal-${size}`;
|
|
132
|
+
modal.id = this._componentId;
|
|
133
|
+
|
|
134
|
+
if (isOpen) {
|
|
135
|
+
modal.classList.add('jux-modal-open');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Modal dialog
|
|
139
|
+
const dialog = document.createElement('div');
|
|
140
|
+
dialog.className = 'jux-modal-dialog';
|
|
141
|
+
|
|
142
|
+
// Modal header
|
|
143
|
+
if (title || showCloseButton) {
|
|
144
|
+
const header = document.createElement('div');
|
|
145
|
+
header.className = 'jux-modal-header';
|
|
146
|
+
|
|
147
|
+
if (title) {
|
|
148
|
+
const titleEl = document.createElement('h3');
|
|
149
|
+
titleEl.className = 'jux-modal-title';
|
|
150
|
+
titleEl.textContent = title;
|
|
151
|
+
header.appendChild(titleEl);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (showCloseButton) {
|
|
155
|
+
const closeBtn = document.createElement('button');
|
|
156
|
+
closeBtn.className = 'jux-modal-close';
|
|
157
|
+
closeBtn.innerHTML = '×';
|
|
158
|
+
header.appendChild(closeBtn);
|
|
159
|
+
|
|
160
|
+
// Event binding - close button
|
|
161
|
+
closeBtn.addEventListener('click', () => this.close());
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
dialog.appendChild(header);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Modal body
|
|
168
|
+
const body = document.createElement('div');
|
|
169
|
+
body.className = 'jux-modal-body';
|
|
170
|
+
body.innerHTML = content;
|
|
171
|
+
dialog.appendChild(body);
|
|
172
|
+
|
|
173
|
+
modal.appendChild(dialog);
|
|
174
|
+
container.appendChild(modal);
|
|
175
|
+
|
|
176
|
+
// Event binding - backdrop click
|
|
177
|
+
if (closeOnBackdropClick) {
|
|
178
|
+
modal.addEventListener('click', (e) => {
|
|
179
|
+
if (e.target === modal) {
|
|
180
|
+
this.close();
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Event binding - escape key
|
|
186
|
+
document.addEventListener('keydown', (e) => {
|
|
187
|
+
if (e.key === 'Escape' && this.state.isOpen) {
|
|
188
|
+
this.close();
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
return this;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Render to another Jux component's container
|
|
197
|
+
*/
|
|
198
|
+
renderTo(juxComponent: any): this {
|
|
199
|
+
if (!juxComponent || typeof juxComponent !== 'object') {
|
|
200
|
+
throw new Error('Modal.renderTo: Invalid component - not an object');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (!juxComponent._componentId || typeof juxComponent._componentId !== 'string') {
|
|
204
|
+
throw new Error('Modal.renderTo: Invalid component - missing _componentId (not a Jux component)');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return this.render(`#${juxComponent._componentId}`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Factory helper
|
|
213
|
+
*/
|
|
214
|
+
export function modal(componentId: string, options: ModalOptions = {}): Modal {
|
|
215
|
+
return new Modal(componentId, options);
|
|
216
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { Reactive, getOrCreateContainer } from './reactivity.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Nav item configuration
|
|
5
|
+
*/
|
|
6
|
+
export interface NavItem {
|
|
7
|
+
label: string;
|
|
8
|
+
href: string;
|
|
9
|
+
active?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Nav component options
|
|
14
|
+
*/
|
|
15
|
+
export interface NavOptions {
|
|
16
|
+
items?: NavItem[];
|
|
17
|
+
variant?: 'default' | 'pills' | 'tabs';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Nav component state
|
|
22
|
+
*/
|
|
23
|
+
type NavState = {
|
|
24
|
+
items: NavItem[];
|
|
25
|
+
variant: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Nav component
|
|
30
|
+
*
|
|
31
|
+
* Usage:
|
|
32
|
+
* const nav = jux.nav('myNav', {
|
|
33
|
+
* variant: 'pills',
|
|
34
|
+
* items: [
|
|
35
|
+
* { label: 'Home', href: '/', active: true },
|
|
36
|
+
* { label: 'About', href: '/about' }
|
|
37
|
+
* ]
|
|
38
|
+
* });
|
|
39
|
+
* nav.render();
|
|
40
|
+
*/
|
|
41
|
+
export class Nav extends Reactive {
|
|
42
|
+
state!: NavState;
|
|
43
|
+
container: HTMLElement | null = null;
|
|
44
|
+
|
|
45
|
+
constructor(componentId: string, options: NavOptions = {}) {
|
|
46
|
+
super();
|
|
47
|
+
this._setComponentId(componentId);
|
|
48
|
+
|
|
49
|
+
this.state = this._createReactiveState({
|
|
50
|
+
items: options.items ?? [],
|
|
51
|
+
variant: options.variant ?? 'default'
|
|
52
|
+
}) as NavState;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* -------------------------
|
|
56
|
+
* Fluent API
|
|
57
|
+
* ------------------------- */
|
|
58
|
+
|
|
59
|
+
items(value: NavItem[]): this {
|
|
60
|
+
this.state.items = value;
|
|
61
|
+
return this;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
addItem(item: NavItem): this {
|
|
65
|
+
this.state.items.push(item);
|
|
66
|
+
return this;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
variant(value: string): this {
|
|
70
|
+
this.state.variant = value;
|
|
71
|
+
return this;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* -------------------------
|
|
75
|
+
* Render
|
|
76
|
+
* ------------------------- */
|
|
77
|
+
|
|
78
|
+
render(targetId?: string): this {
|
|
79
|
+
let container: HTMLElement;
|
|
80
|
+
|
|
81
|
+
if (targetId) {
|
|
82
|
+
const target = document.querySelector(targetId);
|
|
83
|
+
if (!target || !(target instanceof HTMLElement)) {
|
|
84
|
+
throw new Error(`Nav: Target element "${targetId}" not found`);
|
|
85
|
+
}
|
|
86
|
+
container = target;
|
|
87
|
+
} else {
|
|
88
|
+
container = getOrCreateContainer(this._componentId) as HTMLElement;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
this.container = container;
|
|
92
|
+
const { items, variant } = this.state;
|
|
93
|
+
|
|
94
|
+
const nav = document.createElement('nav');
|
|
95
|
+
nav.className = `jux-nav jux-nav-${variant}`;
|
|
96
|
+
nav.id = this._componentId;
|
|
97
|
+
|
|
98
|
+
items.forEach(item => {
|
|
99
|
+
const link = document.createElement('a');
|
|
100
|
+
link.className = 'jux-nav-item';
|
|
101
|
+
link.href = item.href;
|
|
102
|
+
link.textContent = item.label;
|
|
103
|
+
|
|
104
|
+
if (item.active) {
|
|
105
|
+
link.classList.add('jux-nav-item-active');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
nav.appendChild(link);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
container.appendChild(nav);
|
|
112
|
+
return this;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Render to another Jux component's container
|
|
117
|
+
*/
|
|
118
|
+
renderTo(juxComponent: any): this {
|
|
119
|
+
if (!juxComponent || typeof juxComponent !== 'object') {
|
|
120
|
+
throw new Error('Nav.renderTo: Invalid component - not an object');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!juxComponent._componentId || typeof juxComponent._componentId !== 'string') {
|
|
124
|
+
throw new Error('Nav.renderTo: Invalid component - missing _componentId (not a Jux component)');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return this.render(`#${juxComponent._componentId}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Factory helper
|
|
133
|
+
*/
|
|
134
|
+
export function nav(componentId: string, options: NavOptions = {}): Nav {
|
|
135
|
+
return new Nav(componentId, options);
|
|
136
|
+
}
|