@musnows/scriverse 0.4.0 → 0.4.1

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.
@@ -1,5 +1,6 @@
1
1
  const fieldLabels = Object.freeze({
2
2
  name: "标准名",
3
+ code: "编号",
3
4
  aliases: "别名",
4
5
  raceId: "种族",
5
6
  species: "种族",
@@ -15,7 +16,8 @@ const fieldLabels = Object.freeze({
15
16
  export function describeCharacterVersionChanges(snapshot, previousSnapshot) {
16
17
  if (!previousSnapshot) return ["建立人物档案"];
17
18
  return [...new Set(Object.entries(fieldLabels).filter(([key]) => (
18
- JSON.stringify(snapshot?.[key] ?? null) !== JSON.stringify(previousSnapshot?.[key] ?? null)
19
+ JSON.stringify(key === "code" ? String(snapshot?.[key] ?? "") : (snapshot?.[key] ?? null))
20
+ !== JSON.stringify(key === "code" ? String(previousSnapshot?.[key] ?? "") : (previousSnapshot?.[key] ?? null))
19
21
  )).map(([, label]) => label))];
20
22
  }
21
23
 
@@ -10,7 +10,7 @@
10
10
  <link rel="icon" href="/icon.svg?v=20260712" type="image/svg+xml">
11
11
  <link rel="manifest" href="/site.webmanifest">
12
12
  <link rel="stylesheet" href="/vendor/vditor/dist/index.css?v=3.11.2">
13
- <link rel="stylesheet" href="/styles.css?v=20260723-open-prs-6">
13
+ <link rel="stylesheet" href="/styles.css?v=20260724-whitespace-access">
14
14
  </head>
15
15
  <body class="auth-pending">
16
16
  <section id="auth-view" class="auth-view hidden" aria-labelledby="auth-title">
@@ -75,7 +75,7 @@
75
75
  <button id="account-settings-button" type="button" role="menuitem">账户设置</button>
76
76
  <button id="logout-button" type="button" role="menuitem">退出登录</button>
77
77
  </div>
78
- <button id="top-search-button" class="topbar-icon-button" type="button" aria-label="全文检索" title="全文检索" disabled>
78
+ <button id="top-search-button" class="topbar-icon-button" type="button" aria-label="全文检索" title="全文检索(Command/Ctrl+F)" disabled>
79
79
  <svg class="topbar-icon" viewBox="0 0 24 24" aria-hidden="true"><circle cx="11" cy="11" r="6.5"/><path d="m16.2 16.2 4.3 4.3"/></svg>
80
80
  </button>
81
81
  <button id="theme-toggle" class="theme-toggle" type="button" aria-label="切换到黑夜模式" title="切换到黑夜模式" aria-pressed="false">
@@ -180,7 +180,6 @@
180
180
  <button id="insight-button" class="ghost-button" type="button">章节概览</button>
181
181
  <button id="versions-button" class="ghost-button" type="button">版本</button>
182
182
  <button id="tidy-blank-lines-button" class="ghost-button" type="button">整理空行</button>
183
- <button id="toggle-whitespace-button" class="ghost-button" type="button" aria-pressed="true" title="用点标记半角空格,用方框标记全角空格,用箭头标记 Tab">隐藏空白符</button>
184
183
  <button id="save-button" class="primary-button" type="button">保存正文</button>
185
184
  </div>
186
185
  </div>
@@ -202,7 +201,9 @@
202
201
  <h1 id="module-title">模块</h1>
203
202
  <p id="module-description"></p>
204
203
  </div>
205
- <button id="module-create-button" class="primary-button" type="button">新建</button>
204
+ <div id="module-header-actions" class="module-header-actions">
205
+ <button id="module-create-button" class="primary-button" type="button">新建</button>
206
+ </div>
206
207
  </div>
207
208
  <div id="module-content" class="module-content"></div>
208
209
  </section>
@@ -364,6 +365,10 @@
364
365
  <button id="setting-editor-submit" class="primary-button" type="submit">保存设定</button>
365
366
  </header>
366
367
  <div class="setting-editor-workspace">
368
+ <section id="setting-editor-management" class="entity-editor-management hidden" aria-label="设定档案操作">
369
+ <div><strong>档案操作</strong><small>版本历史和高风险操作集中在编辑面板内。</small></div>
370
+ <div class="entity-editor-management-actions"><button id="setting-editor-confirm" class="ghost-button hidden" type="button">确认候选</button><button id="setting-editor-deprecate" class="ghost-button hidden" type="button">弃用</button><button id="setting-editor-history" class="ghost-button" type="button">版本历史</button><button id="setting-editor-delete" class="danger-button" type="button">删除设定</button></div>
371
+ </section>
367
372
  <main class="setting-editor-content">
368
373
  <div class="setting-markdown-field" data-vditor-editor-field>
369
374
  <div id="setting-editor-markdown" class="vditor-editor-host" data-vditor-editor aria-label="Markdown 编辑器"></div>
@@ -382,6 +387,8 @@
382
387
  </div>
383
388
  <div class="character-editor-header-actions">
384
389
  <button id="character-history-button" class="ghost-button" type="button" aria-controls="character-history-panel" aria-expanded="false">版本历史</button>
390
+ <button id="character-merge-button" class="ghost-button hidden" type="button">合并角色</button>
391
+ <button id="character-delete-button" class="danger-button hidden" type="button">删除角色</button>
385
392
  </div>
386
393
  </header>
387
394
  <div class="character-editor-workspace">
@@ -411,7 +418,12 @@
411
418
  <span id="knowledge-editor-eyebrow" class="eyebrow">世界档案</span>
412
419
  <div class="character-editor-title-row"><h1 id="knowledge-editor-title">新建档案</h1><span id="knowledge-editor-version" class="character-version-badge">新档案</span></div>
413
420
  </div>
414
- <div class="character-editor-header-actions"><span id="knowledge-editor-header-note" class="knowledge-editor-header-note">独立资料页</span></div>
421
+ <div class="character-editor-header-actions">
422
+ <span id="knowledge-editor-header-note" class="knowledge-editor-header-note">独立资料页</span>
423
+ <button id="knowledge-editor-history" class="ghost-button hidden" type="button">版本历史</button>
424
+ <button id="knowledge-editor-merge" class="ghost-button hidden" type="button">合并档案</button>
425
+ <button id="knowledge-editor-delete" class="danger-button hidden" type="button">删除档案</button>
426
+ </div>
415
427
  </header>
416
428
  <div class="character-editor-workspace knowledge-editor-workspace">
417
429
  <nav id="knowledge-editor-nav" class="character-editor-nav" role="tablist" aria-label="世界档案编辑分区"></nav>
@@ -488,6 +500,10 @@
488
500
  <option value="relaxed">宽松</option>
489
501
  </select>
490
502
  </label>
503
+ <section class="work-access-field appearance-whitespace-field" aria-labelledby="appearance-whitespace-title">
504
+ <div><strong id="appearance-whitespace-title">正文空白符</strong><small>即时显示或隐藏空格、全角空格和 Tab 的可视标记。</small></div>
505
+ <button id="toggle-whitespace-appearance" class="ghost-button" data-toggle-whitespace type="button" aria-pressed="true">隐藏空白符</button>
506
+ </section>
491
507
  <div id="font-preview" class="font-preview" aria-live="polite">
492
508
  <strong>字体预览 Typography 0123</strong>
493
509
  <span>中文默认使用黑体,English letters always use monospace.</span>
@@ -654,6 +670,6 @@
654
670
  <div id="auth-loading" class="auth-loading" role="status" aria-label="正在载入工作台"></div>
655
671
  <script id="vditorIconScript" src="/vendor/vditor/dist/js/icons/ant.js?v=3.11.2"></script>
656
672
  <script src="/vendor/vditor/dist/index.min.js?v=3.11.2"></script>
657
- <script type="module" src="/app.js?v=20260723-character-save-hint"></script>
673
+ <script type="module" src="/app.js?v=20260724-whitespace-access"></script>
658
674
  </body>
659
675
  </html>
@@ -0,0 +1,9 @@
1
+ export type KeyboardShortcutEvent = {
2
+ key?: unknown;
3
+ metaKey?: boolean;
4
+ ctrlKey?: boolean;
5
+ altKey?: boolean;
6
+ shiftKey?: boolean;
7
+ };
8
+
9
+ export function isGlobalSearchShortcut(event?: KeyboardShortcutEvent | null): boolean;
@@ -0,0 +1,4 @@
1
+ export function isGlobalSearchShortcut(event) {
2
+ if (!event || String(event.key ?? "").toLowerCase() !== "f") return false;
3
+ return (Boolean(event.metaKey) || Boolean(event.ctrlKey)) && !event.altKey && !event.shiftKey;
4
+ }
@@ -23,6 +23,16 @@ const NETWORK_LAYOUTS = Object.freeze({
23
23
  });
24
24
  const RELATIONSHIP_EDGE_GAP = 24;
25
25
  let relationshipRendererSequence = 0;
26
+
27
+ export function getRelationshipNetworkInitialScale(nodeCount, expanded = false) {
28
+ if (expanded) return 1;
29
+ const count = Math.max(0, Number(nodeCount) || 0);
30
+ if (count > 180) return 1.35;
31
+ if (count > 120) return 1.25;
32
+ if (count > 80) return 1.16;
33
+ return 1;
34
+ }
35
+
26
36
  const GALAXY_CELESTIAL_PALETTES = Object.freeze([
27
37
  Object.freeze({ key: "solar", hue: 42, saturation: 96, lightness: 68, color: "#ffc95f", core: "#fff8d4", rim: "#9f3c18", atmosphere: "rgba(255,184,72,.58)", ring: "rgba(255,222,151,.72)" }),
28
38
  Object.freeze({ key: "azure", hue: 211, saturation: 94, lightness: 68, color: "#61b8ff", core: "#effaff", rim: "#173b85", atmosphere: "rgba(79,156,255,.56)", ring: "rgba(164,214,255,.68)" }),
@@ -296,6 +306,44 @@ export function buildRelationshipGraph(characters, relationships) {
296
306
  return { nodes, edges, nodeById, warnings, stats: { nodeCount: nodes.length, edgeCount: edges.length, maxDegree } };
297
307
  }
298
308
 
309
+ function separateOverlappingNetworkNodes(nodes, layout) {
310
+ const iterations = nodes.length > 150 ? 44 : 24;
311
+ const padding = nodes.length > 120 ? 12 : 9;
312
+ for (let iteration = 0; iteration < iterations; iteration += 1) {
313
+ let overlapCount = 0;
314
+ for (let leftIndex = 0; leftIndex < nodes.length; leftIndex += 1) {
315
+ const left = nodes[leftIndex];
316
+ for (let rightIndex = leftIndex + 1; rightIndex < nodes.length; rightIndex += 1) {
317
+ const right = nodes[rightIndex];
318
+ let dx = right.x - left.x;
319
+ let dy = right.y - left.y;
320
+ let distance = Math.hypot(dx, dy);
321
+ if (distance < 0.001) {
322
+ const angle = (hashString(`${left.id}:${right.id}`) / 4294967296) * Math.PI * 2;
323
+ dx = Math.cos(angle);
324
+ dy = Math.sin(angle);
325
+ distance = 1;
326
+ }
327
+ const minimumDistance = left.radius + right.radius + padding;
328
+ if (distance >= minimumDistance) continue;
329
+ overlapCount += 1;
330
+ const correction = (minimumDistance - distance) * 0.52;
331
+ const nx = dx / distance;
332
+ const ny = dy / distance;
333
+ left.x -= nx * correction;
334
+ left.y -= ny * correction;
335
+ right.x += nx * correction;
336
+ right.y += ny * correction;
337
+ }
338
+ }
339
+ for (const node of nodes) {
340
+ node.x = clamp(node.x, layout.marginX + node.radius, layout.width - layout.marginX - node.radius);
341
+ node.y = clamp(node.y, layout.marginY + node.radius, layout.height - layout.marginY - node.radius);
342
+ }
343
+ if (!overlapCount) break;
344
+ }
345
+ }
346
+
299
347
  export function layoutRelationshipNetwork(graph, seed = "relationship-network", options = {}) {
300
348
  const layout = options.expanded ? NETWORK_LAYOUTS.expanded : NETWORK_LAYOUTS.standard;
301
349
  const random = seededRandom(hashString(`${seed}:${graph.nodes.map((node) => node.id).join("|")}`));
@@ -390,6 +438,7 @@ export function layoutRelationshipNetwork(graph, seed = "relationship-network",
390
438
  node.x = layout.marginX + (node.x - minX) / width * (layout.width - layout.marginX * 2);
391
439
  node.y = layout.marginY + (node.y - minY) / height * (layout.height - layout.marginY * 2);
392
440
  }
441
+ separateOverlappingNetworkNodes(nodes, layout);
393
442
  }
394
443
  return { nodes, byId, width: layout.width, height: layout.height };
395
444
  }
@@ -694,7 +743,7 @@ export function renderRelationshipMindMap(container, graph, options = {}) {
694
743
  let selectedId = null;
695
744
  let selectedEdgeId = null;
696
745
  let hoveredId = null;
697
- let viewScale = 1;
746
+ let viewScale = getRelationshipNetworkInitialScale(graph.nodes.length, options.expanded);
698
747
  let viewX = 0;
699
748
  let viewY = 0;
700
749
  let animationFrame = null;
@@ -880,6 +929,7 @@ export function renderRelationshipMindMap(container, graph, options = {}) {
880
929
  stage.append(svg);
881
930
 
882
931
  const nodeElements = new Map();
932
+ const defaultLabelLimit = graph.nodes.length > 180 ? 36 : graph.nodes.length > 120 ? 48 : graph.nodes.length > 80 ? 64 : graph.nodes.length;
883
933
  const updateNodePosition = (nodeId) => {
884
934
  const position = positions.get(nodeId);
885
935
  const button = nodeElements.get(nodeId);
@@ -1201,6 +1251,7 @@ export function renderRelationshipMindMap(container, graph, options = {}) {
1201
1251
  button.className = `mind-node network-node obsidian-node${node.locked ? " is-locked" : ""}${node.degree === 0 ? " is-isolated" : ""}`;
1202
1252
  button.dataset.nodeId = node.id;
1203
1253
  button.dataset.groupKey = node.groupKey || appearance.group.key;
1254
+ button.classList.toggle("is-label-visible", graph.nodes.indexOf(node) < defaultLabelLimit || node.degree >= 4);
1204
1255
  button.style.setProperty("--node-size", `${nodeSize}px`);
1205
1256
  button.style.setProperty("--node-color", node.color || appearance.color);
1206
1257
  button.style.setProperty("--node-glow", node.glow || appearance.glow);
@@ -1304,6 +1355,14 @@ export function renderRelationshipMindMap(container, graph, options = {}) {
1304
1355
  viewY = 0;
1305
1356
  updateViewTransform(true);
1306
1357
  };
1358
+ const setDefaultView = (animate = false) => {
1359
+ viewScale = getRelationshipNetworkInitialScale(graph.nodes.length, options.expanded);
1360
+ const width = viewport.clientWidth;
1361
+ const height = viewport.clientHeight;
1362
+ viewX = width ? (width - width * viewScale) / 2 : 0;
1363
+ viewY = height ? (height - height * viewScale) / 2 : 0;
1364
+ updateViewTransform(animate);
1365
+ };
1307
1366
  const animatePositions = (targets, duration = 650) => {
1308
1367
  freezePhysics();
1309
1368
  if (animationFrame) window.cancelAnimationFrame(animationFrame);
@@ -1342,7 +1401,7 @@ export function renderRelationshipMindMap(container, graph, options = {}) {
1342
1401
  hoveredId = null;
1343
1402
  freezePhysics();
1344
1403
  refreshHighlight();
1345
- fitView();
1404
+ setDefaultView(true);
1346
1405
  animatePositions(originalPositions);
1347
1406
  });
1348
1407
 
@@ -1393,6 +1452,7 @@ export function renderRelationshipMindMap(container, graph, options = {}) {
1393
1452
  clearGraphHighlight();
1394
1453
  shell.append(toolbar, viewport);
1395
1454
  container.append(shell);
1455
+ setDefaultView();
1396
1456
  return {
1397
1457
  destroy() {
1398
1458
  destroyed = true;