juxscript 1.0.3 → 1.0.4
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 +37 -92
- package/bin/cli.js +57 -56
- package/lib/components/alert.ts +240 -0
- package/lib/components/app.ts +216 -82
- package/lib/components/badge.ts +164 -0
- package/lib/components/button.ts +188 -53
- package/lib/components/card.ts +75 -61
- package/lib/components/chart.ts +17 -15
- package/lib/components/checkbox.ts +228 -0
- package/lib/components/code.ts +66 -152
- package/lib/components/container.ts +104 -208
- package/lib/components/data.ts +1 -3
- package/lib/components/datepicker.ts +226 -0
- package/lib/components/dialog.ts +258 -0
- package/lib/components/docs-data.json +1697 -388
- package/lib/components/dropdown.ts +244 -0
- package/lib/components/element.ts +271 -0
- package/lib/components/fileupload.ts +319 -0
- package/lib/components/footer.ts +37 -18
- package/lib/components/header.ts +53 -33
- package/lib/components/heading.ts +119 -0
- package/lib/components/helpers.ts +34 -0
- package/lib/components/hero.ts +57 -31
- package/lib/components/include.ts +292 -0
- package/lib/components/input.ts +166 -78
- package/lib/components/layout.ts +144 -18
- package/lib/components/list.ts +83 -74
- package/lib/components/loading.ts +263 -0
- package/lib/components/main.ts +43 -17
- package/lib/components/menu.ts +108 -24
- package/lib/components/modal.ts +50 -21
- package/lib/components/nav.ts +60 -18
- package/lib/components/paragraph.ts +111 -0
- package/lib/components/progress.ts +276 -0
- package/lib/components/radio.ts +236 -0
- package/lib/components/req.ts +300 -0
- package/lib/components/script.ts +33 -74
- package/lib/components/select.ts +247 -0
- package/lib/components/sidebar.ts +86 -36
- package/lib/components/style.ts +47 -70
- package/lib/components/switch.ts +261 -0
- package/lib/components/table.ts +47 -24
- package/lib/components/tabs.ts +105 -63
- package/lib/components/theme-toggle.ts +361 -0
- package/lib/components/token-calculator.ts +380 -0
- package/lib/components/tooltip.ts +244 -0
- package/lib/components/view.ts +36 -20
- package/lib/components/write.ts +284 -0
- package/lib/globals.d.ts +21 -0
- package/lib/jux.ts +172 -68
- package/lib/presets/notion.css +521 -0
- package/lib/presets/notion.jux +27 -0
- package/lib/reactivity/state.ts +364 -0
- package/machinery/compiler.js +126 -38
- package/machinery/generators/html.js +2 -3
- package/machinery/server.js +2 -2
- package/package.json +29 -3
- package/lib/components/import.ts +0 -430
- package/lib/components/node.ts +0 -200
- package/lib/components/reactivity.js +0 -104
- package/lib/components/theme.ts +0 -97
- package/lib/layouts/notion.css +0 -258
- package/lib/styles/base-theme.css +0 -186
- package/lib/styles/dark-theme.css +0 -144
- package/lib/styles/light-theme.css +0 -144
- package/lib/styles/tokens/dark.css +0 -86
- package/lib/styles/tokens/light.css +0 -86
- package/lib/templates/index.juxt +0 -33
- package/lib/themes/dark.css +0 -86
- package/lib/themes/light.css +0 -86
- /package/lib/{styles → presets}/global.css +0 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { getOrCreateContainer } from './helpers.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Heading options
|
|
5
|
+
*/
|
|
6
|
+
export interface HeadingOptions {
|
|
7
|
+
level?: 1 | 2 | 3 | 4 | 5 | 6;
|
|
8
|
+
text?: string;
|
|
9
|
+
class?: string;
|
|
10
|
+
style?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Heading state
|
|
15
|
+
*/
|
|
16
|
+
type HeadingState = {
|
|
17
|
+
level: 1 | 2 | 3 | 4 | 5 | 6;
|
|
18
|
+
text: string;
|
|
19
|
+
class: string;
|
|
20
|
+
style: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Heading component - semantic heading elements (h1-h6)
|
|
25
|
+
*
|
|
26
|
+
* Usage:
|
|
27
|
+
* jux.heading('title', { level: 1, text: 'Welcome' }).render('#app');
|
|
28
|
+
* jux.heading('subtitle').level(2).text('Getting Started').render('#app');
|
|
29
|
+
*/
|
|
30
|
+
export class Heading {
|
|
31
|
+
state: HeadingState;
|
|
32
|
+
container: HTMLElement | null = null;
|
|
33
|
+
_id: string;
|
|
34
|
+
id: string;
|
|
35
|
+
|
|
36
|
+
constructor(id: string, options: HeadingOptions = {}) {
|
|
37
|
+
this._id = id;
|
|
38
|
+
this.id = id;
|
|
39
|
+
|
|
40
|
+
this.state = {
|
|
41
|
+
level: options.level ?? 1,
|
|
42
|
+
text: options.text ?? '',
|
|
43
|
+
class: options.class ?? '',
|
|
44
|
+
style: options.style ?? ''
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* -------------------------
|
|
49
|
+
* Fluent API
|
|
50
|
+
* ------------------------- */
|
|
51
|
+
|
|
52
|
+
level(value: 1 | 2 | 3 | 4 | 5 | 6): this {
|
|
53
|
+
this.state.level = value;
|
|
54
|
+
return this;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
text(value: string): this {
|
|
58
|
+
this.state.text = value;
|
|
59
|
+
return this;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
class(value: string): this {
|
|
63
|
+
this.state.class = value;
|
|
64
|
+
return this;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
style(value: string): this {
|
|
68
|
+
this.state.style = value;
|
|
69
|
+
return this;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/* -------------------------
|
|
73
|
+
* Render
|
|
74
|
+
* ------------------------- */
|
|
75
|
+
|
|
76
|
+
render(targetId?: string | HTMLElement): this {
|
|
77
|
+
let container: HTMLElement;
|
|
78
|
+
|
|
79
|
+
if (targetId) {
|
|
80
|
+
if (targetId instanceof HTMLElement) {
|
|
81
|
+
container = targetId;
|
|
82
|
+
} else {
|
|
83
|
+
const target = document.querySelector(targetId);
|
|
84
|
+
if (!target || !(target instanceof HTMLElement)) {
|
|
85
|
+
throw new Error(`Heading: Target element "${targetId}" not found`);
|
|
86
|
+
}
|
|
87
|
+
container = target;
|
|
88
|
+
}
|
|
89
|
+
} else {
|
|
90
|
+
container = getOrCreateContainer(this._id);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
this.container = container;
|
|
94
|
+
const { level, text, class: className, style } = this.state;
|
|
95
|
+
|
|
96
|
+
const heading = document.createElement(`h${level}`) as HTMLHeadingElement;
|
|
97
|
+
heading.id = this._id;
|
|
98
|
+
heading.textContent = text;
|
|
99
|
+
|
|
100
|
+
if (className) {
|
|
101
|
+
heading.className = className;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (style) {
|
|
105
|
+
heading.setAttribute('style', style);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
container.appendChild(heading);
|
|
109
|
+
|
|
110
|
+
return this;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Factory helper
|
|
116
|
+
*/
|
|
117
|
+
export function heading(id: string, options: HeadingOptions = {}): Heading {
|
|
118
|
+
return new Heading(id, options);
|
|
119
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component helper utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Get or create a container element for a component
|
|
7
|
+
* Auto-creates if it doesn't exist and appends to appropriate parent
|
|
8
|
+
*/
|
|
9
|
+
export function getOrCreateContainer(id: string): HTMLElement {
|
|
10
|
+
if (typeof document === 'undefined') {
|
|
11
|
+
throw new Error('Document is not available');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let container = document.getElementById(id);
|
|
15
|
+
|
|
16
|
+
// Container already exists, return it
|
|
17
|
+
if (container) {
|
|
18
|
+
return container;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Auto-create container if it doesn't exist
|
|
22
|
+
container = document.createElement('div');
|
|
23
|
+
container.id = id;
|
|
24
|
+
|
|
25
|
+
// Find appropriate parent
|
|
26
|
+
let parent: HTMLElement;
|
|
27
|
+
// Page components go inside #appmain (or [data-jux-page] if no layout)
|
|
28
|
+
const appmain = document.getElementById('appmain');
|
|
29
|
+
const dataJuxPage = document.querySelector('[data-jux-page]');
|
|
30
|
+
parent = (appmain || dataJuxPage || document.body) as HTMLElement;
|
|
31
|
+
parent.appendChild(container);
|
|
32
|
+
|
|
33
|
+
return container;
|
|
34
|
+
}
|
package/lib/components/hero.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getOrCreateContainer } from './helpers.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Hero component options
|
|
@@ -10,6 +10,8 @@ export interface HeroOptions {
|
|
|
10
10
|
ctaLink?: string;
|
|
11
11
|
backgroundImage?: string;
|
|
12
12
|
variant?: 'default' | 'centered' | 'split';
|
|
13
|
+
style?: string;
|
|
14
|
+
class?: string;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
/**
|
|
@@ -22,6 +24,8 @@ type HeroState = {
|
|
|
22
24
|
ctaLink: string;
|
|
23
25
|
backgroundImage: string;
|
|
24
26
|
variant: string;
|
|
27
|
+
style: string;
|
|
28
|
+
class: string;
|
|
25
29
|
};
|
|
26
30
|
|
|
27
31
|
/**
|
|
@@ -35,22 +39,26 @@ type HeroState = {
|
|
|
35
39
|
* });
|
|
36
40
|
* hero.render();
|
|
37
41
|
*/
|
|
38
|
-
export class Hero
|
|
39
|
-
state
|
|
42
|
+
export class Hero {
|
|
43
|
+
state: HeroState;
|
|
40
44
|
container: HTMLElement | null = null;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
this.
|
|
45
|
+
_id: string;
|
|
46
|
+
id: string;
|
|
47
|
+
|
|
48
|
+
constructor(id: string, options: HeroOptions = {}) {
|
|
49
|
+
this._id = id;
|
|
50
|
+
this.id = id;
|
|
51
|
+
|
|
52
|
+
this.state = {
|
|
47
53
|
title: options.title ?? '',
|
|
48
54
|
subtitle: options.subtitle ?? '',
|
|
49
55
|
cta: options.cta ?? '',
|
|
50
56
|
ctaLink: options.ctaLink ?? '#',
|
|
51
57
|
backgroundImage: options.backgroundImage ?? '',
|
|
52
|
-
variant: options.variant ?? 'default'
|
|
53
|
-
|
|
58
|
+
variant: options.variant ?? 'default',
|
|
59
|
+
style: options.style ?? '',
|
|
60
|
+
class: options.class ?? ''
|
|
61
|
+
};
|
|
54
62
|
}
|
|
55
63
|
|
|
56
64
|
/* -------------------------
|
|
@@ -87,13 +95,23 @@ export class Hero extends Reactive {
|
|
|
87
95
|
return this;
|
|
88
96
|
}
|
|
89
97
|
|
|
98
|
+
style(value: string): this {
|
|
99
|
+
this.state.style = value;
|
|
100
|
+
return this;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
class(value: string): this {
|
|
104
|
+
this.state.class = value;
|
|
105
|
+
return this;
|
|
106
|
+
}
|
|
107
|
+
|
|
90
108
|
/* -------------------------
|
|
91
109
|
* Render
|
|
92
110
|
* ------------------------- */
|
|
93
111
|
|
|
94
112
|
render(targetId?: string): this {
|
|
95
113
|
let container: HTMLElement;
|
|
96
|
-
|
|
114
|
+
|
|
97
115
|
if (targetId) {
|
|
98
116
|
const target = document.querySelector(targetId);
|
|
99
117
|
if (!target || !(target instanceof HTMLElement)) {
|
|
@@ -101,37 +119,45 @@ export class Hero extends Reactive {
|
|
|
101
119
|
}
|
|
102
120
|
container = target;
|
|
103
121
|
} else {
|
|
104
|
-
container = getOrCreateContainer(this.
|
|
122
|
+
container = getOrCreateContainer(this._id);
|
|
105
123
|
}
|
|
106
|
-
|
|
124
|
+
|
|
107
125
|
this.container = container;
|
|
108
|
-
const { title, subtitle, cta, ctaLink, backgroundImage, variant } = this.state;
|
|
109
|
-
|
|
126
|
+
const { title, subtitle, cta, ctaLink, backgroundImage, variant, style, class: className } = this.state;
|
|
127
|
+
|
|
110
128
|
const hero = document.createElement('div');
|
|
111
129
|
hero.className = `jux-hero jux-hero-${variant}`;
|
|
112
|
-
hero.id = this.
|
|
113
|
-
|
|
130
|
+
hero.id = this._id;
|
|
131
|
+
|
|
132
|
+
if (className) {
|
|
133
|
+
hero.className += ` ${className}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (style) {
|
|
137
|
+
hero.setAttribute('style', style);
|
|
138
|
+
}
|
|
139
|
+
|
|
114
140
|
if (backgroundImage) {
|
|
115
141
|
hero.style.backgroundImage = `url(${backgroundImage})`;
|
|
116
142
|
}
|
|
117
|
-
|
|
143
|
+
|
|
118
144
|
const content = document.createElement('div');
|
|
119
145
|
content.className = 'jux-hero-content';
|
|
120
|
-
|
|
146
|
+
|
|
121
147
|
if (title) {
|
|
122
148
|
const titleEl = document.createElement('h1');
|
|
123
149
|
titleEl.className = 'jux-hero-title';
|
|
124
150
|
titleEl.textContent = title;
|
|
125
151
|
content.appendChild(titleEl);
|
|
126
152
|
}
|
|
127
|
-
|
|
153
|
+
|
|
128
154
|
if (subtitle) {
|
|
129
155
|
const subtitleEl = document.createElement('p');
|
|
130
156
|
subtitleEl.className = 'jux-hero-subtitle';
|
|
131
157
|
subtitleEl.textContent = subtitle;
|
|
132
158
|
content.appendChild(subtitleEl);
|
|
133
159
|
}
|
|
134
|
-
|
|
160
|
+
|
|
135
161
|
if (cta) {
|
|
136
162
|
const ctaEl = document.createElement('a');
|
|
137
163
|
ctaEl.className = 'jux-hero-cta jux-button jux-button-primary';
|
|
@@ -139,10 +165,10 @@ export class Hero extends Reactive {
|
|
|
139
165
|
ctaEl.textContent = cta;
|
|
140
166
|
content.appendChild(ctaEl);
|
|
141
167
|
}
|
|
142
|
-
|
|
168
|
+
|
|
143
169
|
hero.appendChild(content);
|
|
144
170
|
container.appendChild(hero);
|
|
145
|
-
|
|
171
|
+
|
|
146
172
|
return this;
|
|
147
173
|
}
|
|
148
174
|
|
|
@@ -153,18 +179,18 @@ export class Hero extends Reactive {
|
|
|
153
179
|
if (!juxComponent || typeof juxComponent !== 'object') {
|
|
154
180
|
throw new Error('Hero.renderTo: Invalid component - not an object');
|
|
155
181
|
}
|
|
156
|
-
|
|
157
|
-
if (!juxComponent.
|
|
158
|
-
throw new Error('Hero.renderTo: Invalid component - missing
|
|
182
|
+
|
|
183
|
+
if (!juxComponent._id || typeof juxComponent._id !== 'string') {
|
|
184
|
+
throw new Error('Hero.renderTo: Invalid component - missing _id (not a Jux component)');
|
|
159
185
|
}
|
|
160
|
-
|
|
161
|
-
return this.render(`#${juxComponent.
|
|
186
|
+
|
|
187
|
+
return this.render(`#${juxComponent._id}`);
|
|
162
188
|
}
|
|
163
189
|
}
|
|
164
190
|
|
|
165
191
|
/**
|
|
166
192
|
* Factory helper
|
|
167
193
|
*/
|
|
168
|
-
export function hero(
|
|
169
|
-
return new Hero(
|
|
194
|
+
export function hero(id: string, options: HeroOptions = {}): Hero {
|
|
195
|
+
return new Hero(id, options);
|
|
170
196
|
}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { ErrorHandler } from './error-handler.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Include - Include external resources (CSS, JS, images, fonts, etc.)
|
|
5
|
+
* Auto-detects resource type from URL and provides simple, fluent API
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
type IncludeType = 'stylesheet' | 'script' | 'image' | 'font' | 'preload' | 'prefetch' | 'module';
|
|
9
|
+
type IncludeLocation = 'head' | 'body-start' | 'body-end';
|
|
10
|
+
|
|
11
|
+
interface IncludeOptions {
|
|
12
|
+
as?: string;
|
|
13
|
+
crossOrigin?: 'anonymous' | 'use-credentials';
|
|
14
|
+
integrity?: string;
|
|
15
|
+
async?: boolean;
|
|
16
|
+
defer?: boolean;
|
|
17
|
+
location?: IncludeLocation;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class Include {
|
|
21
|
+
private url: string;
|
|
22
|
+
private type: IncludeType;
|
|
23
|
+
private options: IncludeOptions = {};
|
|
24
|
+
private element: HTMLElement | null = null;
|
|
25
|
+
|
|
26
|
+
constructor(urlOrFile: string) {
|
|
27
|
+
this.url = urlOrFile;
|
|
28
|
+
this.type = this.detectType(urlOrFile);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/* -------------------------
|
|
32
|
+
* Type Detection
|
|
33
|
+
* ------------------------- */
|
|
34
|
+
|
|
35
|
+
private detectType(url: string): IncludeType {
|
|
36
|
+
if (url.endsWith('.css')) return 'stylesheet';
|
|
37
|
+
if (url.endsWith('.js') || url.endsWith('.mjs')) return 'script';
|
|
38
|
+
if (url.match(/\.(png|jpg|jpeg|gif|svg|webp)$/i)) return 'image';
|
|
39
|
+
if (url.match(/\.(woff|woff2|ttf|otf|eot)$/i)) return 'font';
|
|
40
|
+
return 'preload';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* -------------------------
|
|
44
|
+
* Fluent Type Setters
|
|
45
|
+
* ------------------------- */
|
|
46
|
+
|
|
47
|
+
withCss(): this {
|
|
48
|
+
this.type = 'stylesheet';
|
|
49
|
+
return this;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
withJs(options?: { async?: boolean; defer?: boolean }): this {
|
|
53
|
+
this.type = 'script';
|
|
54
|
+
if (options?.async) this.options.async = true;
|
|
55
|
+
if (options?.defer) this.options.defer = true;
|
|
56
|
+
return this;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
withModule(): this {
|
|
60
|
+
this.type = 'module';
|
|
61
|
+
return this;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
withImage(): this {
|
|
65
|
+
this.type = 'image';
|
|
66
|
+
return this;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
withFont(): this {
|
|
70
|
+
this.type = 'font';
|
|
71
|
+
return this;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
withPreload(as?: string): this {
|
|
75
|
+
this.type = 'preload';
|
|
76
|
+
if (as) this.options.as = as;
|
|
77
|
+
return this;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
withPrefetch(): this {
|
|
81
|
+
this.type = 'prefetch';
|
|
82
|
+
return this;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* -------------------------
|
|
86
|
+
* Options
|
|
87
|
+
* ------------------------- */
|
|
88
|
+
|
|
89
|
+
with(options: IncludeOptions): this {
|
|
90
|
+
this.options = { ...this.options, ...options };
|
|
91
|
+
return this;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
inHead(): this {
|
|
95
|
+
this.options.location = 'head';
|
|
96
|
+
return this;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
inBody(): this {
|
|
100
|
+
this.options.location = 'body-end';
|
|
101
|
+
return this;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async(): this {
|
|
105
|
+
this.options.async = true;
|
|
106
|
+
return this;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
defer(): this {
|
|
110
|
+
this.options.defer = true;
|
|
111
|
+
return this;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
crossOrigin(value: 'anonymous' | 'use-credentials' = 'anonymous'): this {
|
|
115
|
+
this.options.crossOrigin = value;
|
|
116
|
+
return this;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
integrity(hash: string): this {
|
|
120
|
+
this.options.integrity = hash;
|
|
121
|
+
return this;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/* -------------------------
|
|
125
|
+
* Render
|
|
126
|
+
* ------------------------- */
|
|
127
|
+
|
|
128
|
+
render(): this {
|
|
129
|
+
if (typeof document === 'undefined') return this;
|
|
130
|
+
|
|
131
|
+
try {
|
|
132
|
+
this.remove();
|
|
133
|
+
|
|
134
|
+
let element: HTMLElement;
|
|
135
|
+
|
|
136
|
+
switch (this.type) {
|
|
137
|
+
case 'stylesheet':
|
|
138
|
+
element = this.createStylesheet();
|
|
139
|
+
break;
|
|
140
|
+
case 'script':
|
|
141
|
+
case 'module':
|
|
142
|
+
element = this.createScript();
|
|
143
|
+
break;
|
|
144
|
+
case 'image':
|
|
145
|
+
case 'font':
|
|
146
|
+
case 'preload':
|
|
147
|
+
case 'prefetch':
|
|
148
|
+
element = this.createLink();
|
|
149
|
+
break;
|
|
150
|
+
default:
|
|
151
|
+
throw new Error(`Unknown include type: ${this.type}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const container = this.getContainer();
|
|
155
|
+
const location = this.options.location || 'head';
|
|
156
|
+
|
|
157
|
+
if (location === 'body-end') {
|
|
158
|
+
container.appendChild(element);
|
|
159
|
+
} else {
|
|
160
|
+
container.insertBefore(element, container.firstChild);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
this.element = element;
|
|
164
|
+
} catch (error: any) {
|
|
165
|
+
ErrorHandler.captureError({
|
|
166
|
+
component: 'Include',
|
|
167
|
+
method: 'render',
|
|
168
|
+
message: error.message,
|
|
169
|
+
stack: error.stack,
|
|
170
|
+
timestamp: new Date(),
|
|
171
|
+
context: {
|
|
172
|
+
type: this.type,
|
|
173
|
+
url: this.url,
|
|
174
|
+
location: this.options.location,
|
|
175
|
+
error: 'runtime_exception'
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return this;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/* -------------------------
|
|
184
|
+
* Element Creation
|
|
185
|
+
* ------------------------- */
|
|
186
|
+
|
|
187
|
+
private createStylesheet(): HTMLLinkElement {
|
|
188
|
+
const link = document.createElement('link');
|
|
189
|
+
link.rel = 'stylesheet';
|
|
190
|
+
link.href = this.url;
|
|
191
|
+
|
|
192
|
+
if (this.options.crossOrigin) link.crossOrigin = this.options.crossOrigin;
|
|
193
|
+
if (this.options.integrity) link.integrity = this.options.integrity;
|
|
194
|
+
|
|
195
|
+
link.onload = () => console.log(`✓ Stylesheet loaded: ${this.url}`);
|
|
196
|
+
link.onerror = () => this.handleError('stylesheet');
|
|
197
|
+
|
|
198
|
+
return link;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private createScript(): HTMLScriptElement {
|
|
202
|
+
const script = document.createElement('script');
|
|
203
|
+
script.src = this.url;
|
|
204
|
+
|
|
205
|
+
if (this.type === 'module') script.type = 'module';
|
|
206
|
+
if (this.options.async) script.async = true;
|
|
207
|
+
if (this.options.defer) script.defer = true;
|
|
208
|
+
if (this.options.crossOrigin) script.crossOrigin = this.options.crossOrigin;
|
|
209
|
+
if (this.options.integrity) script.integrity = this.options.integrity;
|
|
210
|
+
|
|
211
|
+
script.onload = () => console.log(`✓ Script loaded: ${this.url}`);
|
|
212
|
+
script.onerror = () => this.handleError('script');
|
|
213
|
+
|
|
214
|
+
return script;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private createLink(): HTMLLinkElement {
|
|
218
|
+
const link = document.createElement('link');
|
|
219
|
+
|
|
220
|
+
// Set rel based on type
|
|
221
|
+
if (this.type === 'prefetch') {
|
|
222
|
+
link.rel = 'prefetch';
|
|
223
|
+
} else {
|
|
224
|
+
link.rel = 'preload';
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
link.href = this.url;
|
|
228
|
+
|
|
229
|
+
// Set 'as' attribute
|
|
230
|
+
if (this.type === 'image') {
|
|
231
|
+
link.as = 'image';
|
|
232
|
+
} else if (this.type === 'font') {
|
|
233
|
+
link.as = 'font';
|
|
234
|
+
link.crossOrigin = this.options.crossOrigin || 'anonymous';
|
|
235
|
+
} else if (this.options.as) {
|
|
236
|
+
link.as = this.options.as;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (this.options.crossOrigin && this.type !== 'font') {
|
|
240
|
+
link.crossOrigin = this.options.crossOrigin;
|
|
241
|
+
}
|
|
242
|
+
if (this.options.integrity) {
|
|
243
|
+
link.integrity = this.options.integrity;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return link;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/* -------------------------
|
|
250
|
+
* Helpers
|
|
251
|
+
* ------------------------- */
|
|
252
|
+
|
|
253
|
+
private getContainer(): HTMLElement {
|
|
254
|
+
const location = this.options.location || 'head';
|
|
255
|
+
return location === 'head' ? document.head : document.body;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private handleError(type: string): void {
|
|
259
|
+
const error = new Error(`Failed to load ${type}: ${this.url}`);
|
|
260
|
+
ErrorHandler.captureError({
|
|
261
|
+
component: 'Include',
|
|
262
|
+
method: `create${type.charAt(0).toUpperCase() + type.slice(1)}`,
|
|
263
|
+
message: error.message,
|
|
264
|
+
timestamp: new Date(),
|
|
265
|
+
context: { url: this.url, type, error: 'load_failed' }
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
remove(): this {
|
|
270
|
+
if (this.element?.parentNode) {
|
|
271
|
+
this.element.parentNode.removeChild(this.element);
|
|
272
|
+
this.element = null;
|
|
273
|
+
}
|
|
274
|
+
return this;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Factory function - auto-detects type and renders immediately
|
|
280
|
+
*
|
|
281
|
+
* Usage:
|
|
282
|
+
* jux.include('styles.css');
|
|
283
|
+
* jux.include('script.js').async();
|
|
284
|
+
* jux.include('app.mjs').withModule();
|
|
285
|
+
* jux.include('custom.js').withJs({ async: true, defer: true });
|
|
286
|
+
* jux.include('https://cdn.com/lib.js').inHead().defer();
|
|
287
|
+
*/
|
|
288
|
+
export function include(urlOrFile: string): Include {
|
|
289
|
+
const imp = new Include(urlOrFile);
|
|
290
|
+
imp.render();
|
|
291
|
+
return imp;
|
|
292
|
+
}
|