mellos-mapping 0.23.0 → 0.25.0

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/dist/web/app.css CHANGED
@@ -724,7 +724,8 @@ h2 {
724
724
  margin-left: 20px;
725
725
  }
726
726
  footer {
727
- height: 29px;
727
+ min-height: 29px;
728
+ flex-wrap: wrap;
728
729
  flex-shrink: 0;
729
730
  border-top: 1px solid var(--line);
730
731
  display: flex;
@@ -919,7 +920,7 @@ dialog p {
919
920
  #app {
920
921
  display: grid;
921
922
  grid-template-columns: minmax(0, 1fr) 300px;
922
- grid-template-rows: 58px auto auto minmax(0, 1fr) 29px;
923
+ grid-template-rows: 58px auto auto minmax(0, 1fr) auto;
923
924
  }
924
925
  header {
925
926
  grid-column: 1 / -1;
@@ -965,3 +966,106 @@ dialog p {
965
966
  grid-column: 1 / -1;
966
967
  }
967
968
  }
969
+
970
+ #star-reminder {
971
+ --star-surface: linear-gradient(115deg, #15302e, #202341 48%, #36243b);
972
+ --star-text: #f2f5ff;
973
+ --star-muted: #c9d2e5;
974
+ width: 100%;
975
+ display: flex;
976
+ flex-wrap: wrap;
977
+ align-items: center;
978
+ gap: 10px 14px;
979
+ margin: 4px 0 8px;
980
+ padding: 12px 16px;
981
+ border: 1px solid transparent;
982
+ border-radius: 12px;
983
+ background: var(--star-surface) padding-box,
984
+ linear-gradient(110deg, #69ebc9, #8299ff, #d897ff, #ffc17d, #69ebc9) border-box;
985
+ background-size: 200% 100%;
986
+ color: var(--star-text);
987
+ font-size: 14px;
988
+ line-height: 1.5;
989
+ animation: star-color-flow 6s ease-in-out 2, star-glow 3s ease-in-out 4;
990
+ }
991
+ #star-reminder::before {
992
+ content: "★";
993
+ display: grid;
994
+ place-items: center;
995
+ flex: 0 0 36px;
996
+ height: 36px;
997
+ border: 1px solid #ffe49950;
998
+ border-radius: 10px;
999
+ background: #f9c76916;
1000
+ color: #ffe49b;
1001
+ font-size: 26px;
1002
+ line-height: 1;
1003
+ text-shadow: 0 0 14px #ffd47870;
1004
+ animation: star-beat 3s ease-in-out 4;
1005
+ }
1006
+ #star-reminder > span { flex: 1 1 230px; font-weight: 550; }
1007
+ #star-reminder a {
1008
+ display: inline-flex;
1009
+ align-items: center;
1010
+ justify-content: center;
1011
+ min-height: 36px;
1012
+ padding: 7px 16px;
1013
+ border: 1px solid #ffffff50;
1014
+ border-radius: 8px;
1015
+ background: linear-gradient(110deg, #8bf0d0, #babaff 55%, #ffd0aa);
1016
+ color: #17243a;
1017
+ font-size: 13px;
1018
+ font-weight: 750;
1019
+ line-height: 1.4;
1020
+ text-decoration: none;
1021
+ white-space: nowrap;
1022
+ box-shadow: 0 2px 12px #8b96ff25;
1023
+ }
1024
+ #star-reminder a:hover { filter: brightness(1.1); }
1025
+ #star-reminder a[aria-disabled=true] { cursor: progress; opacity: .8; }
1026
+ #star-reminder .star-open-error { flex: 1 1 100%; display: flex; flex-wrap: wrap; align-items: center; gap: 8px; font-size: 12px; color: var(--star-muted); }
1027
+ #star-reminder .star-open-error input { min-width: 0; max-width: 100%; width: 340px; padding: 6px; color: var(--star-text); background: transparent; border: 1px solid #b8c5e470; border-radius: 6px; }
1028
+ #star-reminder button {
1029
+ min-height: 34px;
1030
+ padding: 6px 10px;
1031
+ border: 1px solid #b8c5e42e;
1032
+ border-radius: 8px;
1033
+ background: transparent;
1034
+ color: var(--star-muted);
1035
+ font-size: 12px;
1036
+ line-height: 1.4;
1037
+ white-space: nowrap;
1038
+ }
1039
+ #star-reminder button:hover { background: #a0b5f014; border-color: #b8c5e470; }
1040
+ #star-reminder a:focus-visible, #star-reminder button:focus-visible {
1041
+ outline: 2px solid #b0b9ff;
1042
+ outline-offset: 3px;
1043
+ }
1044
+ #star-reminder:hover, #star-reminder:focus-within,
1045
+ #star-reminder:hover::before, #star-reminder:focus-within::before { animation-play-state: paused; }
1046
+ [data-theme=light] #star-reminder {
1047
+ --star-surface: linear-gradient(115deg, #e4fbf5, #eeedff 48%, #fff0e5);
1048
+ --star-text: #263451;
1049
+ --star-muted: #53607b;
1050
+ }
1051
+ [data-theme=light] #star-reminder::before { color: #a5660c; background: #ffe6aa80; border-color: #dbae6340; text-shadow: none; }
1052
+ @keyframes star-color-flow {
1053
+ 0%, 100% { background-position: 0% 50%; }
1054
+ 50% { background-position: 100% 50%; }
1055
+ }
1056
+ @keyframes star-glow {
1057
+ 0%, 100% { box-shadow: 0 0 0 #8299ff00; }
1058
+ 50% { box-shadow: 0 0 16px #8299ff30, inset 0 0 18px #b897ff0c; }
1059
+ }
1060
+ @keyframes star-beat {
1061
+ 0%, 100% { transform: scale(1) rotate(0deg); }
1062
+ 50% { transform: scale(1.1) rotate(-7deg); }
1063
+ }
1064
+ @media (max-width: 520px) {
1065
+ #star-reminder { padding: 12px; gap: 10px; }
1066
+ #star-reminder > span { flex-basis: calc(100% - 50px); font-size: 13px; }
1067
+ #star-reminder a { margin-left: 46px; }
1068
+ }
1069
+ @media (prefers-reduced-motion: reduce) {
1070
+ #star-reminder, #star-reminder::before { animation: none; }
1071
+ }
package/dist/web/app.js CHANGED
@@ -904,6 +904,82 @@ function zoomScene(state, factor, x, y) {
904
904
  };
905
905
  }
906
906
 
907
+ // src/web/star-reminder.ts
908
+ function createStarNotice(base2) {
909
+ let attempted = false;
910
+ const notice = document.getElementById("star-reminder");
911
+ const link = notice.querySelector("a");
912
+ let opening = false;
913
+ link.title = "\u5728\u7CFB\u7EDF\u9ED8\u8BA4\u6D4F\u89C8\u5668\u4E2D\u6253\u5F00 GitHub";
914
+ const dismiss = () => {
915
+ notice.hidden = true;
916
+ };
917
+ notice.querySelector("button").addEventListener("click", dismiss);
918
+ const launch = async (event) => {
919
+ event.preventDefault();
920
+ if (opening) return;
921
+ opening = true;
922
+ link.setAttribute("aria-disabled", "true");
923
+ link.textContent = "\u6B63\u5728\u6253\u5F00\u2026";
924
+ notice.querySelector(".star-open-error")?.remove();
925
+ try {
926
+ const response = await fetch(new URL("api/star-reminder/open", base2), { method: "POST", signal: AbortSignal.timeout(8e3) });
927
+ const result = await response.json();
928
+ if (!response.ok || result.opened !== true) throw new Error("Not opened");
929
+ dismiss();
930
+ } catch {
931
+ const error = document.createElement("div");
932
+ error.className = "star-open-error";
933
+ error.setAttribute("role", "status");
934
+ const message = document.createElement("span");
935
+ message.textContent = "\u9ED8\u8BA4\u6D4F\u89C8\u5668\u672A\u80FD\u6253\u5F00\uFF0C\u53EF\u91CD\u8BD5\u6216\u590D\u5236\u5730\u5740\u3002";
936
+ const copy = document.createElement("button");
937
+ copy.type = "button";
938
+ copy.textContent = "\u590D\u5236 GitHub \u5730\u5740";
939
+ copy.addEventListener("click", () => {
940
+ void (async () => {
941
+ try {
942
+ await navigator.clipboard.writeText(link.href);
943
+ copy.textContent = "\u5DF2\u590D\u5236";
944
+ } catch {
945
+ const address = document.createElement("input");
946
+ address.readOnly = true;
947
+ address.value = link.href;
948
+ address.setAttribute("aria-label", "GitHub \u5730\u5740");
949
+ copy.replaceWith(address);
950
+ address.focus();
951
+ address.select();
952
+ }
953
+ })();
954
+ });
955
+ error.append(message, copy);
956
+ notice.append(error);
957
+ } finally {
958
+ opening = false;
959
+ link.removeAttribute("aria-disabled");
960
+ link.textContent = "\u53BB\u70B9 Star \u2197";
961
+ }
962
+ };
963
+ link.addEventListener("click", (event) => {
964
+ void launch(event);
965
+ });
966
+ link.addEventListener("auxclick", (event) => {
967
+ if (event.button === 1) void launch(event);
968
+ });
969
+ return (page) => {
970
+ if (attempted || document.visibilityState !== "visible") return;
971
+ attempted = true;
972
+ const url = new URL("api/star-reminder/visit", base2);
973
+ if (page) url.searchParams.set("page", page);
974
+ void fetch(url, { method: "POST", signal: AbortSignal.timeout(3e3) }).then(async (response) => {
975
+ if (!response.ok) return;
976
+ const result = await response.json();
977
+ if (result.show === true && document.visibilityState === "visible") notice.hidden = false;
978
+ }).catch(() => {
979
+ });
980
+ };
981
+ }
982
+
907
983
  // src/web/app.ts
908
984
  var element = (id) => document.getElementById(id);
909
985
  var viewport = element("viewport");
@@ -913,6 +989,7 @@ var pages = element("pages");
913
989
  var search = element("search");
914
990
  var dialog = element("dialog");
915
991
  var base = new URL(".", window.location.href);
992
+ var starNotice = createStarNotice(base);
916
993
  var kinds = { dev: "\u5F00\u53D1\u5730\u56FE", architecture: "\u67B6\u6784\u5730\u56FE", dataflow: "\u6570\u636E\u6D41", "behavior-tree": "\u884C\u4E3A\u6811", sequence: "\u65F6\u5E8F\u5730\u56FE" };
917
994
  var snapshot = { project: "", pages: [] };
918
995
  var revision = "";
@@ -1340,6 +1417,7 @@ async function poll() {
1340
1417
  }
1341
1418
  if (initial || JSON.stringify(priorPage) !== JSON.stringify(current())) render();
1342
1419
  else renderHeading();
1420
+ if ((current()?.map?.nodes.length ?? 0) > 0 && !current()?.error) starNotice(currentId);
1343
1421
  element("updated").textContent = `\u5DF2\u540C\u6B65 ${(/* @__PURE__ */ new Date()).toLocaleTimeString("zh-CN", { hour12: false })}`;
1344
1422
  }
1345
1423
  }
@@ -5,5 +5,5 @@
5
5
  <section class="map-heading"><div class="project-line"><span id="project">本地项目</span><span>/</span><span id="kind">开发地图</span></div><div class="title-row"><h1 id="title">正在读取地图…</h1><select id="pages" aria-label="切换地图页面"></select></div><div id="breadcrumbs"></div><div class="progress-row"><span id="summary"></span><div id="progress"><i></i></div><button id="follow" class="follow active" aria-pressed="true">● 自动跟随</button></div></section>
6
6
  <section class="toolbar"><label class="search"><svg viewBox="0 0 20 20" aria-hidden="true"><circle cx="8" cy="8" r="5.5"/><path d="m12 12 5 5"/></svg><input id="search" placeholder="搜索模块、说明…" aria-label="搜索模块"><kbd>/</kbd></label><button id="overview" class="tool-button" aria-pressed="false">分组概览</button><button id="export" class="icon-button" title="导出 SVG" aria-label="导出 SVG">↓</button><button id="delete" class="icon-button" title="删除当前地图" aria-label="删除当前地图">×</button></section>
7
7
  <section class="canvas-shell"><div id="viewport" tabindex="0" aria-label="交互地图画布"><div id="stage"></div><div id="empty" hidden></div></div><div id="filters" class="filters" aria-label="按状态筛选"><button data-status="all" class="active">全部</button><button data-status="planned"><i class="planned"></i>待开发</button><button data-status="in-progress"><i class="in-progress"></i>进行中</button><button data-status="done"><i class="done"></i>已验证</button><button data-status="regressed"><i class="regressed"></i>回归</button><button data-status="unverified"><i class="unverified"></i>待验证</button></div><div class="zoom-controls"><button id="zoom-out" aria-label="缩小">−</button><output id="zoom-label">100%</output><button id="zoom-in" aria-label="放大">+</button><span></span><button id="fit" title="适应窗口 · 0" aria-label="适应窗口">⛶</button></div><div id="minimap" title="点击移动视口"></div><div id="match-count" role="status"></div></section>
8
- <div id="divider" role="separator" aria-label="调整详情高度" aria-orientation="horizontal" aria-valuemin="100" aria-valuemax="500" aria-valuenow="210" tabindex="0"><i></i></div><aside id="inspector"></aside><footer><span>滚轮缩放 · 拖动画布 · 双击进入子地图</span><span id="updated">等待同步</span></footer>
8
+ <div id="divider" role="separator" aria-label="调整详情高度" aria-orientation="horizontal" aria-valuemin="100" aria-valuemax="500" aria-valuenow="210" tabindex="0"><i></i></div><aside id="inspector"></aside><footer><span>滚轮缩放 · 拖动画布 · 双击进入子地图</span><span id="updated">等待同步</span><aside id="star-reminder" aria-label="支持 Mellos Mapping" hidden><span>地图有帮到你?欢迎在 GitHub 点个 Star。</span><a href="https://github.com/GuangminJu/mellos-mapping" target="_blank" rel="noopener noreferrer">去点 Star ↗</a><button type="button">不再提醒</button></aside></footer>
9
9
  </main><div id="toast" role="status" hidden></div><dialog id="dialog"><h2 id="dialog-title"></h2><p id="dialog-body"></p><div class="dialog-actions"><button id="dialog-cancel">关闭</button><button id="dialog-confirm" class="danger" hidden>删除此地图</button></div></dialog></body></html>
@@ -7,3 +7,106 @@ header{display:flex;align-items:center;justify-content:space-between;gap:12px;pa
7
7
  footer{min-height:32px;display:flex;flex-wrap:wrap;align-items:center;gap:8px 12px;padding:5px 14px;border-top:1px solid #283343;color:#8c9eb5;font-size:11px}#connection{overflow-wrap:anywhere}#connection[data-state="connected"]{color:#81dbae}#connection[data-state="error"]{color:#ffaf87}.hint{margin-left:auto}[hidden]{display:none!important}
8
8
  @media(max-width:440px){.brand span{margin-left:6px}.hint{margin-left:0;width:100%}header{padding:0 10px}#terminal .xterm{padding-left:4px;padding-right:4px}}
9
9
  dialog{max-width:min(420px,90vw);border:1px solid #3b4b60;border-radius:10px;background:#151c25;color:#d9e2ed;padding:24px}dialog::backdrop{background:#0009}dialog h2{font-size:18px;margin-top:0}dialog p{line-height:1.9;font-size:13px}dialog form{text-align:right}
10
+
11
+ #star-reminder {
12
+ --star-surface: linear-gradient(115deg, #15302e, #202341 48%, #36243b);
13
+ --star-text: #f2f5ff;
14
+ --star-muted: #c9d2e5;
15
+ width: 100%;
16
+ display: flex;
17
+ flex-wrap: wrap;
18
+ align-items: center;
19
+ gap: 10px 14px;
20
+ margin: 4px 0 8px;
21
+ padding: 12px 16px;
22
+ border: 1px solid transparent;
23
+ border-radius: 12px;
24
+ background: var(--star-surface) padding-box,
25
+ linear-gradient(110deg, #69ebc9, #8299ff, #d897ff, #ffc17d, #69ebc9) border-box;
26
+ background-size: 200% 100%;
27
+ color: var(--star-text);
28
+ font-size: 14px;
29
+ line-height: 1.5;
30
+ animation: star-color-flow 6s ease-in-out 2, star-glow 3s ease-in-out 4;
31
+ }
32
+ #star-reminder::before {
33
+ content: "★";
34
+ display: grid;
35
+ place-items: center;
36
+ flex: 0 0 36px;
37
+ height: 36px;
38
+ border: 1px solid #ffe49950;
39
+ border-radius: 10px;
40
+ background: #f9c76916;
41
+ color: #ffe49b;
42
+ font-size: 26px;
43
+ line-height: 1;
44
+ text-shadow: 0 0 14px #ffd47870;
45
+ animation: star-beat 3s ease-in-out 4;
46
+ }
47
+ #star-reminder > span { flex: 1 1 230px; font-weight: 550; }
48
+ #star-reminder a {
49
+ display: inline-flex;
50
+ align-items: center;
51
+ justify-content: center;
52
+ min-height: 36px;
53
+ padding: 7px 16px;
54
+ border: 1px solid #ffffff50;
55
+ border-radius: 8px;
56
+ background: linear-gradient(110deg, #8bf0d0, #babaff 55%, #ffd0aa);
57
+ color: #17243a;
58
+ font-size: 13px;
59
+ font-weight: 750;
60
+ line-height: 1.4;
61
+ text-decoration: none;
62
+ white-space: nowrap;
63
+ box-shadow: 0 2px 12px #8b96ff25;
64
+ }
65
+ #star-reminder a:hover { filter: brightness(1.1); }
66
+ #star-reminder a[aria-disabled=true] { cursor: progress; opacity: .8; }
67
+ #star-reminder .star-open-error { flex: 1 1 100%; display: flex; flex-wrap: wrap; align-items: center; gap: 8px; font-size: 12px; color: var(--star-muted); }
68
+ #star-reminder .star-open-error input { min-width: 0; max-width: 100%; width: 340px; padding: 6px; color: var(--star-text); background: transparent; border: 1px solid #b8c5e470; border-radius: 6px; }
69
+ #star-reminder button {
70
+ min-height: 34px;
71
+ padding: 6px 10px;
72
+ border: 1px solid #b8c5e42e;
73
+ border-radius: 8px;
74
+ background: transparent;
75
+ color: var(--star-muted);
76
+ font-size: 12px;
77
+ line-height: 1.4;
78
+ white-space: nowrap;
79
+ }
80
+ #star-reminder button:hover { background: #a0b5f014; border-color: #b8c5e470; }
81
+ #star-reminder a:focus-visible, #star-reminder button:focus-visible {
82
+ outline: 2px solid #b0b9ff;
83
+ outline-offset: 3px;
84
+ }
85
+ #star-reminder:hover, #star-reminder:focus-within,
86
+ #star-reminder:hover::before, #star-reminder:focus-within::before { animation-play-state: paused; }
87
+ [data-theme=light] #star-reminder {
88
+ --star-surface: linear-gradient(115deg, #e4fbf5, #eeedff 48%, #fff0e5);
89
+ --star-text: #263451;
90
+ --star-muted: #53607b;
91
+ }
92
+ [data-theme=light] #star-reminder::before { color: #a5660c; background: #ffe6aa80; border-color: #dbae6340; text-shadow: none; }
93
+ @keyframes star-color-flow {
94
+ 0%, 100% { background-position: 0% 50%; }
95
+ 50% { background-position: 100% 50%; }
96
+ }
97
+ @keyframes star-glow {
98
+ 0%, 100% { box-shadow: 0 0 0 #8299ff00; }
99
+ 50% { box-shadow: 0 0 16px #8299ff30, inset 0 0 18px #b897ff0c; }
100
+ }
101
+ @keyframes star-beat {
102
+ 0%, 100% { transform: scale(1) rotate(0deg); }
103
+ 50% { transform: scale(1.1) rotate(-7deg); }
104
+ }
105
+ @media (max-width: 520px) {
106
+ #star-reminder { padding: 12px; gap: 10px; }
107
+ #star-reminder > span { flex-basis: calc(100% - 50px); font-size: 13px; }
108
+ #star-reminder a { margin-left: 46px; }
109
+ }
110
+ @media (prefers-reduced-motion: reduce) {
111
+ #star-reminder, #star-reminder::before { animation: none; }
112
+ }
@@ -2,6 +2,6 @@
2
2
  <html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Mellos · 终端地图</title><link rel="icon" href="data:,"><link rel="stylesheet" href="./xterm.css"><link rel="stylesheet" href="./terminal.css"><script type="module" src="./terminal.js"></script></head>
3
3
  <body><header><a class="brand" id="graphic" href="./" title="切换到图形地图">Mellos <span>图形地图 ↗</span></a><div class="tools"><label for="font-size">字号</label><select id="font-size" aria-label="终端字号"><option>10</option><option>11</option><option>12</option><option selected>13</option><option>14</option><option>15</option><option>16</option><option>18</option><option>20</option><option>24</option><option>28</option></select><button id="help" aria-label="操作帮助">?</button></div></header>
4
4
  <main id="terminal" aria-label="Mellos 终端地图"></main>
5
- <footer><span id="connection" role="status" aria-live="polite">正在连接…</span><button id="restart" hidden>重新连接</button><span class="hint">滚轮缩放 · 拖动平移 · 点击选择 · ? 帮助</span></footer>
5
+ <footer><span id="connection" role="status" aria-live="polite">正在连接…</span><button id="restart" hidden>重新连接</button><span class="hint">滚轮缩放 · 拖动平移 · 点击选择 · ? 帮助</span><aside id="star-reminder" aria-label="支持 Mellos Mapping" hidden><span>地图有帮到你?欢迎在 GitHub 点个 Star。</span><a href="https://github.com/GuangminJu/mellos-mapping" target="_blank" rel="noopener noreferrer">去点 Star ↗</a><button type="button">不再提醒</button></aside></footer>
6
6
  <dialog id="help-dialog"><h2>终端地图操作</h2><p>滚轮 / + / −:总览 → 模块 → 细节<br>拖动:平移 · 单击:固定详情<br>双击带 ⊞ 的节点:进入子地图<br>Backspace:返回父地图<br>Tab / 数字 1–9:切换页面<br>f:自动跟随 · 0:重置视图<br>x 按两次:确认删除当前页<br>q:关闭地图,可用「重新连接」再次打开</p><p>右上角字号只影响此网页终端。</p><form method="dialog"><button>关闭帮助</button></form></dialog>
7
7
  </body></html>
@@ -9137,6 +9137,82 @@ var o = class {
9137
9137
  }
9138
9138
  };
9139
9139
 
9140
+ // src/web/star-reminder.ts
9141
+ function createStarNotice(base2) {
9142
+ let attempted = false;
9143
+ const notice = document.getElementById("star-reminder");
9144
+ const link = notice.querySelector("a");
9145
+ let opening = false;
9146
+ link.title = "\u5728\u7CFB\u7EDF\u9ED8\u8BA4\u6D4F\u89C8\u5668\u4E2D\u6253\u5F00 GitHub";
9147
+ const dismiss = () => {
9148
+ notice.hidden = true;
9149
+ };
9150
+ notice.querySelector("button").addEventListener("click", dismiss);
9151
+ const launch = async (event) => {
9152
+ event.preventDefault();
9153
+ if (opening) return;
9154
+ opening = true;
9155
+ link.setAttribute("aria-disabled", "true");
9156
+ link.textContent = "\u6B63\u5728\u6253\u5F00\u2026";
9157
+ notice.querySelector(".star-open-error")?.remove();
9158
+ try {
9159
+ const response = await fetch(new URL("api/star-reminder/open", base2), { method: "POST", signal: AbortSignal.timeout(8e3) });
9160
+ const result = await response.json();
9161
+ if (!response.ok || result.opened !== true) throw new Error("Not opened");
9162
+ dismiss();
9163
+ } catch {
9164
+ const error = document.createElement("div");
9165
+ error.className = "star-open-error";
9166
+ error.setAttribute("role", "status");
9167
+ const message = document.createElement("span");
9168
+ message.textContent = "\u9ED8\u8BA4\u6D4F\u89C8\u5668\u672A\u80FD\u6253\u5F00\uFF0C\u53EF\u91CD\u8BD5\u6216\u590D\u5236\u5730\u5740\u3002";
9169
+ const copy = document.createElement("button");
9170
+ copy.type = "button";
9171
+ copy.textContent = "\u590D\u5236 GitHub \u5730\u5740";
9172
+ copy.addEventListener("click", () => {
9173
+ void (async () => {
9174
+ try {
9175
+ await navigator.clipboard.writeText(link.href);
9176
+ copy.textContent = "\u5DF2\u590D\u5236";
9177
+ } catch {
9178
+ const address = document.createElement("input");
9179
+ address.readOnly = true;
9180
+ address.value = link.href;
9181
+ address.setAttribute("aria-label", "GitHub \u5730\u5740");
9182
+ copy.replaceWith(address);
9183
+ address.focus();
9184
+ address.select();
9185
+ }
9186
+ })();
9187
+ });
9188
+ error.append(message, copy);
9189
+ notice.append(error);
9190
+ } finally {
9191
+ opening = false;
9192
+ link.removeAttribute("aria-disabled");
9193
+ link.textContent = "\u53BB\u70B9 Star \u2197";
9194
+ }
9195
+ };
9196
+ link.addEventListener("click", (event) => {
9197
+ void launch(event);
9198
+ });
9199
+ link.addEventListener("auxclick", (event) => {
9200
+ if (event.button === 1) void launch(event);
9201
+ });
9202
+ return (page2) => {
9203
+ if (attempted || document.visibilityState !== "visible") return;
9204
+ attempted = true;
9205
+ const url = new URL("api/star-reminder/visit", base2);
9206
+ if (page2) url.searchParams.set("page", page2);
9207
+ void fetch(url, { method: "POST", signal: AbortSignal.timeout(3e3) }).then(async (response) => {
9208
+ if (!response.ok) return;
9209
+ const result = await response.json();
9210
+ if (result.show === true && document.visibilityState === "visible") notice.hidden = false;
9211
+ }).catch(() => {
9212
+ });
9213
+ };
9214
+ }
9215
+
9140
9216
  // src/web/terminal-app.ts
9141
9217
  var element = (id) => document.getElementById(id);
9142
9218
  var container = element("terminal");
@@ -9145,6 +9221,7 @@ var restart = element("restart");
9145
9221
  var font = element("font-size");
9146
9222
  var graphic = element("graphic");
9147
9223
  var base = new URL(".", location.href);
9224
+ var starNotice = createStarNotice(base);
9148
9225
  var page = new URL(location.href).searchParams.get("page") ?? void 0;
9149
9226
  var socket;
9150
9227
  var retryTimer;
@@ -9264,6 +9341,7 @@ function connect() {
9264
9341
  } else if (message.type === "view") {
9265
9342
  page = message.page;
9266
9343
  syncPage();
9344
+ starNotice(page);
9267
9345
  } else if (message.type === "error") setStatus(message.message, "error");
9268
9346
  });
9269
9347
  connection.addEventListener("close", (event) => {