@withl5e/l5e 0.1.0-alpha.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 (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +24 -0
  3. package/dist/action.js +10 -0
  4. package/dist/action.js.map +1 -0
  5. package/dist/client-D67hK4Yy.js +9 -0
  6. package/dist/client-D67hK4Yy.js.map +1 -0
  7. package/dist/entry-server-Ckh6zfgm.js +258 -0
  8. package/dist/entry-server-Ckh6zfgm.js.map +1 -0
  9. package/dist/entry-server.js +12 -0
  10. package/dist/entry-server.js.map +1 -0
  11. package/dist/generateMetadata-C5QsMS-H.js +144 -0
  12. package/dist/generateMetadata-C5QsMS-H.js.map +1 -0
  13. package/dist/index-BIt7MJT9.js +163 -0
  14. package/dist/index-BIt7MJT9.js.map +1 -0
  15. package/dist/index.js +49 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/island/client.js +5 -0
  18. package/dist/island/client.js.map +1 -0
  19. package/dist/island/runtime.js +98 -0
  20. package/dist/island/runtime.js.map +1 -0
  21. package/dist/island.js +39 -0
  22. package/dist/island.js.map +1 -0
  23. package/dist/jsx-runtime-C2Vw67N2.js +256 -0
  24. package/dist/jsx-runtime-C2Vw67N2.js.map +1 -0
  25. package/dist/jsx-runtime.js +26 -0
  26. package/dist/jsx-runtime.js.map +1 -0
  27. package/dist/middleware.js +9 -0
  28. package/dist/middleware.js.map +1 -0
  29. package/dist/seo.js +7 -0
  30. package/dist/seo.js.map +1 -0
  31. package/dist/server.js +489 -0
  32. package/dist/server.js.map +1 -0
  33. package/dist/swap/server.js +15 -0
  34. package/dist/swap/server.js.map +1 -0
  35. package/dist/swap.js +121 -0
  36. package/dist/swap.js.map +1 -0
  37. package/dist/tooltip.js +129 -0
  38. package/dist/tooltip.js.map +1 -0
  39. package/dist/vite-plugin.js +381 -0
  40. package/dist/vite-plugin.js.map +1 -0
  41. package/index.ts +1 -0
  42. package/package.json +129 -0
  43. package/src/action/define-action.ts +8 -0
  44. package/src/action/index.ts +2 -0
  45. package/src/action/types.ts +21 -0
  46. package/src/core/bundler.ts +275 -0
  47. package/src/core/const.ts +2 -0
  48. package/src/core/entry-server.d.ts +1 -0
  49. package/src/core/entry-server.ts +381 -0
  50. package/src/core/exceptions.ts +80 -0
  51. package/src/core/head-priority.ts +15 -0
  52. package/src/core/index.ts +40 -0
  53. package/src/core/jsx-runtime.ts +325 -0
  54. package/src/core/jsx-types.d.ts +548 -0
  55. package/src/core/render.ts +181 -0
  56. package/src/core/request.ts +31 -0
  57. package/src/core/server.ts +740 -0
  58. package/src/core/vite-plugin.ts +779 -0
  59. package/src/island/ClientIsland.ts +71 -0
  60. package/src/island/client.ts +3 -0
  61. package/src/island/index.ts +3 -0
  62. package/src/island/runtime.ts +149 -0
  63. package/src/island/strategy-registry.ts +10 -0
  64. package/src/island/types.ts +28 -0
  65. package/src/middleware/defineMiddleware.ts +5 -0
  66. package/src/middleware/index.ts +133 -0
  67. package/src/middleware/sequence.ts +105 -0
  68. package/src/middleware/types.ts +28 -0
  69. package/src/seo/generateMetadata.tsx +559 -0
  70. package/src/seo/index.ts +10 -0
  71. package/src/seo/mergeMetadata.ts +200 -0
  72. package/src/seo/types.ts +316 -0
  73. package/src/swap/SwapResponse.tsx +16 -0
  74. package/src/swap/create-swap.ts +121 -0
  75. package/src/swap/index.ts +8 -0
  76. package/src/swap/parse.ts +12 -0
  77. package/src/swap/server.ts +1 -0
  78. package/src/swap/swap.ts +57 -0
  79. package/src/swap/types.ts +47 -0
  80. package/src/swap/utils.ts +7 -0
  81. package/src/tooltip/index.ts +2 -0
  82. package/src/tooltip/tooltip-loader.ts +108 -0
  83. package/src/tooltip/tooltip-runtime.ts +173 -0
  84. package/types.d.ts +14 -0
@@ -0,0 +1,57 @@
1
+ import type { SwapMode } from './types';
2
+
3
+ export function swap(target: Element, nodes: Node[], mode: SwapMode): Node[] {
4
+ switch (mode) {
5
+ case 'innerHTML':
6
+ target.innerHTML = '';
7
+ const innerInserted: Node[] = [];
8
+ for (const n of nodes) {
9
+ const added = target.appendChild(n);
10
+ innerInserted.push(added);
11
+ }
12
+ return innerInserted;
13
+
14
+ case 'outerHTML': {
15
+ const parent = target.parentNode;
16
+ if (!parent) return [];
17
+ const outerInserted: Node[] = [];
18
+ for (const n of nodes) {
19
+ parent.insertBefore(n, target);
20
+ outerInserted.push(n);
21
+ }
22
+ parent.removeChild(target);
23
+ return outerInserted;
24
+ }
25
+
26
+ case 'beforebegin': {
27
+ const parent = target.parentNode;
28
+ if (!parent) return [];
29
+ return nodes.map((n) => parent.insertBefore(n, target));
30
+ }
31
+
32
+ case 'afterbegin': {
33
+ const first = target.firstChild;
34
+ return nodes.map((n) => target.insertBefore(n, first));
35
+ }
36
+
37
+ case 'beforeend':
38
+ return nodes.map((n) => target.appendChild(n));
39
+
40
+ case 'afterend': {
41
+ const parent = target.parentNode;
42
+ if (!parent) return [];
43
+ const ref = target.nextSibling;
44
+ return nodes.map((n) => parent.insertBefore(n, ref));
45
+ }
46
+
47
+ case 'delete':
48
+ target.remove();
49
+ return [];
50
+
51
+ case 'none':
52
+ return [];
53
+
54
+ default:
55
+ return [];
56
+ }
57
+ }
@@ -0,0 +1,47 @@
1
+ export type SwapMode =
2
+ | 'innerHTML'
3
+ | 'outerHTML'
4
+ | 'beforebegin'
5
+ | 'afterbegin'
6
+ | 'beforeend'
7
+ | 'afterend'
8
+ | 'delete'
9
+ | 'none';
10
+
11
+ export interface SwapContext {
12
+ triggerElement: HTMLElement | null;
13
+ triggerDataset: DOMStringMap;
14
+ }
15
+
16
+ export interface SwapOptions {
17
+ action?: (ctx: SwapContext) => Promise<Response>;
18
+ select?: string;
19
+ target: string;
20
+ swap?: SwapMode;
21
+ trigger?: string;
22
+ event?: string;
23
+ loading?: {
24
+ target?: string;
25
+ class?: string;
26
+ html?: string;
27
+ disabled?: boolean;
28
+ };
29
+ onBefore?: (ctx: SwapContext) => boolean | void;
30
+ onNodes?: (nodes: Node[], meta: SwapMeta) => Node[] | void;
31
+ onAfter?: (inserted: Node[], meta: SwapMeta, ctx: SwapContext) => void;
32
+ /**
33
+ * @deprecated Use `onAfter` instead. Will be removed in the next release.
34
+ */
35
+ onSwap?: (inserted: Node[], meta: SwapMeta) => void;
36
+ onError?: (err: Error, status?: number) => void;
37
+ }
38
+
39
+ export interface SwapInstance {
40
+ exec(input?: HTMLElement | string): Promise<void>;
41
+ destroy(): void;
42
+ }
43
+
44
+ export interface SwapMeta {
45
+ root: Element | null;
46
+ response?: Response;
47
+ }
@@ -0,0 +1,7 @@
1
+ export function debounce<T extends (...args: any[]) => void>(fn: T, ms: number): T {
2
+ let timer: ReturnType<typeof setTimeout>;
3
+ return ((...args: any[]) => {
4
+ clearTimeout(timer);
5
+ timer = setTimeout(() => fn(...args), ms);
6
+ }) as unknown as T;
7
+ }
@@ -0,0 +1,2 @@
1
+ export { initTooltips, setupTooltipObserver } from './tooltip-loader';
2
+ export { showTooltip, showTooltipMobile } from './tooltip-runtime';
@@ -0,0 +1,108 @@
1
+ // tooltip-loader.ts
2
+ // File này chỉ làm nhiệm vụ wake up tooltip khi cần thiết
3
+
4
+ // Định nghĩa kiểu cho tooltip host element
5
+ type TooltipHost = HTMLElement & {
6
+ dataset: {
7
+ tooltipId: string;
8
+ tooltipType?: string;
9
+ };
10
+ };
11
+
12
+ /**
13
+ * Kiểm tra xem thiết bị hiện tại có phải là thiết bị di động hay không
14
+ * @returns true nếu là thiết bị di động
15
+ */
16
+ function isMobileDevice(): boolean {
17
+ // Kiểm tra User Agent
18
+ const userAgent = navigator.userAgent.toLowerCase();
19
+ const isMobile =
20
+ /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini|mobile|tablet/i.test(userAgent);
21
+
22
+ // Kiểm tra thêm kích thước màn hình (dưới 768px thường được coi là thiết bị di động)
23
+ const isTouchScreen = window.matchMedia('(max-width: 768px)').matches;
24
+
25
+ return isMobile || isTouchScreen;
26
+ }
27
+
28
+ /**
29
+ * Hàm khởi tạo tooltip, chỉ load runtime khi cần thiết
30
+ */
31
+ export function initTooltips(): void {
32
+ const tooltipTriggers = document.querySelectorAll<TooltipHost>('[data-tooltip-id]');
33
+ if (tooltipTriggers.length === 0) return;
34
+
35
+ const mobile = isMobileDevice();
36
+
37
+ tooltipTriggers.forEach((trigger) => {
38
+ if (trigger.hasAttribute('data-tooltip-initialized')) return;
39
+ trigger.setAttribute('data-tooltip-initialized', 'true');
40
+
41
+ if (mobile) {
42
+ trigger.addEventListener('click', async (e) => {
43
+ e.preventDefault();
44
+ const { showTooltipMobile } = await import('./tooltip-runtime');
45
+ showTooltipMobile(trigger);
46
+ });
47
+ } else {
48
+ trigger.addEventListener(
49
+ 'pointerenter',
50
+ async () => {
51
+ const { showTooltip } = await import('./tooltip-runtime');
52
+ showTooltip(trigger);
53
+ },
54
+ { passive: true },
55
+ );
56
+ }
57
+ });
58
+ }
59
+
60
+ // Thêm một MutationObserver để theo dõi các phần tử DOM mới (cho React components)
61
+ export function setupTooltipObserver(): void {
62
+ // Kiểm tra xem đã có observer chưa
63
+ if (window.__tooltipObserver) return;
64
+
65
+ // Observer theo dõi khi có phần tử mới được thêm vào DOM
66
+ const observer = new MutationObserver((mutations) => {
67
+ let shouldInit = false;
68
+
69
+ mutations.forEach((mutation) => {
70
+ if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
71
+ // Kiểm tra xem có phần tử tooltip nào mới được thêm vào không
72
+ mutation.addedNodes.forEach((node) => {
73
+ if (node.nodeType === Node.ELEMENT_NODE) {
74
+ const element = node as HTMLElement;
75
+ // Kiểm tra nếu phần tử có data-tooltip-id hoặc chứa phần tử con có data-tooltip-id
76
+ if (
77
+ element.hasAttribute('data-tooltip-id') ||
78
+ element.querySelector('[data-tooltip-id]')
79
+ ) {
80
+ shouldInit = true;
81
+ }
82
+ }
83
+ });
84
+ }
85
+ });
86
+
87
+ // Chỉ gọi initTooltips nếu có phát hiện tooltip mới
88
+ if (shouldInit) {
89
+ initTooltips();
90
+ }
91
+ });
92
+
93
+ // Bắt đầu theo dõi toàn bộ DOM
94
+ observer.observe(document.body, {
95
+ childList: true,
96
+ subtree: true,
97
+ });
98
+
99
+ // Lưu observer vào window để tránh tạo nhiều observer
100
+ window.__tooltipObserver = observer;
101
+ }
102
+
103
+ // Typescript declaration cho window object
104
+ declare global {
105
+ interface Window {
106
+ __tooltipObserver?: MutationObserver;
107
+ }
108
+ }
@@ -0,0 +1,173 @@
1
+ // tooltip-runtime.ts
2
+ // Logic đầy đủ cho tooltip, chỉ được tải khi cần thiết
3
+
4
+ import type { Middleware, Placement } from '@floating-ui/dom';
5
+ import { autoUpdate, computePosition, flip, hide, offset, shift } from '@floating-ui/dom';
6
+
7
+ type TooltipHost = HTMLElement & {
8
+ dataset: {
9
+ tooltipId: string;
10
+ tooltipType?: string;
11
+ tooltipPlacement?: Placement;
12
+ href?: string;
13
+ };
14
+ };
15
+
16
+ export async function showTooltip(host: TooltipHost): Promise<void> {
17
+ /*
18
+ * Trong lúc chờ dynamic import, con trỏ có thể đã rời khỏi phần tử host.
19
+ * Nếu vậy, việc tạo tooltip là không cần thiết và sẽ không có sự kiện
20
+ * `pointerleave` nào được kích hoạt để ẩn tooltip.
21
+ */
22
+ if (!host.matches(':hover')) return;
23
+
24
+ const tip = document.createElement('div');
25
+ tip.className = 'tp';
26
+ tip.innerHTML = '<div class="tp-loading">Loading...</div>';
27
+ document.body.append(tip);
28
+
29
+ // --- Cleanup ---
30
+ let cleanupAutoUpdate: (() => void) | null = null;
31
+
32
+ const cleanup = () => {
33
+ if (cleanupAutoUpdate) {
34
+ cleanupAutoUpdate();
35
+ cleanupAutoUpdate = null;
36
+ }
37
+ if (document.body.contains(tip)) tip.remove();
38
+ };
39
+
40
+ host.addEventListener('pointerleave', cleanup);
41
+
42
+ // Con trỏ đã rời trước khi listener được gắn
43
+ if (!host.matches(':hover')) {
44
+ host.removeEventListener('pointerleave', cleanup);
45
+ tip.remove();
46
+ return;
47
+ }
48
+
49
+ // --- Position: autoUpdate thay thế manual scroll/resize ---
50
+ cleanupAutoUpdate = autoUpdate(host, tip, () => position(host, tip));
51
+
52
+ // --- Fetch nội dung từ server ---
53
+ try {
54
+ const { tooltipId: id, tooltipType: type } = host.dataset;
55
+ const html = await fetch(`/tooltip/${type}/${id}`, {
56
+ headers: { Accept: 'text/html' },
57
+ }).then((r) => r.text());
58
+
59
+ if (!document.body.contains(tip)) return;
60
+
61
+ tip.innerHTML = html;
62
+ await position(host, tip);
63
+ } catch {
64
+ if (document.body.contains(tip)) {
65
+ tip.innerHTML = '<div class="tp-error">Không thể tải tooltip</div>';
66
+ }
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Mobile: hiển thị tooltip dạng fullscreen popup.
72
+ * Nếu host có data-href, thêm nút "Xem chi tiết" dẫn đến link đó.
73
+ */
74
+ export async function showTooltipMobile(host: TooltipHost): Promise<void> {
75
+ const overlay = document.createElement('div');
76
+ overlay.className = 'tp-overlay';
77
+
78
+ const popup = document.createElement('div');
79
+ popup.className = 'tp-mobile';
80
+ popup.innerHTML = '<div class="tp-loading">Loading...</div>';
81
+
82
+ overlay.append(popup);
83
+ document.body.append(overlay);
84
+ document.body.style.overflow = 'hidden';
85
+
86
+ const cleanup = () => {
87
+ overlay.remove();
88
+ document.body.style.overflow = '';
89
+ };
90
+
91
+ // Đóng khi bấm vào overlay (bên ngoài popup)
92
+ overlay.addEventListener('click', (e) => {
93
+ if (e.target === overlay) cleanup();
94
+ });
95
+
96
+ try {
97
+ const { tooltipId: id, tooltipType: type } = host.dataset;
98
+ const html = await fetch(`/tooltip/${type}/${id}`, {
99
+ headers: { Accept: 'text/html' },
100
+ }).then((r) => r.text());
101
+
102
+ popup.innerHTML = '';
103
+
104
+ // Nút đóng
105
+ const closeBtn = document.createElement('button');
106
+ closeBtn.className = 'tp-mobile-close';
107
+ closeBtn.innerHTML = '&#x2715;';
108
+ closeBtn.addEventListener('click', cleanup);
109
+ popup.append(closeBtn);
110
+
111
+ // Nội dung
112
+ const content = document.createElement('div');
113
+ content.className = 'tp-mobile-content';
114
+ content.innerHTML = html;
115
+ popup.append(content);
116
+
117
+ // Nút "Xem chi tiết" nếu có data-href
118
+ const href = host.dataset.href;
119
+ if (href) {
120
+ const link = document.createElement('a');
121
+ link.className = 'tp-mobile-link';
122
+ link.href = href;
123
+ link.textContent = 'Xem chi tiết';
124
+ popup.append(link);
125
+ }
126
+ } catch {
127
+ popup.innerHTML = '<div class="tp-error">Không thể tải tooltip</div>';
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Khi tooltip cao hơn 60% viewport, bỏ qua placement trên/dưới host
133
+ * và căn giữa theo chiều dọc màn hình + giới hạn maxHeight để scroll.
134
+ */
135
+ function centerFallback(): Middleware {
136
+ return {
137
+ name: 'centerFallback',
138
+ fn({ rects }) {
139
+ const vh = window.innerHeight;
140
+ const padding = 8;
141
+
142
+ if (rects.floating.height <= vh * 0.6) return {};
143
+
144
+ return {
145
+ y: window.scrollY + Math.max(padding, (vh - rects.floating.height) / 2),
146
+ data: { centered: true, maxHeight: vh - padding * 2 },
147
+ };
148
+ },
149
+ };
150
+ }
151
+
152
+ function position(host: TooltipHost, tip: HTMLDivElement): Promise<void> {
153
+ const preferredPlacement = host.dataset.tooltipPlacement;
154
+ const placement = preferredPlacement ?? 'left';
155
+
156
+ return computePosition(host, tip, {
157
+ placement,
158
+ middleware: [offset(6), flip(), shift({ padding: 5 }), centerFallback(), hide()],
159
+ }).then(({ x, y, middlewareData }) => {
160
+ Object.assign(tip.style, { left: `${x}px`, top: `${y}px` });
161
+
162
+ tip.style.visibility = middlewareData.hide?.referenceHidden ? 'hidden' : 'visible';
163
+
164
+ const center = middlewareData.centerFallback as { centered?: boolean; maxHeight?: number };
165
+ if (center?.centered) {
166
+ tip.style.maxHeight = `${center.maxHeight}px`;
167
+ tip.style.overflowY = 'auto';
168
+ } else {
169
+ tip.style.maxHeight = '';
170
+ tip.style.overflowY = '';
171
+ }
172
+ });
173
+ }
package/types.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ // Minimal global JSX types for consumers of L5E.
2
+ // This file intentionally avoids imports to ensure it loads cleanly everywhere
3
+ // For internal use, prefer src/core/jsx-types.d.ts which has full type definitions
4
+ declare global {
5
+ namespace JSX {
6
+ interface IntrinsicElements {
7
+ [elemName: string]: any;
8
+ }
9
+ // Use any for external consumers, but internal files should use jsx-types.d.ts
10
+ type Element = any;
11
+ }
12
+ }
13
+
14
+ export {};