-
-
-
找到这一页HTTP 404
+
+
+
+
页面未找到 — TaskDock
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
404 · 这个地址没有对应的页面
+
这张卡片走丢了。
+
链接可能已经变了,或者地址里有拼写错误。把它拖回虚线框里,或者直接回首页。
+
回到首页
- 404 · 这个地址没有对应的页面
- 这张卡片走丢了。
- 链接可能已经变了,或者地址里有拼写错误。把它拖回虚线框里,或者直接回首页。
- 回到首页
-
-
-
+
+
diff --git a/website/site/assets/css/site.css b/website/site/assets/css/site.css
index 47a51e1..0988825 100644
--- a/website/site/assets/css/site.css
+++ b/website/site/assets/css/site.css
@@ -26,26 +26,121 @@
--slate: #93a6cc;
--rose: #ef9a8f;
- --font-sans: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI Variable Text", "Segoe UI",
- "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", "Noto Sans SC",
- "Noto Sans CJK SC", "Source Han Sans SC", system-ui, sans-serif;
- --font-display: -apple-system, BlinkMacSystemFont, "SF Pro Display", "Segoe UI Variable Display",
- "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Noto Sans SC",
- "Noto Sans CJK SC", system-ui, sans-serif;
- --font-mono: ui-monospace, "SF Mono", "JetBrains Mono", "Cascadia Code", Menlo, Consolas,
- "Liberation Mono", monospace;
+ --font-sans:
+ -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI Variable Text', 'Segoe UI',
+ 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei UI', 'Microsoft YaHei', 'Noto Sans SC',
+ 'Noto Sans CJK SC', 'Source Han Sans SC', system-ui, sans-serif;
+ --font-display:
+ -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI Variable Display', 'Segoe UI',
+ 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei UI', 'Noto Sans SC', 'Noto Sans CJK SC',
+ system-ui, sans-serif;
+ --font-mono:
+ ui-monospace, 'SF Mono', 'JetBrains Mono', 'Cascadia Code', Menlo, Consolas, 'Liberation Mono',
+ monospace;
/* 由阻尼弹簧采样得到的 linear() 曲线(k/c:420/30、170/17、260/15) */
- --spring-snappy: linear(0, 0.0294, 0.0861, 0.1615, 0.2479, 0.3393, 0.4308, 0.5192, 0.6019, 0.6892,
- 0.7552, 0.8128, 0.8621, 0.9034, 0.9373, 0.9646, 0.9859, 1.0021, 1.014, 1.0222, 1.0274, 1.0302,
- 1.0311, 1.0307, 1.0292, 1.0266, 1.024, 1.0212, 1.0183, 1.0155, 1.0129, 1.0105, 1.0084, 1.0065,
- 1.0048, 1.0035, 1.0023, 1.0014, 1.0007, 1.0001, 0.9997, 0.9994, 0.9992, 0.9991, 0.999, 0.999,
- 0.9991, 0.9991, 1);
- --spring-soft: linear(0, 0.0331, 0.0969, 0.1916, 0.2904, 0.4043, 0.5168, 0.6135, 0.7104, 0.7881,
- 0.8613, 0.9216, 0.9655, 1.0028, 1.0278, 1.0468, 1.0584, 1.0636, 1.0648, 1.0629, 1.0583, 1.052,
- 1.0456, 1.0382, 1.0309, 1.0247, 1.0184, 1.0134, 1.0087, 1.0049, 1.0021, 0.9997, 0.9981, 0.9969,
- 0.9962, 0.9959, 0.9958, 0.9959, 0.9962, 0.9967, 0.9971, 0.9976, 0.998, 0.9984, 0.9988, 0.9991,
- 0.9995, 0.9997, 1);
+ --spring-snappy: linear(
+ 0,
+ 0.0294,
+ 0.0861,
+ 0.1615,
+ 0.2479,
+ 0.3393,
+ 0.4308,
+ 0.5192,
+ 0.6019,
+ 0.6892,
+ 0.7552,
+ 0.8128,
+ 0.8621,
+ 0.9034,
+ 0.9373,
+ 0.9646,
+ 0.9859,
+ 1.0021,
+ 1.014,
+ 1.0222,
+ 1.0274,
+ 1.0302,
+ 1.0311,
+ 1.0307,
+ 1.0292,
+ 1.0266,
+ 1.024,
+ 1.0212,
+ 1.0183,
+ 1.0155,
+ 1.0129,
+ 1.0105,
+ 1.0084,
+ 1.0065,
+ 1.0048,
+ 1.0035,
+ 1.0023,
+ 1.0014,
+ 1.0007,
+ 1.0001,
+ 0.9997,
+ 0.9994,
+ 0.9992,
+ 0.9991,
+ 0.999,
+ 0.999,
+ 0.9991,
+ 0.9991,
+ 1
+ );
+ --spring-soft: linear(
+ 0,
+ 0.0331,
+ 0.0969,
+ 0.1916,
+ 0.2904,
+ 0.4043,
+ 0.5168,
+ 0.6135,
+ 0.7104,
+ 0.7881,
+ 0.8613,
+ 0.9216,
+ 0.9655,
+ 1.0028,
+ 1.0278,
+ 1.0468,
+ 1.0584,
+ 1.0636,
+ 1.0648,
+ 1.0629,
+ 1.0583,
+ 1.052,
+ 1.0456,
+ 1.0382,
+ 1.0309,
+ 1.0247,
+ 1.0184,
+ 1.0134,
+ 1.0087,
+ 1.0049,
+ 1.0021,
+ 0.9997,
+ 0.9981,
+ 0.9969,
+ 0.9962,
+ 0.9959,
+ 0.9958,
+ 0.9959,
+ 0.9962,
+ 0.9967,
+ 0.9971,
+ 0.9976,
+ 0.998,
+ 0.9984,
+ 0.9988,
+ 0.9991,
+ 0.9995,
+ 0.9997,
+ 1
+ );
--spring-snappy-ms: 485ms;
--spring-soft-ms: 850ms;
--ease-out: cubic-bezier(0.22, 1, 0.36, 1);
@@ -93,7 +188,7 @@ body {
/* 极淡的蓝图网格:秩序的底纹 */
body::before {
- content: "";
+ content: '';
position: fixed;
inset: 0;
z-index: -1;
@@ -188,7 +283,9 @@ pre {
z-index: 50;
height: var(--header-h);
view-transition-name: site-header;
- transition: background-color 0.4s var(--ease-out), border-color 0.4s var(--ease-out),
+ transition:
+ background-color 0.4s var(--ease-out),
+ border-color 0.4s var(--ease-out),
backdrop-filter 0.4s var(--ease-out);
border-bottom: 1px solid transparent;
}
@@ -226,7 +323,9 @@ pre {
width: 30px;
height: 30px;
border-radius: 8px;
- box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.06), 0 6px 18px rgba(0, 0, 0, 0.35);
+ box-shadow:
+ 0 0 0 1px rgba(255, 255, 255, 0.06),
+ 0 6px 18px rgba(0, 0, 0, 0.35);
}
.site-nav {
@@ -242,7 +341,9 @@ pre {
border-radius: 999px;
font-size: 14px;
color: var(--text-2);
- transition: color 0.2s, background-color 0.2s;
+ transition:
+ color 0.2s,
+ background-color 0.2s;
}
.site-nav a:hover {
@@ -250,12 +351,12 @@ pre {
background: rgba(255, 255, 255, 0.05);
}
-.site-nav a[aria-current="page"] {
+.site-nav a[aria-current='page'] {
color: var(--text);
}
-.site-nav a[aria-current="page"]::after {
- content: "";
+.site-nav a[aria-current='page']::after {
+ content: '';
position: absolute;
left: 50%;
bottom: 2px;
@@ -279,7 +380,9 @@ pre {
height: 38px;
border-radius: 999px;
color: var(--text-2);
- transition: color 0.2s, background-color 0.2s;
+ transition:
+ color 0.2s,
+ background-color 0.2s;
}
.icon-link:hover {
@@ -309,7 +412,9 @@ pre {
height: 1.5px;
background: var(--text);
border-radius: 2px;
- transition: transform var(--spring-snappy-ms) var(--spring-snappy), opacity 0.2s;
+ transition:
+ transform var(--spring-snappy-ms) var(--spring-snappy),
+ opacity 0.2s;
}
.menu-toggle span:nth-child(1) {
@@ -355,8 +460,11 @@ pre {
font-weight: 600;
font-size: 15px;
white-space: nowrap;
- transition: transform var(--spring-snappy-ms) var(--spring-snappy), background-color 0.25s,
- border-color 0.25s, box-shadow 0.3s;
+ transition:
+ transform var(--spring-snappy-ms) var(--spring-snappy),
+ background-color 0.25s,
+ border-color 0.25s,
+ box-shadow 0.3s;
}
.btn:hover {
@@ -389,12 +497,16 @@ pre {
--btn-bg: var(--sage);
--btn-fg: var(--sage-ink);
border-color: transparent;
- box-shadow: 0 10px 30px -10px rgba(143, 205, 170, 0.55), inset 0 1px 0 rgba(255, 255, 255, 0.35);
+ box-shadow:
+ 0 10px 30px -10px rgba(143, 205, 170, 0.55),
+ inset 0 1px 0 rgba(255, 255, 255, 0.35);
}
.btn-primary:hover {
border-color: transparent;
- box-shadow: 0 16px 40px -12px rgba(143, 205, 170, 0.7), inset 0 1px 0 rgba(255, 255, 255, 0.4);
+ box-shadow:
+ 0 16px 40px -12px rgba(143, 205, 170, 0.7),
+ inset 0 1px 0 rgba(255, 255, 255, 0.4);
}
.btn-small {
@@ -535,11 +647,13 @@ p {
height: 18px;
border-radius: 50%;
border: 1.6px solid var(--c);
- transition: border-color 0.3s, background-color 0.3s;
+ transition:
+ border-color 0.3s,
+ background-color 0.3s;
}
.status::before {
- content: "";
+ content: '';
position: absolute;
inset: 2px;
border-radius: 50%;
@@ -549,7 +663,7 @@ p {
}
.status::after {
- content: "";
+ content: '';
position: absolute;
left: 4.6px;
top: 2.2px;
@@ -561,20 +675,20 @@ p {
transition: transform var(--spring-snappy-ms) var(--spring-snappy);
}
-.status[data-s="doing"] {
+.status[data-s='doing'] {
--c: var(--amber);
}
-.status[data-s="doing"]::before {
+.status[data-s='doing']::before {
transform: scale(1) rotate(0deg);
}
-.status[data-s="done"] {
+.status[data-s='done'] {
--c: var(--sage);
background: var(--sage);
}
-.status[data-s="done"]::after {
+.status[data-s='done']::after {
transform: rotate(45deg) scale(1);
}
@@ -583,7 +697,7 @@ button.status {
background-color: transparent;
}
-button.status[data-s="done"] {
+button.status[data-s='done'] {
background-color: var(--sage);
}
@@ -598,7 +712,9 @@ button.status[data-s="done"] {
border-radius: 14px;
background: linear-gradient(180deg, rgba(36, 52, 92, 0.96), rgba(24, 36, 66, 0.96));
border: 1px solid rgba(168, 186, 228, 0.14);
- box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.07), 0 1px 0 rgba(0, 0, 0, 0.3),
+ box-shadow:
+ inset 0 1px 0 rgba(255, 255, 255, 0.07),
+ 0 1px 0 rgba(0, 0, 0, 0.3),
0 14px 30px -14px rgba(0, 0, 0, 0.7);
color: var(--text);
}
@@ -626,7 +742,7 @@ button.status[data-s="done"] {
margin-top: 3px;
}
-.tcard[data-s="done"] .tcard-title {
+.tcard[data-s='done'] .tcard-title {
color: var(--text-3);
text-decoration: line-through;
text-decoration-color: rgba(143, 205, 170, 0.5);
@@ -719,7 +835,7 @@ button.status[data-s="done"] {
}
.release-badge::before {
- content: "";
+ content: '';
width: 6px;
height: 6px;
border-radius: 50%;
@@ -749,7 +865,9 @@ button.status[data-s="done"] {
.hero-field .tcard.is-dragging {
cursor: grabbing;
z-index: 20;
- box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.09), 0 30px 60px -18px rgba(0, 0, 0, 0.85),
+ box-shadow:
+ inset 0 1px 0 rgba(255, 255, 255, 0.09),
+ 0 30px 60px -18px rgba(0, 0, 0, 0.85),
0 0 0 1px rgba(143, 205, 170, 0.35);
}
@@ -787,11 +905,15 @@ button.status[data-s="done"] {
@keyframes dock-pulse {
0% {
- box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.07), 0 0 0 0 rgba(143, 205, 170, 0.5),
+ box-shadow:
+ inset 0 1px 0 rgba(255, 255, 255, 0.07),
+ 0 0 0 0 rgba(143, 205, 170, 0.5),
0 14px 30px -14px rgba(0, 0, 0, 0.7);
}
100% {
- box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.07), 0 0 0 12px rgba(143, 205, 170, 0),
+ box-shadow:
+ inset 0 1px 0 rgba(255, 255, 255, 0.07),
+ 0 0 0 12px rgba(143, 205, 170, 0),
0 14px 30px -14px rgba(0, 0, 0, 0.7);
}
}
@@ -802,7 +924,9 @@ button.status[data-s="done"] {
border-radius: 22px;
background: linear-gradient(180deg, rgba(17, 26, 50, 0.9), rgba(11, 18, 37, 0.9));
border: 1px solid var(--line);
- box-shadow: 0 40px 100px -40px rgba(0, 0, 0, 0.9), inset 0 1px 0 rgba(255, 255, 255, 0.04);
+ box-shadow:
+ 0 40px 100px -40px rgba(0, 0, 0, 0.9),
+ inset 0 1px 0 rgba(255, 255, 255, 0.04);
opacity: 0;
pointer-events: none;
}
@@ -950,14 +1074,16 @@ button.status[data-s="done"] {
border-radius: 16px;
background: transparent;
text-align: left;
- transition: background-color 0.3s, border-color 0.3s;
+ transition:
+ background-color 0.3s,
+ border-color 0.3s;
}
.model-tab:hover {
background: rgba(255, 255, 255, 0.03);
}
-.model-tab[aria-selected="true"] {
+.model-tab[aria-selected='true'] {
background: rgba(255, 255, 255, 0.045);
border-color: var(--line);
}
@@ -971,7 +1097,7 @@ button.status[data-s="done"] {
transition: color 0.3s;
}
-.model-tab[aria-selected="true"] .k {
+.model-tab[aria-selected='true'] .k {
color: var(--sage);
}
@@ -988,14 +1114,16 @@ button.status[data-s="done"] {
color: var(--text-3);
display: grid;
grid-template-rows: 0fr;
- transition: grid-template-rows var(--spring-soft-ms) var(--spring-soft), color 0.3s;
+ transition:
+ grid-template-rows var(--spring-soft-ms) var(--spring-soft),
+ color 0.3s;
}
.model-tab .d > span {
overflow: hidden;
}
-.model-tab[aria-selected="true"] .d {
+.model-tab[aria-selected='true'] .d {
grid-template-rows: 1fr;
color: var(--text-2);
}
@@ -1052,7 +1180,7 @@ button.status[data-s="done"] {
}
.lens-bar .live::before {
- content: "";
+ content: '';
width: 6px;
height: 6px;
border-radius: 50%;
@@ -1463,7 +1591,9 @@ button.status[data-s="done"] {
user-select: none;
-webkit-user-select: none;
cursor: grab;
- transition: transform var(--spring-snappy-ms) var(--spring-snappy), border-color 0.25s,
+ transition:
+ transform var(--spring-snappy-ms) var(--spring-snappy),
+ border-color 0.25s,
box-shadow 0.25s;
}
@@ -1471,7 +1601,7 @@ button.status[data-s="done"] {
transform: translateY(-2px);
}
-.pill-task[aria-pressed="true"] {
+.pill-task[aria-pressed='true'] {
border-color: var(--sage);
box-shadow: 0 0 0 4px rgba(143, 205, 170, 0.14);
}
@@ -1496,7 +1626,9 @@ button.status[data-s="done"] {
z-index: 80;
pointer-events: none;
cursor: grabbing;
- box-shadow: 0 24px 50px -16px rgba(0, 0, 0, 0.9), 0 0 0 1px rgba(143, 205, 170, 0.4);
+ box-shadow:
+ 0 24px 50px -16px rgba(0, 0, 0, 0.9),
+ 0 0 0 1px rgba(143, 205, 170, 0.4);
}
.timeline {
@@ -1577,7 +1709,7 @@ button.status[data-s="done"] {
}
.tl-col.is-event.has-arrived .tl-head b::after {
- content: " · 已到达";
+ content: ' · 已到达';
font-size: 12px;
font-weight: 500;
color: var(--text-3);
@@ -1613,7 +1745,9 @@ button.status[data-s="done"] {
border-radius: 9px;
font-size: 11.5px;
color: var(--text-3);
- transition: border-color 0.25s, color 0.25s;
+ transition:
+ border-color 0.25s,
+ color 0.25s;
}
.tl-actions {
@@ -1740,7 +1874,9 @@ button.status[data-s="done"] {
stroke: rgba(143, 205, 170, 0.35);
stroke-width: 1.5;
stroke-dasharray: 3 5;
- transition: stroke 0.4s, opacity 0.4s;
+ transition:
+ stroke 0.4s,
+ opacity 0.4s;
}
.sync-wires path.is-down {
@@ -1755,7 +1891,9 @@ button.status[data-s="done"] {
border: 1px solid var(--line);
background: var(--ink-2);
padding: 12px;
- transition: border-color 0.4s, box-shadow 0.4s;
+ transition:
+ border-color 0.4s,
+ box-shadow 0.4s;
}
.client.is-offline {
@@ -1788,7 +1926,7 @@ button.status[data-s="done"] {
}
.net::before {
- content: "";
+ content: '';
width: 6px;
height: 6px;
border-radius: 50%;
@@ -1808,7 +1946,9 @@ button.status[data-s="done"] {
margin: 0;
height: 36px;
font-size: 12.5px;
- transition: border-color 0.4s, background-color 0.4s;
+ transition:
+ border-color 0.4s,
+ background-color 0.4s;
}
.client-list .mini.is-pending {
@@ -1913,7 +2053,8 @@ button.status[data-s="done"] {
padding: 16px 8px;
border-radius: 18px;
border: 1px solid rgba(143, 205, 170, 0.28);
- background: radial-gradient(circle at 50% 30%, rgba(143, 205, 170, 0.12), transparent 70%), var(--ink-2);
+ background:
+ radial-gradient(circle at 50% 30%, rgba(143, 205, 170, 0.12), transparent 70%), var(--ink-2);
text-align: center;
}
@@ -1983,7 +2124,7 @@ button.status[data-s="done"] {
}
.switch .track::after {
- content: "";
+ content: '';
position: absolute;
top: 3px;
left: 3px;
@@ -1995,11 +2136,11 @@ button.status[data-s="done"] {
transition: transform var(--spring-snappy-ms) var(--spring-snappy);
}
-.switch[aria-checked="false"] .track {
+.switch[aria-checked='false'] .track {
background: rgba(239, 154, 143, 0.55);
}
-.switch[aria-checked="false"] .track::after {
+.switch[aria-checked='false'] .track::after {
transform: translateX(0);
}
@@ -2086,10 +2227,12 @@ button.status[data-s="done"] {
background: transparent;
font-size: 12.5px;
color: var(--text-3);
- transition: background-color 0.2s, color 0.2s;
+ transition:
+ background-color 0.2s,
+ color 0.2s;
}
-.term-tab[aria-selected="true"],
+.term-tab[aria-selected='true'],
.term-tab.is-label {
background: rgba(255, 255, 255, 0.07);
color: var(--text);
@@ -2107,7 +2250,9 @@ button.status[data-s="done"] {
background: transparent;
font-size: 12px;
color: var(--text-2);
- transition: color 0.2s, border-color 0.2s;
+ transition:
+ color 0.2s,
+ border-color 0.2s;
}
.copy-btn:hover {
@@ -2171,7 +2316,7 @@ button.status[data-s="done"] {
}
.stack-layer + .stack-layer::before {
- content: "";
+ content: '';
position: absolute;
left: 38px;
top: -11px;
@@ -2248,7 +2393,7 @@ button.status[data-s="done"] {
}
.host-notes li::before {
- content: "—";
+ content: '—';
color: var(--text-3);
}
@@ -2408,7 +2553,7 @@ button.status[data-s="done"] {
}
.demo-swipe::before {
- content: "完成";
+ content: '完成';
position: absolute;
left: 14px;
top: 50%;
@@ -2579,7 +2724,7 @@ button.status[data-s="done"] {
}
.platforms::before {
- content: "";
+ content: '';
position: absolute;
left: 49px;
right: 49px;
@@ -2594,10 +2739,14 @@ button.status[data-s="done"] {
@keyframes sync-flow {
from {
- background-position: -30% 0, 0 0;
+ background-position:
+ -30% 0,
+ 0 0;
}
to {
- background-position: 130% 0, 0 0;
+ background-position:
+ 130% 0,
+ 0 0;
}
}
@@ -2617,7 +2766,9 @@ button.status[data-s="done"] {
border-radius: 22px;
border: 1px solid var(--line);
background: linear-gradient(180deg, rgba(255, 255, 255, 0.035), rgba(255, 255, 255, 0.01));
- transition: transform var(--spring-soft-ms) var(--spring-soft), border-color 0.3s;
+ transition:
+ transform var(--spring-soft-ms) var(--spring-soft),
+ border-color 0.3s;
}
.platform:hover {
@@ -2701,7 +2852,9 @@ button.status[data-s="done"] {
left: calc(var(--x) * 1%);
transform: translateX(-50%) translateY(var(--y, -60px)) rotate(var(--r, 30deg));
opacity: var(--o, 0);
- transition: transform 1.1s var(--spring-soft), opacity 0.6s;
+ transition:
+ transform 1.1s var(--spring-soft),
+ opacity 0.6s;
transition-delay: calc(var(--i) * 70ms);
}
@@ -2767,7 +2920,9 @@ button.status[data-s="done"] {
.reveal {
opacity: 0;
transform: translateY(26px);
- transition: opacity 0.7s var(--ease-out), transform var(--spring-soft-ms) var(--spring-soft);
+ transition:
+ opacity 0.7s var(--ease-out),
+ transform var(--spring-soft-ms) var(--spring-soft);
transition-delay: calc(var(--d, 0) * 80ms);
}
@@ -2946,7 +3101,10 @@ button.status[data-s="done"] {
font-family: var(--font-mono);
font-size: 13px;
color: var(--text-3);
- transition: border-color 0.4s, color 0.4s, background-color 0.4s;
+ transition:
+ border-color 0.4s,
+ color 0.4s,
+ background-color 0.4s;
}
.rail-step.is-reached .num {
@@ -3082,7 +3240,9 @@ button.status[data-s="done"] {
border-radius: 16px;
border: 1px solid var(--line);
background: rgba(255, 255, 255, 0.02);
- transition: border-color 0.25s, transform var(--spring-snappy-ms) var(--spring-snappy);
+ transition:
+ border-color 0.25s,
+ transform var(--spring-snappy-ms) var(--spring-snappy);
}
.doc-link:hover {
@@ -3136,7 +3296,6 @@ button.status[data-s="done"] {
color: var(--text-2);
}
-
/* ========== 响应式 ========== */
@media (max-width: 1080px) {
@@ -3179,14 +3338,19 @@ button.status[data-s="done"] {
transform: translateY(-8px);
opacity: 0;
visibility: hidden;
- transition: transform var(--spring-snappy-ms) var(--spring-snappy), opacity 0.2s, visibility 0s 0.2s;
+ transition:
+ transform var(--spring-snappy-ms) var(--spring-snappy),
+ opacity 0.2s,
+ visibility 0s 0.2s;
}
.site-header.is-open .site-nav {
transform: none;
opacity: 1;
visibility: visible;
- transition: transform var(--spring-snappy-ms) var(--spring-snappy), opacity 0.2s;
+ transition:
+ transform var(--spring-snappy-ms) var(--spring-snappy),
+ opacity 0.2s;
}
.site-nav a {
@@ -3200,7 +3364,7 @@ button.status[data-s="done"] {
background: transparent;
}
- .site-nav a[aria-current="page"]::after {
+ .site-nav a[aria-current='page']::after {
left: auto;
right: 6px;
top: 50%;
@@ -3444,7 +3608,7 @@ button.status[data-s="done"] {
grid-row: 1 / span 2;
}
- [data-client="android"] .outbox {
+ [data-client='android'] .outbox {
display: none;
}
diff --git a/website/site/assets/js/dimensions.js b/website/site/assets/js/dimensions.js
index d78567d..70c2931 100644
--- a/website/site/assets/js/dimensions.js
+++ b/website/site/assets/js/dimensions.js
@@ -8,9 +8,15 @@ const DESC = {
tree: '文件夹可以无限嵌套,同一级里文件夹和任务并存。文件夹没有手动状态,由全部后代任务实时推导。',
time: '同一个任务可以同时出现在多个日期,也可以挂在「额度重置后」「上线前」这样的自定义事件上。',
flow: '把任务放进分阶段的流程看板。一个任务可以加入多个流程,在同一个流程里只出现一次。',
- steps: '在任务内部列出线性步骤,并附上 Markdown 笔记。步骤有自己的三态状态,不会自动改动任务状态。',
+ steps:
+ '在任务内部列出线性步骤,并附上 Markdown 笔记。步骤有自己的三态状态,不会自动改动任务状态。',
+};
+const WHERE = {
+ tree: '/tree/发布-2.0',
+ time: '/time/calendar',
+ flow: '/workflows/发布-2.0',
+ steps: 'TASK-214 · 详情',
};
-const WHERE = { tree: '/tree/发布-2.0', time: '/time/calendar', flow: '/workflows/发布-2.0', steps: 'TASK-214 · 详情' };
const focus = {
title: '迁移脚本加回滚演练',
@@ -110,7 +116,8 @@ function views() {
${focus.steps
.map(
- ([label, s], i) => `- ${label}${LABEL[s]}
`,
+ ([label, s], i) =>
+ `- ${label}${LABEL[s]}
`,
)
.join('')}
@@ -129,7 +136,8 @@ export function initDimensions(lens, tabsEl) {
const tabs = [...tabsEl.querySelectorAll('[role="tab"]')];
let current = 'tree';
- const caption = () => `
点任务前的圆圈:待开始 → 进行中 → 已完成${focus.ref} · ${LABEL[focus.s]}
`;
+ const caption = () =>
+ `
点任务前的圆圈:待开始 → 进行中 → 已完成${focus.ref} · ${LABEL[focus.s]}
`;
function sync() {
stage.querySelectorAll('[data-focus-status]').forEach((el) => {
@@ -159,14 +167,26 @@ export function initDimensions(lens, tabsEl) {
const andTasks = ['doing', 'todo'];
const tdTasks = [...relTasks, ...andTasks];
const devTasks = [...tdTasks, 'todo'];
- const aggs = { rel: derive(relTasks), and: derive(andTasks), td: derive(tdTasks), dev: derive(devTasks) };
+ const aggs = {
+ rel: derive(relTasks),
+ and: derive(andTasks),
+ td: derive(tdTasks),
+ dev: derive(devTasks),
+ };
stage.querySelectorAll('[data-agg]').forEach((el) => {
const s = aggs[el.dataset.agg];
const changed = el.dataset.s && el.dataset.s !== s;
el.dataset.s = s;
el.innerHTML = `${LABEL[s]}`;
el.style.color = s === 'done' ? 'var(--sage)' : s === 'doing' ? 'var(--amber)' : '';
- if (changed) el.animate([{ opacity: 0.2, transform: 'translateY(-4px)' }, { opacity: 1, transform: 'none' }], { duration: 420, easing: 'cubic-bezier(.22,1,.36,1)' });
+ if (changed)
+ el.animate(
+ [
+ { opacity: 0.2, transform: 'translateY(-4px)' },
+ { opacity: 1, transform: 'none' },
+ ],
+ { duration: 420, easing: 'cubic-bezier(.22,1,.36,1)' },
+ );
});
}
diff --git a/website/site/assets/js/hero-field.js b/website/site/assets/js/hero-field.js
index 1eededc..ee8e1fa 100644
--- a/website/site/assets/js/hero-field.js
+++ b/website/site/assets/js/hero-field.js
@@ -2,18 +2,65 @@
// 卡片可以拖动:散落时可以随手甩开,归位后可以在组内重新排序;
// 点状态圆圈按「待开始 → 进行中 → 已完成」切换,卡片会移动到对应分组。
-import { Spring2D, clamp, createLoop, easeInOut, easeOut, integrate, lerp, prefersReducedMotion } from './spring.js';
+import {
+ Spring2D,
+ clamp,
+ createLoop,
+ easeInOut,
+ easeOut,
+ integrate,
+ lerp,
+ prefersReducedMotion,
+} from './spring.js';
const TASKS = [
{ id: 'a', title: '迁移脚本加回滚演练', ref: 'TASK-214', s: 'doing', chip: ['flow', '验证'] },
- { id: 'b', title: '修复 Android 冷启动读取凭据', ref: 'TASK-208', s: 'doing', chip: ['date', '明天'] },
- { id: 'c', title: '整理同步冲突的复现步骤', ref: 'TASK-219', s: 'doing', chip: ['steps', '步骤 2/4'], mobile: false },
- { id: 'd', title: '额度重置后重跑 e2e 门禁', ref: 'TASK-221', s: 'todo', chip: ['event', '额度重置后'] },
- { id: 'e', title: '上线前检查 HTTPS 与 CORS', ref: 'TASK-223', s: 'todo', chip: ['event', '上线前'] },
- { id: 'f', title: '给备份脚本补一次恢复演练', ref: 'TASK-197', s: 'todo', chip: ['date', '周六'] },
+ {
+ id: 'b',
+ title: '修复 Android 冷启动读取凭据',
+ ref: 'TASK-208',
+ s: 'doing',
+ chip: ['date', '明天'],
+ },
+ {
+ id: 'c',
+ title: '整理同步冲突的复现步骤',
+ ref: 'TASK-219',
+ s: 'doing',
+ chip: ['steps', '步骤 2/4'],
+ mobile: false,
+ },
+ {
+ id: 'd',
+ title: '额度重置后重跑 e2e 门禁',
+ ref: 'TASK-221',
+ s: 'todo',
+ chip: ['event', '额度重置后'],
+ },
+ {
+ id: 'e',
+ title: '上线前检查 HTTPS 与 CORS',
+ ref: 'TASK-223',
+ s: 'todo',
+ chip: ['event', '上线前'],
+ },
+ {
+ id: 'f',
+ title: '给备份脚本补一次恢复演练',
+ ref: 'TASK-197',
+ s: 'todo',
+ chip: ['date', '周六'],
+ },
{ id: 'g', title: '升级到 PostgreSQL 18', ref: 'TASK-188', s: 'done', chip: ['note', '笔记'] },
{ id: 'h', title: '统一移动端底部导航', ref: 'TASK-176', s: 'done', chip: ['flow', '发布'] },
- { id: 'i', title: '清理过期的同步游标', ref: 'TASK-181', s: 'done', chip: ['date', '周一'], mobile: false },
+ {
+ id: 'i',
+ title: '清理过期的同步游标',
+ ref: 'TASK-181',
+ s: 'done',
+ chip: ['date', '周一'],
+ mobile: false,
+ },
];
const GROUPS = ['doing', 'todo', 'done'];
@@ -21,13 +68,24 @@ const NEXT = { todo: 'doing', doing: 'done', done: 'todo' };
// 散落构图(中心点,按舞台归一化):桌面避开左侧文案,移动端集中在下方与边缘
const SCATTER_DESKTOP = [
- [0.67, 0.2, -11, 0.86], [0.9, 0.12, 8, 0.8], [0.79, 0.37, -4, 0.9], [0.99, 0.5, 13, 0.78],
- [0.69, 0.6, 7, 0.85], [0.87, 0.73, -9, 0.9], [0.63, 0.9, 4, 0.82], [0.96, 0.92, -6, 0.8],
+ [0.67, 0.2, -11, 0.86],
+ [0.9, 0.12, 8, 0.8],
+ [0.79, 0.37, -4, 0.9],
+ [0.99, 0.5, 13, 0.78],
+ [0.69, 0.6, 7, 0.85],
+ [0.87, 0.73, -9, 0.9],
+ [0.63, 0.9, 4, 0.82],
+ [0.96, 0.92, -6, 0.8],
[0.24, 0.95, -7, 0.78],
];
const SCATTER_MOBILE = [
- [0.22, 0.7, -9, 0.9], [0.8, 0.66, 8, 0.86], [0.5, 0.79, 3, 0.95], [0.12, 0.9, 10, 0.86],
- [0.86, 0.86, -7, 0.9], [0.45, 0.97, -4, 0.92], [1.0, 0.98, 12, 0.82],
+ [0.22, 0.7, -9, 0.9],
+ [0.8, 0.66, 8, 0.86],
+ [0.5, 0.79, 3, 0.95],
+ [0.12, 0.9, 10, 0.86],
+ [0.86, 0.86, -7, 0.9],
+ [0.45, 0.97, -4, 0.92],
+ [1.0, 0.98, 12, 0.82],
];
const ICONS = {
@@ -37,7 +95,13 @@ const ICONS = {
steps: '
',
note: '
',
};
-const CHIP_CLASS = { flow: 'chip-flow', date: 'chip-date', event: 'chip-event', steps: '', note: '' };
+const CHIP_CLASS = {
+ flow: 'chip-flow',
+ date: 'chip-date',
+ event: 'chip-event',
+ steps: '',
+ note: '',
+};
function cardHTML(t) {
const [kind, text] = t.chip;
@@ -89,7 +153,12 @@ export function initHero(track) {
scatterLine.setAttribute('aria-label', text);
scatterLine.textContent = '';
const jitter = [
- [0, 0, 0], [-4, -10, -12], [6, 8, 10], [-3, -6, -8], [0, 0, 0], [0, 0, 0],
+ [0, 0, 0],
+ [-4, -10, -12],
+ [6, 8, 10],
+ [-3, -6, -8],
+ [0, 0, 0],
+ [0, 0, 0],
];
[...text].forEach((ch, i) => {
const span = document.createElement('span');
@@ -105,8 +174,14 @@ export function initHero(track) {
field.querySelectorAll('.tcard').forEach((el) => el.remove());
const pick = TASKS.filter((t) => !mobile || t.mobile !== false);
const scatter = mobile ? SCATTER_MOBILE : SCATTER_DESKTOP;
- tasks = pick.map((t, i) => ({ ...t, anchor: scatter[i % scatter.length].slice(), custom: null }));
- order = Object.fromEntries(GROUPS.map((g) => [g, tasks.filter((t) => t.s === g).map((t) => t.id)]));
+ tasks = pick.map((t, i) => ({
+ ...t,
+ anchor: scatter[i % scatter.length].slice(),
+ custom: null,
+ }));
+ order = Object.fromEntries(
+ GROUPS.map((g) => [g, tasks.filter((t) => t.s === g).map((t) => t.id)]),
+ );
// 归位顺序打散一点,看起来像被一张张吸过去,而不是排队
const settleOrder = [2, 0, 4, 1, 6, 3, 8, 5, 7];
cards = tasks.map((t, i) => {
@@ -160,7 +235,16 @@ export function initHero(track) {
const s = clamp(avail / tall, 0.62, 1);
const ph = tall * s;
const py = mobile ? top : Math.max(top, top + (avail - ph) / 2);
- geo = { W, H, px, pw, py, s, m, fs: scatterLine ? parseFloat(getComputedStyle(scatterLine).fontSize) / 90 : 1 };
+ geo = {
+ W,
+ H,
+ px,
+ pw,
+ py,
+ s,
+ m,
+ fs: scatterLine ? parseFloat(getComputedStyle(scatterLine).fontSize) / 90 : 1,
+ };
const cw = (pw - 28) / s;
field.style.setProperty('--card-w', `${cw}px`);
geo.cw = cw;
@@ -264,7 +348,10 @@ export function initHero(track) {
if (reduced) {
cards.forEach((c) => c.sp.snap());
} else {
- integrate(cards.map((c) => c.sp), dt);
+ integrate(
+ cards.map((c) => c.sp),
+ dt,
+ );
}
const { cw, ch } = geo;
@@ -314,7 +401,8 @@ export function initHero(track) {
const fs = geo.fs;
for (const gl of glyphs) {
const [dx, dy, dr] = gl.j.map((v, i) => (i < 2 ? v * fs : v));
- gl.el.style.transform = ge > 0.001 ? `translate(${dx * ge}px, ${dy * ge}px) rotate(${dr * ge}deg)` : '';
+ gl.el.style.transform =
+ ge > 0.001 ? `translate(${dx * ge}px, ${dy * ge}px) rotate(${dr * ge}deg)` : '';
}
// 移动端:文案让出舞台给面板
@@ -451,7 +539,9 @@ export function initHero(track) {
c.el.dataset.s = orig.s;
c.el.querySelector('.status').dataset.s = orig.s;
}
- order = Object.fromEntries(GROUPS.map((g) => [g, fresh.filter((t) => t.s === g).map((t) => t.id)]));
+ order = Object.fromEntries(
+ GROUPS.map((g) => [g, fresh.filter((t) => t.s === g).map((t) => t.id)]),
+ );
interacted = false;
loop.kick();
});
diff --git a/website/site/assets/js/release.js b/website/site/assets/js/release.js
index 12ac109..250be07 100644
--- a/website/site/assets/js/release.js
+++ b/website/site/assets/js/release.js
@@ -56,7 +56,9 @@ async function fetchLatest() {
const latest = Array.isArray(list) ? list.find((r) => !r.draft) : null;
if (!latest || typeof latest.tag_name !== 'string') return null;
if (!SAFE_SEGMENT.test(latest.tag_name)) return null;
- const apk = (latest.assets || []).find((a) => /\.apk$/i.test(a.name) && SAFE_SEGMENT.test(a.name));
+ const apk = (latest.assets || []).find(
+ (a) => /\.apk$/i.test(a.name) && SAFE_SEGMENT.test(a.name),
+ );
const apkName = apk ? apk.name : null;
const release = {
tag: latest.tag_name,
@@ -82,7 +84,11 @@ const versionOf = (tag) => tag.replace(/^v/, '');
function formatDate(iso) {
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return '';
- return new Intl.DateTimeFormat('zh-CN', { year: 'numeric', month: 'long', day: 'numeric' }).format(d);
+ return new Intl.DateTimeFormat('zh-CN', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ }).format(d);
}
function apply(release) {
diff --git a/website/site/assets/js/sync-demo.js b/website/site/assets/js/sync-demo.js
index 19b8a84..a17b0bd 100644
--- a/website/site/assets/js/sync-demo.js
+++ b/website/site/assets/js/sync-demo.js
@@ -30,8 +30,16 @@ export function initSync(root) {
{ id: 'b', ref: 'TASK-219', title: '整理同步冲突的复现步骤', s: 'todo' },
{ id: 'c', ref: 'TASK-188', title: '升级到 PostgreSQL 18', s: 'done' },
];
- const hub = { cursor: 1042, tasks: Object.fromEntries(seed.map((t) => [t.id, { s: t.s, v: 3 }])) };
- const web = { online: true, tasks: Object.fromEntries(seed.map((t) => [t.id, { s: t.s, v: 3 }])), outbox: [], conflict: null };
+ const hub = {
+ cursor: 1042,
+ tasks: Object.fromEntries(seed.map((t) => [t.id, { s: t.s, v: 3 }])),
+ };
+ const web = {
+ online: true,
+ tasks: Object.fromEntries(seed.map((t) => [t.id, { s: t.s, v: 3 }])),
+ outbox: [],
+ conflict: null,
+ };
const android = { tasks: Object.fromEntries(seed.map((t) => [t.id, { s: t.s, v: 3 }])) };
const meta = Object.fromEntries(seed.map((t) => [t.id, t]));
let busy = false;
@@ -50,7 +58,11 @@ export function initSync(root) {
const status = interactive
? `
`
: `
`;
- const tag = conflict ? '
冲突' : pending ? '
待同步' : '';
+ const tag = conflict
+ ? '
冲突'
+ : pending
+ ? '
待同步'
+ : '';
return `
${status}${t.title}${tag}
`;
})
.join('');
@@ -58,7 +70,9 @@ export function initSync(root) {
function renderOutbox() {
outboxEl.innerHTML = web.outbox
- .map((op) => `
${meta[op.task].ref} → ${LABEL[op.to]}`)
+ .map(
+ (op) => `
${meta[op.task].ref} → ${LABEL[op.to]}`,
+ )
.join('');
outboxCount.textContent = String(web.outbox.length);
}
@@ -137,7 +151,12 @@ export function initSync(root) {
const ch = chip.offsetHeight;
const a = center(fromEl);
const b = center(toEl);
- await flyTo(chip, { x: a.x - cw / 2, y: a.y - ch / 2 }, { x: b.x - cw / 2, y: b.y - ch / 2 }, { duration: 640, arc: 26 }).catch(() => {});
+ await flyTo(
+ chip,
+ { x: a.x - cw / 2, y: a.y - ch / 2 },
+ { x: b.x - cw / 2, y: b.y - ch / 2 },
+ { duration: 640, arc: 26 },
+ ).catch(() => {});
chip.remove();
}
@@ -176,15 +195,25 @@ export function initSync(root) {
web.outbox.forEach((rest) => {
if (rest.task === op.task) rest.base = server.v;
});
- if (!web.outbox.some((rest) => rest.task === op.task)) web.tasks[op.task] = { s: op.to, v: server.v };
+ if (!web.outbox.some((rest) => rest.task === op.task))
+ web.tasks[op.task] = { s: op.to, v: server.v };
changed.add(op.task);
pulse(hubEl, 'is-hit');
- say(`中枢已应用
${meta[op.task].ref} → ${LABEL[op.to]},版本 ${server.v},变更游标前进到 ${hub.cursor}。`);
+ say(
+ `中枢已应用
${meta[op.task].ref} → ${LABEL[op.to]},版本 ${server.v},变更游标前进到 ${hub.cursor}。`,
+ );
} else {
// 版本冲突:保留本地意图与服务器快照,不静默覆盖
web.outbox = web.outbox.filter((rest) => rest.task !== op.task);
- web.conflict = { task: op.task, local: web.tasks[op.task].s, server: server.s, serverV: server.v };
- say(`
${meta[op.task].ref} 的基准版本是 ${op.base},中枢已经是 ${server.v}:返回冲突与服务器快照,等你选择。`);
+ web.conflict = {
+ task: op.task,
+ local: web.tasks[op.task].s,
+ server: server.s,
+ serverV: server.v,
+ };
+ say(
+ `
${meta[op.task].ref} 的基准版本是 ${op.base},中枢已经是 ${server.v}:返回冲突与服务器快照,等你选择。`,
+ );
}
render();
await sleep(reduced ? 0 : 120);
@@ -209,14 +238,21 @@ export function initSync(root) {
const to = NEXT[t.s];
// 先写本地:实体与待同步记录在同一个事务里
const pendingBase = web.outbox.find((op) => op.task === id)?.base;
- web.outbox.push({ id: `${Date.now()}-${Math.random()}`, task: id, to, base: pendingBase ?? t.v });
+ web.outbox.push({
+ id: `${Date.now()}-${Math.random()}`,
+ task: id,
+ to,
+ base: pendingBase ?? t.v,
+ });
t.s = to;
render();
if (web.online) {
say(`本地已写入
${meta[id].ref} → ${LABEL[to]},正在推送……`);
flush();
} else {
- say(`离线:
${meta[id].ref} → ${LABEL[to]} 已写入本地,排进待同步队列(${web.outbox.length})。`);
+ say(
+ `离线:
${meta[id].ref} → ${LABEL[to]} 已写入本地,排进待同步队列(${web.outbox.length})。`,
+ );
}
}
@@ -236,7 +272,12 @@ export function initSync(root) {
render();
} else {
web.tasks[c.task] = { s: c.local, v: hub.tasks[c.task].v };
- web.outbox.push({ id: `r-${Date.now()}`, task: c.task, to: c.local, base: hub.tasks[c.task].v });
+ web.outbox.push({
+ id: `r-${Date.now()}`,
+ task: c.task,
+ to: c.local,
+ base: hub.tasks[c.task].v,
+ });
say(`已采用本地版本,基于最新版本重新推送
${meta[c.task].ref}……`);
render();
flush();
@@ -249,7 +290,11 @@ export function initSync(root) {
web.online = !web.online;
render();
if (web.online) {
- say(web.outbox.length ? `重新连上中枢:按顺序推送 ${web.outbox.length} 条修改……` : '重新连上中枢,没有待同步的修改。');
+ say(
+ web.outbox.length
+ ? `重新连上中枢:按顺序推送 ${web.outbox.length} 条修改……`
+ : '重新连上中枢,没有待同步的修改。',
+ );
flush();
} else {
say('浏览器离线了。照常改任务:界面立刻更新,修改排进待同步队列。');
@@ -262,7 +307,9 @@ export function initSync(root) {
const id = web.outbox[0]?.task || 'a';
const server = hub.tasks[id];
// 选一个与服务器、浏览器本地都不同的状态,冲突才看得出差别
- const to = ['todo', 'doing', 'done'].find((x) => x !== server.s && x !== web.tasks[id].s) || NEXT[server.s];
+ const to =
+ ['todo', 'doing', 'done'].find((x) => x !== server.s && x !== web.tasks[id].s) ||
+ NEXT[server.s];
busy = true;
render();
await fly(`${meta[id].ref} → ${LABEL[to]}`, andEl, hubEl);
@@ -278,7 +325,9 @@ export function initSync(root) {
flush();
return;
}
- say(`Android 把
${meta[id].ref} 改成了 ${LABEL[to]}。浏览器离线,收不到通知;重新连上时会发现版本对不上。`);
+ say(
+ `Android 把
${meta[id].ref} 改成了 ${LABEL[to]}。浏览器离线,收不到通知;重新连上时会发现版本对不上。`,
+ );
});
render();
diff --git a/website/site/assets/js/timeline.js b/website/site/assets/js/timeline.js
index 62565d8..9f62d0c 100644
--- a/website/site/assets/js/timeline.js
+++ b/website/site/assets/js/timeline.js
@@ -32,7 +32,12 @@ export function initTimeline(root) {
for (let i = 0; i < 5; i++) {
const d = new Date(base);
d.setDate(base.getDate() + i);
- cols.push({ id: `d${i}`, kind: 'date', title: i === 0 ? '今天' : i === 1 ? '明天' : wd.format(d), sub: md.format(d) });
+ cols.push({
+ id: `d${i}`,
+ kind: 'date',
+ title: i === 0 ? '今天' : i === 1 ? '明天' : wd.format(d),
+ sub: md.format(d),
+ });
}
cols.push({ id: 'e1', kind: 'event', title: '额度重置后', sub: '自定义事件', arrived: false });
cols.push({ id: 'e2', kind: 'event', title: '上线前', sub: '自定义事件', arrived: false });
@@ -55,7 +60,9 @@ export function initTimeline(root) {
function renderTray() {
tray.innerHTML = ['t1', 't2', 't4']
.map(
- (id) => `