diff --git a/README.md b/README.md index d847ab6..9242d5a 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ ## 特点 - **纯前端,零依赖** — 一个 `index.html`,没有构建步骤、没有 CDN、没有任何外部请求。断网可用。 +- **左设置、右操作** — 设置栏在左,不调任何设置时视线直接落到右侧的密码与「复制密码」。窄屏自动变上下排列,设置在上。 - **密码学安全随机** — 走 `crypto.getRandomValues` + 拒绝采样消除取模偏差,不用 `Math.random()`。 - **生成即复制** — 默认开启自动复制;剪贴板 API 不可用(如 `file://` 下的旧浏览器)时自动降级到 `execCommand`。 - **深浅色** — 跟随系统偏好,可手动切换并记住选择。首帧前定主题,不会白屏闪一下。 @@ -16,7 +17,6 @@ - **自定义排除** — 想避开 `@`、空格或某个符号,直接填进去。 - **强度与熵** — 按 `长度 × log₂(字符池)` 算真实熵,分五档给出评价,同时显示字符池规模。 - **批量候选** — 一次出 5 个,点任意一条即复制。 -- **最近生成** — 只存在本机 `localStorage`,最多 6 条,可一键清空。 ## 使用 @@ -44,7 +44,7 @@ python -m http.server 8000 - 所有运算在浏览器里完成,**不联网、不上传、不写日志**。 - 用 `crypto.getRandomValues` 取随机数,并按「拒绝采样」丢弃落在偏置区间的值,避免取模带来的分布倾斜。 - 勾「每类至少一位」时,先从每类各取一位,剩余位从全池随机,最后整体洗牌 —— 保证覆盖的同时不引入位置可预测性。 -- 历史记录存在 `localStorage` 里,任何能访问这台机器的人都能看到。**在共用电脑上建议用完点「清空」**,或干脆关掉自动复制、不复制到历史。 +- **不保留任何生成记录**。密码只存在于剪贴板与页面显示,刷新即失去痕迹;`localStorage` 里只存偏好设置与主题,不存密码。 ## 浏览器支持 @@ -63,11 +63,18 @@ index.html 全部内容:结构、样式、脚本 改完 `index.html` 后建议跑一遍(脚本在仓库外的 `_tooling/`,配合 `jsdom` 与 `playwright`): ```bash -node _sp_test.js # 功能判据(jsdom,80 条,含反向注入 24 条) -node _sp_visual.js # 真实浏览器几何 / 对比度 / 多视口(约 101 条) -node _sp_clip.js # 真实剪贴板端到端(18 条) +node _sp_test.js # 功能判据(jsdom,78 条) +node _sp_visual.js # 真实浏览器几何 / 对比度 / 多视口(114 条) +node _sp_clip.js # 真实剪贴板端到端(19 条) +node _sp_file_check.js # file:// 双击直开(15 条) +node _sp_falsify.js # 反向注入,证明功能判据有牙齿(22 条) +node _sp_visual_falsify.js # 反向注入,证明几何判据有牙齿(6 条) ``` +反向注入必须单独跑视觉那套:`_sp_falsify.js` 只喂 `_sp_test.js`(jsdom 不做布局), +把「按钮等大」「两栏左右」这类几何 bug 注进去根本影响不到被测判据, +会表现成"注入后仍全绿",看着像判据没牙齿,其实是靶子选错。 + ## License MIT \ No newline at end of file diff --git a/docs/dark.png b/docs/dark.png index 467f66f..d547e0f 100644 Binary files a/docs/dark.png and b/docs/dark.png differ diff --git a/docs/light.png b/docs/light.png index c668722..dd4796e 100644 Binary files a/docs/light.png and b/docs/light.png differ diff --git a/docs/mobile.png b/docs/mobile.png index 8a6b8ef..0367c70 100644 Binary files a/docs/mobile.png and b/docs/mobile.png differ diff --git a/index.html b/index.html index d55c987..f4a0f29 100644 --- a/index.html +++ b/index.html @@ -37,6 +37,8 @@ --surface: rgba(20, 22, 30, .72); --surface-2: rgba(255, 255, 255, .035); + /* 比 surface-2 更实的填充层:用于需要"看起来是个实体按钮"的次操作 */ + --surface-3: rgba(255, 255, 255, .085); --pw-bg: rgba(255, 255, 255, .045); --line: rgba(255, 255, 255, .085); @@ -73,6 +75,7 @@ --surface: rgba(255, 255, 255, .78); --surface-2: rgba(16, 18, 27, .028); + --surface-3: rgba(16, 18, 27, .062); --pw-bg: rgba(16, 18, 27, .035); --line: rgba(16, 18, 27, .085); @@ -215,11 +218,15 @@ body::before { /* ============================================================ 主卡片 + 左 = 设置,右 = 输出。 + 语义是「左侧可选地调设置,右侧直接完成主操作」—— + 不动设置的人,视线直接落到右侧的密码与「复制密码」上。 ============================================================ */ .card { position: relative; display: grid; - grid-template-columns: minmax(0, 1.08fr) minmax(0, .92fr); + /* 设置栏偏窄、输出栏偏宽:密码要用大字排开,设置只需容纳控件 */ + grid-template-columns: minmax(0, .92fr) minmax(0, 1.08fr); border-radius: var(--r-lg); border: 1px solid var(--line); background: var(--surface); @@ -231,7 +238,7 @@ body::before { .pane { padding: clamp(22px, 3vw, 30px); min-width: 0; } -/* 左栏内容比右栏短,直接顶部对齐会在底部留出一大片空档(实测 180px)。 +/* 右栏(输出)内容比左栏短,直接顶部对齐会在底部留出一大片空档(实测 180px)。 主内容用 auto 上下外边距居中,快捷键提示压在底部,版面才立得住。 */ .pane--out { display: flex; @@ -285,7 +292,7 @@ kbd { } .pane--ctl { - border-left: 1px solid var(--line); + border-right: 1px solid var(--line); background: var(--surface-2); display: flex; flex-direction: column; @@ -374,7 +381,9 @@ kbd { } .out-actions { - display: flex; + display: grid; + /* 两个按钮严格等宽:均分而非「主按钮吃掉剩余空间」 */ + grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; margin-top: 14px; } @@ -398,8 +407,8 @@ kbd { .btn svg { width: 16px; height: 16px; flex: none; } .btn:active { transform: scale(.985); } +/* 「复制密码」:主色渐变,实心 */ .btn--primary { - flex: 1 1 auto; color: #fff; background: linear-gradient(180deg, #5b5ee8, #4f46e5); box-shadow: 0 10px 24px -12px rgba(99, 102, 241, .95), @@ -411,13 +420,19 @@ kbd { } .btn--primary:active { box-shadow: 0 6px 14px -10px rgba(99, 102, 241, .9); } +/* 「换一个」:同等尺寸,中性实心填充 + 描边区分层级。 + 早先只是透明描边,和主按钮并排时显得矮一截、也像"不可点"。 */ .btn--ghost { - flex: 0 1 auto; - color: var(--text-2); - border-color: var(--line); - background: transparent; + color: var(--text); + border-color: var(--line-2); + background: var(--surface-3); +} +.btn--ghost:hover { + color: var(--text); + border-color: var(--line-2); + background: var(--surface-3); + box-shadow: 0 1px 0 rgba(255, 255, 255, .04) inset; } -.btn--ghost:hover { color: var(--text); border-color: var(--line-2); background: var(--surface-2); } .btn[disabled] { opacity: .45; cursor: not-allowed; transform: none; } @@ -767,11 +782,12 @@ input[type="range"]::-moz-range-thumb { } /* ============================================================ - 候选 / 历史 + 候选 ============================================================ */ +/* 候选单独一张卡,整宽单栏(原先与本地记录面板并排,那个面板已移除) */ .grid2 { display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); + grid-template-columns: minmax(0, 1fr); gap: clamp(14px, 2vw, 20px); margin-top: clamp(14px, 2vw, 20px); } @@ -888,8 +904,8 @@ input[type="range"]::-moz-range-thumb { ============================================================ */ @media (max-width: 880px) { .card { grid-template-columns: minmax(0, 1fr); } - .pane--ctl { border-left: 0; border-top: 1px solid var(--line); } - .grid2 { grid-template-columns: minmax(0, 1fr); } + /* DOM 顺序就是「设置 → 输出」,单列时天然是设置在上、输出在下 */ + .pane--ctl { border-right: 0; border-bottom: 1px solid var(--line); } .pw { min-height: 96px; } } @@ -941,52 +957,7 @@ input[type="range"]::-moz-range-thumb {
- -
-
-
- 生成的密码 - 自动复制 -
- -
- -
- - -
- -
-
-
- — - — -
-
- -

-
- -

- 空格 换一个 - C 复制 - 点击密码 也可以复制 -

-
- - +
@@ -1054,6 +1025,52 @@ input[type="range"]::-moz-range-thumb {
+ + +
+
+
+ 生成的密码 + 自动复制 +
+ +
+ +
+ + +
+ +
+
+
+ — + — +
+
+ +

+
+ +

+ 空格 换一个 + C 复制 + 点击密码 也可以复制 +

+
+
@@ -1064,14 +1081,6 @@ input[type="range"]::-moz-range-thumb { - -
-
-

最近生成

- -
-
  • 还没有记录。生成并复制后会自动保存在本机。
-
@@ -1097,11 +1106,9 @@ input[type="range"]::-moz-range-thumb { 常量 ========================================================== */ var KEY_PREFS = 'sp.prefs.v1'; - var KEY_HIST = 'sp.history.v1'; var KEY_THEME = 'sp.theme.v1'; var MAX_LEN = 64; - var HIST_MAX = 6; var CLASSES = [ { id: 'lower', label: '小写', sample: 'abc', chars: 'abcdefghijklmnopqrstuvwxyz' }, @@ -1392,7 +1399,6 @@ input[type="range"]::-moz-range-thumb { if (!current) return; copyText(current, function (ok) { if (ok) { - pushHistory(current); toast('已复制到剪贴板'); } else { toast('复制被拦截,请手动选中', true); @@ -1414,63 +1420,6 @@ input[type="range"]::-moz-range-thumb { }, 1900); } - /* ========================================================== - 历史(本机 localStorage) - ========================================================== */ - function readHistory() { - try { - var raw = localStorage.getItem(KEY_HIST); - var arr = raw ? JSON.parse(raw) : []; - if (!Array.isArray(arr)) return []; - return arr.filter(function (x) { return x && typeof x.p === 'string' && x.p; }); - } catch (e) { - return []; - } - } - - function pushHistory(pw) { - var arr = readHistory(); - if (arr.length && arr[0].p === pw) { renderHistory(); return; } - arr.unshift({ p: pw, t: Date.now() }); - arr = arr.slice(0, HIST_MAX); - try { localStorage.setItem(KEY_HIST, JSON.stringify(arr)); } catch (e) {} - renderHistory(); - } - - function renderHistory() { - var box = $('histList'); - var arr = readHistory(); - box.textContent = ''; - - if (!arr.length) { - var li = document.createElement('li'); - li.className = 'empty'; - li.textContent = '还没有记录。生成并复制后会自动保存在本机。'; - box.appendChild(li); - return; - } - - for (var i = 0; i < arr.length; i++) { - (function (item) { - box.appendChild(listItem(item.p, ago(item.t), function () { - copyText(item.p, function (ok) { - toast(ok ? '已从历史复制' : '复制被拦截', !ok); - }); - })); - })(arr[i]); - } - } - - function ago(ts) { - var diff = Math.max(0, Date.now() - ts); - var m = Math.floor(diff / 60000); - if (m < 1) return '刚刚'; - if (m < 60) return m + ' 分钟前'; - var h = Math.floor(m / 60); - if (h < 24) return h + ' 小时前'; - return Math.floor(h / 24) + ' 天前'; - } - function listItem(text, meta, onClick) { var li = document.createElement('li'); li.tabIndex = 0; @@ -1517,7 +1466,7 @@ input[type="range"]::-moz-range-thumb { (function (pw, bits) { box.appendChild(listItem(pw, Math.round(bits) + ' bit', function () { copyText(pw, function (ok) { - if (ok) { pushHistory(pw); toast('已复制候选密码'); } + if (ok) { toast('已复制候选密码'); } else { toast('复制被拦截', true); } }); })); @@ -1787,12 +1736,6 @@ input[type="range"]::-moz-range-thumb { $('batchBtn').addEventListener('click', renderCandidates); - $('clearBtn').addEventListener('click', function () { - try { localStorage.removeItem(KEY_HIST); } catch (e) {} - renderHistory(); - toast('历史记录已清空'); - }); - /* 快捷键:空格=换一个,C=复制(输入框内不拦截) */ document.addEventListener('keydown', function (e) { var tag = (e.target && e.target.tagName || '').toLowerCase(); @@ -1835,8 +1778,6 @@ input[type="range"]::-moz-range-thumb { /* 首屏只生成,不自动复制:避免在没有用户手势时触发剪贴板策略告警 */ regenerate(); - renderHistory(); - var meta = document.querySelector('meta[name="theme-color"]'); if (meta) meta.setAttribute('content', currentTheme() === 'light' ? '#f4f5f8' : '#0a0b0f'); }