@qilin-agent/client-ui-account 0.0.0-stage → 3.1.4
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/LICENSE +21 -0
- package/README.i18n.yaml +34 -0
- package/README.md +79 -2
- package/README.zh.md +82 -0
- package/lib/client.js +437 -0
- package/lib/index.js +6 -0
- package/lib/types/client/AccountMenu.d.ts +35 -0
- package/lib/types/client/account-api.d.ts +57 -0
- package/lib/types/client/index.d.ts +31 -0
- package/lib/types/client/injected.d.ts +35 -0
- package/lib/types/client/locales.d.ts +17 -0
- package/lib/types/client/settings-panel.d.ts +19 -0
- package/lib/types/client/store.d.ts +32 -0
- package/lib/types/index.d.ts +4 -0
- package/package.json +69 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 kkutysllb
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.i18n.yaml
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Bilingual-pair consistency record for README.md (docs/i18n/README.md): per heading
|
|
2
|
+
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
|
3
|
+
# After editing either side, bring the other along and re-record with:
|
|
4
|
+
# pnpm run verify-translation-pairing --write packages/client/ui-account/README.md
|
|
5
|
+
/:
|
|
6
|
+
en: 745f3bb029ae3a78
|
|
7
|
+
zh: ae8a5ee520f28f85
|
|
8
|
+
/qilin-agent-client-ui-account:
|
|
9
|
+
en: 612cafd6a0630fe0
|
|
10
|
+
zh: f4fbc88c6a82302e
|
|
11
|
+
/qilin-agent-client-ui-account/summary:
|
|
12
|
+
en: 4561df8d1dc63d4c
|
|
13
|
+
zh: d54d893571090b7f
|
|
14
|
+
/qilin-agent-client-ui-account/table-of-contents:
|
|
15
|
+
en: 76368b6b1da79c80
|
|
16
|
+
zh: 52384aee991ac542
|
|
17
|
+
/qilin-agent-client-ui-account/use-this-package:
|
|
18
|
+
en: af042f14e4c37707
|
|
19
|
+
zh: fcea82ad20718480
|
|
20
|
+
/qilin-agent-client-ui-account/understand-the-implementation:
|
|
21
|
+
en: 660a673252b8b067
|
|
22
|
+
zh: 5ee8c0f9e9516c21
|
|
23
|
+
/qilin-agent-client-ui-account/model-experience:
|
|
24
|
+
en: 21ae98ac31e1c62a
|
|
25
|
+
zh: b7e2842cbcec0851
|
|
26
|
+
/qilin-agent-client-ui-account/model-experience/kv-cache-effect:
|
|
27
|
+
en: 2a2706075f9729cd
|
|
28
|
+
zh: 91f306246e59762c
|
|
29
|
+
/qilin-agent-client-ui-account/known-limitations-and-deferred-work:
|
|
30
|
+
en: 3c5eb8128c5f064b
|
|
31
|
+
zh: da5bebdc9da2b5d5
|
|
32
|
+
/qilin-agent-client-ui-account/known-limitations-and-deferred-work/dev-note:
|
|
33
|
+
en: ec656e8eba292940
|
|
34
|
+
zh: 52d3225942ce28e7
|
package/README.md
CHANGED
|
@@ -1,3 +1,80 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
description: "Account menu in the Web client sidebar footer: the theme and language switches, the Settings entry, and sign-out."
|
|
3
|
+
kind: "package-reference"
|
|
4
|
+
---
|
|
2
5
|
|
|
3
|
-
|
|
6
|
+
# @qilin-agent/client-ui-account
|
|
7
|
+
|
|
8
|
+
English | [中文](README.zh.md)
|
|
9
|
+
|
|
10
|
+
## Summary
|
|
11
|
+
|
|
12
|
+
The account menu is the footer's account row: the signed-in username's first letter in an avatar. One dropdown carries the username, Settings, Appearance and Language submenus, and Sign out; it is the Web surface for theme and language and the only way to sign out. Its facts come from `GET /api/auth/status`, so a deployment without accounts renders the row under the localized label with no session to end; when the transport reports the device session's operator, the menu names them instead of the account identity. It provides no service, owns no dialog, and contributes one list entry.
|
|
13
|
+
|
|
14
|
+
## Table of Contents
|
|
15
|
+
|
|
16
|
+
- [Use this package](#use-this-package)
|
|
17
|
+
- [Understand the implementation](#understand-the-implementation)
|
|
18
|
+
- [Model Experience](#model-experience)
|
|
19
|
+
- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
|
|
20
|
+
- [Dev Note](#dev-note)
|
|
21
|
+
|
|
22
|
+
-----
|
|
23
|
+
|
|
24
|
+
<a id="use-this-package"></a>
|
|
25
|
+
## Use this package
|
|
26
|
+
|
|
27
|
+
The `qilin` Web profile mounts this package through the `web-app` bundle patch; unloading the plugin removes its footer entry and restores the foot without an account surface.
|
|
28
|
+
|
|
29
|
+
| Row | Present when | Effect |
|
|
30
|
+
|---|---|---|
|
|
31
|
+
| the signed-in username | the gate answered with an account | a menu heading; the name is not a link |
|
|
32
|
+
| Settings | the settings panel is mounted | reveals the settings panel |
|
|
33
|
+
| Appearance | always | opens the Light / Dark / Follow system submenu |
|
|
34
|
+
| Language | always | opens one row per registered locale, each labelled in that locale |
|
|
35
|
+
| Sign out | the account gate is enabled | ends the session, then lands on the public landing page |
|
|
36
|
+
|
|
37
|
+
The identity and the sign-out row arrive with the status answer, so a menu opened before that answer shows neither; the rows below them are there from the first paint. The theme row matching the current preference and the language row matching the active locale are the marked ones.
|
|
38
|
+
|
|
39
|
+
A refused or unreachable sign-out navigates nowhere: the page stays where it is, and the menu keeps the rows usable instead of closing over a request that never landed.
|
|
40
|
+
|
|
41
|
+
<a id="understand-the-implementation"></a>
|
|
42
|
+
## Understand the implementation
|
|
43
|
+
|
|
44
|
+
The plugin fills one list entry in `sidebar.footer.action`, the sidebar's own footer hole, and waits for that declaration through `slots.inject`; the sidebar hands the entry only its column state (`wide`). Copy lives in this package's `account` dictionaries and reaches the component through the framework's `t` seat.
|
|
45
|
+
|
|
46
|
+
The entry declares `createAccountMenuStore()`: the dropdown's open state plus the account facts one status read resolved. The component reads that snapshot through `props.useStore` and writes through `props.actions`; nothing else mutates it, and the specs build the same store handle directly.
|
|
47
|
+
|
|
48
|
+
`createAccountMenuInjected` assembles the component's inject face inside `apply`: the two browser reads (`readAccountStatus`, `endSession`), the service calls (`ctx.settingsShell.open`, `ctx.theme.setTheme`, `ctx.locale.setLocale`), and the theme, locale, and settings-panel sources the renderer binds as `useTheme`, `useLocale`, and `useSettingsPanel`. Components reach no service themselves, and the sidecar reads keep their own request paths and navigation target local. The panel is an optional neighbour: `settingsShell` is read through `ctx.get` and its presence is published on the `useSettingsPanel` source from a scoped injection, so a composition that mounts no settings panel still mounts this menu and simply offers no Settings row.
|
|
49
|
+
|
|
50
|
+
<a id="model-experience"></a>
|
|
51
|
+
## Model Experience
|
|
52
|
+
|
|
53
|
+
None, as the account menu renders browser chrome and registers nothing model-facing.
|
|
54
|
+
|
|
55
|
+
#### KV Cache effect
|
|
56
|
+
|
|
57
|
+
None of its own; the menu adds nothing to a request and subscribes to no cached prefix.
|
|
58
|
+
|
|
59
|
+
## Known Limitations and Deferred Work
|
|
60
|
+
|
|
61
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
62
|
+
|
|
63
|
+
- **Submenu rows draw their own selection mark** — the shared `Menu` marks the top-level rows named in `selectedIds` and renders submenu rows unmarked, so this package draws the check for the active theme and locale row itself and still passes both ids to the primitive. Drop the local mark once the primitive marks submenu rows.
|
|
64
|
+
- **The account is read once per mount** — the status read runs when the footer entry mounts, so a session that changed in another tab keeps showing the previous address until the page reloads.
|
|
65
|
+
- **A refused sign-out is silent** — the menu stays open and the page stays put, but nothing tells the user the request failed; an error surface would need copy this package does not have.
|
|
66
|
+
- **No browser-level assertion covers the assembled foot** — the shipped specs drive the component with fed props and the plugin against a real slot registry; the assembled-surface check is manual.
|
|
67
|
+
|
|
68
|
+
<a id="dev-note"></a>
|
|
69
|
+
### Dev Note
|
|
70
|
+
|
|
71
|
+
<details>
|
|
72
|
+
<summary>Working context for maintainers — click to expand</summary>
|
|
73
|
+
|
|
74
|
+
This Dev Note is working context for maintainers: open design questions and directions that are not decided. It is explicitly non-authoritative — shipped behavior, limits, and accepted rationale live in the sections above, the package code, and the linked Agent Notes.
|
|
75
|
+
|
|
76
|
+
- Whether the menu should show a display name or an avatar image is open; the account record behind the status read carries an address and a creation time only.
|
|
77
|
+
- An enabled gate on a browser that carries no session still offers Sign out, because the sign-in document reads the same status answer; distinguishing "signed out" from "gate on" would need a field the status read does not report today.
|
|
78
|
+
- Flattening the theme and language rows out of their submenus for a narrow column has not been asked for; the submenus keep the dropdown short while the sidebar holds a rail width.
|
|
79
|
+
|
|
80
|
+
</details>
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Web 客户端侧边栏底部的账户菜单:主题与语言切换、设置入口,以及退出登录。"
|
|
3
|
+
kind: "package-reference"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# @qilin-agent/client-ui-account
|
|
7
|
+
|
|
8
|
+
[English](README.md) | 中文
|
|
9
|
+
|
|
10
|
+
## 概述
|
|
11
|
+
|
|
12
|
+
账户菜单是页脚的账号行:头像中携带已登录用户名的首字母。一个下拉菜单承载已登录用户名、设置、外观与语言子菜单以及退出登录;它是 Web 端切换主题与语言的入口,也是唯一退出登录的方式。账户事实来自 `GET /api/auth/status`,因此在未启用账户的部署上渲染出同一个本地化标签下的行,也没有可结束的会话;当传输层上报设备会话的操作者时,菜单以该名称替代账户身份展示。它不提供任何服务、不持有对话框,只贡献一个列表条目。
|
|
13
|
+
|
|
14
|
+
## 目录
|
|
15
|
+
|
|
16
|
+
- [使用本包](#use-this-package)
|
|
17
|
+
- [理解实现](#understand-the-implementation)
|
|
18
|
+
- [模型体验](#model-experience)
|
|
19
|
+
- [已知限制与延期工作](#known-limitations-and-deferred-work)
|
|
20
|
+
- [开发备注](#dev-note)
|
|
21
|
+
|
|
22
|
+
-----
|
|
23
|
+
|
|
24
|
+
<a id="use-this-package"></a>
|
|
25
|
+
## 使用本包
|
|
26
|
+
|
|
27
|
+
`qilin` Web profile 通过 `web-app` bundle 补丁挂载本包;卸载插件会移除它在底部的条目,让侧边栏底部回到没有账户入口的状态。
|
|
28
|
+
|
|
29
|
+
| 行 | 出现条件 | 效果 |
|
|
30
|
+
|---|---|---|
|
|
31
|
+
| 已登录地址 | 门禁应答中带有账户 | 菜单标题行;地址不是链接 |
|
|
32
|
+
| 设置 | 已挂载设置面板 | 展开设置面板 |
|
|
33
|
+
| 主题样式 | 始终 | 打开浅色/深色/跟随系统子菜单 |
|
|
34
|
+
| 语言 | 始终 | 每个已注册语言一行,各自以自己的语言标注 |
|
|
35
|
+
| 退出登录 | 账户门禁已启用 | 结束会话,然后落到公开 landing 页 |
|
|
36
|
+
|
|
37
|
+
身份行与退出登录行随状态应答一同到达,因此在应答之前打开的菜单不会显示这两行;它们下方的行从首帧起就在。当前主题偏好对应的那行、以及当前语言对应的那行,是带选中标记的行。
|
|
38
|
+
|
|
39
|
+
被拒绝或无法到达的退出登录不会跳转:页面停在原处,菜单保持可用,而不是在一个从未落地的请求上关闭。
|
|
40
|
+
|
|
41
|
+
<a id="understand-the-implementation"></a>
|
|
42
|
+
## 理解实现
|
|
43
|
+
|
|
44
|
+
插件填充 `sidebar.footer.action` 中的一个列表条目,也就是侧边栏自己的底部洞口,并通过 `slots.inject` 等待该声明出现;侧边栏只把列状态(`wide`)交给它。文案存在本包的 `account` 字典中,经框架的 `t` 座位抵达组件。
|
|
45
|
+
|
|
46
|
+
条目声明了 `createAccountMenuStore()`:下拉菜单的展开状态,以及一次状态读取解析出的账户事实。组件通过 `props.useStore` 读取该快照、通过 `props.actions` 写入;没有其他写入方,用例也直接构造同一个 store 句柄。
|
|
47
|
+
|
|
48
|
+
`createAccountMenuInjected` 在 `apply` 中组装组件的注入面:两个浏览器读取(`readAccountStatus`、`endSession`)、服务调用(`ctx.settingsShell.open`、`ctx.theme.setTheme`、`ctx.locale.setLocale`),以及渲染器绑定为 `useTheme`、`useLocale` 与 `useSettingsPanel` 的主题、语言与设置面板来源。组件自身不接触任何服务,两个请求各自的路径与跳转目标都留在本包内。设置面板是可选的邻居:`settingsShell` 经 `ctx.get` 读取,其存在与否由一处作用域注入发布到 `useSettingsPanel` 来源上,因此没有挂载设置面板的组合仍会挂载本菜单,只是不提供设置行。
|
|
49
|
+
|
|
50
|
+
<a id="model-experience"></a>
|
|
51
|
+
## 模型体验
|
|
52
|
+
|
|
53
|
+
None, as the account menu renders browser chrome and registers nothing model-facing.
|
|
54
|
+
|
|
55
|
+
#### KV Cache 影响
|
|
56
|
+
|
|
57
|
+
本身没有;菜单既不向请求添加内容,也不订阅任何缓存前缀。
|
|
58
|
+
|
|
59
|
+
## 已知限制与延期工作
|
|
60
|
+
|
|
61
|
+
<a id="known-limitations-and-deferred-work"></a>
|
|
62
|
+
|
|
63
|
+
这些限制描述本菜单目前做不到什么、以及什么时候需要留意。它们是当前约束,不是与其他账户界面之间的对比,也不是任务清单。
|
|
64
|
+
|
|
65
|
+
- **子菜单行自己绘制选中标记**——共享的 `Menu` 只为 `selectedIds` 中点名的顶层行绘制标记,子菜单行不带标记,因此本包自己为主题与语言当前行绘制对勾,同时仍把两个 id 交给该基础组件。等到基础组件为子菜单行也绘制标记后,应删除本地标记。
|
|
66
|
+
- **账户只在挂载时读取一次**——状态读取发生在底部条目挂载时,因此在另一个标签页里换了会话的浏览器,会继续显示此前的地址,直到页面重新加载。
|
|
67
|
+
- **被拒绝的退出登录是静默的**——菜单保持展开、页面停在原处,但没有任何提示告诉用户请求失败了;要给出错误提示,需要一个本包目前没有的文案位。
|
|
68
|
+
- **没有覆盖装配后底部的浏览器级断言**——随包发布的用例以直接喂入的 props 驱动组件、以真实槽位注册表驱动插件;装配面的检查目前是手工的。
|
|
69
|
+
|
|
70
|
+
<a id="dev-note"></a>
|
|
71
|
+
### 开发备注
|
|
72
|
+
|
|
73
|
+
<details>
|
|
74
|
+
<summary>维护者的工作上下文——点击展开</summary>
|
|
75
|
+
|
|
76
|
+
本开发备注是维护者的工作上下文:尚未决定的开放问题与方向。它明确不具备权威性——已发布行为、限制与已接受的取舍见上文各节、包代码以及链接的 Agent Note。
|
|
77
|
+
|
|
78
|
+
- 菜单是否应显示昵称或头像仍待定;状态读取背后的账户记录目前只有地址与创建时间。
|
|
79
|
+
- 门禁已启用但没有会话的浏览器仍会看到「退出登录」,因为登录文档读取的是同一份状态应答;要把「未登录」与「门禁开启」区分开,需要一个状态读取目前不报告的字段。
|
|
80
|
+
- 是否为窄列把主题与语言两行从子菜单中摊平,目前无人提出;子菜单让下拉菜单保持紧凑,而侧边栏还留着 rail 宽度。
|
|
81
|
+
|
|
82
|
+
</details>
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@qilin-agent/client-ui-account",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
8
|
+
let react = require("react");
|
|
9
|
+
let _qilin_agent_client_ui_primitives = require("@qilin-agent/client-ui-primitives");
|
|
10
|
+
let _qilin_agent_client_store = require("@qilin-agent/client-store");
|
|
11
|
+
//#region \0qilin-css:/home/runner/work/QiLin/QiLin/packages/client/ui-account/src/client/AccountMenu.module.css.mjs
|
|
12
|
+
const css = ".Ixy6Ia_slot{width:100%;display:flex}.Ixy6Ia_trigger{box-sizing:border-box;width:100%;height:40px;color:var(--qilin-alias-label-primary);text-align:left;cursor:pointer;background:0 0;border:none;border-radius:10px;align-items:center;gap:8px;padding:0 8px;font-family:inherit;font-size:14px;font-weight:400;line-height:22px;display:flex}.Ixy6Ia_trigger.Ixy6Ia_rail{corner-shape:round;border-radius:50%;justify-content:center;gap:0;width:36px;height:36px;padding:0}.Ixy6Ia_trigger:hover{background:var(--qilin-alias-interactive-bg-hover)}.Ixy6Ia_avatar{corner-shape:round;background:color-mix(in srgb, var(--qilin-alias-brand-primary) 16%, transparent);width:22px;height:22px;color:var(--qilin-alias-brand-primary);border-radius:50%;flex:none;place-items:center;font-size:11px;font-weight:600;line-height:1;display:grid}.Ixy6Ia_name{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}.Ixy6Ia_option{align-items:center;gap:8px;display:flex}.Ixy6Ia_optionText{text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;overflow:hidden}.Ixy6Ia_check{color:var(--qilin-alias-label-primary);flex:none}";
|
|
13
|
+
const tagId = "@qilin-agent/client-ui-account/AccountMenu.module.css";
|
|
14
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
15
|
+
const tag = document.createElement("style");
|
|
16
|
+
tag.dataset.plugin = "@qilin-agent/client-ui-account";
|
|
17
|
+
tag.dataset.pluginCss = tagId;
|
|
18
|
+
tag.textContent = css;
|
|
19
|
+
document.head.appendChild(tag);
|
|
20
|
+
}
|
|
21
|
+
var AccountMenu_module_css_default = {
|
|
22
|
+
"avatar": "Ixy6Ia_avatar",
|
|
23
|
+
"check": "Ixy6Ia_check",
|
|
24
|
+
"name": "Ixy6Ia_name",
|
|
25
|
+
"option": "Ixy6Ia_option",
|
|
26
|
+
"optionText": "Ixy6Ia_optionText",
|
|
27
|
+
"rail": "Ixy6Ia_rail",
|
|
28
|
+
"slot": "Ixy6Ia_slot",
|
|
29
|
+
"trigger": "Ixy6Ia_trigger"
|
|
30
|
+
};
|
|
31
|
+
//#endregion
|
|
32
|
+
//#region lib/types/client/AccountMenu.js
|
|
33
|
+
/**
|
|
34
|
+
* Sidebar-footer account button: the round trigger plus the menu that switches
|
|
35
|
+
* theme and language, opens Settings, and signs out. The account facts come
|
|
36
|
+
* from the menu store, live theme and locale state from the injected sources,
|
|
37
|
+
* and every write goes through the injected callbacks — the component reaches
|
|
38
|
+
* no service itself.
|
|
39
|
+
*/
|
|
40
|
+
/** Row id of the Settings entry. */
|
|
41
|
+
const SETTINGS_ID = "settings";
|
|
42
|
+
/** Row id of the sign-out entry. */
|
|
43
|
+
const SIGN_OUT_ID = "sign-out";
|
|
44
|
+
/** Prefix marking one theme option row; the remainder is the theme id. */
|
|
45
|
+
const THEME_ROW_PREFIX = "theme:";
|
|
46
|
+
/** Prefix marking one language option row; the remainder is the locale id. */
|
|
47
|
+
const LOCALE_ROW_PREFIX = "locale:";
|
|
48
|
+
/** The built-in theme preferences, in menu order. */
|
|
49
|
+
const THEME_ROWS = [
|
|
50
|
+
{
|
|
51
|
+
id: "light",
|
|
52
|
+
labelKey: "appearance.light",
|
|
53
|
+
Icon: _qilin_agent_client_ui_primitives.IconLightOutline16
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: "dark",
|
|
57
|
+
labelKey: "appearance.dark",
|
|
58
|
+
Icon: _qilin_agent_client_ui_primitives.IconDarkOutline16
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: "system",
|
|
62
|
+
labelKey: "appearance.system",
|
|
63
|
+
Icon: _qilin_agent_client_ui_primitives.IconFollowsystemOutline16
|
|
64
|
+
}
|
|
65
|
+
];
|
|
66
|
+
/**
|
|
67
|
+
* Wrap one option row's label with its selection marker. The primitive draws
|
|
68
|
+
* the check for the top-level rows named in `selectedIds` only, so a submenu
|
|
69
|
+
* row that is the active choice carries its own.
|
|
70
|
+
* @param label - the row's localized text.
|
|
71
|
+
* @param selected - whether this row is the active choice.
|
|
72
|
+
* @returns the row label node.
|
|
73
|
+
*/
|
|
74
|
+
function optionLabel(label, selected) {
|
|
75
|
+
return (0, react_jsx_runtime.jsxs)("span", {
|
|
76
|
+
className: AccountMenu_module_css_default.option,
|
|
77
|
+
children: [(0, react_jsx_runtime.jsx)("span", {
|
|
78
|
+
className: AccountMenu_module_css_default.optionText,
|
|
79
|
+
children: label
|
|
80
|
+
}), selected && (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconCheckOutline16, { className: AccountMenu_module_css_default.check })]
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Render the account button and its dropdown.
|
|
85
|
+
* @param props - composed slot props.
|
|
86
|
+
* @returns the sidebar-footer trigger with its anchored menu.
|
|
87
|
+
*/
|
|
88
|
+
function AccountMenu({ wide, useStore, actions, loadAccount, signOut, openSettings, setTheme, setLocale, useTheme, useLocale, useSettingsPanel, t }) {
|
|
89
|
+
const open = useStore((state) => state.open);
|
|
90
|
+
const accountName = useStore((state) => state.accountName);
|
|
91
|
+
const signOutAvailable = useStore((state) => state.signOutAvailable);
|
|
92
|
+
const settingsMounted = useSettingsPanel((mounted) => mounted);
|
|
93
|
+
const preference = useTheme((snapshot) => snapshot.preference);
|
|
94
|
+
const activeLocale = useLocale((snapshot) => snapshot.active);
|
|
95
|
+
const locales = useLocale((snapshot) => snapshot.locales);
|
|
96
|
+
(0, react.useEffect)(() => {
|
|
97
|
+
loadAccount();
|
|
98
|
+
}, [loadAccount]);
|
|
99
|
+
const selected = [];
|
|
100
|
+
const items = [];
|
|
101
|
+
if (accountName !== null) items.push({
|
|
102
|
+
type: "label",
|
|
103
|
+
id: "account",
|
|
104
|
+
text: accountName
|
|
105
|
+
});
|
|
106
|
+
if (settingsMounted) items.push({
|
|
107
|
+
id: SETTINGS_ID,
|
|
108
|
+
label: t("settings"),
|
|
109
|
+
icon: (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconSettingsOutline16, {})
|
|
110
|
+
});
|
|
111
|
+
items.push({
|
|
112
|
+
id: "appearance",
|
|
113
|
+
label: t("appearance"),
|
|
114
|
+
icon: (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconPersonalizationOutline16, {}),
|
|
115
|
+
submenu: THEME_ROWS.map((row) => {
|
|
116
|
+
const id = `${THEME_ROW_PREFIX}${row.id}`;
|
|
117
|
+
const isSelected = row.id === preference;
|
|
118
|
+
if (isSelected) selected.push(id);
|
|
119
|
+
return {
|
|
120
|
+
id,
|
|
121
|
+
label: optionLabel(t(row.labelKey), isSelected),
|
|
122
|
+
icon: (0, react_jsx_runtime.jsx)(row.Icon, {})
|
|
123
|
+
};
|
|
124
|
+
})
|
|
125
|
+
});
|
|
126
|
+
items.push({
|
|
127
|
+
id: "language",
|
|
128
|
+
label: t("language"),
|
|
129
|
+
icon: (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconGlobeOutline14, {}),
|
|
130
|
+
submenu: locales.map((entry) => {
|
|
131
|
+
const id = `${LOCALE_ROW_PREFIX}${entry.id}`;
|
|
132
|
+
const isSelected = entry.id === activeLocale;
|
|
133
|
+
if (isSelected) selected.push(id);
|
|
134
|
+
return {
|
|
135
|
+
id,
|
|
136
|
+
label: optionLabel(entry.label, isSelected)
|
|
137
|
+
};
|
|
138
|
+
})
|
|
139
|
+
});
|
|
140
|
+
if (signOutAvailable) {
|
|
141
|
+
items.push({
|
|
142
|
+
type: "separator",
|
|
143
|
+
id: "sign-out-separator"
|
|
144
|
+
});
|
|
145
|
+
items.push({
|
|
146
|
+
id: SIGN_OUT_ID,
|
|
147
|
+
label: t("signOut"),
|
|
148
|
+
icon: (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconLogoutOutline16, {}),
|
|
149
|
+
danger: true
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
const select = (id) => {
|
|
153
|
+
if (id === SIGN_OUT_ID) {
|
|
154
|
+
signOut();
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
actions.setOpen(false);
|
|
158
|
+
if (id === SETTINGS_ID) {
|
|
159
|
+
openSettings();
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (id.startsWith(THEME_ROW_PREFIX)) {
|
|
163
|
+
setTheme(id.slice(6));
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
setLocale(id.slice(7));
|
|
167
|
+
};
|
|
168
|
+
const label = t("label");
|
|
169
|
+
const name = accountName ?? label;
|
|
170
|
+
const initial = accountName === null ? void 0 : Array.from(accountName)[0]?.toUpperCase();
|
|
171
|
+
return (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.Menu, {
|
|
172
|
+
className: AccountMenu_module_css_default.slot,
|
|
173
|
+
open,
|
|
174
|
+
align: "end",
|
|
175
|
+
side: "top",
|
|
176
|
+
portal: true,
|
|
177
|
+
items,
|
|
178
|
+
selectedIds: selected,
|
|
179
|
+
onSelect: select,
|
|
180
|
+
onClose: () => {
|
|
181
|
+
actions.setOpen(false);
|
|
182
|
+
},
|
|
183
|
+
anchor: (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.Tooltip, {
|
|
184
|
+
label,
|
|
185
|
+
delayMs: 500,
|
|
186
|
+
disabled: wide,
|
|
187
|
+
children: (0, react_jsx_runtime.jsxs)("button", {
|
|
188
|
+
type: "button",
|
|
189
|
+
className: `${AccountMenu_module_css_default.trigger} ${wide ? AccountMenu_module_css_default.wide : AccountMenu_module_css_default.rail}`,
|
|
190
|
+
"aria-label": wide ? void 0 : label,
|
|
191
|
+
"aria-haspopup": "menu",
|
|
192
|
+
"aria-expanded": open,
|
|
193
|
+
onClick: () => {
|
|
194
|
+
actions.setOpen(!open);
|
|
195
|
+
},
|
|
196
|
+
children: [(0, react_jsx_runtime.jsx)("span", {
|
|
197
|
+
className: AccountMenu_module_css_default.avatar,
|
|
198
|
+
"aria-hidden": "true",
|
|
199
|
+
children: initial ?? (0, react_jsx_runtime.jsx)(_qilin_agent_client_ui_primitives.IconUserOutline16, { size: 14 })
|
|
200
|
+
}), wide && (0, react_jsx_runtime.jsx)("span", {
|
|
201
|
+
className: AccountMenu_module_css_default.name,
|
|
202
|
+
children: name
|
|
203
|
+
})]
|
|
204
|
+
})
|
|
205
|
+
})
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
//#endregion
|
|
209
|
+
//#region lib/types/client/account-api.js
|
|
210
|
+
/**
|
|
211
|
+
* Document-relative form of {@link AUTH_STATUS_PATH} the account menu addresses; see
|
|
212
|
+
* .agents/notes/implemented/architecture/2026-09-14-web-document-relative-app-routes.md.
|
|
213
|
+
*/
|
|
214
|
+
const AUTH_STATUS_ROUTE = "/api/auth/status".slice(1);
|
|
215
|
+
/** Document-relative form of {@link AUTH_LOGOUT_PATH} the account menu addresses. */
|
|
216
|
+
const AUTH_LOGOUT_ROUTE = "/api/auth/logout".slice(1);
|
|
217
|
+
/** Page the browser lands on once its session is gone: the public entry surface. */
|
|
218
|
+
const LANDING_PATH = "/";
|
|
219
|
+
/** The answer for a deployment with no reachable account surface. */
|
|
220
|
+
const NO_ACCOUNT = Object.freeze({
|
|
221
|
+
accountName: null,
|
|
222
|
+
signOutAvailable: false
|
|
223
|
+
});
|
|
224
|
+
/**
|
|
225
|
+
* Read the account gate's status.
|
|
226
|
+
*
|
|
227
|
+
* A mounted gate names the session's account and offers sign-out. A disabled
|
|
228
|
+
* gate has no account facts, but the transport can still name the operator of
|
|
229
|
+
* its device session — a display-only identity with no session to end. An
|
|
230
|
+
* unreachable gate stays one answer with both rows empty.
|
|
231
|
+
* @returns the account facts of the current browser session.
|
|
232
|
+
*/
|
|
233
|
+
async function readAccountStatus() {
|
|
234
|
+
try {
|
|
235
|
+
const response = await fetch(AUTH_STATUS_ROUTE);
|
|
236
|
+
if (!response.ok) return NO_ACCOUNT;
|
|
237
|
+
const status = await response.json();
|
|
238
|
+
if (!status.enabled) return {
|
|
239
|
+
accountName: status.accountName ?? null,
|
|
240
|
+
signOutAvailable: false
|
|
241
|
+
};
|
|
242
|
+
return {
|
|
243
|
+
accountName: (status.authenticated ? status.user : null)?.username ?? null,
|
|
244
|
+
signOutAvailable: true
|
|
245
|
+
};
|
|
246
|
+
} catch {
|
|
247
|
+
return NO_ACCOUNT;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* End this browser's session and land on the product's public page.
|
|
252
|
+
*
|
|
253
|
+
* The navigation happens only after the Host accepted the sign-out: a refused
|
|
254
|
+
* or unreachable call leaves the page where it is, so the menu that offered
|
|
255
|
+
* the row keeps working instead of stranding the user on a dead page.
|
|
256
|
+
* @returns whether the session ended (false keeps the current page).
|
|
257
|
+
*/
|
|
258
|
+
async function endSession() {
|
|
259
|
+
try {
|
|
260
|
+
if (!(await fetch(AUTH_LOGOUT_ROUTE, { method: "POST" })).ok) return false;
|
|
261
|
+
location.assign(LANDING_PATH);
|
|
262
|
+
return true;
|
|
263
|
+
} catch {
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
//#endregion
|
|
268
|
+
//#region lib/types/client/injected.js
|
|
269
|
+
/**
|
|
270
|
+
* Build one entry's injected share.
|
|
271
|
+
* @param sources - the store actions and service calls this instance routes to.
|
|
272
|
+
* @returns the inject face the component receives.
|
|
273
|
+
*/
|
|
274
|
+
function createAccountMenuInjected(sources) {
|
|
275
|
+
return {
|
|
276
|
+
loadAccount: async () => {
|
|
277
|
+
sources.actions.resolveAccount(await readAccountStatus());
|
|
278
|
+
},
|
|
279
|
+
signOut: async () => {
|
|
280
|
+
await endSession();
|
|
281
|
+
},
|
|
282
|
+
openSettings: sources.openSettings,
|
|
283
|
+
setTheme: sources.setTheme,
|
|
284
|
+
setLocale: sources.setLocale,
|
|
285
|
+
hooks: {
|
|
286
|
+
theme: sources.theme,
|
|
287
|
+
locale: sources.locale,
|
|
288
|
+
settingsPanel: sources.settingsPanel
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
//#endregion
|
|
293
|
+
//#region lib/types/client/settings-panel.js
|
|
294
|
+
/**
|
|
295
|
+
* Whether the neighbouring settings panel is mounted. The account menu's
|
|
296
|
+
* Settings row is that panel's entry point, and a composition may omit the
|
|
297
|
+
* panel entirely — a fixture surface that serves no settings traffic mounts the
|
|
298
|
+
* menu alone — so the row follows this flag instead of the menu's own
|
|
299
|
+
* activation waiting on the panel's service.
|
|
300
|
+
*/
|
|
301
|
+
/** Live availability of the settings panel behind the menu's Settings row. */
|
|
302
|
+
var SettingsPanelPresence = class {
|
|
303
|
+
/** Reactive presence flag; false until the panel provides its open channel. */
|
|
304
|
+
mounted = (0, _qilin_agent_client_store.createSnapshotStore)(false);
|
|
305
|
+
/**
|
|
306
|
+
* Publish whether the panel's open channel resolves.
|
|
307
|
+
* @param mounted - whether the settings shell is mounted in this composition.
|
|
308
|
+
*/
|
|
309
|
+
publish(mounted) {
|
|
310
|
+
if (this.mounted.getSnapshot() === mounted) return;
|
|
311
|
+
this.mounted.set(mounted);
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
//#endregion
|
|
315
|
+
//#region lib/types/client/store.js
|
|
316
|
+
/**
|
|
317
|
+
* Account menu store: whether the dropdown is open and what the status read
|
|
318
|
+
* resolved about the signed-in account. The plugin body owns the reads and
|
|
319
|
+
* publishes their results here; the component reads through props.useStore.
|
|
320
|
+
*/
|
|
321
|
+
/**
|
|
322
|
+
* Declare the account menu state and write surface.
|
|
323
|
+
* @returns a fresh handle — each registration instance owns its own open state
|
|
324
|
+
* and account facts, so no module-level handle can leak across plugin reloads.
|
|
325
|
+
*/
|
|
326
|
+
function createAccountMenuStore() {
|
|
327
|
+
return (0, _qilin_agent_client_store.defineStore)({
|
|
328
|
+
init: () => ({
|
|
329
|
+
open: false,
|
|
330
|
+
accountName: null,
|
|
331
|
+
signOutAvailable: false
|
|
332
|
+
}),
|
|
333
|
+
actions: {
|
|
334
|
+
setOpen: (draft, open) => {
|
|
335
|
+
draft.open = open;
|
|
336
|
+
},
|
|
337
|
+
resolveAccount: (draft, facts) => {
|
|
338
|
+
draft.accountName = facts.accountName;
|
|
339
|
+
draft.signOutAvailable = facts.signOutAvailable;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
//#endregion
|
|
345
|
+
//#region lib/types/client/locales.js
|
|
346
|
+
/** Copy dictionaries for the sidebar account menu. */
|
|
347
|
+
/** English strings (the key-set source of truth for this pair). */
|
|
348
|
+
const en = {
|
|
349
|
+
label: "Account",
|
|
350
|
+
settings: "Settings",
|
|
351
|
+
appearance: "Appearance",
|
|
352
|
+
"appearance.light": "Light",
|
|
353
|
+
"appearance.dark": "Dark",
|
|
354
|
+
"appearance.system": "Follow system",
|
|
355
|
+
language: "Language",
|
|
356
|
+
signOut: "Sign out"
|
|
357
|
+
};
|
|
358
|
+
/** Chinese strings for the same key set. */
|
|
359
|
+
const zh = {
|
|
360
|
+
label: "账户",
|
|
361
|
+
settings: "设置",
|
|
362
|
+
appearance: "主题样式",
|
|
363
|
+
"appearance.light": "浅色",
|
|
364
|
+
"appearance.dark": "深色",
|
|
365
|
+
"appearance.system": "跟随系统",
|
|
366
|
+
language: "语言",
|
|
367
|
+
signOut: "退出登录"
|
|
368
|
+
};
|
|
369
|
+
//#endregion
|
|
370
|
+
//#region lib/types/client/index.js
|
|
371
|
+
/** Dictionary namespace owned by this plugin. */
|
|
372
|
+
const NS = "account";
|
|
373
|
+
/** Services required by the account menu: the slot it fills and the two
|
|
374
|
+
* preference services behind its rows. The settings panel is an optional
|
|
375
|
+
* neighbour — see the scoped injection in {@link apply}. */
|
|
376
|
+
const inject = [
|
|
377
|
+
"slots",
|
|
378
|
+
"locale",
|
|
379
|
+
"theme"
|
|
380
|
+
];
|
|
381
|
+
/**
|
|
382
|
+
* Register the account menu into the sidebar foot's account row, once the
|
|
383
|
+
* sidebar declares that seat, and hand it the one store its entry shares.
|
|
384
|
+
* @param ctx - client root context.
|
|
385
|
+
*/
|
|
386
|
+
function apply(ctx) {
|
|
387
|
+
ctx.effect(() => ctx.locale.register(NS, {
|
|
388
|
+
zh,
|
|
389
|
+
en
|
|
390
|
+
}), "ui-account: dictionaries");
|
|
391
|
+
const store = createAccountMenuStore();
|
|
392
|
+
const settingsPanel = new SettingsPanelPresence();
|
|
393
|
+
const injected = (actions) => createAccountMenuInjected({
|
|
394
|
+
actions,
|
|
395
|
+
openSettings: () => {
|
|
396
|
+
ctx.get("settingsShell")?.open();
|
|
397
|
+
},
|
|
398
|
+
setTheme: (id) => {
|
|
399
|
+
ctx.theme.setTheme(id);
|
|
400
|
+
},
|
|
401
|
+
setLocale: (id) => {
|
|
402
|
+
ctx.locale.setLocale(id);
|
|
403
|
+
},
|
|
404
|
+
theme: {
|
|
405
|
+
getSnapshot: () => ctx.theme.getTheme(),
|
|
406
|
+
subscribe: (listener) => ctx.on("theme/change", listener)
|
|
407
|
+
},
|
|
408
|
+
locale: {
|
|
409
|
+
getSnapshot: () => ctx.locale.getSnapshot(),
|
|
410
|
+
subscribe: (listener) => ctx.locale.subscribe(listener)
|
|
411
|
+
},
|
|
412
|
+
settingsPanel: settingsPanel.mounted
|
|
413
|
+
});
|
|
414
|
+
ctx.slots.inject("sidebar.account", () => ctx.slots.register({
|
|
415
|
+
name: "sidebar.account",
|
|
416
|
+
locale: NS,
|
|
417
|
+
store,
|
|
418
|
+
inject: injected
|
|
419
|
+
}, AccountMenu));
|
|
420
|
+
ctx.inject(["settingsShell"], (scope) => {
|
|
421
|
+
scope.effect(() => {
|
|
422
|
+
settingsPanel.publish(true);
|
|
423
|
+
return () => {
|
|
424
|
+
settingsPanel.publish(false);
|
|
425
|
+
};
|
|
426
|
+
}, "ui-account: settings row availability");
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
//#endregion
|
|
430
|
+
exports.NS = NS;
|
|
431
|
+
exports.apply = apply;
|
|
432
|
+
exports.inject = inject;
|
|
433
|
+
return module.exports;
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
//# sourceMappingURL=client.js.map
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { HostObservable, InjectFace, PropsLocale, PropsRuntime, PropsStore } from '@qilin-agent/client-ui-slots';
|
|
3
|
+
import type { LocaleSnapshot } from '@qilin-agent/client-locale/client';
|
|
4
|
+
import type { ThemeSnapshot } from '@qilin-agent/client-ui-theme/client';
|
|
5
|
+
import type { AccountMenuStoreHandle } from './store.ts';
|
|
6
|
+
/** Injected dependencies of {@link AccountMenu} (the plugin's inject face). */
|
|
7
|
+
export interface AccountMenuInjected {
|
|
8
|
+
/** Read the account gate's status once and publish what it resolved. */
|
|
9
|
+
loadAccount: () => Promise<void>;
|
|
10
|
+
/** End the session; a refused call navigates nowhere and leaves the menu open. */
|
|
11
|
+
signOut: () => Promise<void>;
|
|
12
|
+
/** Reveal the settings panel. */
|
|
13
|
+
openSettings: () => void;
|
|
14
|
+
/** Switch the theme preference to a built-in id. */
|
|
15
|
+
setTheme: (id: string) => void;
|
|
16
|
+
/** Switch the active locale to a registered id. */
|
|
17
|
+
setLocale: (id: string) => void;
|
|
18
|
+
hooks: {
|
|
19
|
+
/** Theme state source, bound by the renderer as useTheme. */
|
|
20
|
+
theme: HostObservable<ThemeSnapshot>;
|
|
21
|
+
/** Locale state source, bound by the renderer as useLocale. */
|
|
22
|
+
locale: HostObservable<LocaleSnapshot>;
|
|
23
|
+
/** Settings-panel presence, bound by the renderer as useSettingsPanel. */
|
|
24
|
+
settingsPanel: HostObservable<boolean>;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/** Full component props: sidebar column state, store share, inject face, and copy seat. */
|
|
28
|
+
export type AccountMenuProps = PropsRuntime<'sidebar.account'> & PropsStore<AccountMenuStoreHandle> & InjectFace<AccountMenuInjected> & PropsLocale<'account'>;
|
|
29
|
+
/**
|
|
30
|
+
* Render the account button and its dropdown.
|
|
31
|
+
* @param props - composed slot props.
|
|
32
|
+
* @returns the sidebar-footer trigger with its anchored menu.
|
|
33
|
+
*/
|
|
34
|
+
export declare function AccountMenu({ wide, useStore, actions, loadAccount, signOut, openSettings, setTheme, setLocale, useTheme, useLocale, useSettingsPanel, t, }: AccountMenuProps): ReactNode;
|
|
35
|
+
//# sourceMappingURL=AccountMenu.d.ts.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `/api/auth` reads behind the account menu. The session cookie is HttpOnly
|
|
3
|
+
* and same-origin, so neither request states a credentials option: the browser
|
|
4
|
+
* attaches the cookie on its own.
|
|
5
|
+
*/
|
|
6
|
+
/** The signed-in account, as `/api/auth/status` reports it. */
|
|
7
|
+
export interface AccountUser {
|
|
8
|
+
/** Account id. */
|
|
9
|
+
readonly id: string;
|
|
10
|
+
/** Name the account signs in with; the menu's heading text. */
|
|
11
|
+
readonly username: string;
|
|
12
|
+
/** Address the account carries, or null when it registered without one. */
|
|
13
|
+
readonly email: string | null;
|
|
14
|
+
/** Account creation time in epoch milliseconds. */
|
|
15
|
+
readonly createdAt: number;
|
|
16
|
+
}
|
|
17
|
+
/** The fields of one `/api/auth/status` answer this menu reads. */
|
|
18
|
+
export interface AccountStatus {
|
|
19
|
+
/** Whether the account gate is active for this deployment. */
|
|
20
|
+
readonly enabled: boolean;
|
|
21
|
+
/** Whether the request carried a verified session. */
|
|
22
|
+
readonly authenticated: boolean;
|
|
23
|
+
/** Account behind the session; null when the request carried none. */
|
|
24
|
+
readonly user: AccountUser | null;
|
|
25
|
+
/**
|
|
26
|
+
* Operator name the transport reports for a gate-less deployment's device
|
|
27
|
+
* session; absent on a mounted gate, whose identity is the account's.
|
|
28
|
+
*/
|
|
29
|
+
readonly accountName?: string | null;
|
|
30
|
+
}
|
|
31
|
+
/** What the menu knows about the signed-in account. */
|
|
32
|
+
export interface AccountFacts {
|
|
33
|
+
/** Name shown as the menu heading; null when no account is known. */
|
|
34
|
+
readonly accountName: string | null;
|
|
35
|
+
/** Whether the menu offers the sign-out row. */
|
|
36
|
+
readonly signOutAvailable: boolean;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Read the account gate's status.
|
|
40
|
+
*
|
|
41
|
+
* A mounted gate names the session's account and offers sign-out. A disabled
|
|
42
|
+
* gate has no account facts, but the transport can still name the operator of
|
|
43
|
+
* its device session — a display-only identity with no session to end. An
|
|
44
|
+
* unreachable gate stays one answer with both rows empty.
|
|
45
|
+
* @returns the account facts of the current browser session.
|
|
46
|
+
*/
|
|
47
|
+
export declare function readAccountStatus(): Promise<AccountFacts>;
|
|
48
|
+
/**
|
|
49
|
+
* End this browser's session and land on the product's public page.
|
|
50
|
+
*
|
|
51
|
+
* The navigation happens only after the Host accepted the sign-out: a refused
|
|
52
|
+
* or unreachable call leaves the page where it is, so the menu that offered
|
|
53
|
+
* the row keeps working instead of stranding the user on a dead page.
|
|
54
|
+
* @returns whether the session ended (false keeps the current page).
|
|
55
|
+
*/
|
|
56
|
+
export declare function endSession(): Promise<boolean>;
|
|
57
|
+
//# sourceMappingURL=account-api.d.ts.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Account menu plugin, browser half: the sidebar footer's account button and
|
|
3
|
+
* its menu — the one surface that switches theme and language, opens Settings,
|
|
4
|
+
* and signs out. It joins the sidebar's footer action list, reads the account
|
|
5
|
+
* gate's status itself, and owns no other state.
|
|
6
|
+
*/
|
|
7
|
+
import type { Context as ClientContext } from '@qilin-agent/kylin';
|
|
8
|
+
import { type AccountLocaleKey } from './locales.ts';
|
|
9
|
+
export type { AccountMenuInjected, AccountMenuProps } from './AccountMenu.tsx';
|
|
10
|
+
export type { AccountFacts, AccountStatus, AccountUser } from './account-api.ts';
|
|
11
|
+
export type { AccountMenuActions, AccountMenuState, AccountMenuStoreHandle } from './store.ts';
|
|
12
|
+
export type { AccountLocaleKey } from './locales.ts';
|
|
13
|
+
declare module '@qilin-agent/client-ui-slots' {
|
|
14
|
+
interface LocaleNamespaceMap {
|
|
15
|
+
/** Sidebar account menu copy. */
|
|
16
|
+
account: AccountLocaleKey;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** Dictionary namespace owned by this plugin. */
|
|
20
|
+
export declare const NS = "account";
|
|
21
|
+
/** Services required by the account menu: the slot it fills and the two
|
|
22
|
+
* preference services behind its rows. The settings panel is an optional
|
|
23
|
+
* neighbour — see the scoped injection in {@link apply}. */
|
|
24
|
+
export declare const inject: string[];
|
|
25
|
+
/**
|
|
26
|
+
* Register the account menu into the sidebar foot's account row, once the
|
|
27
|
+
* sidebar declares that seat, and hand it the one store its entry shares.
|
|
28
|
+
* @param ctx - client root context.
|
|
29
|
+
*/
|
|
30
|
+
export declare function apply(ctx: ClientContext): void;
|
|
31
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The account menu's injected share: the browser reads and service calls the
|
|
3
|
+
* component may make, taken from the plugin body's own context. Building the
|
|
4
|
+
* face here keeps `apply` and the specs that drive the real status and
|
|
5
|
+
* sign-out requests on one wiring.
|
|
6
|
+
*/
|
|
7
|
+
import type { BoundActions, HostObservable } from '@qilin-agent/client-ui-slots';
|
|
8
|
+
import type { LocaleSnapshot } from '@qilin-agent/client-locale/client';
|
|
9
|
+
import type { ThemeSnapshot } from '@qilin-agent/client-ui-theme/client';
|
|
10
|
+
import type { AccountMenuInjected } from './AccountMenu.tsx';
|
|
11
|
+
import type { AccountMenuStoreHandle } from './store.ts';
|
|
12
|
+
/** Services and sources one account menu instance is wired to. */
|
|
13
|
+
export interface AccountMenuSources {
|
|
14
|
+
/** Baked write set of the entry's own store instance. */
|
|
15
|
+
readonly actions: BoundActions<AccountMenuStoreHandle>;
|
|
16
|
+
/** Reveal the settings panel. */
|
|
17
|
+
readonly openSettings: () => void;
|
|
18
|
+
/** Switch the theme preference to a built-in id. */
|
|
19
|
+
readonly setTheme: (id: string) => void;
|
|
20
|
+
/** Switch the active locale to a registered id. */
|
|
21
|
+
readonly setLocale: (id: string) => void;
|
|
22
|
+
/** Live theme state; the renderer binds it as useTheme. */
|
|
23
|
+
readonly theme: HostObservable<ThemeSnapshot>;
|
|
24
|
+
/** Live locale state; the renderer binds it as useLocale. */
|
|
25
|
+
readonly locale: HostObservable<LocaleSnapshot>;
|
|
26
|
+
/** Live settings-panel presence; the renderer binds it as useSettingsPanel. */
|
|
27
|
+
readonly settingsPanel: HostObservable<boolean>;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Build one entry's injected share.
|
|
31
|
+
* @param sources - the store actions and service calls this instance routes to.
|
|
32
|
+
* @returns the inject face the component receives.
|
|
33
|
+
*/
|
|
34
|
+
export declare function createAccountMenuInjected(sources: AccountMenuSources): AccountMenuInjected;
|
|
35
|
+
//# sourceMappingURL=injected.d.ts.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Copy dictionaries for the sidebar account menu. */
|
|
2
|
+
/** English strings (the key-set source of truth for this pair). */
|
|
3
|
+
export declare const en: {
|
|
4
|
+
label: string;
|
|
5
|
+
settings: string;
|
|
6
|
+
appearance: string;
|
|
7
|
+
'appearance.light': string;
|
|
8
|
+
'appearance.dark': string;
|
|
9
|
+
'appearance.system': string;
|
|
10
|
+
language: string;
|
|
11
|
+
signOut: string;
|
|
12
|
+
};
|
|
13
|
+
/** Chinese strings for the same key set. */
|
|
14
|
+
export declare const zh: Record<keyof typeof en, string>;
|
|
15
|
+
/** Key set of one account menu dictionary. */
|
|
16
|
+
export type AccountLocaleKey = keyof typeof en;
|
|
17
|
+
//# sourceMappingURL=locales.d.ts.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether the neighbouring settings panel is mounted. The account menu's
|
|
3
|
+
* Settings row is that panel's entry point, and a composition may omit the
|
|
4
|
+
* panel entirely — a fixture surface that serves no settings traffic mounts the
|
|
5
|
+
* menu alone — so the row follows this flag instead of the menu's own
|
|
6
|
+
* activation waiting on the panel's service.
|
|
7
|
+
*/
|
|
8
|
+
import { type SnapshotStore } from '@qilin-agent/client-store';
|
|
9
|
+
/** Live availability of the settings panel behind the menu's Settings row. */
|
|
10
|
+
export declare class SettingsPanelPresence {
|
|
11
|
+
/** Reactive presence flag; false until the panel provides its open channel. */
|
|
12
|
+
readonly mounted: SnapshotStore<boolean>;
|
|
13
|
+
/**
|
|
14
|
+
* Publish whether the panel's open channel resolves.
|
|
15
|
+
* @param mounted - whether the settings shell is mounted in this composition.
|
|
16
|
+
*/
|
|
17
|
+
publish(mounted: boolean): void;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=settings-panel.d.ts.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Account menu store: whether the dropdown is open and what the status read
|
|
3
|
+
* resolved about the signed-in account. The plugin body owns the reads and
|
|
4
|
+
* publishes their results here; the component reads through props.useStore.
|
|
5
|
+
*/
|
|
6
|
+
import { type EngineStoreHandle } from '@qilin-agent/client-store';
|
|
7
|
+
import type { AccountFacts } from './account-api.ts';
|
|
8
|
+
/** What the account menu renders at any moment. */
|
|
9
|
+
export interface AccountMenuState {
|
|
10
|
+
/** Whether the dropdown is showing. */
|
|
11
|
+
open: boolean;
|
|
12
|
+
/** Signed-in name shown as the menu heading; null when none is known. */
|
|
13
|
+
accountName: string | null;
|
|
14
|
+
/** Whether the menu offers the sign-out row. */
|
|
15
|
+
signOutAvailable: boolean;
|
|
16
|
+
}
|
|
17
|
+
/** Declared action shape giving the exported factory a stable return type. */
|
|
18
|
+
export type AccountMenuActions = {
|
|
19
|
+
/** Open or close the dropdown. */
|
|
20
|
+
setOpen: (draft: AccountMenuState, open: boolean) => void;
|
|
21
|
+
/** Publish what one status read resolved. */
|
|
22
|
+
resolveAccount: (draft: AccountMenuState, facts: AccountFacts) => void;
|
|
23
|
+
};
|
|
24
|
+
/** The declared handle one registration instance is created from. */
|
|
25
|
+
export type AccountMenuStoreHandle = EngineStoreHandle<AccountMenuState, AccountMenuActions>;
|
|
26
|
+
/**
|
|
27
|
+
* Declare the account menu state and write surface.
|
|
28
|
+
* @returns a fresh handle — each registration instance owns its own open state
|
|
29
|
+
* and account facts, so no module-level handle can leak across plugin reloads.
|
|
30
|
+
*/
|
|
31
|
+
export declare function createAccountMenuStore(): AccountMenuStoreHandle;
|
|
32
|
+
//# sourceMappingURL=store.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,72 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qilin-agent/client-ui-account",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"description": "Account menu in the Web client sidebar footer: theme, language, Settings, and sign-out",
|
|
4
|
+
"version": "3.1.4",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/kkutysllb/QiLin.git",
|
|
11
|
+
"directory": "packages/client/ui-account"
|
|
12
|
+
},
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": "^22.19.0 || >=24.0.0"
|
|
15
|
+
},
|
|
16
|
+
"type": "module",
|
|
17
|
+
"main": "lib/index.js",
|
|
18
|
+
"types": "lib/types/index.d.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./lib/types/index.d.ts",
|
|
22
|
+
"default": "./lib/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./client": {
|
|
25
|
+
"types": "./lib/types/client/index.d.ts",
|
|
26
|
+
"default": "./lib/client.js"
|
|
27
|
+
},
|
|
28
|
+
"./src/*": "./src/*",
|
|
29
|
+
"./package.json": "./package.json"
|
|
30
|
+
},
|
|
31
|
+
"qilin": {
|
|
32
|
+
"client": {
|
|
33
|
+
"inject": [
|
|
34
|
+
"@qilin-agent/client-ui-sidebar",
|
|
35
|
+
"@qilin-agent/client-locale",
|
|
36
|
+
"@qilin-agent/client-ui-theme",
|
|
37
|
+
"@qilin-agent/client-ui-settings-general",
|
|
38
|
+
"@qilin-agent/client-ui-renderer"
|
|
39
|
+
],
|
|
40
|
+
"platform": "web"
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"license": "MIT",
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"@qilin-agent/kylin": "~4.0.3"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@testing-library/react": "^16.1.0",
|
|
49
|
+
"@types/react": "~18.3.1",
|
|
50
|
+
"react": "^18.2.0",
|
|
51
|
+
"react-dom": "^18.2.0",
|
|
52
|
+
"@qilin-agent/kylin": "~4.0.3",
|
|
53
|
+
"@qilin-agent/client-store": "3.1.4",
|
|
54
|
+
"@qilin-agent/client-ui-primitives": "3.1.4",
|
|
55
|
+
"@qilin-agent/client-ui-renderer": "3.1.4",
|
|
56
|
+
"@qilin-agent/client-locale": "3.1.4",
|
|
57
|
+
"@qilin-agent/client-test-runtime": "3.1.4",
|
|
58
|
+
"@qilin-agent/client-ui-settings-general": "3.1.4",
|
|
59
|
+
"@qilin-agent/client-ui-sidebar": "3.1.4",
|
|
60
|
+
"@qilin-agent/client-ui-slots": "3.1.4",
|
|
61
|
+
"@qilin-agent/client-ui-theme": "3.1.4"
|
|
62
|
+
},
|
|
63
|
+
"files": [
|
|
64
|
+
"lib/index.js",
|
|
65
|
+
"lib/client.js",
|
|
66
|
+
"lib/types/**/*.d.ts"
|
|
67
|
+
],
|
|
68
|
+
"scripts": {
|
|
69
|
+
"bundle": "tsdown",
|
|
70
|
+
"watch": "tsdown --watch"
|
|
71
|
+
}
|
|
6
72
|
}
|