universal-modal-monorepo 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.
Files changed (106) hide show
  1. package/AUTHORS +5 -0
  2. package/CHANGELOG.md +10 -0
  3. package/LICENSE +21 -0
  4. package/NOTICE +6 -0
  5. package/README.md +239 -0
  6. package/SECURITY.md +21 -0
  7. package/docs/ACCESSIBILITY.md +43 -0
  8. package/docs/API.md +56 -0
  9. package/docs/THEMING.md +65 -0
  10. package/examples/angular/app.component.html +137 -0
  11. package/examples/angular/app.component.ts +53 -0
  12. package/examples/angular/index.html +99 -0
  13. package/examples/angular/main.js +121 -0
  14. package/examples/angular/styles.css +25 -0
  15. package/examples/index.html +53 -0
  16. package/examples/react/App.tsx +242 -0
  17. package/examples/react/index.html +26 -0
  18. package/examples/react/main.js +144 -0
  19. package/examples/react/styles.css +1 -0
  20. package/examples/shared/gallery.css +177 -0
  21. package/examples/vanilla/index.html +89 -0
  22. package/examples/vanilla/main.js +174 -0
  23. package/examples/vanilla/styles.css +1 -0
  24. package/examples/vue/App.vue +190 -0
  25. package/examples/vue/index.html +23 -0
  26. package/examples/vue/main.js +174 -0
  27. package/examples/vue/styles.css +1 -0
  28. package/examples/web-component/index.html +190 -0
  29. package/examples/web-component/main.js +58 -0
  30. package/examples/web-component/styles.css +11 -0
  31. package/package.json +36 -0
  32. package/packages/universal-modal/AUTHORS +5 -0
  33. package/packages/universal-modal/CHANGELOG.md +10 -0
  34. package/packages/universal-modal/LICENSE +21 -0
  35. package/packages/universal-modal/NOTICE +6 -0
  36. package/packages/universal-modal/README.md +29 -0
  37. package/packages/universal-modal/package.json +83 -0
  38. package/packages/universal-modal/scripts/copy-styles.mjs +11 -0
  39. package/packages/universal-modal/src/core/constants.ts +50 -0
  40. package/packages/universal-modal/src/core/event-emitter.ts +46 -0
  41. package/packages/universal-modal/src/core/focus-trap.ts +101 -0
  42. package/packages/universal-modal/src/core/index.ts +39 -0
  43. package/packages/universal-modal/src/core/modal-engine.ts +410 -0
  44. package/packages/universal-modal/src/core/modal-stack.ts +68 -0
  45. package/packages/universal-modal/src/core/scroll-lock.ts +78 -0
  46. package/packages/universal-modal/src/core/theme.ts +50 -0
  47. package/packages/universal-modal/src/core/types.ts +196 -0
  48. package/packages/universal-modal/src/core/utils.ts +114 -0
  49. package/packages/universal-modal/src/dom/index.ts +1 -0
  50. package/packages/universal-modal/src/dom/modal-renderer.ts +191 -0
  51. package/packages/universal-modal/src/index.ts +2 -0
  52. package/packages/universal-modal/src/styles/modal.css +368 -0
  53. package/packages/universal-modal/tsconfig.json +24 -0
  54. package/packages/universal-modal/tsup.config.ts +12 -0
  55. package/packages/universal-modal-angular/AUTHORS +5 -0
  56. package/packages/universal-modal-angular/CHANGELOG.md +9 -0
  57. package/packages/universal-modal-angular/LICENSE +21 -0
  58. package/packages/universal-modal-angular/NOTICE +6 -0
  59. package/packages/universal-modal-angular/README.md +12 -0
  60. package/packages/universal-modal-angular/package.json +85 -0
  61. package/packages/universal-modal-angular/src/index.ts +10 -0
  62. package/packages/universal-modal-angular/src/universal-modal.component.ts +275 -0
  63. package/packages/universal-modal-angular/tsconfig.json +20 -0
  64. package/packages/universal-modal-angular/tsup.config.ts +10 -0
  65. package/packages/universal-modal-react/AUTHORS +5 -0
  66. package/packages/universal-modal-react/CHANGELOG.md +9 -0
  67. package/packages/universal-modal-react/LICENSE +21 -0
  68. package/packages/universal-modal-react/NOTICE +6 -0
  69. package/packages/universal-modal-react/README.md +12 -0
  70. package/packages/universal-modal-react/package.json +87 -0
  71. package/packages/universal-modal-react/src/UniversalModal.tsx +340 -0
  72. package/packages/universal-modal-react/src/index.ts +10 -0
  73. package/packages/universal-modal-react/tsconfig.json +19 -0
  74. package/packages/universal-modal-react/tsup.config.ts +13 -0
  75. package/packages/universal-modal-vanilla/AUTHORS +5 -0
  76. package/packages/universal-modal-vanilla/CHANGELOG.md +9 -0
  77. package/packages/universal-modal-vanilla/LICENSE +21 -0
  78. package/packages/universal-modal-vanilla/NOTICE +6 -0
  79. package/packages/universal-modal-vanilla/README.md +12 -0
  80. package/packages/universal-modal-vanilla/package.json +79 -0
  81. package/packages/universal-modal-vanilla/src/create-modal.ts +125 -0
  82. package/packages/universal-modal-vanilla/src/index.ts +14 -0
  83. package/packages/universal-modal-vanilla/tsconfig.json +18 -0
  84. package/packages/universal-modal-vanilla/tsup.config.ts +10 -0
  85. package/packages/universal-modal-vue/AUTHORS +5 -0
  86. package/packages/universal-modal-vue/CHANGELOG.md +9 -0
  87. package/packages/universal-modal-vue/LICENSE +21 -0
  88. package/packages/universal-modal-vue/NOTICE +6 -0
  89. package/packages/universal-modal-vue/README.md +12 -0
  90. package/packages/universal-modal-vue/package.json +83 -0
  91. package/packages/universal-modal-vue/src/UniversalModal.ts +342 -0
  92. package/packages/universal-modal-vue/src/index.ts +10 -0
  93. package/packages/universal-modal-vue/tsconfig.json +18 -0
  94. package/packages/universal-modal-vue/tsup.config.ts +10 -0
  95. package/packages/universal-modal-web-component/AUTHORS +5 -0
  96. package/packages/universal-modal-web-component/CHANGELOG.md +9 -0
  97. package/packages/universal-modal-web-component/LICENSE +21 -0
  98. package/packages/universal-modal-web-component/NOTICE +6 -0
  99. package/packages/universal-modal-web-component/README.md +12 -0
  100. package/packages/universal-modal-web-component/package.json +83 -0
  101. package/packages/universal-modal-web-component/src/index.ts +11 -0
  102. package/packages/universal-modal-web-component/src/universal-modal-element.ts +424 -0
  103. package/packages/universal-modal-web-component/tsconfig.json +18 -0
  104. package/packages/universal-modal-web-component/tsup.config.ts +10 -0
  105. package/scripts/copy-styles.mjs +11 -0
  106. package/scripts/serve-examples.mjs +48 -0
@@ -0,0 +1,424 @@
1
+ import {
2
+ CSS_CLASSES,
3
+ ModalEngine,
4
+ type CloseReason,
5
+ type ModalHostElements,
6
+ type ModalPlacement,
7
+ type ModalRole,
8
+ type ModalSize,
9
+ } from '@poluru-labs/universal-modal';
10
+
11
+ const observed = [
12
+ 'open',
13
+ 'size',
14
+ 'placement',
15
+ 'title',
16
+ 'description',
17
+ 'persistent',
18
+ 'close-on-esc',
19
+ 'close-on-backdrop',
20
+ 'show-close-icon',
21
+ 'role',
22
+ 'confirm-text',
23
+ 'cancel-text',
24
+ 'hide-actions',
25
+ 'loading',
26
+ 'z-index-base',
27
+ ] as const;
28
+
29
+ type ObservedAttr = (typeof observed)[number];
30
+
31
+ function boolAttr(value: string | null): boolean {
32
+ return value !== null && value !== 'false';
33
+ }
34
+
35
+ /**
36
+ * `<universal-modal>` custom element.
37
+ *
38
+ * Light-DOM children are projected by moving them into the modal panel
39
+ * (not cloning), so event listeners and form state keep working.
40
+ *
41
+ * Slots: `header`, default (body), `footer`
42
+ * Events: `beforeOpen`, `afterOpen`, `beforeClose`, `afterClose`, `confirm`, `cancel`, `openChange`
43
+ */
44
+ export class UniversalModalElement extends HTMLElement {
45
+ static get observedAttributes(): string[] {
46
+ return [...observed];
47
+ }
48
+
49
+ private engine: ModalEngine | null = null;
50
+ private hostEls: ModalHostElements | null = null;
51
+ private titleEl: HTMLHeadingElement | null = null;
52
+ private descriptionEl: HTMLParagraphElement | null = null;
53
+ private defaultActions: HTMLElement | null = null;
54
+ private ready = false;
55
+ private opening = false;
56
+ private closing = false;
57
+
58
+ connectedCallback(): void {
59
+ if (this.ready) return;
60
+ this.ready = true;
61
+
62
+ // Ensure upgrades wait for children to be available.
63
+ if (this.childNodes.length === 0) {
64
+ queueMicrotask(() => this.initialize());
65
+ return;
66
+ }
67
+ this.initialize();
68
+ }
69
+
70
+ disconnectedCallback(): void {
71
+ this.engine?.destroy();
72
+ this.hostEls?.root.remove();
73
+ this.engine = null;
74
+ this.hostEls = null;
75
+ this.titleEl = null;
76
+ this.descriptionEl = null;
77
+ this.defaultActions = null;
78
+ this.ready = false;
79
+ }
80
+
81
+ attributeChangedCallback(
82
+ name: ObservedAttr,
83
+ oldValue: string | null,
84
+ newValue: string | null,
85
+ ): void {
86
+ if (oldValue === newValue || !this.engine || !this.hostEls) return;
87
+
88
+ if (name === 'open') {
89
+ if (boolAttr(newValue)) {
90
+ if (!this.engine.isOpen && !this.opening) void this.openFromAttr();
91
+ } else if (this.engine.isOpen && !this.closing) {
92
+ void this.engine.close('programmatic');
93
+ }
94
+ return;
95
+ }
96
+
97
+ if (name === 'loading') {
98
+ this.engine.setLoading(boolAttr(newValue));
99
+ return;
100
+ }
101
+
102
+ this.applyOptions();
103
+ }
104
+
105
+ async open(): Promise<boolean> {
106
+ if (!this.engine) this.initialize();
107
+ if (!this.engine || this.engine.isOpen || this.opening) return false;
108
+
109
+ this.opening = true;
110
+ try {
111
+ const ok = await this.engine.open();
112
+ if (ok && !this.hasAttribute('open')) {
113
+ this.setAttribute('open', '');
114
+ }
115
+ return ok;
116
+ } finally {
117
+ this.opening = false;
118
+ }
119
+ }
120
+
121
+ async close(reason: CloseReason = 'programmatic'): Promise<boolean> {
122
+ if (!this.engine || !this.engine.isOpen || this.closing) return false;
123
+
124
+ this.closing = true;
125
+ try {
126
+ return await this.engine.close(reason);
127
+ } finally {
128
+ this.closing = false;
129
+ }
130
+ }
131
+
132
+ async confirm(): Promise<void> {
133
+ await this.engine?.confirm();
134
+ }
135
+
136
+ async cancel(): Promise<void> {
137
+ await this.engine?.cancel();
138
+ }
139
+
140
+ get isOpen(): boolean {
141
+ return this.engine?.isOpen ?? false;
142
+ }
143
+
144
+ private async openFromAttr(): Promise<void> {
145
+ this.opening = true;
146
+ try {
147
+ await this.engine?.open();
148
+ } finally {
149
+ this.opening = false;
150
+ }
151
+ }
152
+
153
+ private initialize(): void {
154
+ if (this.engine) return;
155
+
156
+ this.engine = new ModalEngine({
157
+ ...this.readOptions(),
158
+ // Content is projected from light DOM; avoid HTML injection factories.
159
+ header: undefined,
160
+ body: undefined,
161
+ footer: undefined,
162
+ });
163
+
164
+ this.hostEls = this.buildHost();
165
+ this.projectLightDom();
166
+ this.applyOptions();
167
+ this.engine.attach(this.hostEls);
168
+ document.body.appendChild(this.hostEls.root);
169
+ this.wireEvents();
170
+
171
+ // Keep the host out of layout; modal lives on document.body.
172
+ this.style.display = 'none';
173
+
174
+ if (this.hasAttribute('open')) {
175
+ void this.openFromAttr();
176
+ }
177
+ }
178
+
179
+ private readOptions() {
180
+ return {
181
+ id: this.id || undefined,
182
+ size: (this.getAttribute('size') as ModalSize) || undefined,
183
+ placement: (this.getAttribute('placement') as ModalPlacement) || undefined,
184
+ title: this.getAttribute('title') || undefined,
185
+ description: this.getAttribute('description') || undefined,
186
+ persistent: this.hasAttribute('persistent'),
187
+ closeOnEsc: this.hasAttribute('close-on-esc')
188
+ ? boolAttr(this.getAttribute('close-on-esc'))
189
+ : undefined,
190
+ closeOnBackdrop: this.hasAttribute('close-on-backdrop')
191
+ ? boolAttr(this.getAttribute('close-on-backdrop'))
192
+ : undefined,
193
+ showCloseIcon: this.hasAttribute('show-close-icon')
194
+ ? boolAttr(this.getAttribute('show-close-icon'))
195
+ : undefined,
196
+ role: (this.getAttribute('role') as ModalRole) || undefined,
197
+ confirmText: this.getAttribute('confirm-text') || undefined,
198
+ cancelText: this.getAttribute('cancel-text') || undefined,
199
+ hideActions: this.hasAttribute('hide-actions'),
200
+ zIndexBase: this.hasAttribute('z-index-base')
201
+ ? Number(this.getAttribute('z-index-base'))
202
+ : undefined,
203
+ };
204
+ }
205
+
206
+ private applyOptions(): void {
207
+ if (!this.engine || !this.hostEls) return;
208
+
209
+ const opts = this.readOptions();
210
+ this.engine.updateOptions(opts);
211
+
212
+ const { panel, header, footer, closeButton, confirmButton, cancelButton } = this.hostEls;
213
+ panel.dataset.size = opts.size ?? 'md';
214
+ panel.dataset.placement = opts.placement ?? 'center';
215
+
216
+ if (closeButton) {
217
+ closeButton.hidden = opts.showCloseIcon === false;
218
+ }
219
+
220
+ const hasCustomHeader = Boolean(header?.querySelector('[data-um-projected="header"]'));
221
+ if (!hasCustomHeader && this.titleEl && this.descriptionEl && header) {
222
+ const title = opts.title ?? '';
223
+ const description = opts.description ?? '';
224
+ this.titleEl.textContent = title;
225
+ this.titleEl.hidden = !title;
226
+ this.titleEl.id = `${this.engine.id}-title`;
227
+ this.descriptionEl.textContent = description;
228
+ this.descriptionEl.hidden = !description;
229
+ this.descriptionEl.id = `${this.engine.id}-description`;
230
+ header.hidden = !title && !description;
231
+ }
232
+
233
+ if (confirmButton) {
234
+ const spinner = confirmButton.querySelector(`.${CSS_CLASSES.spinner}`);
235
+ confirmButton.textContent = opts.confirmText ?? 'Confirm';
236
+ if (spinner) confirmButton.appendChild(spinner);
237
+ }
238
+ if (cancelButton) cancelButton.textContent = opts.cancelText ?? 'Cancel';
239
+
240
+ const hasCustomFooter = Boolean(footer?.querySelector('[data-um-projected="footer"]'));
241
+ if (this.defaultActions) {
242
+ this.defaultActions.hidden = Boolean(opts.hideActions) || hasCustomFooter;
243
+ }
244
+ if (footer) {
245
+ footer.hidden = Boolean(opts.hideActions) && !hasCustomFooter;
246
+ }
247
+ }
248
+
249
+ private projectLightDom(): void {
250
+ if (!this.hostEls) return;
251
+
252
+ const { header, body, footer } = this.hostEls;
253
+ const projected = Array.from(this.childNodes);
254
+
255
+ const headerNodes: Node[] = [];
256
+ const footerNodes: Node[] = [];
257
+ const bodyNodes: Node[] = [];
258
+
259
+ for (const node of projected) {
260
+ if (node.nodeType === Node.ELEMENT_NODE) {
261
+ const el = node as HTMLElement;
262
+ const slot = el.getAttribute('slot');
263
+ if (slot === 'header') {
264
+ el.setAttribute('data-um-projected', 'header');
265
+ headerNodes.push(el);
266
+ continue;
267
+ }
268
+ if (slot === 'footer') {
269
+ el.setAttribute('data-um-projected', 'footer');
270
+ footerNodes.push(el);
271
+ continue;
272
+ }
273
+ bodyNodes.push(el);
274
+ continue;
275
+ }
276
+ if (node.nodeType === Node.TEXT_NODE && node.textContent?.trim()) {
277
+ bodyNodes.push(node);
278
+ }
279
+ }
280
+
281
+ if (header) {
282
+ if (headerNodes.length) {
283
+ header.replaceChildren(...headerNodes);
284
+ header.hidden = false;
285
+ } else {
286
+ header.replaceChildren();
287
+ if (this.titleEl) header.appendChild(this.titleEl);
288
+ if (this.descriptionEl) header.appendChild(this.descriptionEl);
289
+ }
290
+ }
291
+
292
+ if (body) {
293
+ body.replaceChildren(...bodyNodes);
294
+ }
295
+
296
+ if (footer) {
297
+ footer.replaceChildren();
298
+ if (footerNodes.length) {
299
+ footer.append(...footerNodes);
300
+ footer.hidden = false;
301
+ } else if (this.defaultActions) {
302
+ footer.appendChild(this.defaultActions);
303
+ footer.hidden = this.hasAttribute('hide-actions');
304
+ } else {
305
+ footer.hidden = true;
306
+ }
307
+ }
308
+ }
309
+
310
+ private buildHost(): ModalHostElements {
311
+ const root = document.createElement('div');
312
+ root.className = CSS_CLASSES.root;
313
+ root.hidden = true;
314
+ root.setAttribute('aria-hidden', 'true');
315
+
316
+ const backdrop = document.createElement('div');
317
+ backdrop.className = CSS_CLASSES.backdrop;
318
+ backdrop.setAttribute('data-um-backdrop', '');
319
+
320
+ const dialog = document.createElement('div');
321
+ dialog.className = CSS_CLASSES.dialog;
322
+
323
+ const panel = document.createElement('div');
324
+ panel.className = CSS_CLASSES.panel;
325
+
326
+ const closeButton = document.createElement('button');
327
+ closeButton.type = 'button';
328
+ closeButton.className = CSS_CLASSES.close;
329
+ closeButton.setAttribute('aria-label', 'Close dialog');
330
+ closeButton.innerHTML =
331
+ '<svg viewBox="0 0 24 24" width="20" height="20" aria-hidden="true" focusable="false"><path d="M6.4 6.4l11.2 11.2M17.6 6.4L6.4 17.6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>';
332
+
333
+ const header = document.createElement('div');
334
+ header.className = CSS_CLASSES.header;
335
+
336
+ this.titleEl = document.createElement('h2');
337
+ this.titleEl.className = CSS_CLASSES.title;
338
+
339
+ this.descriptionEl = document.createElement('p');
340
+ this.descriptionEl.className = CSS_CLASSES.description;
341
+
342
+ const body = document.createElement('div');
343
+ body.className = CSS_CLASSES.body;
344
+
345
+ const footer = document.createElement('div');
346
+ footer.className = CSS_CLASSES.footer;
347
+
348
+ const cancelButton = document.createElement('button');
349
+ cancelButton.type = 'button';
350
+ cancelButton.className = CSS_CLASSES.cancel;
351
+
352
+ const confirmButton = document.createElement('button');
353
+ confirmButton.type = 'button';
354
+ confirmButton.className = CSS_CLASSES.confirm;
355
+ const spinner = document.createElement('span');
356
+ spinner.className = CSS_CLASSES.spinner;
357
+ spinner.setAttribute('aria-hidden', 'true');
358
+ confirmButton.appendChild(spinner);
359
+
360
+ this.defaultActions = document.createElement('div');
361
+ this.defaultActions.className = CSS_CLASSES.actions;
362
+ this.defaultActions.append(cancelButton, confirmButton);
363
+
364
+ panel.append(closeButton, header, body, footer);
365
+ dialog.appendChild(panel);
366
+ root.append(backdrop, dialog);
367
+
368
+ return {
369
+ root,
370
+ backdrop,
371
+ dialog,
372
+ panel,
373
+ header,
374
+ body,
375
+ footer,
376
+ closeButton,
377
+ confirmButton,
378
+ cancelButton,
379
+ };
380
+ }
381
+
382
+ private wireEvents(): void {
383
+ if (!this.engine) return;
384
+
385
+ const forward = (type: string, detail?: unknown) => {
386
+ this.dispatchEvent(
387
+ new CustomEvent(type, {
388
+ detail,
389
+ bubbles: true,
390
+ composed: true,
391
+ }),
392
+ );
393
+ };
394
+
395
+ this.engine.on('beforeOpen', () => forward('beforeOpen'));
396
+ this.engine.on('afterOpen', () => forward('afterOpen'));
397
+ this.engine.on('beforeClose', (reason) => forward('beforeClose', reason));
398
+ this.engine.on('afterClose', (reason) => {
399
+ if (this.hasAttribute('open')) this.removeAttribute('open');
400
+ forward('afterClose', reason);
401
+ });
402
+ this.engine.on('confirm', () => forward('confirm'));
403
+ this.engine.on('cancel', () => forward('cancel'));
404
+ this.engine.on('openChange', (open) => forward('openChange', open));
405
+ this.engine.on('loadingChange', (loading) => {
406
+ if (loading) this.setAttribute('loading', '');
407
+ else this.removeAttribute('loading');
408
+ forward('loadingChange', loading);
409
+ });
410
+ }
411
+ }
412
+
413
+ export function defineUniversalModal(tag = 'universal-modal'): void {
414
+ if (typeof customElements === 'undefined') return;
415
+ if (!customElements.get(tag)) {
416
+ customElements.define(tag, UniversalModalElement);
417
+ }
418
+ }
419
+
420
+ declare global {
421
+ interface HTMLElementTagNameMap {
422
+ 'universal-modal': UniversalModalElement;
423
+ }
424
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
7
+ "strict": true,
8
+ "skipLibCheck": true,
9
+ "noUnusedLocals": true,
10
+ "noUnusedParameters": true,
11
+ "esModuleInterop": true,
12
+ "isolatedModules": true,
13
+ "rootDir": "src",
14
+ "outDir": "dist"
15
+ },
16
+ "include": ["src/**/*"],
17
+ "exclude": ["node_modules", "dist"]
18
+ }
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from 'tsup';
2
+
3
+ export default defineConfig({
4
+ entry: { index: 'src/index.ts' },
5
+ format: ['esm', 'cjs'],
6
+ dts: true,
7
+ sourcemap: true,
8
+ clean: true,
9
+ external: ['@poluru-labs/universal-modal'],
10
+ });
@@ -0,0 +1,11 @@
1
+ import { copyFileSync, mkdirSync } from 'node:fs';
2
+ import { dirname, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
6
+ const source = resolve(root, 'src/styles/modal.css');
7
+ const target = resolve(root, 'dist/styles.css');
8
+
9
+ mkdirSync(dirname(target), { recursive: true });
10
+ copyFileSync(source, target);
11
+ console.log('Copied styles → dist/styles.css');
@@ -0,0 +1,48 @@
1
+ import { createServer } from 'node:http';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { extname, join, normalize } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ const root = join(fileURLToPath(new URL('.', import.meta.url)), '..');
7
+ const port = Number(process.env.PORT || 8080);
8
+
9
+ const types = {
10
+ '.html': 'text/html; charset=utf-8',
11
+ '.js': 'text/javascript; charset=utf-8',
12
+ '.css': 'text/css; charset=utf-8',
13
+ '.map': 'application/json; charset=utf-8',
14
+ '.svg': 'image/svg+xml',
15
+ '.ts': 'text/plain; charset=utf-8',
16
+ '.tsx': 'text/plain; charset=utf-8',
17
+ '.vue': 'text/plain; charset=utf-8',
18
+ };
19
+
20
+ createServer(async (req, res) => {
21
+ try {
22
+ const url = new URL(req.url || '/', `http://localhost:${port}`);
23
+ let pathname = decodeURIComponent(url.pathname);
24
+ if (pathname === '/') pathname = '/examples/index.html';
25
+
26
+ const filePath = normalize(join(root, pathname.replace(/^\//, '')));
27
+ if (!filePath.startsWith(root)) {
28
+ res.writeHead(403).end('Forbidden');
29
+ return;
30
+ }
31
+
32
+ const data = await readFile(filePath);
33
+ res.writeHead(200, {
34
+ 'Content-Type': types[extname(filePath)] || 'application/octet-stream',
35
+ });
36
+ res.end(data);
37
+ } catch {
38
+ res.writeHead(404).end('Not found');
39
+ }
40
+ }).listen(port, () => {
41
+ console.log(`Examples server: http://localhost:${port}`);
42
+ console.log(`Hub: http://localhost:${port}/examples/index.html`);
43
+ console.log(`Vanilla: http://localhost:${port}/examples/vanilla/index.html`);
44
+ console.log(`Web Component: http://localhost:${port}/examples/web-component/index.html`);
45
+ console.log(`React: http://localhost:${port}/examples/react/index.html`);
46
+ console.log(`Vue: http://localhost:${port}/examples/vue/index.html`);
47
+ console.log(`Angular: http://localhost:${port}/examples/angular/index.html`);
48
+ });