contactsheet 0.1.2 → 0.1.3

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/README.md CHANGED
@@ -160,6 +160,11 @@ open(橙) ──标记完成──▶ resolved(绿·待核验) ──核验通
160
160
  ## 左侧列表与布局
161
161
 
162
162
  左侧是图层列表:**上半是页面**(显示各自的真实路由,点一下聚焦过去),**下半是组件**(按文件分组)。
163
+
164
+ **页面画板默认全部收起**,从列表按需打开。一块页面画板 = 一个完整 app 实例的 iframe
165
+ (React、provider、realtime 全套),二十几块同时挂载会把 dev server 和浏览器一起拖垮。
166
+ 收起的画板完全不发请求,打开那一刻才挂载;你开过/关过谁会被记住,新出现的页面画板才默认收起。
167
+ 组件画板轻得多,默认全部显示。
163
168
  顶部搜索框按名字/文件/路由过滤;每行右边的圆点是显隐开关——隐藏只是从墙上撤下,随时点回来。
164
169
  分区标题(「页面」「组件」)和每个文件组都能**折叠**;标题行右侧的三态圆点是**整组一键显隐**
165
170
  (`●` 全显示 / `◐` 部分 / `◌` 全隐藏,点一下在「全显示」和「全隐藏」之间切)。折叠状态按项目记住。
@@ -223,10 +228,36 @@ iframe 的画布永远是不透明的,所以组件画板必然有一层底。
223
228
  - **token 存在 localStorage / sessionStorage 里的项目要多登一次**:storage 按源隔离,`:5199` 是另一个源。
224
229
  在 `:5199` 下把你的登录流程走一遍(页面画板正好可以直接开 `/login`),之后就一直有了。
225
230
 
231
+ ## 接入方式:已有项目 / 新项目
232
+
233
+ **已有项目**(先把看得见的上墙,再逐步补数据):
234
+
235
+ 1. `npx contactsheet init` —— 组件骨架画板自动铺出来;
236
+ 2. 公开页面(登录页、文档页)直接写进 `design/screens.artboard.ts`,立即可看;
237
+ 3. 受登录保护的页面:在 dev server 的端口登一次测试账号即可(cookie 不分端口,画布共享登录态);
238
+ 4. 有**服务端权限守卫**的页面(管理后台这类):mock 救不了它——守卫跑在服务端,
239
+ 唯一正解是给测试账号相应权限(dev seed 提权)。这是项目侧的一次性工作;
240
+ 5. 带参数的路由(`/t/[teamId]`、`/projects/[id]`):让项目提供一组**重置后不变的 demo id**
241
+ (固定 slug 的种子数据),画板 url 里直接写死。
242
+
243
+ 页面多了按区拆文件:`screens-public.artboard.ts` / `screens-admin.artboard.ts` / `screens-team.artboard.ts`,
244
+ 侧栏会按文件分组。`design/` 整个提交进 git——画板即文档,团队共享。
245
+
246
+ **新项目**(design-first,从第一天就把回路建起来):
247
+
248
+ 1. 写组件前先写 artboard —— 画板就是组件的规格(要哪些状态,一个 export 一个);
249
+ 2. 每建一条路由,顺手在 screens 文件里加一行,路由和画板同步生长;
250
+ 3. seed 脚本第一天就准备**两个账号**:一个永远空(审空态),一个数据丰富(审满态)——
251
+ 这比任何 mock 层都便宜,而且看到的就是真实渲染路径。
252
+
253
+ **为什么没有 mock 模式**:页面画板渲染的是你 dev server 的真实输出。服务端组件在服务端取数、
254
+ 守卫在服务端判权,浏览器侧的 mock 层拦不到它们;拦到了,你看的也不再是用户会看到的东西。
255
+ 数据问题在数据侧解决(种子、测试账号、demo id),画布只负责让你同时看见。
256
+
226
257
  ## 边界(先说清楚,省得你试)
227
258
 
228
- - **没有 mock 层**。画板拿到的数据就是你 dev 环境里的真数据。「空态 / 错误态 / 加载中」这类需要拦网络的场景,
229
- v1 只能靠你自己在组件层传 props 摆出来;内置 mocks 排在 v1.1。
259
+ - **没有 mock 层,也不打算做**(理由见上一节)。组件的「空态 / 错误态 / 加载中」在组件层传 props 摆出来;
260
+ 页面的空态/满态用两个种子账号解决。
230
261
  - **走查模式很勉强**。它就是把一块画板放大居中,方便盯细节;要走完整流程(多页跳转、真实滚动、devtools),
231
262
  请照常开浏览器访问 `:3000`。
232
263
  - **它不是开发环境,是一扇窗**。构建、测试、调试照旧在你原来的地方做。contactsheet 只负责「同时看见很多状态」
@@ -1539,6 +1539,7 @@ ${board.entry.kind} \xB7 ${board.entry.id}`);
1539
1539
  if (state.scale < MOUNT_MIN_SCALE) continue;
1540
1540
  const id = r.target.dataset.id;
1541
1541
  const board = id ? state.boards.get(id) : null;
1542
+ if (board?.el.hidden) continue;
1542
1543
  if (board) requestMount(board);
1543
1544
  io.unobserve(r.target);
1544
1545
  }
@@ -1568,6 +1569,7 @@ ${board.entry.kind} \xB7 ${board.entry.id}`);
1568
1569
  heightWatchers.get(id)?.disconnect();
1569
1570
  heightWatchers.delete(id);
1570
1571
  }
1572
+ const defaultHid = markSeenAndDefaultHide(entries);
1571
1573
  for (const [file, list] of byFile) {
1572
1574
  const ids = list.map((e) => e.id);
1573
1575
  const kept = (state.order.get(file) ?? []).filter((id) => ids.includes(id));
@@ -1587,6 +1589,7 @@ ${board.entry.kind} \xB7 ${board.entry.id}`);
1587
1589
  applyPositions();
1588
1590
  if (n && !firstSync) toast(`\u65B0\u753B\u677F ${n} \u5757 \xB7 \u5DF2\u653E\u5230\u5899\u5E95\u90E8`, "info");
1589
1591
  }
1592
+ if (defaultHid) toast("\u9875\u9762\u753B\u677F\u9ED8\u8BA4\u6536\u8D77(\u6574\u9875 iframe \u592A\u91CD),\u5728\u5DE6\u4FA7\u300C\u9875\u9762\u300D\u5206\u533A\u6309\u9700\u6253\u5F00", "info");
1590
1593
  if (state.boards.size) firstSync = false;
1591
1594
  renderSidebar();
1592
1595
  }
@@ -1647,6 +1650,7 @@ ${board.entry.kind} \xB7 ${board.entry.id}`);
1647
1650
  }
1648
1651
  attachResizeHandles(board);
1649
1652
  attachBoardDrag(board);
1653
+ el.hidden = isHidden(entry.id);
1650
1654
  applySize(board);
1651
1655
  updateBoard(board, entry);
1652
1656
  io.observe(el);
@@ -1729,6 +1733,34 @@ ${board.entry.kind} \xB7 ${board.entry.id}`);
1729
1733
  function hiddenKey() {
1730
1734
  return `cs-hidden:${state.info?.projectRoot ?? "unknown"}`;
1731
1735
  }
1736
+ function seenKey() {
1737
+ return `cs-seen:${state.info?.projectRoot ?? "unknown"}`;
1738
+ }
1739
+ function markSeenAndDefaultHide(entries) {
1740
+ let seen;
1741
+ try {
1742
+ seen = new Set(JSON.parse(localStorage.getItem(seenKey()) ?? "[]"));
1743
+ } catch {
1744
+ seen = /* @__PURE__ */ new Set();
1745
+ }
1746
+ const fresh = entries.filter((e) => !seen.has(e.id));
1747
+ if (fresh.length === 0) return false;
1748
+ const hid = hiddenSet();
1749
+ let hidSomething = false;
1750
+ for (const e of fresh) {
1751
+ seen.add(e.id);
1752
+ if (e.kind === "screen") {
1753
+ hid.add(e.id);
1754
+ hidSomething = true;
1755
+ }
1756
+ }
1757
+ try {
1758
+ localStorage.setItem(seenKey(), JSON.stringify([...seen]));
1759
+ if (hidSomething) localStorage.setItem(hiddenKey(), JSON.stringify([...hid]));
1760
+ } catch {
1761
+ }
1762
+ return hidSomething;
1763
+ }
1732
1764
  function hiddenSet() {
1733
1765
  try {
1734
1766
  return new Set(JSON.parse(localStorage.getItem(hiddenKey()) ?? "[]"));
@@ -1979,6 +2011,7 @@ ${board.entry.kind} \xB7 ${board.entry.id}`);
1979
2011
  }
1980
2012
  }
1981
2013
  function requestMount(board) {
2014
+ if (board.el.hidden) return;
1982
2015
  if (board.iframe || mountQueue.includes(board)) return;
1983
2016
  if (loadingCount >= MAX_LOADING) {
1984
2017
  mountQueue.push(board);