From 3966a86f42cccd3de5bf747d8a6508f8f6273fdf Mon Sep 17 00:00:00 2001 From: Jason <150260202+estelledc@users.noreply.github.com> Date: Sun, 12 Jul 2026 10:30:06 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=E7=94=9F=E5=91=BD=E5=91=A8?= =?UTF-8?q?=E6=9C=9F=E6=BC=94=E7=A4=BA=EF=BC=9A=E6=94=AF=E6=8C=81=E5=8F=AF?= =?UTF-8?q?=E4=B8=AD=E6=96=AD=E5=9B=9E=E6=94=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4 --- docs/assets/app.js | 84 ++++++++++++++++++++++++----------- docs/assets/jx/VERSION | 2 +- docs/assets/jx/base.css | 8 +--- docs/assets/jx/components.css | 83 +++++++++++++++++++++++++++++----- docs/assets/jx/tokens.css | 14 ++++-- docs/assets/style.css | 64 +++++++++++++++++--------- docs/index.html | 2 +- scripts/audit-showcase.py | 40 +++++++++++++++-- 8 files changed, 226 insertions(+), 71 deletions(-) diff --git a/docs/assets/app.js b/docs/assets/app.js index 9d9598a..f5c519b 100644 --- a/docs/assets/app.js +++ b/docs/assets/app.js @@ -58,15 +58,36 @@ const saveLab = document.querySelector("[data-save-lab]"); const runSave = saveLab?.querySelector("[data-run-save]"); + const saveTrace = saveLab?.querySelector("[data-save-trace]"); const saveOutput = saveLab?.querySelector("[data-save-output]"); const saveSteps = Array.from(saveLab?.querySelectorAll("[data-mechanism]") || []); + const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)"); + const SAVE_TRACE_RESET_MS = 180; + const SAVE_TRACE_STEP_MS = 480; let saveTimers = []; + let saveRunId = 0; function clearSaveTimers() { saveTimers.forEach((timer) => window.clearTimeout(timer)); saveTimers = []; } + function cancelSaveTrace() { + saveRunId += 1; + clearSaveTimers(); + saveTrace?.removeAttribute("aria-busy"); + return saveRunId; + } + + function scheduleSaveTimer(runId, callback, delay) { + const timer = window.setTimeout(() => { + saveTimers = saveTimers.filter((pending) => pending !== timer); + if (runId !== saveRunId) return; + callback(); + }, delay); + saveTimers.push(timer); + } + function resetSaveTrace() { saveSteps.forEach((step) => { step.classList.remove("is-active", "is-complete"); @@ -89,41 +110,54 @@ } } - function finishSaveTrace() { + function finishSaveTrace(runId) { + if (runId !== saveRunId) return; saveSteps.forEach((step) => { step.classList.remove("is-active"); step.classList.add("is-complete"); step.removeAttribute("aria-current"); }); - saveLab?.removeAttribute("aria-busy"); - if (runSave) { - runSave.disabled = false; - runSave.textContent = "Replay Save trace"; - } + saveTrace?.removeAttribute("aria-busy"); + if (runSave) runSave.textContent = "Replay Save trace"; if (saveOutput) saveOutput.textContent = "Save 完成:closure 回传后,列表 snapshot 已刷新。"; } - if (saveLab && runSave && saveSteps.length === 4) { + function startSaveTrace() { + const wasRunning = saveTrace?.getAttribute("aria-busy") === "true"; + const runId = cancelSaveTrace(); + resetSaveTrace(); + runSave.textContent = "Restart Save trace"; + saveTrace.setAttribute("aria-busy", "true"); + if (saveOutput) { + saveOutput.textContent = wasRunning + ? "上一轮已取消。Save trace 已重新开始,准备进入 01 · delegate。" + : "Save trace 已开始,准备进入 01 · delegate。"; + } + + if (reducedMotion.matches) { + saveSteps.forEach((_, index) => activateSaveStep(index)); + finishSaveTrace(runId); + return; + } + + saveSteps.forEach((_, index) => { + scheduleSaveTimer( + runId, + () => activateSaveStep(index), + SAVE_TRACE_RESET_MS + index * SAVE_TRACE_STEP_MS, + ); + }); + scheduleSaveTimer( + runId, + () => finishSaveTrace(runId), + SAVE_TRACE_RESET_MS + saveSteps.length * SAVE_TRACE_STEP_MS, + ); + } + + if (saveLab && runSave && saveTrace && saveSteps.length === 4) { saveLab.classList.add("is-enhanced"); runSave.hidden = false; - runSave.addEventListener("click", () => { - clearSaveTimers(); - resetSaveTrace(); - runSave.disabled = true; - runSave.textContent = "Running Save trace…"; - saveLab.setAttribute("aria-busy", "true"); - - if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) { - saveSteps.forEach((_, index) => activateSaveStep(index)); - finishSaveTrace(); - return; - } - - saveSteps.forEach((_, index) => { - saveTimers.push(window.setTimeout(() => activateSaveStep(index), index * 480)); - }); - saveTimers.push(window.setTimeout(finishSaveTrace, saveSteps.length * 480)); - }); + runSave.addEventListener("click", startSaveTrace); } media.addEventListener("change", () => { diff --git a/docs/assets/jx/VERSION b/docs/assets/jx/VERSION index 7ec1d6d..ccbccc3 100644 --- a/docs/assets/jx/VERSION +++ b/docs/assets/jx/VERSION @@ -1 +1 @@ -2.1.0 +2.2.0 diff --git a/docs/assets/jx/base.css b/docs/assets/jx/base.css index 96dc573..4bb0069 100644 --- a/docs/assets/jx/base.css +++ b/docs/assets/jx/base.css @@ -1,4 +1,4 @@ -/* Jason DS · Base v2.0.0 +/* Jason DS · Base v2.2.0 * 默认 reset + body 排版基底 + chrome layer 强制 sans */ @@ -155,11 +155,7 @@ hr { color: var(--jx-ink); } -/* Reduce motion */ +/* Reduced motion keeps semantic color/opacity feedback; components remove travel. */ @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } - * { - animation-duration: 0.01ms !important; - transition-duration: 0.01ms !important; - } } diff --git a/docs/assets/jx/components.css b/docs/assets/jx/components.css index c430b87..2f6d438 100644 --- a/docs/assets/jx/components.css +++ b/docs/assets/jx/components.css @@ -82,7 +82,7 @@ --_chip-glyph: var(--jx-state-archived-glyph); } -/* Jason DS · External Pill v1.0.0 +/* Jason DS · External Pill v2.2.0 * 出站链接统一 hairline + 右上 ↗ 箭头 (no icon font) */ @@ -112,15 +112,25 @@ color: var(--jx-accent); box-shadow: 0 0 0 1px var(--jx-accent); } -.jx-pill:hover::after { - color: var(--jx-accent); - transform: translate(1px, -1px); -} +.jx-pill:hover::after { color: var(--jx-accent); } .jx-pill:focus-visible { outline: none; box-shadow: var(--jx-shadow-focus); } +@media (hover: hover) and (pointer: fine) { + .jx-pill:hover::after { + transform: translate(1px, -1px); + } +} + +@media (prefers-reduced-motion: reduce) { + .jx-pill::after { + transform: none; + transition: color var(--jx-duration-fast) var(--jx-ease-out); + } +} + /* Jason DS · Footer v1.0.0 * 三栏:colophon / 索引 / last-updated * Layer 1:9 仓必有此结构 @@ -207,7 +217,7 @@ box-shadow: var(--jx-shadow-focus); } -/* Jason DS · Site chrome v2.0.0 +/* Jason DS · Site chrome v2.2.0 * Shared identity, accessible navigation, skip link, and action styles. */ @@ -237,6 +247,7 @@ background: color-mix(in oklab, var(--jx-paper), transparent 8%); border-bottom: 1px solid var(--jx-rule); backdrop-filter: blur(16px) saturate(120%); + -webkit-backdrop-filter: blur(16px) saturate(120%); } .jx-site-header__inner { @@ -311,13 +322,18 @@ font-weight: var(--jx-weight-ui); line-height: 1; text-decoration: none; - transition: transform var(--jx-duration-fast) var(--jx-easing), - background var(--jx-duration-fast) var(--jx-easing); + transition: transform var(--jx-duration-press) var(--jx-ease-out), + color var(--jx-duration-fast) var(--jx-ease-out), + background-color var(--jx-duration-fast) var(--jx-ease-out), + border-color var(--jx-duration-fast) var(--jx-ease-out); } .jx-action:hover { color: var(--jx-cta-fg); - transform: translateY(-1px); +} + +.jx-action:active { + transform: scale(var(--jx-press-scale)); } .jx-action--secondary { @@ -331,6 +347,54 @@ background: var(--jx-surface-sunk); } +@media (hover: hover) and (pointer: fine) { + .jx-action:hover { + transform: translateY(-1px); + } + + .jx-action:active { + transform: scale(var(--jx-press-scale)); + } +} + +@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) { + .jx-site-header { + background: var(--jx-paper); + } +} + +@media (prefers-reduced-transparency: reduce) { + .jx-site-header { + background: var(--jx-paper); + backdrop-filter: none; + -webkit-backdrop-filter: none; + } +} + +@media (prefers-contrast: more) { + .jx-site-header { + background: var(--jx-paper); + border-bottom-color: var(--jx-rule-strong); + backdrop-filter: none; + -webkit-backdrop-filter: none; + } +} + +@media (prefers-reduced-motion: reduce) { + .jx-skip-link { + transition: none; + } + + .jx-action, + .jx-action:hover, + .jx-action:active { + transform: none; + transition-property: color, background-color, border-color; + transition-duration: var(--jx-duration-fast); + transition-timing-function: var(--jx-ease-out); + } +} + @media (max-width: 760px) { .jx-site-header__inner { align-items: flex-start; @@ -383,7 +447,6 @@ } } - /* Jason DS · Project proof v2.0.0 * Hiring-facing evidence block used on project and documentation homepages. */ diff --git a/docs/assets/jx/tokens.css b/docs/assets/jx/tokens.css index c668a15..73048b1 100644 --- a/docs/assets/jx/tokens.css +++ b/docs/assets/jx/tokens.css @@ -1,4 +1,4 @@ -/* Jason DS · Tokens v2.0.0 +/* Jason DS · Tokens v2.2.0 * SoT: dotfiles/home-claude/design-system/jx-tokens.css * 展示站通过 vendor copy 同步:scripts/sync-jx-to-repos.sh * @@ -134,12 +134,18 @@ --jx-shadow-focus: 0 0 0 3px var(--jx-focus-ring); --jx-shadow-press: inset 0 1px 0 rgba(22,24,29,0.06); - /* ── Motion ── */ - --jx-easing: cubic-bezier(0.2, 0.8, 0.2, 1); - --jx-easing-press: cubic-bezier(0.4, 0, 0.6, 1); + /* ── Motion · responsive by default, quiet by frequency ── */ + --jx-ease-out: cubic-bezier(0.23, 1, 0.32, 1); + --jx-ease-in-out: cubic-bezier(0.77, 0, 0.175, 1); + --jx-ease-drawer: cubic-bezier(0.32, 0.72, 0, 1); + --jx-easing: var(--jx-ease-out); /* compatibility alias */ + --jx-easing-press: var(--jx-ease-out); --jx-duration-fast: 120ms; + --jx-duration-press: 140ms; + --jx-duration-popover: 180ms; --jx-duration-base: 200ms; --jx-duration-slow: 320ms; + --jx-press-scale: 0.97; } /* ── Color · dark mirror ── */ diff --git a/docs/assets/style.css b/docs/assets/style.css index def6750..64d721b 100644 --- a/docs/assets/style.css +++ b/docs/assets/style.css @@ -94,15 +94,12 @@ main { display: block; } cursor: pointer; font-family: var(--jx-font-mono); font-size: var(--jx-fs-xs); - transition: color var(--jx-duration-fast) var(--jx-easing), - border-color var(--jx-duration-fast) var(--jx-easing), - background var(--jx-duration-fast) var(--jx-easing); -} -.theme-button:hover { - color: var(--jx-ink); - background: var(--jx-surface-sunk); - border-color: var(--jx-rule-strong); + transition: transform var(--jx-duration-press) var(--jx-ease-out), + color var(--jx-duration-fast) var(--jx-ease-out), + border-color var(--jx-duration-fast) var(--jx-ease-out), + background-color var(--jx-duration-fast) var(--jx-ease-out); } +.theme-button:active { transform: scale(var(--jx-press-scale)); } .case-hero { display: grid; @@ -241,9 +238,10 @@ main { display: block; } font-size: var(--jx-fs-xs); font-weight: 700; letter-spacing: var(--jx-tracking-mono); + transition: transform var(--jx-duration-press) var(--jx-ease-out), + background-color var(--jx-duration-fast) var(--jx-ease-out); } -.runtime-run:hover { background: #9bc8ff; } -.runtime-run:disabled { cursor: wait; opacity: .7; } +.runtime-run:active { transform: scale(var(--jx-press-scale)); } .runtime-trace { margin: 0; padding: 0; @@ -258,19 +256,24 @@ main { display: block; } padding: var(--jx-space-2) var(--jx-space-3); border-bottom: 1px solid rgba(255, 255, 255, .07); border-inline-start: 2px solid transparent; - transition: opacity 180ms ease, background 180ms ease, border-color 180ms ease; + transform: translateX(0); + transition: transform 180ms var(--jx-ease-out), + opacity 180ms var(--jx-ease-out), + background-color 180ms var(--jx-ease-out), + border-color 180ms var(--jx-ease-out); } .runtime-trace li:last-child { border-bottom: 0; } -.runtime-card.is-enhanced .runtime-trace li { opacity: 1; } +.runtime-card.is-enhanced .runtime-trace li { opacity: .72; } .runtime-card.is-enhanced .runtime-trace li.is-active, .runtime-card.is-enhanced .runtime-trace li.is-complete { opacity: 1; - background: rgba(100, 168, 255, .09); + background-color: rgba(100, 168, 255, .09); border-inline-start-color: #64a8ff; } .runtime-card.is-enhanced .runtime-trace li.is-active { - background: rgba(75, 193, 115, .12); + background-color: rgba(75, 193, 115, .12); border-inline-start-color: var(--uikit-green); + transform: translateX(4px); } .runtime-trace span { color: #78adf0; @@ -315,7 +318,16 @@ main { display: block; } } .runtime-test span, .runtime-test small { font-size: 9px; } -.runtime-test:hover span { color: #d8e2ef; } + +@media (hover: hover) and (pointer: fine) { + .theme-button:hover { + color: var(--jx-ink); + background-color: var(--jx-surface-sunk); + border-color: var(--jx-rule-strong); + } + .runtime-run:hover { background-color: #9bc8ff; } + .runtime-test:hover span { color: #d8e2ef; } +} .case-proof { margin-top: var(--jx-section-y); } .case-proof h2 { @@ -531,11 +543,21 @@ main { display: block; } } @media (prefers-reduced-motion: reduce) { - *, *::before, *::after { - scroll-behavior: auto !important; - animation-duration: .01ms !important; - animation-iteration-count: 1 !important; - transition-duration: .01ms !important; - } .runtime-card { transform: none; } + .theme-button, + .theme-button:active, + .runtime-run, + .runtime-run:active { + transform: none; + transition-property: color, background-color, border-color; + transition-duration: var(--jx-duration-fast); + transition-timing-function: var(--jx-ease-out); + } + .runtime-card.is-enhanced .runtime-trace li, + .runtime-card.is-enhanced .runtime-trace li.is-active { + transform: none; + transition-property: opacity, background-color, border-color; + transition-duration: var(--jx-duration-fast); + transition-timing-function: var(--jx-ease-out); + } } diff --git a/docs/index.html b/docs/index.html index ca860d0..b49e3c1 100644 --- a/docs/index.html +++ b/docs/index.html @@ -129,7 +129,7 @@

把 UIKit 的隐式调用,变成一条可以观察的证
  • 03 · target-actionsaveReminderSave 按钮触发注册的 action
  • 04 · closureonSave → snapshot业务 closure 回传并刷新列表
  • - 静态顺序已完整展示。 + 静态顺序已完整展示。 diff --git a/scripts/audit-showcase.py b/scripts/audit-showcase.py index 63b0e19..74c972a 100644 --- a/scripts/audit-showcase.py +++ b/scripts/audit-showcase.py @@ -106,6 +106,7 @@ def audit() -> list[str]: 'data-mechanism="lifecycle"', 'data-mechanism="target-action"', 'data-mechanism="closure"', + 'data-save-output aria-live="polite" aria-atomic="true"', "2 / 2 UI TEST PASS", "LAST VERIFIED · 2026-07-11", "9.456s edit/save · 27.483s Logs/Guide", @@ -182,14 +183,26 @@ def audit() -> list[str]: errors.append(str(error)) version = (DOCS / "assets" / "jx" / "VERSION").read_text(encoding="utf-8").strip() - if version != "2.1.0": - errors.append(f"Jason DS version is {version}, expected 2.1.0") + if version != "2.2.0": + errors.append(f"Jason DS version is {version}, expected 2.2.0") css = (DOCS / "assets" / "style.css").read_text(encoding="utf-8") if "prefers-reduced-motion: reduce" not in css: errors.append("style.css: missing reduced-motion support") if "@media (max-width: 420px)" not in css or ".runtime-card__body { grid-template-columns: 1fr; }" not in css: errors.append("style.css: missing 320px Save-trace layout contract") + for marker in ( + "transition: transform 180ms var(--jx-ease-out)", + "@media (hover: hover) and (pointer: fine)", + ".theme-button:active", + ".runtime-run:active", + ): + if marker not in css: + errors.append(f"style.css: interaction contract missing: {marker}") + if "transition: all" in css: + errors.append("style.css: transition: all is not allowed") + if "*, *::before, *::after" in css: + errors.append("style.css: reduced motion must not globally erase semantic feedback") if "focus-visible" not in (DOCS / "assets" / "jx" / "base.css").read_text(encoding="utf-8"): errors.append("Jason DS base: missing focus-visible styles") @@ -197,11 +210,32 @@ def audit() -> list[str]: for marker in ( "activateSaveStep", "finishSaveTrace", + "cancelSaveTrace", + "scheduleSaveTimer", "prefers-reduced-motion: reduce", - 'setAttribute("aria-busy", "true")', + "const runId = cancelSaveTrace();", + "if (runId !== saveRunId) return;", + 'saveTrace.setAttribute("aria-busy", "true")', + 'saveTrace?.removeAttribute("aria-busy")', + 'runSave.textContent = "Restart Save trace"', + "saveTimers = saveTimers.filter", + "SAVE_TRACE_RESET_MS = 180", ): if marker not in app_js: errors.append(f"app.js: progressive Save trace marker missing: {marker}") + if "runSave.disabled" in app_js: + errors.append("app.js: Save trace trigger must remain interruptible") + + start_trace = app_js.find("function startSaveTrace()") + cancel_trace = app_js.find("const runId = cancelSaveTrace();", start_trace) + immediate_status = app_js.find("saveOutput.textContent = wasRunning", start_trace) + reduced_branch = app_js.find("if (reducedMotion.matches)", start_trace) + schedule_trace = app_js.find("scheduleSaveTimer(", reduced_branch) + if not ( + start_trace >= 0 + and start_trace < cancel_trace < immediate_status < reduced_branch < schedule_trace + ): + errors.append("app.js: restart must cancel, announce, branch reduced motion, then schedule") hero_image = re.search(r']+src="assets/uikit-list.png"[^>]*>', html) if not hero_image: