@huaqiu/dsh-auth 0.4.13 → 0.5.2

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/lib/client.js CHANGED
@@ -107,6 +107,89 @@ window.__ModuleLoader__.load({
107
107
  /**「Go to profile」destination: the eda.cn account page. */
108
108
  const PROFILE_URL = "https://www.eda.cn/account/profile";
109
109
  /**
110
+ * Subscription landing page — the same page `hq-eda-ai` serves at
111
+ * `gen.eda.cn/subscription` (token taken from the URL query; see its
112
+ * `UserInfoContext`). The preferred window size for BOTH surfaces (KiCad
113
+ * native dialog via DialogService.OpenUrl and the standalone iframe dialog)
114
+ * is 1400×740.
115
+ */
116
+ const SUBSCRIPTION_ORIGIN = "https://gen.eda.cn";
117
+ const SUBSCRIPTION_PATH = "/subscription";
118
+ const SUBSCRIPTION_PREFERRED_SIZE = {
119
+ width: 1400,
120
+ height: 740
121
+ };
122
+ /**
123
+ * Build the subscription landing URL. Mirrors `buildProfileUrl`: the token is
124
+ * always attached — `gen.eda.cn/subscription` consumes it to establish the
125
+ * session and hides it itself. `encodeURIComponent` is required (tokens are
126
+ * base64-ish and may contain `+`, `/` or `=`).
127
+ */
128
+ function buildSubscriptionUrl(options) {
129
+ return `${SUBSCRIPTION_ORIGIN}${SUBSCRIPTION_PATH}?token=${encodeURIComponent(options.token)}`;
130
+ }
131
+ const SUB_API_PREFIX = "/sub-api";
132
+ const SUB_API_SUCCESS_CODES = [200, 2e5];
133
+ function unwrapSubApiPayload(payload) {
134
+ if (payload && typeof payload === "object" && "code" in payload) {
135
+ const p = payload;
136
+ if (!SUB_API_SUCCESS_CODES.includes(p.code)) return null;
137
+ if (p.result !== void 0) return p.result;
138
+ if (p.data !== void 0) return p.data;
139
+ return;
140
+ }
141
+ return payload;
142
+ }
143
+ function quotaOf(subscription) {
144
+ if (!subscription || typeof subscription === "object" && Object.keys(subscription).length === 0) return {
145
+ hasSubscription: false,
146
+ packageName: "",
147
+ currentQuota: 0,
148
+ totalQuota: 0
149
+ };
150
+ const currentQuota = Number(subscription.currentQuota) || 0;
151
+ return {
152
+ hasSubscription: currentQuota > 0,
153
+ packageName: typeof subscription.packageName === "string" ? subscription.packageName : "",
154
+ currentQuota,
155
+ totalQuota: Number(subscription.totalQuota) || 0
156
+ };
157
+ }
158
+ /**
159
+ * Fetch the active subscription quota DIRECTLY from eda.cn
160
+ * (`GET {baseUrl}/sub-api/subscriptions/active?userId&productCode` with
161
+ * `x-token` + `X-User-Id` headers) — the same API `hq-eda-ai`'s
162
+ * `getActiveSubscription` and `hq-edge`'s ERC quota proxy call, so displaying
163
+ * the available quota never depends on an hq-edge hop.
164
+ *
165
+ * Returns null when the fetch/unwrap fails (callers decide fail-open vs
166
+ * fail-closed); a well-formed response always yields a `SubscriptionQuota`
167
+ * (possibly with `hasSubscription: false`).
168
+ */
169
+ async function fetchSubscriptionQuota(options) {
170
+ const { userId, token, productCode, baseUrl = "https://www.eda.cn" } = options;
171
+ const fetchImpl = options.fetchImpl ?? fetch;
172
+ const url = `${baseUrl}${SUB_API_PREFIX}/subscriptions/active?${new URLSearchParams({
173
+ userId: String(userId),
174
+ productCode
175
+ }).toString()}`;
176
+ try {
177
+ const res = await fetchImpl(url, {
178
+ method: "GET",
179
+ headers: {
180
+ Accept: "application/json, text/plain, */*",
181
+ "Content-Type": "application/json",
182
+ "x-token": token,
183
+ "X-User-Id": String(userId)
184
+ }
185
+ });
186
+ if (!res || !res.ok) return null;
187
+ return quotaOf(unwrapSubApiPayload(await res.json()));
188
+ } catch {
189
+ return null;
190
+ }
191
+ }
192
+ /**
110
193
  * Build the「Go to profile」URL: the eda.cn account page WITH the access token
111
194
  * in the query, mirroring `hq-eda-ai`'s `UserMenu`
112
195
  * (`/account/profile?token=…&phone=…`).
@@ -484,6 +567,22 @@ window.__ModuleLoader__.load({
484
567
  "sidebar.account": "华秋EDA AI · 已登录",
485
568
  "menu.profile": "个人中心",
486
569
  "menu.logout": "退出登录",
570
+ "subscription.title": "订阅",
571
+ "subscription.dialogTitle": "华秋 EDA 订阅",
572
+ "subscription.gen": "系统设计(GEN)",
573
+ "subscription.erc": "电路检查(ERC)",
574
+ "subscription.package": "套餐:{name}",
575
+ "subscription.quota": "可用次数:{count}",
576
+ "subscription.quotaCount": "{used} / {total}次",
577
+ "subscription.noQuota": "无可用次数",
578
+ "subscription.unsubscribed": "未订阅",
579
+ "subscription.subscribe": "订阅 / 升级",
580
+ "subscription.subscribing": "正在打开订阅…",
581
+ "subscription.desc": "当前账号没有可用的 GEN 生成次数,请订阅或升级套餐后重试。",
582
+ "subscription.hostUnavailable": "无法打开订阅页面(宿主未提供对话框服务)。",
583
+ "subscription.quotaHeader": "套餐与使用额度",
584
+ "subscription.upgrade": "订阅 / 升级套餐",
585
+ "subscription.online": "在线",
487
586
  "card.title": "华秋 EDA AI(eda.cn)登录",
488
587
  "card.desc": "工具「{tool}」需要登录华秋 EDA AI 账号才能继续。请在下方的登录框完成登录(或点击左侧「华秋EDA AI登录」按钮);登录完成后,回复助手「已登录,请重试」,助手会自动重新调用该工具。",
489
588
  "card.loggedIn": "✓ 已登录{nickname} —— 现在可以回复助手「已登录,请重试」,助手会重新调用工具。",
@@ -502,6 +601,22 @@ window.__ModuleLoader__.load({
502
601
  "sidebar.account": "Huaqiu EDA AI · signed in",
503
602
  "menu.profile": "Go to profile",
504
603
  "menu.logout": "Log out",
604
+ "subscription.title": "Subscription",
605
+ "subscription.dialogTitle": "Huaqiu EDA subscription",
606
+ "subscription.gen": "System design (GEN)",
607
+ "subscription.erc": "Circuit check (ERC)",
608
+ "subscription.package": "Plan: {name}",
609
+ "subscription.quota": "Available: {count}",
610
+ "subscription.quotaCount": "{used} / {total}",
611
+ "subscription.noQuota": "No quota left",
612
+ "subscription.unsubscribed": "Not subscribed",
613
+ "subscription.subscribe": "Subscribe / Upgrade",
614
+ "subscription.subscribing": "Opening subscription…",
615
+ "subscription.desc": "This account has no GEN generation quota left. Subscribe or upgrade to continue.",
616
+ "subscription.hostUnavailable": "Cannot open the subscription page (the host provides no dialog service).",
617
+ "subscription.quotaHeader": "Plan & usage quota",
618
+ "subscription.upgrade": "Subscribe / Upgrade plan",
619
+ "subscription.online": "Online",
505
620
  "card.title": "Huaqiu EDA AI (eda.cn) login",
506
621
  "card.desc": "Tool \"{tool}\" needs a Huaqiu EDA AI account. Complete the login below (or use the Huaqiu EDA AI button in the sidebar), then reply \"I have logged in, please retry\" so the assistant can retry the tool.",
507
622
  "card.loggedIn": "✓ Logged in{nickname} — reply \"I have logged in, please retry\" and the assistant will retry the tool.",
@@ -583,8 +698,8 @@ window.__ModuleLoader__.load({
583
698
  */
584
699
  const IFRAME_HEIGHT = 440;
585
700
  const CARD_MAX_WIDTH = 768;
586
- let container = null;
587
- let unsubscribe$1 = null;
701
+ let container$1 = null;
702
+ let unsubscribe$2 = null;
588
703
  let onCloseRequested = null;
589
704
  /**
590
705
  * Open the login dialog. Idempotent: a second call while open is a no-op
@@ -594,7 +709,7 @@ window.__ModuleLoader__.load({
594
709
  * unblock its own `isOpen` state.
595
710
  */
596
711
  function openLoginDialog(options = {}, onClose) {
597
- if (container) return;
712
+ if (container$1) return;
598
713
  syncUiEnv();
599
714
  const locale = options.lang ?? getCurrentLocale();
600
715
  const root = document.createElement("div");
@@ -672,6 +787,163 @@ window.__ModuleLoader__.load({
672
787
  card.appendChild(iframe);
673
788
  root.appendChild(card);
674
789
  document.body.appendChild(root);
790
+ root.addEventListener("mousedown", backdropMouseDown$1);
791
+ card.addEventListener("mousedown", stopPropagation$1);
792
+ document.addEventListener("keydown", onKeyDown$1);
793
+ unsubscribe$2 = subscribeUiEnv(() => {
794
+ if (!container$1) return;
795
+ applyCardColors();
796
+ iframe.style.background = getCurrentSurfaceColor();
797
+ closeButton.title = translate(getCurrentLocale(), "dialog.close");
798
+ closeButton.setAttribute("aria-label", closeButton.title);
799
+ });
800
+ container$1 = root;
801
+ onCloseRequested = onClose ?? null;
802
+ }
803
+ /** Programmatic close (used by the auth client after a successful login). */
804
+ function closeLoginDialog() {
805
+ if (!container$1) return;
806
+ container$1.remove();
807
+ container$1 = null;
808
+ document.removeEventListener("keydown", onKeyDown$1);
809
+ unsubscribe$2?.();
810
+ unsubscribe$2 = null;
811
+ const cb = onCloseRequested;
812
+ onCloseRequested = null;
813
+ cb?.();
814
+ }
815
+ /** True while the dialog is mounted. */
816
+ function isLoginDialogOpen() {
817
+ return container$1 !== null;
818
+ }
819
+ function backdropMouseDown$1(event) {
820
+ if (event.target === container$1) closeLoginDialog();
821
+ }
822
+ function stopPropagation$1(event) {
823
+ event.stopPropagation();
824
+ }
825
+ function onKeyDown$1(event) {
826
+ if (event.key === "Escape") {
827
+ event.stopPropagation();
828
+ closeLoginDialog();
829
+ }
830
+ }
831
+ //#endregion
832
+ //#region src/client/ui/subscription-dialog.ts
833
+ /**
834
+ * The subscription dialog — a real modal (backdrop + centered card + iframe)
835
+ * for `gen.eda.cn/subscription`, mirroring the login dialog's DOM pattern
836
+ * (`ui/login-dialog.ts`): a fixed full-viewport semi-transparent backdrop +
837
+ * centered host-surface card + the subscription iframe.
838
+ *
839
+ * WHY A CARD AND NOT A FULL-VIEWPORT IFRAME
840
+ *
841
+ * Same reason as the login dialog: the iframe element sits on top of the
842
+ * host page, and Blink paints a WHITE base canvas behind any transparent
843
+ * region of the embedded doc. Putting the iframe inside a host-painted card
844
+ * masks that white in both light and dark themes (the card surface shows
845
+ * through wherever the embedded doc is transparent).
846
+ *
847
+ * SIZING: the subscription page prefers 1400×740 (`SUBSCRIPTION_PREFERRED_SIZE`,
848
+ * shared with the host-side native dialog via DialogService.OpenUrl). The card
849
+ * clamps to the viewport (`min(96vw, width)` × `min(92vh, height)`) so it can
850
+ * never overflow a small host window; the iframe fills the card.
851
+ *
852
+ * Close paths: backdrop click, the × button, and Escape. Unlike the login
853
+ * embed there is no postMessage close contract to honor — the page is
854
+ * `gen.eda.cn/subscription`, not `auth.eda.cn`.
855
+ */
856
+ /** Aria / data attribute names — stable so tests and CSS can target them. */
857
+ const SUB_DIALOG_OVERLAY_ATTR = "data-hq-sub-dialog";
858
+ const SUB_DIALOG_CARD_ATTR = "data-hq-sub-dialog-card";
859
+ const SUB_DIALOG_IFRAME_ATTR = "data-hq-sub-dialog-iframe";
860
+ const SUB_DIALOG_CLOSE_ATTR = "data-hq-sub-dialog-close";
861
+ let container = null;
862
+ let unsubscribe$1 = null;
863
+ /**
864
+ * Open the subscription dialog. Idempotent: a second call while open is a
865
+ * no-op. `size` overrides the default 1400×740 preferred size (clamped to the
866
+ * viewport).
867
+ */
868
+ function openSubscriptionDialog(url, options = {}, onClose) {
869
+ if (container) return;
870
+ syncUiEnv();
871
+ const locale = getCurrentLocale();
872
+ const width = Math.max(320, options.size?.width ?? SUBSCRIPTION_PREFERRED_SIZE.width);
873
+ const height = Math.max(240, options.size?.height ?? SUBSCRIPTION_PREFERRED_SIZE.height);
874
+ const root = document.createElement("div");
875
+ root.setAttribute(SUB_DIALOG_OVERLAY_ATTR, "");
876
+ root.style.cssText = [
877
+ "position:fixed",
878
+ "inset:0",
879
+ "width:100vw",
880
+ "height:100vh",
881
+ "border:0",
882
+ "z-index:2147483646",
883
+ "background:rgba(0, 0, 0, 0.55)",
884
+ "display:flex",
885
+ "align-items:center",
886
+ "justify-content:center",
887
+ "box-sizing:border-box"
888
+ ].join(";");
889
+ const card = document.createElement("div");
890
+ card.setAttribute(SUB_DIALOG_CARD_ATTR, "");
891
+ const applyCardColors = () => {
892
+ card.style.cssText = [
893
+ `width:min(96vw, ${width}px)`,
894
+ `height:min(92vh, ${height}px)`,
895
+ "border-radius:12px",
896
+ "box-shadow:0 24px 48px rgba(0, 0, 0, 0.32)",
897
+ "position:relative",
898
+ "box-sizing:border-box",
899
+ `background:${getCurrentSurfaceColor()}`,
900
+ "display:flex",
901
+ "flex-direction:column",
902
+ "overflow:hidden"
903
+ ].join(";");
904
+ };
905
+ applyCardColors();
906
+ const closeButton = document.createElement("button");
907
+ closeButton.setAttribute(SUB_DIALOG_CLOSE_ATTR, "");
908
+ closeButton.type = "button";
909
+ closeButton.setAttribute("aria-label", translate(locale, "dialog.close"));
910
+ closeButton.title = translate(locale, "dialog.close");
911
+ closeButton.textContent = "×";
912
+ closeButton.style.cssText = [
913
+ "position:absolute",
914
+ "top:6px",
915
+ "right:10px",
916
+ "width:28px",
917
+ "height:28px",
918
+ "border:0",
919
+ "background:transparent",
920
+ "color:var(--dsw-alias-label-secondary, #5b6472)",
921
+ "font-size:22px",
922
+ "line-height:1",
923
+ "cursor:pointer",
924
+ "border-radius:6px",
925
+ "padding:0",
926
+ "z-index:1"
927
+ ].join(";");
928
+ closeButton.addEventListener("click", closeSubscriptionDialog);
929
+ const iframe = document.createElement("iframe");
930
+ iframe.setAttribute(SUB_DIALOG_IFRAME_ATTR, "");
931
+ iframe.src = url;
932
+ iframe.title = translate(locale, "subscription.dialogTitle");
933
+ iframe.allow = "clipboard-write";
934
+ iframe.style.cssText = [
935
+ "width:100%",
936
+ "height:100%",
937
+ "border:0",
938
+ "border-radius:8px",
939
+ `background:${getCurrentSurfaceColor()}`,
940
+ "display:block",
941
+ "flex:1 1 auto"
942
+ ].join(";");
943
+ card.appendChild(closeButton);
944
+ card.appendChild(iframe);
945
+ root.appendChild(card);
946
+ document.body.appendChild(root);
675
947
  root.addEventListener("mousedown", backdropMouseDown);
676
948
  card.addEventListener("mousedown", stopPropagation);
677
949
  document.addEventListener("keydown", onKeyDown);
@@ -683,26 +955,27 @@ window.__ModuleLoader__.load({
683
955
  closeButton.setAttribute("aria-label", closeButton.title);
684
956
  });
685
957
  container = root;
686
- onCloseRequested = onClose ?? null;
958
+ onCloseCallback = onClose ?? null;
687
959
  }
688
- /** Programmatic close (used by the auth client after a successful login). */
689
- function closeLoginDialog() {
960
+ /** Programmatic close (used by the auth client / tests). */
961
+ function closeSubscriptionDialog() {
690
962
  if (!container) return;
691
963
  container.remove();
692
964
  container = null;
693
965
  document.removeEventListener("keydown", onKeyDown);
694
966
  unsubscribe$1?.();
695
967
  unsubscribe$1 = null;
696
- const cb = onCloseRequested;
697
- onCloseRequested = null;
968
+ const cb = onCloseCallback;
969
+ onCloseCallback = null;
698
970
  cb?.();
699
971
  }
700
- /** True while the dialog is mounted. */
701
- function isLoginDialogOpen() {
972
+ /** True while the subscription dialog is mounted. */
973
+ function isSubscriptionDialogOpen() {
702
974
  return container !== null;
703
975
  }
976
+ let onCloseCallback = null;
704
977
  function backdropMouseDown(event) {
705
- if (event.target === container) closeLoginDialog();
978
+ if (event.target === container) closeSubscriptionDialog();
706
979
  }
707
980
  function stopPropagation(event) {
708
981
  event.stopPropagation();
@@ -710,7 +983,7 @@ window.__ModuleLoader__.load({
710
983
  function onKeyDown(event) {
711
984
  if (event.key === "Escape") {
712
985
  event.stopPropagation();
713
- closeLoginDialog();
986
+ closeSubscriptionDialog();
714
987
  }
715
988
  }
716
989
  //#endregion
@@ -810,6 +1083,37 @@ window.__ModuleLoader__.load({
810
1083
  emit(null);
811
1084
  closeIframe();
812
1085
  },
1086
+ getSubscriptionQuota: async (productCode) => {
1087
+ if (hostMode && !hostSessionLoaded) await resolveHost();
1088
+ const token = await auth.getAccessToken();
1089
+ const id = hostMode ? hostSession?.id ?? null : hostSession?.id ?? storage.get()?.id ?? null;
1090
+ if (!token || !id) return null;
1091
+ return fetchSubscriptionQuota({
1092
+ userId: id,
1093
+ token,
1094
+ productCode
1095
+ });
1096
+ },
1097
+ openSubscriptionDialog: async () => {
1098
+ if (hostMode && !hostSessionLoaded) await resolveHost();
1099
+ const token = await auth.getAccessToken();
1100
+ if (!token) return;
1101
+ const url = buildSubscriptionUrl({ token });
1102
+ if (hostMode) {
1103
+ if (!deps.openHostDialog) return;
1104
+ await deps.openHostDialog(url, {
1105
+ title: "Huaqiu EDA subscription",
1106
+ size: {
1107
+ width: SUBSCRIPTION_PREFERRED_SIZE.width,
1108
+ height: SUBSCRIPTION_PREFERRED_SIZE.height
1109
+ },
1110
+ key: "hq-subscription-dialog"
1111
+ });
1112
+ return;
1113
+ }
1114
+ if (isSubscriptionDialogOpen()) return;
1115
+ openSubscriptionDialog(url, { size: SUBSCRIPTION_PREFERRED_SIZE });
1116
+ },
813
1117
  onAuthStateChanged: (listener) => {
814
1118
  listeners.add(listener);
815
1119
  return () => listeners.delete(listener);
@@ -892,6 +1196,7 @@ window.__ModuleLoader__.load({
892
1196
  dispose() {
893
1197
  deps.windowLike.removeEventListener("message", onWindowMessage);
894
1198
  closeIframe();
1199
+ if (isSubscriptionDialogOpen()) closeSubscriptionDialog();
895
1200
  hostMode = false;
896
1201
  hostSession = null;
897
1202
  hostSessionLoaded = false;
@@ -1184,18 +1489,40 @@ window.__ModuleLoader__.load({
1184
1489
  danger: "var(--dsw-alias-state-error-primary, #d4380d)",
1185
1490
  dangerHover: "rgba(216, 56, 13, 0.08)",
1186
1491
  avatarBg: "var(--dsw-alias-bg-layer-2, #eef2f7)",
1187
- shadow: "0 12px 32px rgba(15, 23, 42, 0.16)"
1492
+ shadow: "0 20px 40px -15px rgba(0, 0, 0, 0.12), 0 0 1px 1px rgba(0, 0, 0, 0.05)",
1493
+ card: "#f7f8fa",
1494
+ cardBorder: "#e8ebf0",
1495
+ metric: "#f7f8fa",
1496
+ metricHover: "#eef1f5",
1497
+ primary: "#e62e2d",
1498
+ primaryHover: "#cc2423",
1499
+ primarySoft: "rgba(230, 46, 45, 0.08)",
1500
+ track: "#e4e7ec",
1501
+ genGradient: "linear-gradient(90deg, #3b82f6, #6366f1)",
1502
+ ercGradient: "linear-gradient(90deg, #a855f7, #ec4899)",
1503
+ online: "#10b981"
1188
1504
  };
1189
1505
  const DARK_PALETTE = {
1190
- surface: "var(--dsw-alias-bg-overlay, #20242c)",
1191
- border: "var(--dsw-alias-border-l1, rgba(255, 255, 255, 0.14))",
1506
+ surface: "var(--dsw-alias-bg-overlay, #1e1b24)",
1507
+ border: "var(--dsw-alias-border-l1, rgba(255, 255, 255, 0.10))",
1192
1508
  text: "var(--dsw-alias-label-primary, #e6eaf0)",
1193
1509
  muted: "var(--dsw-alias-label-secondary, #8b95a5)",
1194
1510
  hover: "var(--dsw-alias-interactive-bg-hover, rgba(255, 255, 255, 0.08))",
1195
1511
  danger: "var(--dsw-alias-state-error-primary, #ff7875)",
1196
1512
  dangerHover: "rgba(255, 120, 117, 0.14)",
1197
1513
  avatarBg: "var(--dsw-alias-bg-layer-2, rgba(255, 255, 255, 0.10))",
1198
- shadow: "0 12px 32px rgba(0, 0, 0, 0.46)"
1514
+ shadow: "0 20px 40px -15px rgba(0, 0, 0, 0.7), 0 0 1px 1px rgba(255, 255, 255, 0.1)",
1515
+ card: "rgba(255, 255, 255, 0.03)",
1516
+ cardBorder: "rgba(255, 255, 255, 0.08)",
1517
+ metric: "rgba(0, 0, 0, 0.20)",
1518
+ metricHover: "rgba(0, 0, 0, 0.30)",
1519
+ primary: "#e62e2d",
1520
+ primaryHover: "#cc2423",
1521
+ primarySoft: "rgba(230, 46, 45, 0.10)",
1522
+ track: "rgba(255, 255, 255, 0.10)",
1523
+ genGradient: "linear-gradient(90deg, #3b82f6, #6366f1)",
1524
+ ercGradient: "linear-gradient(90deg, #a855f7, #ec4899)",
1525
+ online: "#10b981"
1199
1526
  };
1200
1527
  const TRIGGER_BASE = {
1201
1528
  width: "100%",
@@ -1216,11 +1543,11 @@ window.__ModuleLoader__.load({
1216
1543
  const MENU_BASE = {
1217
1544
  position: "fixed",
1218
1545
  zIndex: 2147483e3,
1219
- minWidth: 184,
1220
- padding: 6,
1546
+ width: 320,
1547
+ padding: 12,
1221
1548
  borderWidth: 1,
1222
1549
  borderStyle: "solid",
1223
- borderRadius: 12,
1550
+ borderRadius: 16,
1224
1551
  fontFamily: "inherit",
1225
1552
  fontSize: 13
1226
1553
  };
@@ -1247,22 +1574,47 @@ window.__ModuleLoader__.load({
1247
1574
  };
1248
1575
  /**
1249
1576
  * One menu row. Hover is tracked in state: the client bundle ships no CSS
1250
- * file, so inline styles cannot express `:hover`.
1577
+ * file, so inline styles cannot express `:hover`. `trailing` renders a
1578
+ * chevron on the right (prototype nav rows), `danger` switches to the danger
1579
+ * color scheme (prototype logout).
1251
1580
  */
1252
- function MenuItem({ label, icon, danger, palette, onSelect }) {
1581
+ function MenuItem({ label, icon, trailing, danger, palette, onSelect }) {
1253
1582
  const [hovered, setHovered] = (0, react.useState)(false);
1254
1583
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1255
1584
  type: "button",
1256
1585
  role: "menuitem",
1257
1586
  style: {
1258
1587
  ...MENU_ITEM_BASE,
1588
+ justifyContent: "space-between",
1259
1589
  color: danger ? palette.danger : palette.text,
1260
1590
  background: hovered ? danger ? palette.dangerHover : palette.hover : "transparent"
1261
1591
  },
1262
1592
  onMouseEnter: () => setHovered(true),
1263
1593
  onMouseLeave: () => setHovered(false),
1264
1594
  onClick: onSelect,
1265
- children: [icon, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label })]
1595
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1596
+ style: {
1597
+ display: "flex",
1598
+ alignItems: "center",
1599
+ gap: 10,
1600
+ minWidth: 0
1601
+ },
1602
+ children: [icon, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1603
+ style: {
1604
+ overflow: "hidden",
1605
+ textOverflow: "ellipsis",
1606
+ whiteSpace: "nowrap"
1607
+ },
1608
+ children: label
1609
+ })]
1610
+ }), trailing ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1611
+ style: {
1612
+ display: "flex",
1613
+ flex: "0 0 auto",
1614
+ color: hovered ? danger ? palette.danger : palette.text : palette.muted
1615
+ },
1616
+ children: trailing
1617
+ }) : null]
1266
1618
  });
1267
1619
  }
1268
1620
  function UserIcon() {
@@ -1310,6 +1662,286 @@ window.__ModuleLoader__.load({
1310
1662
  ]
1311
1663
  });
1312
1664
  }
1665
+ /** GEN metric icon (lucide `cpu`, blue like the prototype). */
1666
+ function CpuIcon() {
1667
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
1668
+ xmlns: "http://www.w3.org/2000/svg",
1669
+ width: 13,
1670
+ height: 13,
1671
+ viewBox: "0 0 24 24",
1672
+ fill: "none",
1673
+ stroke: "#3b82f6",
1674
+ strokeWidth: 2,
1675
+ strokeLinecap: "round",
1676
+ strokeLinejoin: "round",
1677
+ "aria-hidden": true,
1678
+ style: { flex: "0 0 auto" },
1679
+ children: [
1680
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
1681
+ x: "4",
1682
+ y: "4",
1683
+ width: "16",
1684
+ height: "16",
1685
+ rx: "2"
1686
+ }),
1687
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
1688
+ x: "9",
1689
+ y: "9",
1690
+ width: "6",
1691
+ height: "6"
1692
+ }),
1693
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M9 2v2M15 2v2M9 20v2M15 20v2M2 9h2M2 15h2M20 9h2M20 15h2" })
1694
+ ]
1695
+ });
1696
+ }
1697
+ /** ERC metric icon (lucide `waves`, purple like the prototype). */
1698
+ function WaveIcon() {
1699
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
1700
+ xmlns: "http://www.w3.org/2000/svg",
1701
+ width: 13,
1702
+ height: 13,
1703
+ viewBox: "0 0 24 24",
1704
+ fill: "none",
1705
+ stroke: "#a855f7",
1706
+ strokeWidth: 2,
1707
+ strokeLinecap: "round",
1708
+ strokeLinejoin: "round",
1709
+ "aria-hidden": true,
1710
+ style: { flex: "0 0 auto" },
1711
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M2 12c1.5-3 3-3 4.5 0s3 3 4.5 0 3-3 4.5 0 3 3 4.5 0" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M2 17c1.5-3 3-3 4.5 0s3 3 4.5 0 3-3 4.5 0 3 3 4.5 0" })]
1712
+ });
1713
+ }
1714
+ /** Upgrade-button crown (solid amber like the prototype). */
1715
+ function CrownIcon() {
1716
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
1717
+ xmlns: "http://www.w3.org/2000/svg",
1718
+ width: 14,
1719
+ height: 14,
1720
+ viewBox: "0 0 24 24",
1721
+ fill: "#fcd34d",
1722
+ "aria-hidden": true,
1723
+ style: { flex: "0 0 auto" },
1724
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M2 18h20l-1.5-9-5.5 4L12 5.5 9 13l-5.5-4L2 18z" })
1725
+ });
1726
+ }
1727
+ /** Trailing chevron for nav rows (lucide `chevron-right`). */
1728
+ function ChevronRightIcon() {
1729
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
1730
+ xmlns: "http://www.w3.org/2000/svg",
1731
+ width: 12,
1732
+ height: 12,
1733
+ viewBox: "0 0 24 24",
1734
+ fill: "none",
1735
+ stroke: "currentColor",
1736
+ strokeWidth: 2,
1737
+ strokeLinecap: "round",
1738
+ strokeLinejoin: "round",
1739
+ "aria-hidden": true,
1740
+ style: { flex: "0 0 auto" },
1741
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("polyline", { points: "9 18 15 12 9 6" })
1742
+ });
1743
+ }
1744
+ /** Quota metric card: icon + label, mono count and a gradient progress bar. */
1745
+ function QuotaMetric({ label, icon, quota, gradient, palette }) {
1746
+ const t = useT();
1747
+ const [hovered, setHovered] = (0, react.useState)(false);
1748
+ const used = quota?.currentQuota;
1749
+ const total = quota?.totalQuota;
1750
+ const percent = quota?.hasSubscription && typeof total === "number" && total > 0 ? Math.max(0, Math.min(100, Math.round((used ?? 0) / total * 100))) : 0;
1751
+ const value = quota ? quota.hasSubscription ? typeof total === "number" ? t("subscription.quotaCount", {
1752
+ used: used ?? 0,
1753
+ total
1754
+ }) : String(used ?? 0) : t("subscription.unsubscribed") : "—";
1755
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1756
+ role: "presentation",
1757
+ style: {
1758
+ background: hovered ? palette.metricHover : palette.metric,
1759
+ borderRadius: 12,
1760
+ border: `1px solid ${palette.cardBorder}`,
1761
+ padding: 10,
1762
+ boxSizing: "border-box",
1763
+ transition: "background 0.15s ease"
1764
+ },
1765
+ onMouseEnter: () => setHovered(true),
1766
+ onMouseLeave: () => setHovered(false),
1767
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1768
+ style: {
1769
+ display: "flex",
1770
+ justifyContent: "space-between",
1771
+ alignItems: "center",
1772
+ gap: 8,
1773
+ fontSize: 12,
1774
+ marginBottom: 6
1775
+ },
1776
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1777
+ style: {
1778
+ display: "flex",
1779
+ alignItems: "center",
1780
+ gap: 6,
1781
+ fontWeight: 500,
1782
+ color: palette.text,
1783
+ minWidth: 0
1784
+ },
1785
+ children: [icon, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1786
+ style: {
1787
+ overflow: "hidden",
1788
+ textOverflow: "ellipsis",
1789
+ whiteSpace: "nowrap"
1790
+ },
1791
+ children: label
1792
+ })]
1793
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1794
+ style: {
1795
+ fontWeight: 700,
1796
+ color: quota?.hasSubscription ? palette.text : palette.danger,
1797
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
1798
+ fontSize: 12,
1799
+ whiteSpace: "nowrap",
1800
+ flexShrink: 0
1801
+ },
1802
+ title: value,
1803
+ children: value
1804
+ })]
1805
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1806
+ style: {
1807
+ width: "100%",
1808
+ height: 6,
1809
+ borderRadius: 999,
1810
+ background: palette.track,
1811
+ overflow: "hidden"
1812
+ },
1813
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { style: {
1814
+ width: `${percent}%`,
1815
+ height: "100%",
1816
+ borderRadius: 999,
1817
+ background: gradient
1818
+ } })
1819
+ })]
1820
+ });
1821
+ }
1822
+ /**
1823
+ * Profile header card (prototype): name + plan badge + phone line on the
1824
+ * left, an "online" pill on the right. No redundant avatar — the trigger
1825
+ * already shows it.
1826
+ */
1827
+ function ProfileHeaderCard({ displayName, packageName, phone, palette }) {
1828
+ const t = useT();
1829
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1830
+ role: "presentation",
1831
+ style: {
1832
+ padding: 10,
1833
+ borderRadius: 12,
1834
+ background: palette.card,
1835
+ border: `1px solid ${palette.cardBorder}`,
1836
+ display: "flex",
1837
+ alignItems: "center",
1838
+ justifyContent: "space-between",
1839
+ gap: 8,
1840
+ marginBottom: 12,
1841
+ boxSizing: "border-box"
1842
+ },
1843
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1844
+ style: {
1845
+ minWidth: 0,
1846
+ flex: "1 1 auto"
1847
+ },
1848
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1849
+ style: {
1850
+ display: "flex",
1851
+ alignItems: "center",
1852
+ gap: 8,
1853
+ minWidth: 0
1854
+ },
1855
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1856
+ style: {
1857
+ fontSize: 14,
1858
+ fontWeight: 700,
1859
+ color: palette.text,
1860
+ overflow: "hidden",
1861
+ textOverflow: "ellipsis",
1862
+ whiteSpace: "nowrap"
1863
+ },
1864
+ children: displayName
1865
+ }), packageName ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1866
+ style: {
1867
+ fontSize: 10,
1868
+ background: palette.primarySoft,
1869
+ color: palette.primary,
1870
+ padding: "2px 6px",
1871
+ borderRadius: 4,
1872
+ fontWeight: 500,
1873
+ border: `1px solid ${palette.primarySoft}`,
1874
+ whiteSpace: "nowrap",
1875
+ flexShrink: 0
1876
+ },
1877
+ children: packageName
1878
+ }) : null]
1879
+ }), phone ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1880
+ style: {
1881
+ margin: 0,
1882
+ marginTop: 2,
1883
+ fontSize: 12,
1884
+ color: palette.muted,
1885
+ overflow: "hidden",
1886
+ textOverflow: "ellipsis",
1887
+ whiteSpace: "nowrap"
1888
+ },
1889
+ children: phone
1890
+ }) : null]
1891
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1892
+ style: {
1893
+ fontSize: 10,
1894
+ background: "rgba(16, 185, 129, 0.10)",
1895
+ color: palette.online,
1896
+ padding: "2px 8px",
1897
+ borderRadius: 999,
1898
+ fontWeight: 500,
1899
+ border: "1px solid rgba(16, 185, 129, 0.20)",
1900
+ display: "flex",
1901
+ alignItems: "center",
1902
+ gap: 5,
1903
+ flexShrink: 0
1904
+ },
1905
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { style: {
1906
+ width: 6,
1907
+ height: 6,
1908
+ borderRadius: 6,
1909
+ background: palette.online
1910
+ } }), t("subscription.online")]
1911
+ })]
1912
+ });
1913
+ }
1914
+ /** Full-width red-gradient upgrade button (prototype CTA). */
1915
+ function UpgradeButton({ subscribing, palette, onClick }) {
1916
+ const t = useT();
1917
+ const [hovered, setHovered] = (0, react.useState)(false);
1918
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1919
+ type: "button",
1920
+ style: {
1921
+ width: "100%",
1922
+ marginTop: 10,
1923
+ padding: "9px 12px",
1924
+ borderRadius: 12,
1925
+ border: "none",
1926
+ background: subscribing ? `linear-gradient(90deg, ${palette.primary}, #e11d48)` : hovered ? `linear-gradient(90deg, ${palette.primaryHover}, #be123c)` : `linear-gradient(90deg, ${palette.primary}, #e11d48)`,
1927
+ color: "#ffffff",
1928
+ fontSize: 12,
1929
+ fontWeight: 600,
1930
+ display: "flex",
1931
+ alignItems: "center",
1932
+ justifyContent: "center",
1933
+ gap: 8,
1934
+ cursor: subscribing ? "default" : "pointer",
1935
+ opacity: subscribing ? .72 : 1,
1936
+ boxShadow: "0 8px 20px rgba(230, 46, 45, 0.22)",
1937
+ transition: "background 0.15s ease, opacity 0.15s ease"
1938
+ },
1939
+ onMouseEnter: () => setHovered(true),
1940
+ onMouseLeave: () => setHovered(false),
1941
+ onClick,
1942
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CrownIcon, {}), subscribing ? t("subscription.subscribing") : t("subscription.upgrade")]
1943
+ });
1944
+ }
1313
1945
  const HuaqiuAuthSidebarAction = (0, react.memo)(function HuaqiuAuthSidebarAction({ wide }) {
1314
1946
  const authState = (0, react.useSyncExternalStore)(subscribeAuth, getAuthState);
1315
1947
  const auth = getAuth();
@@ -1320,6 +1952,8 @@ window.__ModuleLoader__.load({
1320
1952
  const [menuStyle, setMenuStyle] = (0, react.useState)(null);
1321
1953
  const [avatarBroken, setAvatarBroken] = (0, react.useState)(false);
1322
1954
  const [hovered, setHovered] = (0, react.useState)(false);
1955
+ const [quota, setQuota] = (0, react.useState)(null);
1956
+ const [subscribing, setSubscribing] = (0, react.useState)(false);
1323
1957
  const triggerRef = (0, react.useRef)(null);
1324
1958
  const menuRef = (0, react.useRef)(null);
1325
1959
  const palette = dark ? DARK_PALETTE : LIGHT_PALETTE;
@@ -1357,6 +1991,22 @@ window.__ModuleLoader__.load({
1357
1991
  (0, react.useEffect)(() => {
1358
1992
  if (!authenticated) setMenuOpen(false);
1359
1993
  }, [authenticated]);
1994
+ (0, react.useEffect)(() => {
1995
+ if (!menuOpen || !authenticated) {
1996
+ if (!menuOpen) setQuota(null);
1997
+ return;
1998
+ }
1999
+ let cancelled = false;
2000
+ Promise.all([auth?.getSubscriptionQuota?.("auto-design").catch(() => null) ?? Promise.resolve(null), auth?.getSubscriptionQuota?.("erc").catch(() => null) ?? Promise.resolve(null)]).then(([gen, erc]) => {
2001
+ if (!cancelled) setQuota({
2002
+ gen,
2003
+ erc
2004
+ });
2005
+ });
2006
+ return () => {
2007
+ cancelled = true;
2008
+ };
2009
+ }, [menuOpen, authenticated]);
1360
2010
  (0, react.useLayoutEffect)(() => {
1361
2011
  if (!menuOpen || !triggerRef.current) return;
1362
2012
  const rect = triggerRef.current.getBoundingClientRect();
@@ -1367,12 +2017,10 @@ window.__ModuleLoader__.load({
1367
2017
  color: palette.text,
1368
2018
  boxShadow: palette.shadow,
1369
2019
  left: Math.max(8, Math.round(rect.left)),
1370
- bottom: Math.max(8, Math.round(window.innerHeight - rect.top + 8)),
1371
- ...wide ? { width: Math.round(rect.width) } : {}
2020
+ bottom: Math.max(8, Math.round(window.innerHeight - rect.top + 8))
1372
2021
  });
1373
2022
  }, [
1374
2023
  menuOpen,
1375
- wide,
1376
2024
  avatar,
1377
2025
  palette
1378
2026
  ]);
@@ -1423,6 +2071,16 @@ window.__ModuleLoader__.load({
1423
2071
  const label = authenticated ? displayName ?? t("sidebar.account") : t("sidebar.login");
1424
2072
  const title = authenticated ? t("sidebar.accountTitle") : t("sidebar.loginTitle");
1425
2073
  const triggerBackground = menuOpen || hovered ? palette.hover : "transparent";
2074
+ /**
2075
+ * Open the subscription page. Host mode → the EDA host's native dialog
2076
+ * (DialogService.OpenUrl, preferred size 1400×740); standalone DSH → the
2077
+ * in-app iframe dialog at the same preferred size. No credential → no-op.
2078
+ */
2079
+ const openSubscription = () => {
2080
+ if (subscribing) return;
2081
+ setSubscribing(true);
2082
+ auth.openSubscriptionDialog().catch(() => {}).finally(() => setSubscribing(false));
2083
+ };
1426
2084
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1427
2085
  style: {
1428
2086
  position: "relative",
@@ -1487,19 +2145,85 @@ window.__ModuleLoader__.load({
1487
2145
  role: "menu",
1488
2146
  style: menuStyle,
1489
2147
  children: [
1490
- displayName ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2148
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProfileHeaderCard, {
2149
+ displayName: displayName ?? t("sidebar.account"),
2150
+ packageName: quota?.gen?.packageName ?? quota?.erc?.packageName ?? null,
2151
+ phone: authState.phone ?? null,
2152
+ palette
2153
+ }),
2154
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2155
+ role: "presentation",
1491
2156
  style: {
1492
2157
  ...MENU_HEADER_BASE,
2158
+ paddingTop: 0,
2159
+ paddingBottom: 6,
2160
+ fontSize: 11,
2161
+ fontWeight: 600,
2162
+ letterSpacing: .4,
2163
+ textTransform: "uppercase",
1493
2164
  color: palette.muted
1494
2165
  },
1495
- title: displayName,
1496
- children: displayName
1497
- }) : null,
1498
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MenuItem, {
1499
- label: t("menu.profile"),
1500
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UserIcon, {}),
2166
+ children: t("subscription.quotaHeader")
2167
+ }),
2168
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2169
+ role: "presentation",
2170
+ style: {
2171
+ display: "flex",
2172
+ flexDirection: "column",
2173
+ gap: 8
2174
+ },
2175
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(QuotaMetric, {
2176
+ label: t("subscription.gen"),
2177
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CpuIcon, {}),
2178
+ quota: quota?.gen ?? null,
2179
+ gradient: palette.genGradient,
2180
+ palette
2181
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(QuotaMetric, {
2182
+ label: t("subscription.erc"),
2183
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WaveIcon, {}),
2184
+ quota: quota?.erc ?? null,
2185
+ gradient: palette.ercGradient,
2186
+ palette
2187
+ })]
2188
+ }),
2189
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UpgradeButton, {
2190
+ subscribing,
1501
2191
  palette,
1502
- onSelect: openProfile
2192
+ onClick: () => {
2193
+ if (subscribing) return;
2194
+ openSubscription();
2195
+ }
2196
+ }),
2197
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2198
+ role: "presentation",
2199
+ style: {
2200
+ height: 1,
2201
+ margin: "12px 2px",
2202
+ background: palette.border
2203
+ }
2204
+ }),
2205
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2206
+ role: "presentation",
2207
+ style: {
2208
+ display: "flex",
2209
+ flexDirection: "column",
2210
+ gap: 2
2211
+ },
2212
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MenuItem, {
2213
+ label: t("menu.profile"),
2214
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UserIcon, {}),
2215
+ trailing: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronRightIcon, {}),
2216
+ palette,
2217
+ onSelect: openProfile
2218
+ })
2219
+ }),
2220
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2221
+ role: "presentation",
2222
+ style: {
2223
+ height: 1,
2224
+ margin: "12px 2px",
2225
+ background: palette.border
2226
+ }
1503
2227
  }),
1504
2228
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MenuItem, {
1505
2229
  label: t("menu.logout"),
@@ -1570,7 +2294,16 @@ window.__ModuleLoader__.load({
1570
2294
  storage: createAuthStorage(localStorage),
1571
2295
  transport: createWebServerAuthTransport(),
1572
2296
  windowLike: window,
1573
- documentLike: document
2297
+ documentLike: document,
2298
+ openHostDialog: async (url, options) => {
2299
+ const hqEdge = ctx.get?.("hqEdge");
2300
+ if (!hqEdge || typeof hqEdge.openUrl !== "function") return;
2301
+ await hqEdge.openUrl(url, {
2302
+ ...options.title ? { title: options.title } : {},
2303
+ ...options.size ? { size: options.size } : {},
2304
+ ...options.key ? { key: options.key } : {}
2305
+ });
2306
+ }
1574
2307
  });
1575
2308
  registerHqEdgeGetter(() => ctx.get?.("hqEdge"));
1576
2309
  const disposers = [];