@lingxia/bridge 0.5.0 → 0.5.1

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.
@@ -0,0 +1,170 @@
1
+ const ANIMATION_MS = 200;
2
+ const STYLE_ID = 'lx-modal-style';
3
+ let activeModal = null;
4
+ function ensureModalStyle() {
5
+ if (document.getElementById(STYLE_ID))
6
+ return;
7
+ const style = document.createElement('style');
8
+ style.id = STYLE_ID;
9
+ style.textContent = `
10
+ .lx-modal-backdrop {
11
+ position: fixed; inset: 0; z-index: 99999;
12
+ background: rgba(0,0,0,0.4);
13
+ opacity: 0; transition: opacity ${ANIMATION_MS}ms ease;
14
+ display: flex; align-items: center; justify-content: center;
15
+ }
16
+ .lx-modal-backdrop.lx-modal-visible { opacity: 1; }
17
+
18
+ .lx-modal-dialog {
19
+ width: min(92vw, 320px);
20
+ background: #fff;
21
+ border-radius: 14px;
22
+ overflow: hidden;
23
+ font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI', Roboto, sans-serif;
24
+ -webkit-font-smoothing: antialiased;
25
+ transform: scale(0.95); opacity: 0;
26
+ transition: transform ${ANIMATION_MS}ms ease, opacity ${ANIMATION_MS}ms ease;
27
+ }
28
+ .lx-modal-backdrop.lx-modal-visible .lx-modal-dialog {
29
+ transform: scale(1); opacity: 1;
30
+ }
31
+
32
+ .lx-modal-body {
33
+ padding: 24px 20px 16px;
34
+ text-align: center;
35
+ }
36
+ .lx-modal-title {
37
+ margin: 0 0 8px;
38
+ font-size: 17px;
39
+ font-weight: 600;
40
+ color: #000;
41
+ line-height: 1.35;
42
+ }
43
+ .lx-modal-content {
44
+ margin: 0;
45
+ font-size: 14px;
46
+ color: #666;
47
+ line-height: 1.5;
48
+ word-break: break-word;
49
+ }
50
+ .lx-modal-body:has(.lx-modal-title:empty) .lx-modal-content {
51
+ font-size: 17px;
52
+ font-weight: 600;
53
+ color: #000;
54
+ }
55
+
56
+ .lx-modal-footer {
57
+ display: flex;
58
+ border-top: 1px solid #E0E0E0;
59
+ }
60
+ .lx-modal-btn {
61
+ flex: 1;
62
+ height: 44px;
63
+ line-height: 44px;
64
+ padding: 0;
65
+ border: none;
66
+ background: none;
67
+ font-size: 17px;
68
+ text-align: center;
69
+ cursor: pointer;
70
+ -webkit-tap-highlight-color: transparent;
71
+ }
72
+ .lx-modal-btn:active { background: rgba(0,0,0,0.06); }
73
+ .lx-modal-btn + .lx-modal-btn {
74
+ border-left: 1px solid #E0E0E0;
75
+ }
76
+ .lx-modal-btn-cancel {
77
+ color: #666;
78
+ font-weight: 400;
79
+ }
80
+ .lx-modal-btn-confirm {
81
+ font-weight: 600;
82
+ }
83
+
84
+ @media (prefers-color-scheme: dark) {
85
+ .lx-modal-dialog { background: #2c2c2e; }
86
+ .lx-modal-title { color: #f5f5f7; }
87
+ .lx-modal-content { color: #ababab; }
88
+ .lx-modal-body:has(.lx-modal-title:empty) .lx-modal-content {
89
+ color: #f5f5f7;
90
+ }
91
+ .lx-modal-footer { border-top-color: #3a3a3c; }
92
+ .lx-modal-btn:active { background: rgba(255,255,255,0.08); }
93
+ .lx-modal-btn + .lx-modal-btn { border-left-color: #3a3a3c; }
94
+ .lx-modal-btn-cancel { color: #ababab; }
95
+ }
96
+ `;
97
+ document.head.appendChild(style);
98
+ }
99
+ export function showModal(params) {
100
+ const p = params;
101
+ const title = p?.title ?? '';
102
+ const content = p?.content ?? '';
103
+ const showCancel = p?.showCancel ?? true;
104
+ const cancelText = p?.cancelText ?? 'Cancel';
105
+ const cancelColor = p?.cancelColor;
106
+ const confirmText = p?.confirmText ?? 'OK';
107
+ const confirmColor = p?.confirmColor ?? '#007AFF';
108
+ const task = (activeModal ?? Promise.resolve()).then(() => renderModal(title, content, showCancel, cancelText, cancelColor, confirmText, confirmColor), () => renderModal(title, content, showCancel, cancelText, cancelColor, confirmText, confirmColor));
109
+ activeModal = task;
110
+ return task;
111
+ }
112
+ function renderModal(title, content, showCancel, cancelText, cancelColor, confirmText, confirmColor) {
113
+ return new Promise((resolve) => {
114
+ let dismissed = false;
115
+ ensureModalStyle();
116
+ const backdrop = document.createElement('div');
117
+ backdrop.className = 'lx-modal-backdrop';
118
+ const dialog = document.createElement('div');
119
+ dialog.className = 'lx-modal-dialog';
120
+ // Body
121
+ const body = document.createElement('div');
122
+ body.className = 'lx-modal-body';
123
+ const titleEl = document.createElement('div');
124
+ titleEl.className = 'lx-modal-title';
125
+ titleEl.textContent = title;
126
+ body.appendChild(titleEl);
127
+ if (content) {
128
+ const contentEl = document.createElement('p');
129
+ contentEl.className = 'lx-modal-content';
130
+ contentEl.textContent = content;
131
+ body.appendChild(contentEl);
132
+ }
133
+ dialog.appendChild(body);
134
+ // Footer
135
+ const footer = document.createElement('div');
136
+ footer.className = 'lx-modal-footer';
137
+ if (showCancel) {
138
+ const cancelBtn = document.createElement('button');
139
+ cancelBtn.className = 'lx-modal-btn lx-modal-btn-cancel';
140
+ cancelBtn.textContent = cancelText;
141
+ if (cancelColor)
142
+ cancelBtn.style.color = cancelColor;
143
+ cancelBtn.addEventListener('click', () => dismiss(false));
144
+ footer.appendChild(cancelBtn);
145
+ }
146
+ const confirmBtn = document.createElement('button');
147
+ confirmBtn.className = 'lx-modal-btn lx-modal-btn-confirm';
148
+ confirmBtn.textContent = confirmText;
149
+ confirmBtn.style.color = confirmColor;
150
+ confirmBtn.addEventListener('click', () => dismiss(true));
151
+ footer.appendChild(confirmBtn);
152
+ dialog.appendChild(footer);
153
+ backdrop.appendChild(dialog);
154
+ document.body.appendChild(backdrop);
155
+ // Trigger enter animation
156
+ requestAnimationFrame(() => {
157
+ backdrop.classList.add('lx-modal-visible');
158
+ });
159
+ function dismiss(confirmed) {
160
+ if (dismissed)
161
+ return;
162
+ dismissed = true;
163
+ backdrop.classList.remove('lx-modal-visible');
164
+ setTimeout(() => {
165
+ backdrop.remove();
166
+ resolve({ confirm: confirmed, cancel: !confirmed });
167
+ }, ANIMATION_MS);
168
+ }
169
+ });
170
+ }
@@ -0,0 +1,145 @@
1
+ const ANIMATION_MS = 200;
2
+ const STYLE_ID = 'lx-toast-style';
3
+ let currentToast = null;
4
+ const SVG_SUCCESS = `<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="18" cy="18" r="17" stroke="currentColor" stroke-width="2"/><path d="M11 18l5 5 9-9" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
5
+ const SVG_ERROR = `<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="18" cy="18" r="17" stroke="currentColor" stroke-width="2"/><path d="M13 13l10 10M23 13l-10 10" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"/></svg>`;
6
+ const SVG_LOADING = `<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M18 4a14 14 0 0 1 14 14" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><animateTransform attributeName="transform" type="rotate" from="0 18 18" to="360 18 18" dur="0.8s" repeatCount="indefinite"/></path></svg>`;
7
+ function ensureToastStyle() {
8
+ if (document.getElementById(STYLE_ID))
9
+ return;
10
+ const style = document.createElement('style');
11
+ style.id = STYLE_ID;
12
+ style.textContent = `
13
+ .lx-toast-mask {
14
+ position: fixed; inset: 0; z-index: 99998;
15
+ }
16
+ .lx-toast-container {
17
+ position: fixed; left: 0; right: 0; z-index: 99999;
18
+ display: flex; justify-content: center;
19
+ pointer-events: none;
20
+ }
21
+ .lx-toast-container.lx-toast-top { top: 0; padding-top: 80px; }
22
+ .lx-toast-container.lx-toast-center { top: 0; bottom: 0; align-items: center; }
23
+ .lx-toast-container.lx-toast-bottom { bottom: 0; padding-bottom: 80px; }
24
+
25
+ .lx-toast-box {
26
+ display: flex; flex-direction: column; align-items: center; gap: 10px;
27
+ min-width: 120px; max-width: 280px;
28
+ padding: 20px;
29
+ background: rgba(0,0,0,0.8);
30
+ border-radius: 12px;
31
+ color: #fff;
32
+ pointer-events: auto;
33
+ transform: scale(0.8); opacity: 0;
34
+ transition: transform ${ANIMATION_MS}ms ease, opacity ${ANIMATION_MS}ms ease;
35
+ }
36
+ .lx-toast-box.lx-toast-no-icon {
37
+ min-width: auto; min-height: auto;
38
+ padding: 12px 20px;
39
+ }
40
+ .lx-toast-box.lx-toast-visible {
41
+ transform: scale(1); opacity: 1;
42
+ }
43
+ .lx-toast-icon {
44
+ width: 36px; height: 36px;
45
+ color: #fff;
46
+ line-height: 0;
47
+ }
48
+ .lx-toast-title {
49
+ font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI', Roboto, sans-serif;
50
+ font-size: 16px; font-weight: 500;
51
+ text-align: center;
52
+ line-height: 1.4;
53
+ word-break: break-word;
54
+ -webkit-font-smoothing: antialiased;
55
+ }
56
+ `;
57
+ document.head.appendChild(style);
58
+ }
59
+ export function showToast(params) {
60
+ const p = params;
61
+ const title = p?.title ?? '';
62
+ const icon = p?.icon ?? 'none';
63
+ const duration = p?.duration ?? 1500;
64
+ const mask = p?.mask ?? false;
65
+ const position = p?.position ?? 'center';
66
+ // Hide existing toast first
67
+ if (currentToast) {
68
+ currentToast.dismiss();
69
+ currentToast = null;
70
+ }
71
+ ensureToastStyle();
72
+ // Mask (prevents interaction)
73
+ let maskEl = null;
74
+ if (mask) {
75
+ maskEl = document.createElement('div');
76
+ maskEl.className = 'lx-toast-mask';
77
+ document.body.appendChild(maskEl);
78
+ }
79
+ // Container
80
+ const container = document.createElement('div');
81
+ container.className = 'lx-toast-container';
82
+ if (position === 'top')
83
+ container.classList.add('lx-toast-top');
84
+ else if (position === 'bottom')
85
+ container.classList.add('lx-toast-bottom');
86
+ else
87
+ container.classList.add('lx-toast-center');
88
+ // Toast box
89
+ const box = document.createElement('div');
90
+ box.className = 'lx-toast-box';
91
+ const hasIcon = icon !== 'none';
92
+ if (!hasIcon)
93
+ box.classList.add('lx-toast-no-icon');
94
+ // Icon
95
+ if (hasIcon) {
96
+ const iconEl = document.createElement('div');
97
+ iconEl.className = 'lx-toast-icon';
98
+ if (icon === 'success')
99
+ iconEl.innerHTML = SVG_SUCCESS;
100
+ else if (icon === 'error')
101
+ iconEl.innerHTML = SVG_ERROR;
102
+ else if (icon === 'loading')
103
+ iconEl.innerHTML = SVG_LOADING;
104
+ box.appendChild(iconEl);
105
+ }
106
+ // Title
107
+ const titleEl = document.createElement('div');
108
+ titleEl.className = 'lx-toast-title';
109
+ titleEl.textContent = title;
110
+ box.appendChild(titleEl);
111
+ container.appendChild(box);
112
+ document.body.appendChild(container);
113
+ // Animate in
114
+ requestAnimationFrame(() => {
115
+ box.classList.add('lx-toast-visible');
116
+ });
117
+ let dismissed = false;
118
+ function dismiss() {
119
+ if (dismissed)
120
+ return;
121
+ dismissed = true;
122
+ if (currentToast?.dismiss === dismiss)
123
+ currentToast = null;
124
+ box.classList.remove('lx-toast-visible');
125
+ if (timer)
126
+ clearTimeout(timer);
127
+ setTimeout(() => {
128
+ container.remove();
129
+ maskEl?.remove();
130
+ }, ANIMATION_MS);
131
+ }
132
+ let timer = null;
133
+ if (duration > 0) {
134
+ timer = setTimeout(dismiss, duration);
135
+ }
136
+ currentToast = { dismiss, timer };
137
+ return Promise.resolve();
138
+ }
139
+ export function hideToast() {
140
+ if (currentToast) {
141
+ currentToast.dismiss();
142
+ currentToast = null;
143
+ }
144
+ return Promise.resolve();
145
+ }
@@ -0,0 +1,34 @@
1
+ import { initBridge } from './bridge';
2
+ import { hasError, getErrorInfo, renderErrorUI } from './error';
3
+ var domReadyListener = {
4
+ handleEvent: function () { return maybeRenderErrorUI(); },
5
+ };
6
+ function maybeRenderErrorUI() {
7
+ if (hasError()) {
8
+ var errorInfo = getErrorInfo();
9
+ if (errorInfo) {
10
+ renderErrorUI(errorInfo);
11
+ }
12
+ }
13
+ }
14
+ /**
15
+ * Immediately initialize bridge and render error UI if present.
16
+ * Use this when DOM is already ready.
17
+ */
18
+ export function boot() {
19
+ initBridge();
20
+ maybeRenderErrorUI();
21
+ }
22
+ /**
23
+ * Initialize bridge immediately, but wait for DOM to render error UI.
24
+ * Use this at page load time.
25
+ */
26
+ export function bootWhenReady() {
27
+ initBridge();
28
+ if (document.readyState === 'loading') {
29
+ document.addEventListener('DOMContentLoaded', domReadyListener);
30
+ }
31
+ else {
32
+ maybeRenderErrorUI();
33
+ }
34
+ }