@kgdiff/viewer 0.1.0 → 0.3.0
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 +44 -3
- package/dist/controller.d.ts +40 -7
- package/dist/controller.d.ts.map +1 -1
- package/dist/controller.js +194 -45
- package/dist/controller.js.map +1 -1
- package/dist/element.d.ts +12 -2
- package/dist/element.d.ts.map +1 -1
- package/dist/element.js +132 -8
- package/dist/element.js.map +1 -1
- package/dist/framing.d.ts +20 -1
- package/dist/framing.d.ts.map +1 -1
- package/dist/framing.js +49 -0
- package/dist/framing.js.map +1 -1
- package/dist/i18n.d.ts +2 -3
- package/dist/i18n.d.ts.map +1 -1
- package/dist/i18n.js +4 -6
- package/dist/i18n.js.map +1 -1
- package/dist/index.d.ts +48 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/prefs.d.ts +7 -1
- package/dist/prefs.d.ts.map +1 -1
- package/dist/prefs.js +15 -0
- package/dist/prefs.js.map +1 -1
- package/dist/standalone/kgdiff-viewer-compute.js +1 -1
- package/dist/standalone/kgdiff-viewer.js +4 -4
- package/dist/styles.d.ts.map +1 -1
- package/dist/styles.js +29 -17
- package/dist/styles.js.map +1 -1
- package/dist/text-diff.d.ts +14 -2
- package/dist/text-diff.d.ts.map +1 -1
- package/dist/text-diff.js +52 -2
- package/dist/text-diff.js.map +1 -1
- package/dist/ui/detail.d.ts.map +1 -1
- package/dist/ui/detail.js +194 -151
- package/dist/ui/detail.js.map +1 -1
- package/dist/ui/stage.d.ts +4 -0
- package/dist/ui/stage.d.ts.map +1 -1
- package/dist/ui/stage.js +30 -4
- package/dist/ui/stage.js.map +1 -1
- package/package.json +6 -6
- package/src/controller.ts +206 -41
- package/src/element.ts +148 -8
- package/src/framing.ts +65 -1
- package/src/i18n.ts +4 -6
- package/src/index.ts +47 -1
- package/src/prefs.ts +26 -1
- package/src/styles.ts +29 -17
- package/src/text-diff.ts +57 -2
- package/src/ui/detail.ts +253 -245
- package/src/ui/stage.ts +44 -3
package/src/framing.ts
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* float over the canvas; a fit or flight must keep its target clear of them without wasting the
|
|
4
4
|
* rest of the canvas. Each obstacle can be avoided through one side (an inset); all assignments
|
|
5
5
|
* are tried (4ⁿ, n ≤ ~6 visible chips) and the one that shows `bounds` largest wins, ties going
|
|
6
|
-
* to the free region closest to the canvas center.
|
|
6
|
+
* to the free region closest to the canvas center. `zoomFit` fits content whose world extent
|
|
7
|
+
* depends on the zoom (labels). Pure; all sizes in canvas px.
|
|
7
8
|
*/
|
|
8
9
|
import type { Rect } from '@kgdiff/core';
|
|
9
10
|
|
|
@@ -139,6 +140,69 @@ export function frameTarget(
|
|
|
139
140
|
return { x: cx - (sx - width / 2) / k, y: cy - (sy - height / 2) / k, k };
|
|
140
141
|
}
|
|
141
142
|
|
|
143
|
+
/** Lowest zoom a fit considers (the renderer's default `minZoom`). */
|
|
144
|
+
const MIN_FIT_ZOOM = 0.05;
|
|
145
|
+
/** Bisection steps for the zoom of a label-aware fit (a fraction of a percent). */
|
|
146
|
+
const FIT_STEPS = 16;
|
|
147
|
+
|
|
148
|
+
/** A fit whose content depends on the zoom it reaches. */
|
|
149
|
+
export interface ZoomFit {
|
|
150
|
+
/** The zoom the fit lands on. */
|
|
151
|
+
readonly k: number;
|
|
152
|
+
/** What it shows (world units). */
|
|
153
|
+
readonly content: Rect;
|
|
154
|
+
/** Bounds for the camera: `content` centred in a rect that a fit lands on at exactly `k`. */
|
|
155
|
+
readonly bounds: Rect;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Fit content whose world extent depends on the zoom, `extent(k)`: labels keep their pixel size
|
|
160
|
+
* (and change sides as they make room for each other), so the further out the camera, the more
|
|
161
|
+
* world they cover. Finds the largest zoom up to `maxK` at which the content fits the canvas
|
|
162
|
+
* minus `insets` at that very zoom, by bisection (what fits only grows on screen as the camera
|
|
163
|
+
* zooms in), and lands exactly there, since the labels could move at another zoom. When not
|
|
164
|
+
* even the smallest zoom has room, `extent(Infinity)` (the discs alone) is fitted plainly.
|
|
165
|
+
*/
|
|
166
|
+
export function zoomFit(
|
|
167
|
+
extent: (k: number) => Rect | null,
|
|
168
|
+
width: number,
|
|
169
|
+
height: number,
|
|
170
|
+
insets: Insets,
|
|
171
|
+
maxK: number,
|
|
172
|
+
): ZoomFit | null {
|
|
173
|
+
const fits = (k: number): boolean => {
|
|
174
|
+
const b = extent(k);
|
|
175
|
+
return b !== null && fitZoom(b, width, height, insets) >= k;
|
|
176
|
+
};
|
|
177
|
+
let k = maxK;
|
|
178
|
+
if (!fits(k)) {
|
|
179
|
+
let lo = Math.min(MIN_FIT_ZOOM, maxK);
|
|
180
|
+
if (!fits(lo)) {
|
|
181
|
+
const discs = extent(Number.POSITIVE_INFINITY);
|
|
182
|
+
if (!discs) return null;
|
|
183
|
+
return { k: fitZoom(discs, width, height, insets), content: discs, bounds: discs };
|
|
184
|
+
}
|
|
185
|
+
let hi = k;
|
|
186
|
+
for (let i = 0; i < FIT_STEPS; i++) {
|
|
187
|
+
const mid = Math.sqrt(lo * hi);
|
|
188
|
+
if (fits(mid)) lo = mid;
|
|
189
|
+
else hi = mid;
|
|
190
|
+
}
|
|
191
|
+
k = lo;
|
|
192
|
+
}
|
|
193
|
+
const content = extent(k);
|
|
194
|
+
if (!content) return null;
|
|
195
|
+
const w = (width - insets.left - insets.right) / k;
|
|
196
|
+
const h = (height - insets.top - insets.bottom) / k;
|
|
197
|
+
const bounds = {
|
|
198
|
+
x: content.x + content.width / 2 - w / 2,
|
|
199
|
+
y: content.y + content.height / 2 - h / 2,
|
|
200
|
+
width: w,
|
|
201
|
+
height: h,
|
|
202
|
+
};
|
|
203
|
+
return { k, content, bounds };
|
|
204
|
+
}
|
|
205
|
+
|
|
142
206
|
/** The part of `view` (world units) not covered by `insets` at zoom `k`. */
|
|
143
207
|
export function freeView(view: Rect, k: number, insets: Insets): Rect {
|
|
144
208
|
return {
|
package/src/i18n.ts
CHANGED
|
@@ -85,6 +85,8 @@ const en = {
|
|
|
85
85
|
'detail.emptyHint': 'Use j / k to step through changes.',
|
|
86
86
|
'detail.prev': 'Previous change',
|
|
87
87
|
'detail.next': 'Next change',
|
|
88
|
+
'detail.changes': 'Changes',
|
|
89
|
+
'detail.changedTo': 'changed to',
|
|
88
90
|
'detail.identity': 'Overview',
|
|
89
91
|
'detail.id': 'Id',
|
|
90
92
|
'detail.types': 'Types',
|
|
@@ -93,7 +95,6 @@ const en = {
|
|
|
93
95
|
'detail.before': 'Before',
|
|
94
96
|
'detail.after': 'After',
|
|
95
97
|
'detail.attributes': 'Attributes',
|
|
96
|
-
'detail.key': 'Key',
|
|
97
98
|
'detail.aliases': 'Synonyms',
|
|
98
99
|
'detail.hierarchy': 'Hierarchy',
|
|
99
100
|
'detail.relations': 'Relations',
|
|
@@ -123,8 +124,6 @@ const en = {
|
|
|
123
124
|
'detail.undeprecated': 'No longer deprecated',
|
|
124
125
|
'detail.replacedBy': 'Replaced by',
|
|
125
126
|
'detail.replaces': 'Replaces',
|
|
126
|
-
'detail.emptyValue': 'empty',
|
|
127
|
-
'detail.none': 'none',
|
|
128
127
|
'detail.unchangedNode': 'Unchanged node',
|
|
129
128
|
'detail.implicit': 'Created for a dangling relation',
|
|
130
129
|
'detail.collapsed': 'duplicate, collapsed',
|
|
@@ -275,6 +274,8 @@ const zhCN: Messages = {
|
|
|
275
274
|
'detail.emptyHint': '按 j / k 逐项浏览变更。',
|
|
276
275
|
'detail.prev': '上一项变更',
|
|
277
276
|
'detail.next': '下一项变更',
|
|
277
|
+
'detail.changes': '变更内容',
|
|
278
|
+
'detail.changedTo': '改为',
|
|
278
279
|
'detail.identity': '基本信息',
|
|
279
280
|
'detail.id': 'ID',
|
|
280
281
|
'detail.types': '类型',
|
|
@@ -283,7 +284,6 @@ const zhCN: Messages = {
|
|
|
283
284
|
'detail.before': '变更前',
|
|
284
285
|
'detail.after': '变更后',
|
|
285
286
|
'detail.attributes': '属性',
|
|
286
|
-
'detail.key': '键',
|
|
287
287
|
'detail.aliases': '别名',
|
|
288
288
|
'detail.hierarchy': '层级',
|
|
289
289
|
'detail.relations': '关系',
|
|
@@ -313,8 +313,6 @@ const zhCN: Messages = {
|
|
|
313
313
|
'detail.undeprecated': '取消弃用',
|
|
314
314
|
'detail.replacedBy': '替代为',
|
|
315
315
|
'detail.replaces': '替代了',
|
|
316
|
-
'detail.emptyValue': '空',
|
|
317
|
-
'detail.none': '无',
|
|
318
316
|
'detail.unchangedNode': '未变节点',
|
|
319
317
|
'detail.implicit': '因悬空关系自动创建',
|
|
320
318
|
'detail.collapsed': '重复关系,已合并',
|
package/src/index.ts
CHANGED
|
@@ -27,6 +27,10 @@ import { viewerTags } from './registry.js';
|
|
|
27
27
|
|
|
28
28
|
export type ViewerMode = 'overlay' | 'swipe' | 'replay' | 'changes';
|
|
29
29
|
export type ThemePreference = 'auto' | 'light' | 'dark';
|
|
30
|
+
/** `none`: the graph alone, for small embeds (see `KgdiffViewerElement.chrome`). */
|
|
31
|
+
export type ViewerChrome = 'full' | 'none';
|
|
32
|
+
/** The legend card, one line of dots, no legend, or `auto` (the card, one line without chrome). */
|
|
33
|
+
export type ViewerLegend = 'auto' | 'full' | 'mini' | 'none';
|
|
30
34
|
|
|
31
35
|
/** A precomputed diff + layout: what the CLI embeds in HTML reports. Deterministic. */
|
|
32
36
|
export interface ReportPayload {
|
|
@@ -83,6 +87,18 @@ export interface KgdiffViewerEventMap {
|
|
|
83
87
|
readonly progress: ReviewState;
|
|
84
88
|
}>;
|
|
85
89
|
'kgdiff-error': CustomEvent<{ readonly error: Error }>;
|
|
90
|
+
/**
|
|
91
|
+
* Playback reached the end of the replay (not fired for seeking or stepping there). `loop`:
|
|
92
|
+
* it starts again after `loopDelay` ms; set `loop = false` or call `pause()` to stop it.
|
|
93
|
+
*/
|
|
94
|
+
'kgdiff-replay-end': CustomEvent<{ readonly loop: boolean }>;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface FitOptions {
|
|
98
|
+
/** Default true (false with reduced motion). */
|
|
99
|
+
readonly animate?: boolean;
|
|
100
|
+
/** Room kept around the graph and its labels, px. Default 24; later automatic fits keep it. */
|
|
101
|
+
readonly padding?: number;
|
|
86
102
|
}
|
|
87
103
|
|
|
88
104
|
/**
|
|
@@ -96,6 +112,9 @@ export interface KgdiffViewerEventMap {
|
|
|
96
112
|
*
|
|
97
113
|
* `header="compact"` leaves the brand and the compared names out of the header, for hosts that
|
|
98
114
|
* show their own (such as the playground).
|
|
115
|
+
*
|
|
116
|
+
* A small looping replay for another page: `mode="replay" chrome="none" loop` (with a height of
|
|
117
|
+
* your choosing, such as 320px).
|
|
99
118
|
*/
|
|
100
119
|
export interface KgdiffViewerElement extends HTMLElement {
|
|
101
120
|
/** Reflected to the `mode` attribute. Default `overlay` (`changes` for large graphs). */
|
|
@@ -106,6 +125,32 @@ export interface KgdiffViewerElement extends HTMLElement {
|
|
|
106
125
|
palette: PaletteName;
|
|
107
126
|
/** Reflected to the `locale` attribute. Default from `navigator.language`. */
|
|
108
127
|
locale: Locale;
|
|
128
|
+
/**
|
|
129
|
+
* Reflected to the `chrome` attribute. `none` shows the graph alone: no header, change list,
|
|
130
|
+
* step caption, playback bar, zoom buttons, minimap or legend card; the hover tooltip and a
|
|
131
|
+
* one-line legend stay. Clicks and keyboard shortcuts do nothing, the minimum height drops
|
|
132
|
+
* from 420px to 120px, and the view stays fitted: when the canvas is resized (unless the user
|
|
133
|
+
* panned or zoomed since) and at the start of every loop. Labels are never cut at the edge:
|
|
134
|
+
* one pushed from its spot by a neighbour moves inside or is left out. Default `full`.
|
|
135
|
+
*/
|
|
136
|
+
chrome: ViewerChrome;
|
|
137
|
+
/**
|
|
138
|
+
* Reflected to the `legend` attribute: `full` (the card), `mini` (one line: the families this
|
|
139
|
+
* diff has, then unchanged), `none`. Default `auto`: `full`, or `mini` with `chrome="none"`.
|
|
140
|
+
*/
|
|
141
|
+
legend: ViewerLegend;
|
|
142
|
+
/**
|
|
143
|
+
* Reflected to the boolean `loop` attribute. In `replay` mode the replay plays by itself and
|
|
144
|
+
* starts again `loopDelay` ms after it ends. `pause()` stops it until `play()`. Default false.
|
|
145
|
+
*/
|
|
146
|
+
loop: boolean;
|
|
147
|
+
/** Reflected to `loop-delay`: ms between the end of a looping replay and its restart. Default 1200. */
|
|
148
|
+
loopDelay: number;
|
|
149
|
+
/**
|
|
150
|
+
* With `loop`, the replay pauses while the pointer is over the graph and continues when it
|
|
151
|
+
* leaves (a mouse or pen; not touch). Reflected as `pause-on-hover="false"` when off. Default true.
|
|
152
|
+
*/
|
|
153
|
+
pauseOnHover: boolean;
|
|
109
154
|
/** Set a precomputed report (no diff / layout work in the browser). */
|
|
110
155
|
payload: ReportPayload | null;
|
|
111
156
|
/** Parse (if needed), diff, lay out and show two graphs. Rejects with a KgdiffError on bad input. */
|
|
@@ -116,7 +161,8 @@ export interface KgdiffViewerElement extends HTMLElement {
|
|
|
116
161
|
prev(): void;
|
|
117
162
|
play(): void;
|
|
118
163
|
pause(): void;
|
|
119
|
-
|
|
164
|
+
/** Fit the whole graph, labels included, into view. */
|
|
165
|
+
fit(options?: FitOptions): Promise<void>;
|
|
120
166
|
/** The full delta of the current diff. */
|
|
121
167
|
exportDelta(): Delta;
|
|
122
168
|
reviewState(): ReviewState;
|
package/src/prefs.ts
CHANGED
|
@@ -6,11 +6,22 @@
|
|
|
6
6
|
import type { Locale } from '@kgdiff/core';
|
|
7
7
|
import type { PaletteName } from '@kgdiff/scene';
|
|
8
8
|
import { normalizeLocale } from './i18n.js';
|
|
9
|
-
import type {
|
|
9
|
+
import type {
|
|
10
|
+
ReportPayload,
|
|
11
|
+
ThemePreference,
|
|
12
|
+
ViewerChrome,
|
|
13
|
+
ViewerLegend,
|
|
14
|
+
ViewerMode,
|
|
15
|
+
} from './index.js';
|
|
10
16
|
|
|
11
17
|
const MODES: readonly ViewerMode[] = ['overlay', 'swipe', 'replay', 'changes'];
|
|
12
18
|
const THEMES: readonly ThemePreference[] = ['auto', 'light', 'dark'];
|
|
13
19
|
const PALETTES: readonly PaletteName[] = ['default', 'colorblind'];
|
|
20
|
+
const CHROMES: readonly ViewerChrome[] = ['full', 'none'];
|
|
21
|
+
const LEGENDS: readonly ViewerLegend[] = ['auto', 'full', 'mini', 'none'];
|
|
22
|
+
|
|
23
|
+
/** Pause between the end of a looping replay and its next start, ms. */
|
|
24
|
+
export const DEFAULT_LOOP_DELAY = 1200;
|
|
14
25
|
|
|
15
26
|
export const THEME_PREFERENCES = THEMES;
|
|
16
27
|
|
|
@@ -30,6 +41,20 @@ export function toLocale(value: unknown): Locale | null {
|
|
|
30
41
|
return typeof value === 'string' ? normalizeLocale(value) : null;
|
|
31
42
|
}
|
|
32
43
|
|
|
44
|
+
export function toChrome(value: unknown): ViewerChrome | null {
|
|
45
|
+
return CHROMES.includes(value as ViewerChrome) ? (value as ViewerChrome) : null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function toLegend(value: unknown): ViewerLegend | null {
|
|
49
|
+
return LEGENDS.includes(value as ViewerLegend) ? (value as ViewerLegend) : null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** A delay in ms (number or numeric string), at least 0. */
|
|
53
|
+
export function toDelay(value: unknown): number | null {
|
|
54
|
+
const n = typeof value === 'string' && value.trim() !== '' ? Number(value) : value;
|
|
55
|
+
return typeof n === 'number' && Number.isFinite(n) && n >= 0 ? n : null;
|
|
56
|
+
}
|
|
57
|
+
|
|
33
58
|
type View = NonNullable<ReportPayload['view']>;
|
|
34
59
|
|
|
35
60
|
/** Keep only the valid fields of a report's `view` (warning about the rest). */
|
package/src/styles.ts
CHANGED
|
@@ -90,6 +90,8 @@ export const STYLES = /* css */ `
|
|
|
90
90
|
-webkit-font-smoothing: antialiased;
|
|
91
91
|
}
|
|
92
92
|
:host([hidden]) { display: none; }
|
|
93
|
+
/* chrome="none" shows the graph alone: small embeds (a 320 px strip) are fine. */
|
|
94
|
+
:host([chrome="none"]) { min-height: 120px; }
|
|
93
95
|
*, *::before, *::after { box-sizing: border-box; }
|
|
94
96
|
[hidden] { display: none !important; }
|
|
95
97
|
button, input { font: inherit; color: inherit; letter-spacing: inherit; }
|
|
@@ -237,6 +239,15 @@ ${familyRules}
|
|
|
237
239
|
.legend-row { display: grid; grid-template-columns: 22px auto; align-items: center; column-gap: 8px; height: 26px; padding: 0 10px 0 4px; border-radius: 6px; cursor: default; }
|
|
238
240
|
.legend-row .name { font-size: 12px; font-weight: 550; color: var(--kg-text); }
|
|
239
241
|
.swatch { width: 22px; height: 22px; display: block; }
|
|
242
|
+
.mini-legend { display: none; }
|
|
243
|
+
.stage-wrap[data-legend="mini"] .mini-legend {
|
|
244
|
+
display: flex; flex-wrap: wrap; align-items: center; gap: 2px 12px; bottom: 8px; left: 12px; max-width: calc(100% - 24px);
|
|
245
|
+
font-size: 11px; line-height: 16px; color: var(--kg-text-2);
|
|
246
|
+
}
|
|
247
|
+
.mini-item { display: inline-flex; align-items: center; gap: 5px; white-space: nowrap; }
|
|
248
|
+
.mini-item .dot { width: 8px; height: 8px; border-radius: 50%; flex: none; background: var(--fam-base); }
|
|
249
|
+
.mini-item[data-family="removed"] .dot { background: none; box-shadow: inset 0 0 0 1.5px var(--fam-base); }
|
|
250
|
+
.stage-wrap[data-legend="mini"] .legend, .stage-wrap[data-legend="none"] .legend { display: none; }
|
|
240
251
|
|
|
241
252
|
.caption {
|
|
242
253
|
position: absolute; top: 12px; left: 50%; transform: translateX(-50%); max-width: min(560px, calc(100% - 200px));
|
|
@@ -515,22 +526,20 @@ ${familyRules}
|
|
|
515
526
|
.vchip.add { color: var(--kg-added-text); background: var(--kg-added-soft); box-shadow: none; }
|
|
516
527
|
.vchip.del { color: var(--kg-removed-text); background: var(--kg-removed-soft); box-shadow: none; text-decoration: line-through; text-decoration-thickness: 1px; }
|
|
517
528
|
.vchip .sign { font-weight: 700; text-decoration: none; display: inline-block; }
|
|
518
|
-
.
|
|
519
|
-
.
|
|
520
|
-
.
|
|
521
|
-
.
|
|
522
|
-
.
|
|
523
|
-
.
|
|
529
|
+
.pair { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 6px; }
|
|
530
|
+
.pair .b, .line.old { color: var(--kg-text-2); }
|
|
531
|
+
.pair .arr { color: var(--kg-text-3); }
|
|
532
|
+
.line { display: grid; grid-template-columns: 10px minmax(0, 1fr); gap: 6px; align-items: baseline; }
|
|
533
|
+
.line .sign { font: 600 12px/1 var(--kg-mono); text-align: center; }
|
|
534
|
+
.line.old .sign { color: var(--kg-removed-text); }
|
|
535
|
+
.line.new .sign { color: var(--kg-added-text); }
|
|
524
536
|
del.hl { text-decoration: line-through; text-decoration-color: var(--kg-removed); background: var(--kg-removed-soft); color: var(--kg-removed-text); border-radius: 3px; padding: 0 1px; }
|
|
525
537
|
ins.hl { text-decoration: none; background: var(--kg-added-soft); color: var(--kg-added-text); border-radius: 3px; padding: 0 1px; }
|
|
526
|
-
.attrs { width: 100%; border-collapse: separate; border-spacing: 0; font-size:
|
|
527
|
-
.attrs
|
|
528
|
-
.attrs td {
|
|
529
|
-
.attrs td
|
|
530
|
-
.attrs td.
|
|
531
|
-
.attrs tr.changed td.k { color: var(--kg-text); }
|
|
532
|
-
.attrs tr.unchanged td { color: var(--kg-text-3); }
|
|
533
|
-
.attrs .empty { color: var(--kg-text-3); font-style: italic; }
|
|
538
|
+
.attrs { width: 100%; border-collapse: separate; border-spacing: 0; font-size: 12.5px; table-layout: fixed; }
|
|
539
|
+
.attrs td { vertical-align: baseline; padding: 6px 8px 6px 0; border-top: 1px solid var(--kg-border); overflow-wrap: anywhere; }
|
|
540
|
+
.attrs tr:first-child td { border-top: 0; padding-top: 0; }
|
|
541
|
+
.attrs td > * + * { margin-top: 4px; }
|
|
542
|
+
.attrs td.k { color: var(--kg-text-2); }
|
|
534
543
|
.attrs col.k { width: 28%; }
|
|
535
544
|
.val { white-space: pre-wrap; }
|
|
536
545
|
.val.clamped { display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical; overflow: hidden; }
|
|
@@ -566,9 +575,6 @@ ins.hl { text-decoration: none; background: var(--kg-added-soft); color: var(--k
|
|
|
566
575
|
.bar-row .fill { height: 100%; border-radius: 3px; background: var(--kg-identity); }
|
|
567
576
|
.bar-row .v { text-align: right; color: var(--kg-text-2); font-variant-numeric: tabular-nums; }
|
|
568
577
|
.note-line { font-size: 12px; color: var(--kg-text-3); margin-top: 6px; }
|
|
569
|
-
.hier { display: grid; grid-template-columns: 56px minmax(0, 1fr); gap: 6px 12px; font-size: 12.5px; align-items: baseline; }
|
|
570
|
-
.hier dt { color: var(--kg-text-3); }
|
|
571
|
-
.hier dd { margin: 0; display: flex; flex-wrap: wrap; gap: 4px; }
|
|
572
578
|
|
|
573
579
|
/* ── Popovers ────────────────────────────────────────────────────────────── */
|
|
574
580
|
.menu {
|
|
@@ -600,6 +606,12 @@ ins.hl { text-decoration: none; background: var(--kg-added-soft); color: var(--k
|
|
|
600
606
|
.key-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; min-height: 34px; border-bottom: 1px solid var(--kg-border); font-size: 12.5px; color: var(--kg-text-2); }
|
|
601
607
|
.key-row .ks { display: inline-flex; align-items: center; gap: 3px; flex: none; color: var(--kg-text-3); font-size: 11px; }
|
|
602
608
|
|
|
609
|
+
/* ── chrome="none": the graph, its hover tooltip and the legend line only ──── */
|
|
610
|
+
.root[data-chrome="none"] .header, .root[data-chrome="none"] .sidebar, .root[data-chrome="none"] .caption,
|
|
611
|
+
.root[data-chrome="none"] .transport, .root[data-chrome="none"] .finished, .root[data-chrome="none"] .stepper,
|
|
612
|
+
.root[data-chrome="none"] .map-cluster, .root[data-chrome="none"] .side-chip { display: none !important; }
|
|
613
|
+
.stage-wrap[data-selectable="false"] .tooltip .t-hint { display: none; }
|
|
614
|
+
|
|
603
615
|
/* ── Compact (< 760 px): bottom sheet ────────────────────────────────────── */
|
|
604
616
|
.root[data-compact="true"] .body { flex-direction: column; }
|
|
605
617
|
.root[data-compact="true"] .sidebar {
|
package/src/text-diff.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Myers O((N+M)·D) sequence diff for the detail panel: character-level name diffs
|
|
3
|
-
*
|
|
2
|
+
* Myers O((N+M)·D) sequence diff for the detail panel: character-level name diffs, word-level
|
|
3
|
+
* attribute diffs and set changes read as edits. Small, deterministic, no dependencies.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
export type DiffOp = 'equal' | 'insert' | 'delete';
|
|
@@ -206,3 +206,58 @@ export function sideOf(segs: readonly DiffSegment[], side: 'before' | 'after'):
|
|
|
206
206
|
for (const seg of segs) if (seg.op !== skip) s += seg.text;
|
|
207
207
|
return s;
|
|
208
208
|
}
|
|
209
|
+
|
|
210
|
+
/** A set change read as edits: removed → added pairs, and the items left over. */
|
|
211
|
+
export interface PairedItems {
|
|
212
|
+
readonly pairs: readonly (readonly [removed: string, added: string])[];
|
|
213
|
+
readonly removed: readonly string[];
|
|
214
|
+
readonly added: readonly string[];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Below this share of common characters a removal and an addition stay apart. */
|
|
218
|
+
const PAIR_MIN = 0.5;
|
|
219
|
+
/** Above this many removal × addition candidates, only a lone pair is paired. */
|
|
220
|
+
const PAIR_MAX_CANDIDATES = 400;
|
|
221
|
+
|
|
222
|
+
/** 2 · common code points / total code points. */
|
|
223
|
+
function closeness(a: readonly string[], b: readonly string[]): number {
|
|
224
|
+
let common = 0;
|
|
225
|
+
for (const s of diffSequences(a, b)) if (s.op === 'equal') common += s.items.length;
|
|
226
|
+
return (2 * common) / (a.length + b.length);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Read a set change (synonyms, types, parents, list values) as edits: a lone removal and a lone
|
|
231
|
+
* addition pair up (`老赖 → 失信人`); otherwise removals and additions sharing at least half
|
|
232
|
+
* their characters pair up, closest first. Pairs keep the order of `removed`.
|
|
233
|
+
*/
|
|
234
|
+
export function pairItems(removed: readonly string[], added: readonly string[]): PairedItems {
|
|
235
|
+
const found: [score: number, i: number, j: number][] = [];
|
|
236
|
+
if (removed.length === 1 && added.length === 1) found.push([1, 0, 0]);
|
|
237
|
+
else if (removed.length * added.length <= PAIR_MAX_CANDIDATES) {
|
|
238
|
+
const as = added.map((x) => Array.from(x));
|
|
239
|
+
removed.forEach((r, i) => {
|
|
240
|
+
const rs = Array.from(r);
|
|
241
|
+
as.forEach((a, j) => {
|
|
242
|
+
const score = closeness(rs, a);
|
|
243
|
+
if (score >= PAIR_MIN) found.push([score, i, j]);
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
found.sort((x, y) => y[0] - x[0] || x[1] - y[1] || x[2] - y[2]);
|
|
247
|
+
}
|
|
248
|
+
const usedR = new Set<number>();
|
|
249
|
+
const usedA = new Set<number>();
|
|
250
|
+
const pairs: [number, number][] = [];
|
|
251
|
+
for (const [, i, j] of found) {
|
|
252
|
+
if (usedR.has(i) || usedA.has(j)) continue;
|
|
253
|
+
usedR.add(i);
|
|
254
|
+
usedA.add(j);
|
|
255
|
+
pairs.push([i, j]);
|
|
256
|
+
}
|
|
257
|
+
pairs.sort((x, y) => x[0] - y[0]);
|
|
258
|
+
return {
|
|
259
|
+
pairs: pairs.map(([i, j]) => [removed[i] as string, added[j] as string] as const),
|
|
260
|
+
removed: removed.filter((_, i) => !usedR.has(i)),
|
|
261
|
+
added: added.filter((_, j) => !usedA.has(j)),
|
|
262
|
+
};
|
|
263
|
+
}
|