Regression / regression (push) Canceled after 0s
## 品牌标识(本次会话) 起因:品牌此前没有任何图形标识 —— 唯一 favicon 是内联 data-URI 里的字母「A」, 那是 v2.0.0「Aurora Admin → Kole UI」改名漏掉的一处(PC 顶栏也是「A」, 移动端站已是「K」;移动端文档站则完全没有 favicon)。 - 几何:24 网格三个互不接触的笔画(竖 + 两斜),圆头描边; 描边 2.25 → 16px 标签页尺寸下正好 1.5px = 规范原文「描边1.5px」 - 取色分两套(刻意):favicon 硬编码品牌蓝/白(渲染在浏览器标签栏,不继承 kole-dark); 顶栏标记走 currentColor(实测暗色下自动转 rgb(20,22,28)) - 新增 theme-color 双条(light #FFFFFF / dark #1C1F26,取 --kole-color-card-bg) - 修 site/app.js hero 标语 KOLE ADMIN → KOLE UI(改名变形残留) - 移动端 7 个模板补 favicon(此前计数 0) 验收:门禁 9 条全 OK(site-routing/site-routes/mobile-docs/mobile-site/isolation/ theme/nav/i18n/icons);PC 回归 1464/1464 · 移动端 807/807,各连跑 8 次一致; 两端 favicon 405 字节逐字节一致;PC 站控制台错误 1→0。 ## 并行会话成果(本次一并入库) - 图标系统:2576 图标(TDesign/Element Plus,MIT)+ 11 端注入 + 5 个构建门禁工具 + IconPreview 预览页 + ICON-SPEC.md 冻结规格 - 移动端平台:47 组件 × 6 端 + 文档站 53 页 + 隔离门禁 - PC 组件:103 个大后台组件 / 组件11 批次 - uni-app:PC 端试点 + 移动端端实现 + 真实编译验证 ## 工程 - .gitignore 补 .scratch/ 与 .zcode-preexisting-*.txt(会话中间产物,实测 9.1MB,不入库) - CHANGELOG 补品牌标识条目 - ROADMAP 登 S8-P4(品牌标识任务包 + og:image/apple-touch-icon 未做部分)
395 lines
16 KiB
HTML
395 lines
16 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>图标预览 · Kole UI</title>
|
||
<meta name="description" content="Kole UI 图标预览:2574 个图标的网格浏览、搜索、按名/按代码复制、尺寸调节。">
|
||
<meta name="color-scheme" content="light dark">
|
||
<link rel="stylesheet" href="../.design_library/kole-ui/colors_and_type.css">
|
||
<style>
|
||
/* 图标预览(IconPreview)— 对齐 .design_library/kole-ui/specs/组件5.txt「图标预览 IconPreview」:
|
||
展示系统所有图标 / 网格布局,支持搜索 / 点击图标复制名称或代码 / 可调整图标大小
|
||
自包含样式:不引 site/style.css(本页是工具页,与文档站 SPA 解耦,同 playground.html 的取向)。 */
|
||
|
||
*, *::before, *::after { box-sizing: border-box; }
|
||
|
||
body {
|
||
margin: 0;
|
||
font-family: var(--kole-font-family);
|
||
color: var(--kole-color-text-body);
|
||
background: var(--kole-color-page-bg);
|
||
}
|
||
|
||
.ip-shell { max-width: 1280px; margin: 0 auto; padding: 24px; }
|
||
|
||
.ip-head { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; flex-wrap: wrap; margin-bottom: 4px; }
|
||
.ip-head h1 { margin: 0; font-size: 22px; color: var(--kole-color-text-title); }
|
||
.ip-head .back { font-size: 13px; color: var(--kole-color-brand); text-decoration: none; }
|
||
.ip-head .back:hover { text-decoration: underline; }
|
||
.ip-sub { margin: 0 0 18px; font-size: 13px; color: var(--kole-color-text-secondary); }
|
||
|
||
/* ---------- 工具条 ---------- */
|
||
.ip-toolbar {
|
||
display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
|
||
padding: 12px 16px; margin-bottom: 16px;
|
||
background: var(--kole-color-card-bg);
|
||
border: 1px solid var(--kole-color-border);
|
||
border-radius: var(--kole-radius-large);
|
||
}
|
||
.ip-field { display: flex; align-items: center; gap: 6px; font-size: 13px; color: var(--kole-color-text-secondary); }
|
||
.ip-input {
|
||
height: 32px; min-width: 220px; padding: 0 10px;
|
||
font: inherit; font-size: 13px;
|
||
color: var(--kole-color-text-body);
|
||
background: var(--kole-color-card-bg);
|
||
border: 1px solid var(--kole-color-border);
|
||
border-radius: var(--kole-radius-base);
|
||
}
|
||
.ip-input:focus-visible { outline: 2px solid var(--kole-color-brand); outline-offset: 1px; border-color: var(--kole-color-brand); }
|
||
|
||
/* size 档位:对齐图标 API 的四档关键字(ICON-SPEC §4.2) */
|
||
.ip-sizes { display: inline-flex; border: 1px solid var(--kole-color-border); border-radius: var(--kole-radius-base); overflow: hidden; }
|
||
.ip-size {
|
||
height: 32px; padding: 0 12px; border: 0; border-inline-end: 1px solid var(--kole-color-border);
|
||
background: var(--kole-color-card-bg); color: var(--kole-color-text-body);
|
||
font: inherit; font-size: 13px; cursor: pointer;
|
||
}
|
||
.ip-size:last-child { border-inline-end: 0; }
|
||
.ip-size:hover { background: var(--kole-color-table-header-bg); }
|
||
.ip-size[aria-pressed="true"] { background: var(--kole-color-brand); color: var(--kole-color-text-inverse); }
|
||
.ip-size:focus-visible { outline: 2px solid var(--kole-color-brand); outline-offset: -2px; }
|
||
|
||
.ip-toggle { display: inline-flex; align-items: center; gap: 6px; font-size: 13px; color: var(--kole-color-text-body); cursor: pointer; }
|
||
.ip-count { margin-inline-start: auto; font-size: 13px; color: var(--kole-color-text-secondary); font-family: var(--kole-font-family-num); }
|
||
|
||
/* ---------- 分组 ---------- */
|
||
.ip-group { margin-bottom: 20px; }
|
||
.ip-group > summary {
|
||
cursor: pointer; padding: 8px 4px; font-size: 14px; font-weight: 600;
|
||
color: var(--kole-color-text-title); list-style: none; display: flex; align-items: center; gap: 8px;
|
||
}
|
||
.ip-group > summary::-webkit-details-marker { display: none; }
|
||
.ip-group > summary::before { content: '▸'; color: var(--kole-color-text-secondary); font-size: 12px; }
|
||
.ip-group[open] > summary::before { content: '▾'; }
|
||
.ip-group .badge {
|
||
font-family: var(--kole-font-family-num); font-size: 12px; font-weight: 400;
|
||
padding: 1px 7px; border-radius: var(--kole-radius-small);
|
||
background: var(--kole-color-table-header-bg); color: var(--kole-color-text-secondary);
|
||
}
|
||
|
||
/* ---------- 网格 ---------- */
|
||
.ip-grid {
|
||
display: grid; gap: 8px;
|
||
grid-template-columns: repeat(auto-fill, minmax(132px, 1fr));
|
||
padding: 4px 0 8px;
|
||
}
|
||
.ip-cell {
|
||
display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px;
|
||
min-height: 96px; padding: 12px 8px;
|
||
background: var(--kole-color-card-bg);
|
||
border: 1px solid var(--kole-color-border);
|
||
border-radius: var(--kole-radius-medium);
|
||
cursor: pointer; text-align: center;
|
||
font: inherit; color: inherit;
|
||
}
|
||
.ip-cell:hover { border-color: var(--kole-color-brand); background: var(--kole-color-brand-bg); }
|
||
.ip-cell:focus-visible { outline: 2px solid var(--kole-color-brand); outline-offset: 2px; }
|
||
.ip-cell .glyph { display: flex; align-items: center; justify-content: center; color: var(--kole-color-text-title); }
|
||
.ip-cell .glyph svg { width: 1em; height: 1em; display: block; }
|
||
.ip-cell .label {
|
||
font-size: 11px; line-height: 1.35; color: var(--kole-color-text-secondary);
|
||
word-break: break-all; font-family: var(--kole-font-family-num);
|
||
}
|
||
.ip-cell.is-copied { border-color: var(--kole-color-success); background: color-mix(in srgb, var(--kole-color-success) 8%, var(--kole-color-card-bg)); }
|
||
.ip-cell.is-copied .label { color: var(--kole-color-success); }
|
||
|
||
.ip-empty { padding: 40px 16px; text-align: center; color: var(--kole-color-text-secondary); font-size: 14px; }
|
||
|
||
/* 提示条:复制反馈(同 playground 的做法,不引第三方 toast) */
|
||
.ip-toast {
|
||
position: fixed; inset-inline: 0; bottom: 24px; margin: 0 auto; width: fit-content; max-width: 90vw;
|
||
padding: 9px 16px; border-radius: var(--kole-radius-base);
|
||
background: var(--kole-color-text-title); color: var(--kole-color-text-inverse);
|
||
font-size: 13px; opacity: 0; transition: opacity .18s; pointer-events: none; z-index: 50;
|
||
}
|
||
.ip-toast.show { opacity: 1; }
|
||
|
||
.ip-note {
|
||
margin: 20px 0 0; padding: 12px 16px; font-size: 12px; line-height: 1.7;
|
||
color: var(--kole-color-text-secondary);
|
||
background: var(--kole-color-table-header-bg);
|
||
border-radius: var(--kole-radius-base);
|
||
}
|
||
.ip-note code { font-family: var(--kole-font-family-num); }
|
||
|
||
@media (prefers-reduced-motion: reduce) { .ip-toast { transition: none; } }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="ip-shell">
|
||
<div class="ip-head">
|
||
<h1>图标预览</h1>
|
||
<a class="back" href="./">← 返回文档站</a>
|
||
</div>
|
||
<p class="ip-sub">2574 个图标 · 点击任意图标复制名称(Shift + 点击复制内联 SVG 代码)</p>
|
||
|
||
<div class="ip-toolbar">
|
||
<label class="ip-field">
|
||
搜索
|
||
<input id="ip-q" class="ip-input" type="search" placeholder="图标名,如 search / 箭头 / chart" autocomplete="off">
|
||
</label>
|
||
<div class="ip-sizes" role="group" aria-label="图标大小">
|
||
<button class="ip-size" type="button" data-size="16" aria-pressed="false">16</button>
|
||
<button class="ip-size" type="button" data-size="20" aria-pressed="true">20</button>
|
||
<button class="ip-size" type="button" data-size="24" aria-pressed="false">24</button>
|
||
<button class="ip-size" type="button" data-size="32" aria-pressed="false">32</button>
|
||
</div>
|
||
<label class="ip-toggle"><input id="ip-stroke-only" type="checkbox"> 仅线性</label>
|
||
<span id="ip-count" class="ip-count">—</span>
|
||
</div>
|
||
|
||
<div id="ip-groups"></div>
|
||
<p id="ip-empty" class="ip-empty" hidden>没有匹配的图标。试试更短的关键词,或清空搜索。</p>
|
||
|
||
<p class="ip-note">
|
||
图标名直接用在组件上:<code><KoleIcon name="search" /></code>(PC)与
|
||
<code><Icon name="search" /></code>(移动端)。<br>
|
||
数据真源 <code>.design_library/kole-ui/icons/</code>;来源与许可见
|
||
<code>ATTRIBUTION.md</code>(TDesign + Element Plus,MIT)。描边宽度统一 1.5px。
|
||
</p>
|
||
</div>
|
||
|
||
<div id="ip-toast" class="ip-toast" role="status" aria-live="polite"></div>
|
||
|
||
<script>
|
||
/* 图标预览页 —— 自包含实现(零依赖、无外部脚本),同 playground.html 的取向。
|
||
*
|
||
* 数据分两段加载:
|
||
* 1) icons.json(51KB)—— 名字 / 层 / 分组,首屏立即出网格骨架;
|
||
* 2) registry.json(670KB)—— 路径数据,按需装载,装载前网格显示占位。
|
||
* 为什么不合并:合并后首屏要等 670KB,而用户往往只搜几个图标。
|
||
*/
|
||
(function () {
|
||
var SIZE = 20;
|
||
var DATA = null; /* registry:{ icons: { name: 'S…|F…' } } */
|
||
var INDEX = null; /* icons.json:{ names:[], tier:[], group:[], groups:[] } */
|
||
var GROUPS = []; /* 分组名,来自 icons.json */
|
||
|
||
function $(id) { return document.getElementById(id); }
|
||
|
||
/* ---------- 渲染 ---------- */
|
||
function viewBoxOf(name, raw) {
|
||
var vb = (DATA && DATA.viewBox) || {};
|
||
/* 例外表按 canonical 名给;别名与 canonical 路径串相同,按路径反查覆盖别名 */
|
||
if (vb[name]) return vb[name];
|
||
if (raw && DATA && DATA.byPath && DATA.byPath[raw]) return DATA.byPath[raw];
|
||
return '0 0 24 24';
|
||
}
|
||
|
||
function svgFor(name, sizePx) {
|
||
var raw = DATA.icons[name];
|
||
if (!raw) return null;
|
||
var sw = (DATA && DATA.strokeWidth) || 1.5;
|
||
var segs = raw.split('|');
|
||
var body = '';
|
||
for (var i = 0; i < segs.length; i++) {
|
||
var seg = segs[i];
|
||
var mode = seg.charAt(0) === 'S' ? 'stroke' : 'fill';
|
||
var d = seg.slice(1);
|
||
body += mode === 'stroke'
|
||
? '<path d="' + d + '" fill="none" stroke="currentColor" stroke-width="' + sw + '" stroke-linecap="round" stroke-linejoin="round"/>'
|
||
: '<path d="' + d + '" fill="currentColor"/>';
|
||
}
|
||
return '<svg viewBox="' + viewBoxOf(name, raw) + '" width="' + sizePx + '" height="' + sizePx +
|
||
'" aria-hidden="true" focusable="false">' + body + '</svg>';
|
||
}
|
||
|
||
function buildByPath() {
|
||
/* 路径串 → viewBox 反查表(覆盖别名) */
|
||
var vb = (DATA && DATA.viewBox) || {};
|
||
var icons = DATA.icons;
|
||
var out = {};
|
||
for (var k in vb) {
|
||
if (icons[k] && !out[icons[k]]) out[icons[k]] = vb[k];
|
||
}
|
||
DATA.byPath = out;
|
||
}
|
||
|
||
function matches(name, q) {
|
||
if (!q) return true;
|
||
return name.toLowerCase().indexOf(q) >= 0;
|
||
}
|
||
|
||
/* "仅线性"判据:该图标没有任何填充段 */
|
||
function isStrokeOnly(name) {
|
||
var raw = DATA && DATA.icons[name];
|
||
if (!raw) return true;
|
||
return raw.split('|').every(function (s) { return s.charAt(0) === 'S'; });
|
||
}
|
||
|
||
function render() {
|
||
var q = ($('ip-q').value || '').trim().toLowerCase();
|
||
var strokeOnly = $('ip-stroke-only').checked;
|
||
var host = $('ip-groups');
|
||
host.textContent = '';
|
||
|
||
if (!DATA) {
|
||
/* 路径数据还没到:先用索引渲染名字(图标位留空),避免白屏 */
|
||
var pending = indexNames().filter(function (n) { return matches(n, q); });
|
||
$('ip-count').textContent = pending.length + ' / ' + indexNames().length + '(加载图标数据…)';
|
||
return;
|
||
}
|
||
|
||
var total = 0;
|
||
var shown = 0;
|
||
GROUPS.forEach(function (g, gi) {
|
||
var names = [];
|
||
for (var i = 0; i < INDEX.names.length; i++) {
|
||
if (INDEX.group[i] !== gi) continue;
|
||
total++;
|
||
var n = INDEX.names[i];
|
||
if (!matches(n, q)) continue;
|
||
if (strokeOnly && !isStrokeOnly(n)) continue;
|
||
names.push(n);
|
||
}
|
||
if (!names.length) return;
|
||
shown += names.length;
|
||
|
||
var d = document.createElement('details');
|
||
/* 搜索时展开全部(否则用户搜到了却看不见);无搜索词时只展开前两组 */
|
||
d.className = 'ip-group';
|
||
d.open = !!q || gi < 2;
|
||
|
||
var sum = document.createElement('summary');
|
||
sum.innerHTML = g + ' <span class="badge">' + names.length + '</span>';
|
||
d.appendChild(sum);
|
||
|
||
var grid = document.createElement('div');
|
||
grid.className = 'ip-grid';
|
||
names.forEach(function (name) {
|
||
var cell = document.createElement('button');
|
||
cell.type = 'button';
|
||
cell.className = 'ip-cell';
|
||
cell.setAttribute('data-name', name);
|
||
cell.setAttribute('aria-label', '图标 ' + name + ',点击复制名称');
|
||
var glyph = document.createElement('span');
|
||
glyph.className = 'glyph';
|
||
glyph.innerHTML = svgFor(name, SIZE) || '';
|
||
var lab = document.createElement('span');
|
||
lab.className = 'label';
|
||
lab.textContent = name;
|
||
cell.appendChild(glyph);
|
||
cell.appendChild(lab);
|
||
grid.appendChild(cell);
|
||
});
|
||
d.appendChild(grid);
|
||
host.appendChild(d);
|
||
});
|
||
|
||
$('ip-count').textContent = shown + ' / ' + total;
|
||
$('ip-empty').hidden = shown > 0;
|
||
}
|
||
|
||
function indexNames() { return (INDEX && INDEX.names) || []; }
|
||
|
||
/* ---------- 复制 ---------- */
|
||
var toastTimer = null;
|
||
function toast(msg) {
|
||
var t = $('ip-toast');
|
||
t.textContent = msg;
|
||
t.classList.add('show');
|
||
if (toastTimer) clearTimeout(toastTimer);
|
||
toastTimer = setTimeout(function () { t.classList.remove('show'); }, 1600);
|
||
}
|
||
|
||
function copyText(text) {
|
||
/* navigator.clipboard 需要安全上下文(https / localhost);http 局域网下回退 execCommand */
|
||
if (navigator.clipboard && window.isSecureContext) {
|
||
return navigator.clipboard.writeText(text);
|
||
}
|
||
return new Promise(function (resolve, reject) {
|
||
var ta = document.createElement('textarea');
|
||
ta.value = text;
|
||
ta.setAttribute('readonly', '');
|
||
ta.style.position = 'fixed';
|
||
ta.style.opacity = '0';
|
||
document.body.appendChild(ta);
|
||
ta.select();
|
||
try { document.execCommand('copy') ? resolve() : reject(new Error('execCommand failed')); }
|
||
catch (e) { reject(e); }
|
||
finally { document.body.removeChild(ta); }
|
||
});
|
||
}
|
||
|
||
function flash(cell) {
|
||
cell.classList.add('is-copied');
|
||
setTimeout(function () { cell.classList.remove('is-copied'); }, 700);
|
||
}
|
||
|
||
/* ---------- 事件 ---------- */
|
||
$('ip-groups').addEventListener('click', function (e) {
|
||
var cell = e.target.closest('.ip-cell');
|
||
if (!cell) return;
|
||
var name = cell.getAttribute('data-name');
|
||
/* Shift + 点击复制内联 SVG 代码(规范要求「复制名称或代码」) */
|
||
var payload = e.shiftKey ? (svgFor(name, SIZE) || name) : name;
|
||
copyText(payload).then(function () {
|
||
flash(cell);
|
||
toast(e.shiftKey ? '已复制 SVG:' + name : '已复制名称:' + name);
|
||
}).catch(function () {
|
||
toast('复制失败,请手动选中:' + name);
|
||
});
|
||
});
|
||
|
||
$('ip-q').addEventListener('input', render);
|
||
$('ip-stroke-only').addEventListener('change', render);
|
||
|
||
Array.prototype.forEach.call(document.querySelectorAll('.ip-size'), function (btn) {
|
||
btn.addEventListener('click', function () {
|
||
SIZE = Number(btn.getAttribute('data-size'));
|
||
Array.prototype.forEach.call(document.querySelectorAll('.ip-size'), function (b) {
|
||
b.setAttribute('aria-pressed', b === btn ? 'true' : 'false');
|
||
});
|
||
Array.prototype.forEach.call(document.querySelectorAll('.ip-cell'), function (c) {
|
||
var g = c.querySelector('.glyph');
|
||
if (!g) return;
|
||
var svg = g.querySelector('svg');
|
||
if (svg) { svg.setAttribute('width', SIZE); svg.setAttribute('height', SIZE); }
|
||
});
|
||
});
|
||
});
|
||
|
||
/* ---------- 装载 ---------- */
|
||
function loadIndex() {
|
||
return fetch('icons.json').then(function (r) { return r.ok ? r.json() : null; });
|
||
}
|
||
function loadRegistry() {
|
||
return fetch('../.design_library/kole-ui/icons/registry.json').then(function (r) {
|
||
return r.ok ? r.json() : null;
|
||
});
|
||
}
|
||
|
||
loadIndex().then(function (idx) {
|
||
if (!idx) { $('ip-count').textContent = '索引加载失败'; return; }
|
||
INDEX = idx;
|
||
GROUPS = idx.groups || [];
|
||
render();
|
||
return loadRegistry().then(function (reg) {
|
||
if (!reg) { $('ip-count').textContent = '图标数据加载失败'; return; }
|
||
/* registry.json 的键是 icons(与 build-icons.mjs 产物一致) */
|
||
DATA = { icons: reg.icons || {}, viewBox: reg.viewBox || {}, strokeWidth: reg.strokeWidth || 1.5 };
|
||
if (!Object.keys(DATA.viewBox).length) {
|
||
/* viewBox 例外表在 registry 里可能只含非默认项;缺失时按 24 网格渲染 */
|
||
}
|
||
buildByPath();
|
||
render();
|
||
});
|
||
}).catch(function () {
|
||
$('ip-count').textContent = '加载失败';
|
||
});
|
||
}());
|
||
</script>
|
||
</body>
|
||
</html>
|