@huaqiu/dsh-auth 0.5.1 → 0.5.3

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,183 @@
1
+ /**
2
+ * The subscription dialog — a real modal (backdrop + centered card + iframe)
3
+ * for `gen.eda.cn/subscription`, mirroring the login dialog's DOM pattern
4
+ * (`ui/login-dialog.ts`): a fixed full-viewport semi-transparent backdrop +
5
+ * centered host-surface card + the subscription iframe.
6
+ *
7
+ * WHY A CARD AND NOT A FULL-VIEWPORT IFRAME
8
+ *
9
+ * Same reason as the login dialog: the iframe element sits on top of the
10
+ * host page, and Blink paints a WHITE base canvas behind any transparent
11
+ * region of the embedded doc. Putting the iframe inside a host-painted card
12
+ * masks that white in both light and dark themes (the card surface shows
13
+ * through wherever the embedded doc is transparent).
14
+ *
15
+ * SIZING: the subscription page prefers 1400×740 (`SUBSCRIPTION_PREFERRED_SIZE`,
16
+ * shared with the host-side native dialog via DialogService.OpenUrl). The card
17
+ * clamps to the viewport (`min(96vw, width)` × `min(92vh, height)`) so it can
18
+ * never overflow a small host window; the iframe fills the card.
19
+ *
20
+ * Close paths: backdrop click, the × button, and Escape. Unlike the login
21
+ * embed there is no postMessage close contract to honor — the page is
22
+ * `gen.eda.cn/subscription`, not `auth.eda.cn`.
23
+ */
24
+ import { SUBSCRIPTION_PREFERRED_SIZE } from '../lib.js'
25
+ import { translate } from '../i18n.js'
26
+ import { getCurrentLocale, getCurrentSurfaceColor, subscribeUiEnv, syncUiEnv } from '../ui-env.js'
27
+
28
+ /** Aria / data attribute names — stable so tests and CSS can target them. */
29
+ export const SUB_DIALOG_OVERLAY_ATTR = 'data-hq-sub-dialog'
30
+ export const SUB_DIALOG_CARD_ATTR = 'data-hq-sub-dialog-card'
31
+ export const SUB_DIALOG_IFRAME_ATTR = 'data-hq-sub-dialog-iframe'
32
+ export const SUB_DIALOG_CLOSE_ATTR = 'data-hq-sub-dialog-close'
33
+
34
+ let container: HTMLDivElement | null = null
35
+ let unsubscribe: (() => void) | null = null
36
+
37
+ /**
38
+ * Open the subscription dialog. Idempotent: a second call while open is a
39
+ * no-op. `size` overrides the default 1400×740 preferred size (clamped to the
40
+ * viewport).
41
+ */
42
+ export function openSubscriptionDialog(
43
+ url: string,
44
+ options: { size?: { width?: number; height?: number } } = {},
45
+ onClose?: () => void,
46
+ ): void {
47
+ if (container) return
48
+ syncUiEnv()
49
+ const locale = getCurrentLocale()
50
+
51
+ const width = Math.max(320, options.size?.width ?? SUBSCRIPTION_PREFERRED_SIZE.width)
52
+ const height = Math.max(240, options.size?.height ?? SUBSCRIPTION_PREFERRED_SIZE.height)
53
+
54
+ const root = document.createElement('div')
55
+ root.setAttribute(SUB_DIALOG_OVERLAY_ATTR, '')
56
+ root.style.cssText = [
57
+ 'position:fixed',
58
+ 'inset:0',
59
+ 'width:100vw',
60
+ 'height:100vh',
61
+ 'border:0',
62
+ 'z-index:2147483646',
63
+ 'background:rgba(0, 0, 0, 0.55)',
64
+ 'display:flex',
65
+ 'align-items:center',
66
+ 'justify-content:center',
67
+ 'box-sizing:border-box',
68
+ ].join(';')
69
+
70
+ const card = document.createElement('div')
71
+ card.setAttribute(SUB_DIALOG_CARD_ATTR, '')
72
+ const applyCardColors = (): void => {
73
+ card.style.cssText = [
74
+ `width:min(96vw, ${width}px)`,
75
+ `height:min(92vh, ${height}px)`,
76
+ 'border-radius:12px',
77
+ 'box-shadow:0 24px 48px rgba(0, 0, 0, 0.32)',
78
+ 'position:relative',
79
+ 'box-sizing:border-box',
80
+ `background:${getCurrentSurfaceColor()}`,
81
+ 'display:flex',
82
+ 'flex-direction:column',
83
+ 'overflow:hidden',
84
+ ].join(';')
85
+ }
86
+ applyCardColors()
87
+
88
+ const closeButton = document.createElement('button')
89
+ closeButton.setAttribute(SUB_DIALOG_CLOSE_ATTR, '')
90
+ closeButton.type = 'button'
91
+ closeButton.setAttribute('aria-label', translate(locale, 'dialog.close'))
92
+ closeButton.title = translate(locale, 'dialog.close')
93
+ closeButton.textContent = '×'
94
+ closeButton.style.cssText = [
95
+ 'position:absolute',
96
+ 'top:6px',
97
+ 'right:10px',
98
+ 'width:28px',
99
+ 'height:28px',
100
+ 'border:0',
101
+ 'background:transparent',
102
+ 'color:var(--dsw-alias-label-secondary, #5b6472)',
103
+ 'font-size:22px',
104
+ 'line-height:1',
105
+ 'cursor:pointer',
106
+ 'border-radius:6px',
107
+ 'padding:0',
108
+ 'z-index:1',
109
+ ].join(';')
110
+ closeButton.addEventListener('click', closeSubscriptionDialog)
111
+
112
+ const iframe = document.createElement('iframe')
113
+ iframe.setAttribute(SUB_DIALOG_IFRAME_ATTR, '')
114
+ iframe.src = url
115
+ iframe.title = translate(locale, 'subscription.dialogTitle')
116
+ iframe.allow = 'clipboard-write'
117
+ iframe.style.cssText = [
118
+ 'width:100%',
119
+ 'height:100%',
120
+ 'border:0',
121
+ 'border-radius:8px',
122
+ `background:${getCurrentSurfaceColor()}`,
123
+ 'display:block',
124
+ 'flex:1 1 auto',
125
+ ].join(';')
126
+
127
+ card.appendChild(closeButton)
128
+ card.appendChild(iframe)
129
+ root.appendChild(card)
130
+ document.body.appendChild(root)
131
+
132
+ root.addEventListener('mousedown', backdropMouseDown)
133
+ card.addEventListener('mousedown', stopPropagation)
134
+ document.addEventListener('keydown', onKeyDown)
135
+
136
+ // Track theme/locale flips so the card surface + iframe background follow
137
+ // the host while the dialog is open.
138
+ unsubscribe = subscribeUiEnv(() => {
139
+ if (!container) return
140
+ applyCardColors()
141
+ iframe.style.background = getCurrentSurfaceColor()
142
+ closeButton.title = translate(getCurrentLocale(), 'dialog.close')
143
+ closeButton.setAttribute('aria-label', closeButton.title)
144
+ })
145
+
146
+ container = root
147
+ onCloseCallback = onClose ?? null
148
+ }
149
+
150
+ /** Programmatic close (used by the auth client / tests). */
151
+ export function closeSubscriptionDialog(): void {
152
+ if (!container) return
153
+ container.remove()
154
+ container = null
155
+ document.removeEventListener('keydown', onKeyDown)
156
+ unsubscribe?.()
157
+ unsubscribe = null
158
+ const cb = onCloseCallback
159
+ onCloseCallback = null
160
+ cb?.()
161
+ }
162
+
163
+ /** True while the subscription dialog is mounted. */
164
+ export function isSubscriptionDialogOpen(): boolean {
165
+ return container !== null
166
+ }
167
+
168
+ let onCloseCallback: (() => void) | null = null
169
+
170
+ function backdropMouseDown(event: MouseEvent): void {
171
+ if (event.target === container) closeSubscriptionDialog()
172
+ }
173
+
174
+ function stopPropagation(event: MouseEvent): void {
175
+ event.stopPropagation()
176
+ }
177
+
178
+ function onKeyDown(event: KeyboardEvent): void {
179
+ if (event.key === 'Escape') {
180
+ event.stopPropagation()
181
+ closeSubscriptionDialog()
182
+ }
183
+ }