@deepseek-ai/dsh-client-ui-settings-account 0.2.1-alpha.3 → 0.2.1-alpha.5

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/README.i18n.yaml CHANGED
@@ -15,8 +15,8 @@
15
15
  en: c26f81e8adf0128b
16
16
  zh: e11ab371f51f66bc
17
17
  /deepseek-ai-dsh-client-ui-settings-account/use-this-package:
18
- en: a0e4b5bbef33ca6d
19
- zh: 697978822aeddf6e
18
+ en: 45aa7b44d5424d9b
19
+ zh: 20c7d344c4e9bce0
20
20
  /deepseek-ai-dsh-client-ui-settings-account/use-this-package/desktop-onboarding:
21
21
  en: c1c4db71fd09ca13
22
22
  zh: a40c0a01e9951945
package/README.md CHANGED
@@ -52,7 +52,7 @@ The account card’s More account information link has no underline and opens th
52
52
 
53
53
  Sign out first queries running account-token tasks and opens a confirmation dialog. The warning describes interruption when such tasks exist; otherwise it explains that data is retained and the account can be signed in again. Cancel, close, and Escape dismiss without signing out. Failed impact queries still open confirmation with an explicit unknown-task warning; failed sign-out keeps the dialog available for retry.
54
54
 
55
- A granted bonus appears as a server-authored notice above the sidebar account launcher without taking focus. The client reads the unnotified bonus when an account becomes active and once per Settings entry; it never polls. The server orders the candidates and owns the copy, so the client displays the first one and renders its message verbatim.
55
+ A granted bonus appears as a server-authored notice above the sidebar account launcher without taking focus. The client reads the unnotified bonus when an account becomes active and once per Settings entry; it never polls. The server orders the candidates and owns the copy, so the client stacks every live notice in that order and renders its message verbatim. Only the foreground card is interactive and reports display for acknowledgement. Closing it promotes the next card with a 250ms ease-out position and scale transition, including changes in content height; reduced-motion preferences disable the transition. A server-supplied title replaces the localized bonus title; responses without title retain that default.
56
56
 
57
57
  The notice waits until Desktop onboarding no longer covers the workspace, including loading, completion saving, and the exit animation, and until model credential onboarding unmounts. Closing the application during onboarding leaves the bonus unacknowledged. In the main interface, the notice is painted as soon as the server offers it, including underneath the translucent settings panel, once the document is visible and the card has passed one presented frame. That painted frame is the whole display signal: the card measures no cover, so a card behind the open overlay counts exactly as any other, and closing the card counts as a display too. Displaying a card queues its acknowledgement, so one mounted card is acknowledged once. A failed acknowledgement retries with a capped backoff for the rest of the signed-in session and never surfaces as an account UI error. A read that fails leaves the visible card and its pending acknowledgement alone, so a transport reconnect keeps retrying. Nothing is written to browser storage. The second signed-in frame of one sign-in and a reconnected stream's replayed state are one session, not a new one, so they keep the card and its retries; signing out, replacing the credential with another account's, or unloading the plugin discards them. Identity-only updates do not reach the account UI stream. Every signed-in frame reads again, and a newer read supersedes one still in flight, so a credential swapped in place is named by a fresh read rather than inheriting the replaced account's; the batch's account id then drops the previous account's card and queue even with no intervening signed-out state. The next sign-in displays whatever the server still offers, including an order an earlier sign-in acknowledged, and a card the user has already seen stays up until it is closed, even when a later read returns no bonus.
58
58
 
package/README.zh.md CHANGED
@@ -52,7 +52,7 @@ Chat 通过 `shell.overlay` 中的宿主把已绑定 Session 的实时欠费交
52
52
 
53
53
  退出登录先查询正在使用账号 token 的任务,再打开确认弹窗。有此类任务时提示执行将被打断,否则提示数据保留且可重新登录。取消、关闭和 Escape 均不退登。影响查询失败仍打开确认弹窗,并明确提示无法确认任务状态;退登失败保留弹窗以便重试。
54
54
 
55
- 赠金到账时,侧栏账号入口上方显示一条由服务端提供文案的通知,不抢占焦点。客户端仅在账号进入登录态时、以及每次进入设置页时读取一次未通知赠金,不做轮询。候选顺序与文案均由服务端决定,客户端只展示第一条并原样渲染其消息。
55
+ 赠金到账时,侧栏账号入口上方显示一条由服务端提供文案的通知,不抢占焦点。客户端仅在账号进入登录态时、以及每次进入设置页时读取一次未通知赠金,不做轮询。候选顺序与文案均由服务端决定,客户端按该顺序层叠展示全部未过期通知并原样渲染消息。只有最前面的卡片可交互,并在实际展示后触发确认。关闭前卡后,下一张卡片通过 250ms ease-out 的位置与缩放过渡(包括内容高度变化)进入前台;偏好减少动态效果时禁用过渡。服务端提供的 title 替换本地化赠金标题;未返回 title 时沿用默认标题。
56
56
 
57
57
  通知等待 Desktop 引导界面不再遮盖主界面且模型凭证引导卸载后才展示,Desktop 引导的等待范围包括加载、完成时的保存和退出动画。在引导期间关闭应用不会确认赠金通知。进入主界面后,服务端返回即绘制通知,包括在设置面板的半透明蒙层之下;同时要求文档可见且卡片已完成一帧绘制。这一帧绘制就是全部的展示信号:卡片不检测遮挡,因此处于打开蒙层之下的卡片与其他情形同样计数,用户关闭卡片同样计入展示。卡片展示后即排队确认,因此同一张挂载的卡只确认一次。确认失败会在本次登录会话内按带上限的退避重试,且不会在账号界面报错。读取失败不会影响已展示的卡片及其待确认队列,因此连接重连后仍会继续重试。不写入浏览器存储。同一次登录的第二个登录态帧以及重连后重放的登录态属于同一会话而非新会话,因此保留卡片与重试;只有退出登录、凭证被换成另一账号,或卸载插件才会丢弃。仅身份信息更新不会进入账号界面订阅流。每个登录态帧都会重新读取,且新的读取会取代仍在进行中的旧读取,因此就地更换的凭证会由一次全新的读取来识别,而不会沿用被替换账号的读取结果;读取结果中的账号 ID 随后会丢弃上一个账号的卡片与队列,即使没有中间的退出登录状态。下一次登录会展示服务端仍然下发的内容,包括上一次登录已确认过的订单;用户已看到的卡片会保留到关闭为止,即使之后的读取没有返回赠金。
58
58
 
package/lib/client.js CHANGED
@@ -1535,7 +1535,7 @@ window.__ModuleLoader__.load({
1535
1535
  }
1536
1536
  //#endregion
1537
1537
  //#region \0dsh-css:/home/runner/work/deepseek-harness/deepseek-harness/packages/client/ui-settings-account/src/client/AccountNotice.module.css.mjs
1538
- const css$4 = ".zwW4-W_card{z-index:40;box-sizing:border-box;border:.5px solid var(--dsw-alias-border-l2-darkmode-thin);background:var(--dsw-specific-input-major);width:260px;max-width:calc(100vw - 16px);color:var(--dsw-alias-label-secondary);border-radius:16px;padding:20px;font-size:14px;line-height:22px;position:fixed;box-shadow:0 2px 4px #0000000d}.zwW4-W_copy{overflow-wrap:anywhere;flex-direction:column;gap:2px;display:flex}.zwW4-W_copy p{white-space:pre-wrap;margin:0}.zwW4-W_title{align-items:flex-start;gap:2px;font-weight:500;line-height:20px;display:flex}.zwW4-W_icon{width:14px;height:14px;color:var(--dsw-alias-brand-primary-new-colorprimary-new-color);flex:none;margin-top:1px;display:flex}.zwW4-W_close{width:18px;height:18px;color:var(--dsw-alias-label-caption);cursor:pointer;background:0 0;border:0;justify-content:center;align-items:center;padding:4px;display:flex;position:absolute;top:6px;right:6px}.zwW4-W_close:focus-visible{outline:2px solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:2px;border-radius:4px}";
1538
+ const css$4 = ".zwW4-W_stack{z-index:40;isolation:isolate;width:280px;max-width:calc(100vw - 16px);position:fixed}.zwW4-W_card{box-sizing:border-box;background:var(--dsw-specific-input-major);width:100%;box-shadow:var(--dsw-elevation-panel);color:var(--dsw-alias-label-secondary);z-index:calc(0 - var(--notice-depth));transform:translateY(calc(-4px * var(--notice-depth))) scaleX(var(--notice-scale));transform-origin:top;border:0;border-radius:16px;padding:20px;font-size:14px;line-height:22px;transition:transform .25s ease-out;position:relative}.zwW4-W_card[data-front=false]{pointer-events:none;height:100%;position:absolute;inset:0;overflow:hidden}@media (prefers-reduced-motion:reduce){.zwW4-W_card{transition:none}}.zwW4-W_copy{overflow-wrap:anywhere;flex-direction:column;gap:2px;display:flex}.zwW4-W_copy p{white-space:pre-wrap;margin:0}.zwW4-W_title{align-items:flex-start;gap:2px;font-weight:500;line-height:20px;display:flex}.zwW4-W_icon{width:14px;height:14px;color:var(--dsw-alias-brand-primary-new-colorprimary-new-color);flex:none;margin-top:1px;display:flex}.zwW4-W_close{width:18px;height:18px;color:var(--dsw-alias-label-caption);cursor:pointer;background:0 0;border:0;justify-content:center;align-items:center;padding:4px;display:flex;position:absolute;top:6px;right:6px}.zwW4-W_close:focus-visible{outline:2px solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:2px;border-radius:4px}";
1539
1539
  const tagId$4 = "@deepseek-ai/dsh-client-ui-settings-account/AccountNotice.module.css";
1540
1540
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$4) + "]") === null) {
1541
1541
  const tag = document.createElement("style");
@@ -1549,9 +1549,45 @@ window.__ModuleLoader__.load({
1549
1549
  "close": "zwW4-W_close",
1550
1550
  "copy": "zwW4-W_copy",
1551
1551
  "icon": "zwW4-W_icon",
1552
+ "stack": "zwW4-W_stack",
1552
1553
  "title": "zwW4-W_title"
1553
1554
  };
1554
1555
  //#endregion
1556
+ //#region lib/types/client/notice-transition.js
1557
+ /** Animate a notification promotion across the old and new card dimensions. */
1558
+ /**
1559
+ * Close a card and animate surviving siblings from their currently displayed rectangles.
1560
+ * @param card - mounted foreground card whose direct parent owns the stack.
1561
+ * @param dismiss - synchronous notification-store update for the close gesture.
1562
+ */
1563
+ function dismissNotice(card, dismiss) {
1564
+ if (typeof card.animate !== "function" || getComputedStyle(card).transitionDuration === "0s") {
1565
+ dismiss();
1566
+ return;
1567
+ }
1568
+ const before = [...card.parentElement.querySelectorAll("aside")].map((element) => ({
1569
+ element,
1570
+ rect: element.getBoundingClientRect()
1571
+ }));
1572
+ for (const { element } of before) {
1573
+ for (const animation of element.getAnimations()) animation.cancel();
1574
+ element.style.transition = "none";
1575
+ }
1576
+ (0, react_dom.flushSync)(dismiss);
1577
+ for (const { element, rect } of before) {
1578
+ if (!element.isConnected) continue;
1579
+ const transform = getComputedStyle(element).transform;
1580
+ element.style.transform = "none";
1581
+ const next = element.getBoundingClientRect();
1582
+ element.style.transform = "";
1583
+ element.animate([{ transform: `translate(${rect.x + rect.width / 2 - next.x - next.width / 2}px, ${rect.y - next.y}px) scale(${rect.width / next.width}, ${rect.height / next.height})` }, { transform }], {
1584
+ duration: 250,
1585
+ easing: "ease-out"
1586
+ });
1587
+ element.style.transition = "";
1588
+ }
1589
+ }
1590
+ //#endregion
1555
1591
  //#region lib/types/client/AccountNotice.js
1556
1592
  /**
1557
1593
  * Sidebar bonus notice. The card reports a display once it has a position, the
@@ -1563,6 +1599,7 @@ window.__ModuleLoader__.load({
1563
1599
  */
1564
1600
  /** @param expiresAt - server expiration timestamp. @returns whether the award can still be spent. */
1565
1601
  function live(expiresAt) {
1602
+ if (expiresAt === "") return true;
1566
1603
  const expires = Date.parse(expiresAt);
1567
1604
  return Number.isNaN(expires) || expires > Date.now();
1568
1605
  }
@@ -1590,19 +1627,18 @@ window.__ModuleLoader__.load({
1590
1627
  if (timer !== void 0) clearTimeout(timer);
1591
1628
  };
1592
1629
  }
1593
- /** @param props - notice copy and identity, sidebar anchor, and display callbacks.
1594
- * @returns a non-modal notice above the account launcher until closed.
1630
+ /** @param props - server-ordered notices, localized fallback copy, anchor, and display callbacks.
1631
+ * @returns a non-modal stack whose foreground card alone can report display or be closed.
1595
1632
  */
1596
- function AccountNoticeCard({ notice, anchor, title, closeLabel, onShown, onDismiss }) {
1633
+ function AccountNoticeStack({ notices, anchor, title, closeLabel, onShown, onDismiss }) {
1597
1634
  const [position, setPosition] = (0, react.useState)();
1598
- const reported = (0, react.useRef)(false);
1599
1635
  (0, react.useLayoutEffect)(() => {
1600
- if (!anchor.current || !live(notice.expiresAt)) return;
1636
+ if (!anchor.current) return;
1601
1637
  const element = anchor.current;
1602
1638
  const update = () => {
1603
1639
  const rect = element.getBoundingClientRect();
1604
1640
  setPosition({
1605
- left: Math.max(8, Math.min(rect.left, window.innerWidth - 268)),
1641
+ left: Math.max(8, Math.min(rect.left, window.innerWidth - 288)),
1606
1642
  bottom: Math.max(8, window.innerHeight - rect.top + 4)
1607
1643
  });
1608
1644
  };
@@ -1616,13 +1652,28 @@ window.__ModuleLoader__.load({
1616
1652
  window.removeEventListener("resize", update);
1617
1653
  window.removeEventListener("scroll", update, true);
1618
1654
  };
1619
- }, [
1620
- anchor,
1621
- notice.orderId,
1622
- notice.expiresAt
1623
- ]);
1655
+ }, [anchor]);
1656
+ const liveNotices = notices.filter((notice) => live(notice.expiresAt));
1657
+ return (0, react_dom.createPortal)((0, react_jsx_runtime.jsx)("div", {
1658
+ className: AccountNotice_module_css_default.stack,
1659
+ style: position ?? { visibility: "hidden" },
1660
+ children: liveNotices.map((notice, depth) => (0, react_jsx_runtime.jsx)(NoticeCard, {
1661
+ notice,
1662
+ depth,
1663
+ ready: position !== void 0,
1664
+ title: notice.title ?? title,
1665
+ closeLabel,
1666
+ onShown,
1667
+ onDismiss
1668
+ }, notice.orderId))
1669
+ }), document.body);
1670
+ }
1671
+ /** One stable card node remains mounted as the cards before it are dismissed. */
1672
+ function NoticeCard({ notice, depth, ready, title, closeLabel, onShown, onDismiss }) {
1673
+ const foreground = depth === 0;
1674
+ const reported = (0, react.useRef)(false);
1624
1675
  (0, react.useEffect)(() => {
1625
- if (position === void 0 || reported.current || !live(notice.expiresAt)) return;
1676
+ if (!ready || !foreground || reported.current || !live(notice.expiresAt)) return;
1626
1677
  let cancel;
1627
1678
  const attempt = () => {
1628
1679
  if (reported.current || cancel !== void 0 || !live(notice.expiresAt)) return;
@@ -1641,15 +1692,20 @@ window.__ModuleLoader__.load({
1641
1692
  cancel?.();
1642
1693
  };
1643
1694
  }, [
1644
- position,
1695
+ ready,
1696
+ foreground,
1645
1697
  notice.orderId,
1646
1698
  notice.expiresAt,
1647
1699
  onShown
1648
1700
  ]);
1649
- if (!live(notice.expiresAt)) return null;
1650
- return (0, react_dom.createPortal)((0, react_jsx_runtime.jsxs)("aside", {
1701
+ return (0, react_jsx_runtime.jsxs)("aside", {
1651
1702
  className: AccountNotice_module_css_default.card,
1652
- style: position ?? { visibility: "hidden" },
1703
+ "data-front": foreground,
1704
+ "aria-hidden": !foreground,
1705
+ style: {
1706
+ "--notice-depth": depth,
1707
+ "--notice-scale": Math.pow(272 / 280, depth)
1708
+ },
1653
1709
  children: [(0, react_jsx_runtime.jsxs)("div", {
1654
1710
  role: "status",
1655
1711
  className: AccountNotice_module_css_default.copy,
@@ -1679,14 +1735,17 @@ window.__ModuleLoader__.load({
1679
1735
  }), (0, react_jsx_runtime.jsx)("p", { children: notice.message })]
1680
1736
  }), (0, react_jsx_runtime.jsx)("button", {
1681
1737
  type: "button",
1738
+ tabIndex: foreground ? void 0 : -1,
1682
1739
  className: AccountNotice_module_css_default.close,
1683
1740
  "aria-label": closeLabel,
1684
- onClick: () => {
1685
- onDismiss(notice.orderId);
1741
+ onClick: (event) => {
1742
+ dismissNotice(event.currentTarget.parentElement, () => {
1743
+ onDismiss(notice.orderId);
1744
+ });
1686
1745
  },
1687
1746
  children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCloseOutlineRegular, { size: 10 })
1688
1747
  })]
1689
- }), document.body);
1748
+ });
1690
1749
  }
1691
1750
  //#endregion
1692
1751
  //#region \0dsh-css:/home/runner/work/deepseek-harness/deepseek-harness/packages/client/ui-settings-account/src/client/AccountMenu.module.css.mjs
@@ -1770,14 +1829,14 @@ window.__ModuleLoader__.load({
1770
1829
  setExpiryNotice(false);
1771
1830
  }
1772
1831
  }),
1773
- signedIn && !account.desktopOnboardingVisible && !account.onboarding && account.notice && (0, react_jsx_runtime.jsx)(AccountNoticeCard, {
1774
- notice: account.notice,
1832
+ signedIn && !account.desktopOnboardingVisible && !account.onboarding && account.notices && (0, react_jsx_runtime.jsx)(AccountNoticeStack, {
1833
+ notices: account.notices,
1775
1834
  anchor,
1776
1835
  title: t("bonusNoticeTitle"),
1777
1836
  closeLabel: t("close"),
1778
1837
  onShown: bonusNoticeShown,
1779
1838
  onDismiss: bonusNoticeDismissed
1780
- }, account.notice.orderId),
1839
+ }),
1781
1840
  (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
1782
1841
  open,
1783
1842
  side: "top",
@@ -2605,6 +2664,7 @@ window.__ModuleLoader__.load({
2605
2664
  * @returns whether the bonus can no longer be shown.
2606
2665
  */
2607
2666
  function expired(expiresAt, at) {
2667
+ if (expiresAt === "") return false;
2608
2668
  const expires = Date.parse(expiresAt);
2609
2669
  return !Number.isNaN(expires) && expires <= at;
2610
2670
  }
@@ -2616,7 +2676,7 @@ window.__ModuleLoader__.load({
2616
2676
  function createBonusNoticeController(options) {
2617
2677
  let scope;
2618
2678
  /** Notice currently published to the sidebar, including the expiration checked at display time. */
2619
- let current;
2679
+ let current = [];
2620
2680
  /** Newest read; an older read still in flight has its result dropped once a newer one starts. */
2621
2681
  let readGeneration = 0;
2622
2682
  let ackTimer;
@@ -2630,12 +2690,15 @@ window.__ModuleLoader__.load({
2630
2690
  ackTimer = void 0;
2631
2691
  }
2632
2692
  };
2633
- const setCurrent = (notice) => {
2634
- if (notice === void 0 && current === void 0) return;
2635
- if (notice?.orderId === current?.orderId && notice?.message === current?.message && notice?.expiresAt === current?.expiresAt) return;
2636
- if (notice?.orderId !== current?.orderId) reported.clear();
2637
- current = notice;
2638
- options.publish(notice ?? null);
2693
+ const setCurrent = (notices) => {
2694
+ if (notices.length === current.length && notices.every((notice, index) => {
2695
+ const previous = current[index];
2696
+ return notice.orderId === previous?.orderId && notice.message === previous.message && notice.title === previous.title && notice.expiresAt === previous.expiresAt;
2697
+ })) return;
2698
+ const retained = new Set(notices.map((notice) => notice.orderId));
2699
+ for (const orderId of reported) if (!retained.has(orderId)) reported.delete(orderId);
2700
+ current = notices;
2701
+ options.publish(notices);
2639
2702
  };
2640
2703
  const scheduleAck = (delay) => {
2641
2704
  clearAckTimer();
@@ -2653,36 +2716,23 @@ window.__ModuleLoader__.load({
2653
2716
  if (scope !== void 0 && scope.pending.length > 0) scheduleAck(0);
2654
2717
  };
2655
2718
  /**
2656
- * Apply one read result. A newly offered order replaces the on-screen card;
2657
- * an offer equal to the on-screen card refreshes its copy in place, and an
2658
- * empty batch leaves a card the user has already seen alone.
2719
+ * Apply the server-ordered stack, excluding expired notices. An empty batch
2720
+ * retains only the foreground card if the user has already seen it.
2659
2721
  */
2660
2722
  const apply = (scopeAtRead, batch) => {
2661
2723
  if (scopeAtRead.accountId !== batch.accountId) {
2662
2724
  scopeAtRead.accountId = batch.accountId;
2663
2725
  scopeAtRead.pending.length = 0;
2664
2726
  resetAcks();
2665
- setCurrent(void 0);
2727
+ setCurrent([]);
2666
2728
  }
2667
- const candidate = batch.bonuses[0];
2668
- if (candidate === void 0) {
2669
- if (current !== void 0 && !reported.has(current.orderId)) setCurrent(void 0);
2729
+ const candidates = batch.bonuses.filter((notice) => !expired(notice.expiresAt, Date.now()));
2730
+ if (candidates.length === 0) {
2731
+ const foreground = current[0];
2732
+ setCurrent(foreground !== void 0 && reported.has(foreground.orderId) ? [foreground] : []);
2670
2733
  return;
2671
2734
  }
2672
- if (candidate.orderId === current?.orderId) {
2673
- setCurrent({
2674
- orderId: candidate.orderId,
2675
- message: candidate.message,
2676
- expiresAt: candidate.expiresAt
2677
- });
2678
- return;
2679
- }
2680
- if (expired(candidate.expiresAt, Date.now())) return;
2681
- setCurrent({
2682
- orderId: candidate.orderId,
2683
- message: candidate.message,
2684
- expiresAt: candidate.expiresAt
2685
- });
2735
+ setCurrent(candidates);
2686
2736
  };
2687
2737
  /**
2688
2738
  * Read once for one live lifecycle. A newer read supersedes one in flight: the
@@ -2742,7 +2792,7 @@ window.__ModuleLoader__.load({
2742
2792
  /** Queue a presented order, so an acknowledgement interrupted by a failure retries within this sign-in. */
2743
2793
  const report = (orderId) => {
2744
2794
  const scopeAtReport = scope;
2745
- if (scopeAtReport === void 0 || current?.orderId !== orderId || reported.has(orderId)) return;
2795
+ if (scopeAtReport === void 0 || current[0]?.orderId !== orderId || reported.has(orderId)) return;
2746
2796
  reported.add(orderId);
2747
2797
  if (!scopeAtReport.pending.includes(orderId)) scopeAtReport.pending.push(orderId);
2748
2798
  ackDelay = options.ackRetryDelayMs;
@@ -2753,7 +2803,7 @@ window.__ModuleLoader__.load({
2753
2803
  resetAcks();
2754
2804
  scope = void 0;
2755
2805
  reported.clear();
2756
- setCurrent(void 0);
2806
+ setCurrent([]);
2757
2807
  };
2758
2808
  return {
2759
2809
  begin() {
@@ -2767,14 +2817,15 @@ window.__ModuleLoader__.load({
2767
2817
  return runRead(scope);
2768
2818
  },
2769
2819
  shown(orderId) {
2770
- if (current?.orderId !== orderId) return;
2771
- if (expired(current.expiresAt, Date.now())) return;
2820
+ setCurrent(current.filter((notice) => !expired(notice.expiresAt, Date.now())));
2821
+ if (current[0]?.orderId !== orderId) return;
2772
2822
  report(orderId);
2773
2823
  },
2774
2824
  dismiss(orderId) {
2775
- if (current?.orderId !== orderId) return;
2825
+ setCurrent(current.filter((notice) => !expired(notice.expiresAt, Date.now())));
2826
+ if (current[0]?.orderId !== orderId) return;
2776
2827
  report(orderId);
2777
- setCurrent(void 0);
2828
+ setCurrent(current.slice(1).filter((notice) => !expired(notice.expiresAt, Date.now())));
2778
2829
  }
2779
2830
  };
2780
2831
  }
@@ -4410,7 +4461,7 @@ window.__ModuleLoader__.load({
4410
4461
  avatar.dispose();
4411
4462
  }, "account: profile picture lifetime");
4412
4463
  /** @returns the client identity for one account call, read at call time so it carries the language and zone in effect then. */
4413
- const client = () => accountClientMetadata(ctx.locale.getSnapshot().active, "0.2.1-alpha.3");
4464
+ const client = () => accountClientMetadata(ctx.locale.getSnapshot().active, "0.2.1-alpha.5");
4414
4465
  const notices = createBonusNoticeController({
4415
4466
  ackRetryDelayMs: config.bonusAckRetryDelayMs,
4416
4467
  ackRetryMaxDelayMs: config.bonusAckRetryMaxDelayMs,
@@ -4424,11 +4475,11 @@ window.__ModuleLoader__.load({
4424
4475
  if (!result.ok) throw new Error("account bonus acknowledgement failed");
4425
4476
  return result.value;
4426
4477
  },
4427
- publish: (notice) => {
4428
- const { notice: _replaced, ...withoutNotice } = snapshot;
4429
- publish(notice === null ? withoutNotice : {
4430
- ...withoutNotice,
4431
- notice
4478
+ publish: (notices) => {
4479
+ const { notices: _replaced, ...withoutNotices } = snapshot;
4480
+ publish(notices.length === 0 ? withoutNotices : {
4481
+ ...withoutNotices,
4482
+ notices
4432
4483
  });
4433
4484
  }
4434
4485
  });
@@ -4492,14 +4543,14 @@ window.__ModuleLoader__.load({
4492
4543
  refreshing = void 0;
4493
4544
  avatar.reset();
4494
4545
  const initialize = frame.value.status === "credential-stored" && frame.value.attempt?.phase === "succeeded" && snapshot.view?.attempt?.phase !== "succeeded";
4495
- const { notice, ...previous } = snapshot;
4546
+ const { notices: previousNotices, ...previous } = snapshot;
4496
4547
  publish({
4497
4548
  ...previous,
4498
4549
  view: frame.value,
4499
4550
  details: void 0,
4500
4551
  avatar: void 0,
4501
4552
  failed: false,
4502
- ...frame.value.status === "credential-stored" && notice ? { notice } : {}
4553
+ ...frame.value.status === "credential-stored" && previousNotices ? { notices: previousNotices } : {}
4503
4554
  });
4504
4555
  browser.update(frame.value);
4505
4556
  frame.accept();
@@ -4552,7 +4603,7 @@ window.__ModuleLoader__.load({
4552
4603
  const profile = snapshot.details?.profile;
4553
4604
  const context = {
4554
4605
  uid: profile?.status === "ready" ? profile.value.id : null,
4555
- version: "0.2.1-alpha.3",
4606
+ version: "0.2.1-alpha.5",
4556
4607
  width: window.screen.width,
4557
4608
  height: window.screen.height,
4558
4609
  pixelRatio: window.devicePixelRatio
@@ -8,17 +8,17 @@
8
8
  */
9
9
  import { type RefObject } from 'react';
10
10
  import type { BonusNotice } from './bonus-notices.ts';
11
- /** @param props - notice copy and identity, sidebar anchor, and display callbacks.
12
- * @returns a non-modal notice above the account launcher until closed.
11
+ /** @param props - server-ordered notices, localized fallback copy, anchor, and display callbacks.
12
+ * @returns a non-modal stack whose foreground card alone can report display or be closed.
13
13
  */
14
- export declare function AccountNoticeCard({ notice, anchor, title, closeLabel, onShown, onDismiss }: {
15
- notice: BonusNotice;
14
+ export declare function AccountNoticeStack({ notices, anchor, title, closeLabel, onShown, onDismiss }: {
15
+ notices: readonly BonusNotice[];
16
16
  anchor: RefObject<HTMLElement>;
17
17
  title: string;
18
18
  closeLabel: string;
19
- /** Called once, after the visible card has passed a presented frame. */
19
+ /** Called after the foreground card has passed a visible presented frame. */
20
20
  onShown: (orderId: BonusNotice['orderId']) => void;
21
- /** Called when the user closes the card, which counts as having seen it. */
21
+ /** Called when the user closes the foreground card. */
22
22
  onDismiss: (orderId: BonusNotice['orderId']) => void;
23
- }): import("react").ReactPortal | null;
23
+ }): import("react").ReactPortal;
24
24
  //# sourceMappingURL=AccountNotice.d.ts.map
@@ -6,8 +6,8 @@ import type { AccountAvatarSnapshot } from './account-avatar.ts';
6
6
  import type { BonusNotice } from './bonus-notices.ts';
7
7
  /** Safe account snapshot shared by the settings page and launcher. */
8
8
  export interface AccountSnapshot {
9
- /** Server-authored bonus notice awaiting display, absent when none is available. */
10
- notice?: BonusNotice;
9
+ /** Server-authored notices in front-to-back order, absent when none are available. */
10
+ notices?: readonly BonusNotice[];
11
11
  /** Latest Host state, absent until the stream responds. */
12
12
  view: AccountView | undefined;
13
13
  /** Sanitized profile and balance query outcomes, absent while loading. */
@@ -1,10 +1,10 @@
1
1
  /**
2
- * Lifecycle of bonus notices: one displayed notice at a time, one read result in
2
+ * Lifecycle of bonus notices: one foreground card in a stack, one read result in
3
3
  * force at a time, and one acknowledgement per displayed card. Reads happen when
4
4
  * the account becomes active and when the user asks for a refresh, never on a
5
5
  * timer; the server owns which bonus is unnotified and what its copy says.
6
6
  *
7
- * An order is acknowledged once its card reports a presented frame: the card has
7
+ * An order is acknowledged once its foreground card reports a presented frame: the card has
8
8
  * a position, the document is visible, and it painted one frame. The card measures
9
9
  * no cover, so a card painted under the open Settings overlay reports like any
10
10
  * other, and closing the card counts as a display too.
@@ -20,11 +20,13 @@
20
20
  import type { AccountBonusBatch, AccountBonusOrderId, AccountUserId } from '@deepseek-ai/dsh-deepseek-account/types';
21
21
  /** Notice eligible for display; the message is server-authored for the active locale. */
22
22
  export interface BonusNotice {
23
+ /** Server-localized plain-text title; absent responses use the localized bonus title. */
24
+ readonly title?: string;
23
25
  /** Server order identity. */
24
26
  readonly orderId: AccountBonusOrderId;
25
27
  /** Server-localized plain text, including any award amount or expiration. */
26
28
  readonly message: string;
27
- /** Server expiration, re-checked at display time so a notice cannot appear after its award expired. */
29
+ /** Server expiration, re-checked at display time; an empty string means no expiration. */
28
30
  readonly expiresAt: string;
29
31
  }
30
32
  /** Deployment-varying acknowledgement retry timings. */
@@ -45,8 +47,8 @@ export interface BonusNoticeControllerOptions extends BonusNoticeTiming {
45
47
  * @returns false when the account changed or signed out; a protocol failure rejects.
46
48
  */
47
49
  acknowledge: (accountId: AccountUserId, orderId: AccountBonusOrderId) => Promise<boolean>;
48
- /** @param notice - notice to display, or null when none is shown. */
49
- publish: (notice: BonusNotice | null) => void;
50
+ /** @param notices - notices in front-to-back order; empty when none are shown. */
51
+ publish: (notices: readonly BonusNotice[]) => void;
50
52
  }
51
53
  /** Commands the account plugin issues to the bonus notice lifecycle. */
52
54
  export interface BonusNoticeController {
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Close a card and animate surviving siblings from their currently displayed rectangles.
3
+ * @param card - mounted foreground card whose direct parent owns the stack.
4
+ * @param dismiss - synchronous notification-store update for the close gesture.
5
+ */
6
+ export declare function dismissNotice(card: HTMLElement, dismiss: () => void): void;
7
+ //# sourceMappingURL=notice-transition.d.ts.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-client-ui-settings-account",
3
3
  "description": "Manage DeepSeek login and open Platform billing pages",
4
- "version": "0.2.1-alpha.3",
4
+ "version": "0.2.1-alpha.5",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -45,27 +45,27 @@
45
45
  },
46
46
  "devDependencies": {
47
47
  "@deepseek-ai/cordis": "~4.0.5-alpha.2",
48
- "@deepseek-ai/dsh-api-remotes": "0.2.1-alpha.3",
49
- "@deepseek-ai/dsh-client-locale": "0.2.1-alpha.3",
50
- "@deepseek-ai/dsh-client-store": "0.2.1-alpha.3",
51
- "@deepseek-ai/dsh-client-test-runtime": "0.2.1-alpha.3",
52
- "@deepseek-ai/dsh-client-ui-chat": "0.2.1-alpha.3",
53
- "@deepseek-ai/dsh-client-ui-layout": "0.2.1-alpha.3",
54
- "@deepseek-ai/dsh-client-ui-primitives": "0.2.1-alpha.3",
55
- "@deepseek-ai/dsh-client-ui-renderer": "0.2.1-alpha.3",
56
- "@deepseek-ai/dsh-client-ui-settings": "0.2.1-alpha.3",
57
- "@deepseek-ai/dsh-client-ui-slots": "0.2.1-alpha.3",
58
- "@deepseek-ai/dsh-client-ui-theme": "0.2.1-alpha.3",
59
- "@deepseek-ai/dsh-deepseek-account": "0.2.1-alpha.3",
60
- "@deepseek-ai/dsh-settings": "0.2.1-alpha.3",
48
+ "@deepseek-ai/dsh-api-remotes": "0.2.1-alpha.5",
49
+ "@deepseek-ai/dsh-client-locale": "0.2.1-alpha.5",
50
+ "@deepseek-ai/dsh-client-store": "0.2.1-alpha.5",
51
+ "@deepseek-ai/dsh-client-test-runtime": "0.2.1-alpha.5",
52
+ "@deepseek-ai/dsh-client-ui-chat": "0.2.1-alpha.5",
53
+ "@deepseek-ai/dsh-client-ui-layout": "0.2.1-alpha.5",
54
+ "@deepseek-ai/dsh-client-ui-primitives": "0.2.1-alpha.5",
55
+ "@deepseek-ai/dsh-client-ui-renderer": "0.2.1-alpha.5",
56
+ "@deepseek-ai/dsh-client-ui-settings": "0.2.1-alpha.5",
57
+ "@deepseek-ai/dsh-client-ui-slots": "0.2.1-alpha.5",
58
+ "@deepseek-ai/dsh-client-ui-theme": "0.2.1-alpha.5",
59
+ "@deepseek-ai/dsh-deepseek-account": "0.2.1-alpha.5",
60
+ "@deepseek-ai/dsh-settings": "0.2.1-alpha.5",
61
61
  "@testing-library/react": "^16.3.3",
62
62
  "@types/big.js": "^6.2.2",
63
63
  "react": "^18.2.0",
64
64
  "react-dom": "^18.2.0",
65
- "@deepseek-ai/dsh-client-ui-settings-models": "0.2.1-alpha.3",
66
- "@deepseek-ai/dsh-host-webserver": "0.2.1-alpha.3",
65
+ "@deepseek-ai/dsh-client-ui-settings-models": "0.2.1-alpha.5",
66
+ "@deepseek-ai/dsh-host-webserver": "0.2.1-alpha.5",
67
67
  "big.js": "^7.0.1",
68
- "@deepseek-ai/dsh-client-product-analytics": "0.2.1-alpha.3"
68
+ "@deepseek-ai/dsh-client-product-analytics": "0.2.1-alpha.5"
69
69
  },
70
70
  "files": [
71
71
  "locale/*.json",