quickdbd-cli 0.3.3__py3-none-any.whl → 0.3.4__py3-none-any.whl

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.
quickdbd_cli/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.3.3"
1
+ __version__ = "0.3.4"
@@ -547,6 +547,54 @@
547
547
  return lines.join("\n");
548
548
  }
549
549
 
550
+ /* ------------------------------------------------------------------ lit relationships (viewer only, never exported) */
551
+ var CARD_LABEL = { one: "1", zeroOne: "0..1", many: "*", zeroMany: "0..*" };
552
+ var CARD_WORD = { one: "one", zeroOne: "zero or one", many: "many", zeroMany: "zero or many" };
553
+
554
+ /* Cardinality labels for a lit line: one just past each end, on the side the line leaves from. a end first. */
555
+ function edgeLabels(model, pos, mode, rel, geo) {
556
+ var e = edgeGeom(model, geo || geometry(model, mode), pos, rel);
557
+ if (!e) return null;
558
+ function spot(x, y, dir, card) {
559
+ return { x: n(x + dir * 8), y: n(y - 9), anchor: dir > 0 ? "start" : "end", text: CARD_LABEL[card] || "" };
560
+ }
561
+ return [spot(e.xa, e.ya, e.da, rel.a.card), spot(e.xb, e.yb, e.db, rel.b.card)];
562
+ }
563
+
564
+ /* Text of the relationship card for model.rels[i]: the child (FK / many) side first, a plain-English sentence and
565
+ * the JOIN, built like `qdbd path` so quoting and composite keys match. */
566
+ function relCard(model, i) {
567
+ var r = model.rels[i], byId = {};
568
+ model.tables.forEach(function (t) { byId[t.id] = t; });
569
+ var aMany = r.a.card === "many" || r.a.card === "zeroMany", bMany = r.b.card === "many" || r.b.card === "zeroMany";
570
+ var aIsChild = aMany || !bMany, child = aIsChild ? r.a : r.b, parent = aIsChild ? r.b : r.a;
571
+ var pairs = stepPairs(model, i, child.t);
572
+ function side(end, k) {
573
+ var t = byId[end.t], cols = pairs.map(function (p) {
574
+ var c = t.cols.filter(function (x) { return x.id === p[k]; })[0];
575
+ return c ? c.name : p[k];
576
+ });
577
+ return t.name + (cols.length > 1 ? ".(" + cols.join(", ") + ")" : cols.length ? "." + cols[0] : "");
578
+ }
579
+ var plural = child.card === "many" || child.card === "zeroMany";
580
+ return {
581
+ from: side(child, 0),
582
+ to: side(parent, 1),
583
+ cardFrom: CARD_LABEL[child.card] || "",
584
+ cardTo: CARD_LABEL[parent.card] || "",
585
+ sentence: "Each " + byId[parent.t].name + " row has " + CARD_WORD[child.card] + " " + byId[child.t].name + (plural ? " rows" : " row"),
586
+ sql: child.t === parent.t ? selfJoin(byId[child.t], pairs) : pathSql(model, child.t, [{ from: child, to: parent, pairs: pairs }]),
587
+ };
588
+ }
589
+ /* A table joined to itself needs an alias, or every column is ambiguous. */
590
+ function selfJoin(t, pairs) {
591
+ var name = sqlIdent(t.name), alias = sqlPart(t.name.split(".").pop() + "_2");
592
+ function col(c) { var x = t.cols.filter(function (k) { return k.id === c; })[0]; return sqlPart(x ? x.name : c); }
593
+ var on = pairs.map(function (p) { return alias + "." + col(p[1]) + " = " + name + "." + col(p[0]); });
594
+ return "FROM " + name + "\nJOIN " + name + " AS " + alias + " ON " +
595
+ (on.length ? on.join(" AND ") : "... -- table-level relationship: join columns not specified");
596
+ }
597
+
550
598
  root.qdbd = {
551
599
  findPath: findPath,
552
600
  stepPairs: stepPairs,
@@ -563,8 +611,12 @@
563
611
  renderSvg: renderSvg,
564
612
  bounds: bounds,
565
613
  neighbourhood: neighbourhood,
614
+ edgeLabels: edgeLabels,
615
+ relCard: relCard,
566
616
  esc: esc,
567
617
  };
618
+ root.qdbdEdgeLabels = edgeLabels;
619
+ root.qdbdRelCard = relCard;
568
620
  root.qdbdRenderSvg = renderSvg;
569
621
  root.qdbdSelect = select;
570
622
  root.qdbdRenderDrawio = renderDrawio;
@@ -34,6 +34,8 @@
34
34
  --t-edge: #b8bcc2;
35
35
  --row-hl: #dbe6f6;
36
36
  --row-hover: #eef3fa;
37
+ --lit: #4361ee;
38
+ --lit-deep: #2a3fb8;
37
39
  }
38
40
  /* Dark theme. viewer.js sets data-theme from the toggle, or from prefers-color-scheme when it's on auto. */
39
41
  :root[data-theme="dark"] {
@@ -71,6 +73,8 @@
71
73
  --t-edge: #5b6472;
72
74
  --row-hl: #24344d;
73
75
  --row-hover: #23282f;
76
+ --lit: #8aa0ff;
77
+ --lit-deep: #c3ceff;
74
78
  }
75
79
  ::selection { background: var(--pick); color: var(--ink); }
76
80
  :focus-visible { outline: 2px solid var(--amber); outline-offset: 1px; }
@@ -105,6 +109,36 @@ html, body { margin: 0; height: 100%; background: var(--bg); color: var(--ink);
105
109
  .r.hl .rb { fill: var(--row-hl); fill-opacity: 1; }
106
110
  .r:hover .rb { fill: var(--row-hover); fill-opacity: 1; }
107
111
  .t.flash .tb { stroke: #f0a020; stroke-width: 3; }
112
+ .t .tb { transition: filter .18s; }
113
+ .t:hover .tb { filter: drop-shadow(0 6px 14px rgba(16, 24, 40, .16)); }
114
+
115
+ /* lit relationships: hovered or selected table, hovered or clicked line (viewer.js light()) */
116
+ .e.lit .ep { stroke: var(--lit); stroke-width: 2.2; }
117
+ .e.lit path, .e.lit .mc { stroke: var(--lit); }
118
+ .r.lit .rb { fill: var(--row-hl); fill-opacity: 1; }
119
+ .efocus .t:not(.on) { opacity: .18; }
120
+ .efocus .e:not(.lit) { opacity: .07; }
121
+ .fx { pointer-events: none; }
122
+ .fx .flow { fill: none; stroke: var(--lit-deep); stroke-width: 4; stroke-dasharray: 4 18; animation: qdbd-flow 1.1s linear infinite; }
123
+ .fx .flow.amber { stroke: #b86e00; }
124
+ @keyframes qdbd-flow { to { stroke-dashoffset: -22; } }
125
+ .fx .lbl { font: 600 11px Inter, "Helvetica Neue", Helvetica, Arial, sans-serif; fill: var(--lit); paint-order: stroke;
126
+ stroke: var(--bg); stroke-width: 4px; stroke-linejoin: round; animation: qdbd-fade .2s ease-out; }
127
+ @keyframes qdbd-fade { from { opacity: 0; } }
128
+ .hit path { fill: none; stroke: transparent; stroke-width: 14; pointer-events: stroke; cursor: pointer; }
129
+ /* relationship card (a clicked line) */
130
+ .rcard { position: absolute; z-index: 6; display: none; min-width: 240px; max-width: min(460px, calc(100% - 16px)); padding: 10px 12px;
131
+ background: var(--panel); border: 1px solid var(--line); border-radius: 10px; box-shadow: var(--shadow); font-size: 12px;
132
+ color: var(--ink); transform-origin: top left; }
133
+ .rcard.open { display: block; animation: qdbd-cardin .16s ease-out; }
134
+ @keyframes qdbd-cardin { from { opacity: 0; transform: scale(.96) translateY(-4px); } }
135
+ .rcard .rt { font-weight: 600; margin-bottom: 2px; word-break: break-word; }
136
+ .rcard .rt b { color: var(--lit); font-weight: 600; padding: 0 6px; white-space: nowrap; }
137
+ .rcard .rk { color: var(--muted); margin-bottom: 8px; }
138
+ .rcard pre { margin: 0 0 8px; padding: 7px 8px; background: var(--bg); border: 1px solid var(--line); border-radius: 7px;
139
+ font: 11px ui-monospace, Menlo, Consolas, monospace; white-space: pre-wrap; user-select: text; }
140
+ .rcard .acts a { color: var(--blue); cursor: pointer; margin-right: 12px; }
141
+ @media (prefers-reduced-motion: reduce) { .fx .flow, .fx .lbl, .rcard.open { animation: none; } }
108
142
  .t { cursor: grab; }
109
143
  #canvas.panning .t { cursor: grabbing; }
110
144
 
@@ -177,8 +211,10 @@ html, body { margin: 0; height: 100%; background: var(--bg); color: var(--ink);
177
211
  /* details drawer (right) */
178
212
  .details { position: absolute; top: 64px; right: 16px; bottom: 16px; width: 380px; background: var(--panel);
179
213
  border: 1px solid var(--line); border-radius: 10px; box-shadow: var(--shadow); overflow: auto;
180
- display: none; z-index: 3; padding: 16px 18px; }
181
- .details.open { display: block; }
214
+ z-index: 3; padding: 16px 18px; visibility: hidden; opacity: 0; transform: translateX(16px); pointer-events: none;
215
+ transition: opacity .2s, transform .2s, visibility 0s .2s; }
216
+ .details.open { visibility: visible; opacity: 1; transform: none; pointer-events: auto; transition: opacity .2s, transform .2s; }
217
+ @media (prefers-reduced-motion: reduce) { .details, .details.open { transition: none; } }
182
218
  .has-details .minimap { right: 412px; }
183
219
  .details .close { position: absolute; top: 10px; right: 10px; width: 28px; height: 28px; border: 0; border-radius: 6px;
184
220
  background: transparent; display: flex; align-items: center; justify-content: center; color: var(--muted); cursor: pointer; }
@@ -16,6 +16,8 @@
16
16
  shown: {},
17
17
  regions: !!data.model.regions,
18
18
  path: null, // {to: id, steps: [...]} from the selected table
19
+ edge: null, // the clicked (locked) relationship line: "a|ac|b|bc", like the .e data attributes
20
+ hover: null, // {t: table id} or {e: line key} under the mouse: lights lines without dimming anything
19
21
  };
20
22
  var svg = document.getElementById("canvas");
21
23
  var vp = document.getElementById("vp");
@@ -29,7 +31,12 @@
29
31
 
30
32
  /* ------------------------------------------------------------- render */
31
33
  function render() {
32
- vp.innerHTML = Q.renderInner(S.vm, S.pos, S.mode, { docMarks: true, regions: S.regions });
34
+ vp.innerHTML = Q.renderInner(S.vm, S.pos, S.mode, { docMarks: true, regions: S.regions }) +
35
+ '<g class="fx"></g><g class="hit"></g>';
36
+ vp.appendChild(vp.querySelector(".tables")); // lit overlay and line hit targets sit under the tables
37
+ relByKey = {};
38
+ S.vm.rels.forEach(function (r, i) { relByKey[[r.a.t, r.a.c, r.b.t, r.b.c].join("|")] = i; });
39
+ buildHits();
33
40
  applyFocus();
34
41
  drawMinimap();
35
42
  }
@@ -37,9 +44,114 @@
37
44
  function renderEdges() {
38
45
  var g = vp.querySelector(".edges");
39
46
  if (g) g.innerHTML = Q.renderEdges(S.vm, S.pos, S.mode);
47
+ buildHits();
40
48
  applyFocus();
41
49
  }
42
50
 
51
+ /* ------------------------------------------------------------- lit relationships
52
+ * A lit line gets the --lit colour, flowing dashes and cardinality labels (drawn in .fx), and its rows are
53
+ * highlighted. Lines light up under a hovered table or line, for the selected table, and for the clicked line. */
54
+ var relByKey = {};
55
+ function edgeKey(e) {
56
+ return [e.getAttribute("data-a"), e.getAttribute("data-ac"), e.getAttribute("data-b"), e.getAttribute("data-bc")].join("|");
57
+ }
58
+ /* Wide transparent copies of every line, so a 1px curve can be hovered and clicked. */
59
+ function buildHits() {
60
+ var hit = vp.querySelector(".hit");
61
+ if (!hit) return;
62
+ hit.innerHTML = [].map.call(vp.querySelectorAll(".e"), function (e) {
63
+ return '<path data-k="' + Q.esc(edgeKey(e)) + '" d="' + e.querySelector(".ep").getAttribute("d") + '"/>';
64
+ }).join("");
65
+ }
66
+ function light() {
67
+ var fx = vp.querySelector(".fx");
68
+ if (!fx) return;
69
+ if (S.edge && relByKey[S.edge] === undefined) S.edge = null; // the clicked line was filtered away or edited out
70
+ if (!S.edge) closeCard();
71
+ vp.querySelectorAll(".lit, .t.on").forEach(function (el) { el.classList.remove("lit", "on"); });
72
+ svg.classList.toggle("efocus", !!S.edge);
73
+ var focused = svg.classList.contains("focus"), h = S.hover || {}, geo = null, parts = [];
74
+ vp.querySelectorAll(".e").forEach(function (e) {
75
+ var k = edgeKey(e), a = e.getAttribute("data-a"), b = e.getAttribute("data-b"), d = e.querySelector(".ep").getAttribute("d");
76
+ var dk = ' data-k="' + Q.esc(k) + '"';
77
+ if (e.classList.contains("ph")) { parts.push('<path class="flow amber"' + dk + ' d="' + d + '"/>'); return; }
78
+ if (!(k === S.edge || h.e === k || h.t === a || h.t === b || e.classList.contains("hl") ||
79
+ (focused && !S.path && !e.classList.contains("dim")))) return;
80
+ var r = S.vm.rels[relByKey[k]];
81
+ if (!r) return;
82
+ e.classList.add("lit");
83
+ [[r.a.t, r.a.c], [r.b.t, r.b.c]].forEach(function (end) {
84
+ var t = vp.querySelector('.t[data-id="' + cssEsc(end[0]) + '"]');
85
+ if (!t) return;
86
+ if (k === S.edge) t.classList.add("on");
87
+ var row = t.querySelector('.r[data-c="' + cssEsc(end[1]) + '"]');
88
+ if (row) row.classList.add("lit");
89
+ });
90
+ parts.push('<path class="flow"' + dk + ' d="' + d + '"/>');
91
+ (Q.edgeLabels(S.vm, S.pos, S.mode, r, geo = geo || Q.geometry(S.vm, S.mode)) || []).forEach(function (l) {
92
+ parts.push('<text class="lbl"' + dk + ' x="' + l.x + '" y="' + l.y + '" text-anchor="' + l.anchor + '">' + Q.esc(l.text) + "</text>");
93
+ });
94
+ });
95
+ fx.innerHTML = parts.join("");
96
+ }
97
+ var lightRaf = null;
98
+ function scheduleLight() {
99
+ if (!lightRaf) lightRaf = requestAnimationFrame(function () { lightRaf = null; light(); });
100
+ }
101
+ vp.addEventListener("mouseover", function (ev) {
102
+ var hp = ev.target.closest && ev.target.closest(".hit path"), t = ev.target.closest && ev.target.closest(".t");
103
+ var next = hp ? { e: hp.getAttribute("data-k") } : t ? { t: t.getAttribute("data-id") } : null;
104
+ if (JSON.stringify(next) !== JSON.stringify(S.hover)) { S.hover = next; scheduleLight(); }
105
+ });
106
+ /* A click on a line locks it: everything else dims. A second click on it, Esc or a click elsewhere clears it. */
107
+ function lockEdge(k, x, y) {
108
+ select(null);
109
+ S.edge = k;
110
+ light();
111
+ openCard(k, x, y);
112
+ }
113
+
114
+ /* The relationship card: both ends with their cardinality, a plain-English sentence and the JOIN. */
115
+ var rcard = document.getElementById("rcard"), cardInfo = null;
116
+ function openCard(k, x, y) {
117
+ var r = S.vm.rels[relByKey[k]], c = Q.relCard(S.vm, relByKey[k]);
118
+ cardInfo = { sql: c.sql, tables: [r.a.t, r.b.t] };
119
+ rcard.innerHTML = '<div class="rt">' + Q.esc(c.from) + "<b>" + Q.esc(c.cardFrom) + " &mdash; " + Q.esc(c.cardTo) + "</b>" + Q.esc(c.to) +
120
+ '</div><div class="rk">' + Q.esc(c.sentence) + "</div><pre>" + Q.esc(c.sql) +
121
+ '</pre><div class="acts"><a id="rcopy">Copy JOIN</a><a id="rboth">Show both tables</a></div>';
122
+ rcard.classList.remove("open");
123
+ void rcard.offsetWidth; // restart the open animation
124
+ rcard.classList.add("open");
125
+ var st = document.getElementById("stage").getBoundingClientRect();
126
+ rcard.style.left = Math.max(8, Math.min(x - st.left + 12, st.width - rcard.offsetWidth - 8)) + "px";
127
+ rcard.style.top = Math.max(8, Math.min(y - st.top + 12, st.height - rcard.offsetHeight - 8)) + "px";
128
+ }
129
+ function closeCard() { rcard.classList.remove("open"); }
130
+ rcard.addEventListener("click", function (ev) {
131
+ if (ev.target.id === "rcopy" && cardInfo) copyText(cardInfo.sql, "JOIN copied", rcard.querySelector("pre"));
132
+ else if (ev.target.id === "rboth" && cardInfo) {
133
+ var names = cardInfo.tables.filter(function (t, i, all) { return all.indexOf(t) === i; }).map(function (t) { return byId[t].name; });
134
+ closeCard();
135
+ applyFilter(names.map(function (n) { return n.replace(/[\s,+!]/g, "?"); }).join(" "));
136
+ }
137
+ });
138
+ document.addEventListener("pointerdown", function (ev) {
139
+ if (!rcard.contains(ev.target) && !(ev.target.closest && ev.target.closest(".hit path"))) closeCard();
140
+ });
141
+
142
+ /* The clipboard is blocked in some frames (the VS Code preview): select the text instead and say how to copy it. */
143
+ function copyText(text, done, pre) {
144
+ function fallback() {
145
+ var sel = window.getSelection(), range = document.createRange();
146
+ range.selectNodeContents(pre);
147
+ sel.removeAllRanges();
148
+ sel.addRange(range);
149
+ showToast("Press Ctrl/Cmd+C");
150
+ }
151
+ if (navigator.clipboard && window.isSecureContext) navigator.clipboard.writeText(text).then(function () { showToast(done); }, fallback);
152
+ else fallback();
153
+ }
154
+
43
155
  // Zoomed out on a big diagram, table text is unreadable but repainting it made every zoom step take ~150 ms
44
156
  // at 1000 tables, so it is hidden there. Smaller diagrams always show it.
45
157
  var LOD_TABLES = 300, LOD_ZOOM = 0.3;
@@ -57,7 +169,28 @@
57
169
  return [r.width || 800, r.height || 600];
58
170
  }
59
171
 
60
- function fit(only) {
172
+ /* Zoom, fit and jump glide instead of snapping. The next pointer press or wheel stops a glide. Instant with
173
+ * prefers-reduced-motion, on load and while a shared link is applied. */
174
+ var glide = null, glideTarget = null, reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
175
+ function glideTo(v, ms) {
176
+ cancelGlide();
177
+ if (!ms || reduceMotion.matches || applyingHash) { S.view = v; applyView(); return; }
178
+ var from = { x: S.view.x, y: S.view.y, k: S.view.k }, t0 = performance.now();
179
+ glideTarget = v;
180
+ (function step(now) {
181
+ var f = Math.min(1, (now - t0) / ms), e = 1 - Math.pow(1 - f, 3);
182
+ S.view = { x: from.x + (v.x - from.x) * e, y: from.y + (v.y - from.y) * e, k: from.k + (v.k - from.k) * e };
183
+ applyView();
184
+ glide = f < 1 ? requestAnimationFrame(step) : null;
185
+ if (!glide) glideTarget = null;
186
+ })(t0);
187
+ }
188
+ function cancelGlide() {
189
+ if (glide) cancelAnimationFrame(glide);
190
+ glide = glideTarget = null;
191
+ }
192
+
193
+ function fit(only, animate) {
61
194
  var b = Q.bounds(only ? Q.subset(S.vm, only) : S.vm, S.pos, S.mode), sz = stageSize(), pad = 40;
62
195
  if (S.regions && !only) b = [b[0] - 30, b[1] - 60, b[2] + 30, b[3] + 30]; // room for region borders and labels
63
196
  if (only) sz = [sz[0] - (details.classList.contains("open") ? 400 : 0), sz[1]]; // keep clear of the details panel
@@ -65,24 +198,21 @@
65
198
  var top = 70, bottom = sz[0] < 900 ? 70 : pad, availH = sz[1] - top - bottom;
66
199
  var k = Math.min((sz[0] - 2 * pad) / (b[2] - b[0]), availH / (b[3] - b[1]), 1.5);
67
200
  k = Math.max(k, 0.03); // same floor as manual zoom, so wide diagrams still fit a phone screen
68
- S.view = { k: k, x: (sz[0] - (b[2] - b[0]) * k) / 2 - b[0] * k, y: top + (availH - (b[3] - b[1]) * k) / 2 - b[1] * k };
69
- applyView();
201
+ glideTo({ k: k, x: (sz[0] - (b[2] - b[0]) * k) / 2 - b[0] * k, y: top + (availH - (b[3] - b[1]) * k) / 2 - b[1] * k }, animate ? 350 : 0);
70
202
  }
71
203
 
72
- function zoomAt(factor, cx, cy) {
73
- var v = S.view, k = Math.min(4, Math.max(0.03, v.k * factor));
74
- S.view = { k: k, x: cx - (cx - v.x) * (k / v.k), y: cy - (cy - v.y) * (k / v.k) };
75
- applyView();
204
+ function zoomAt(factor, cx, cy, ms) {
205
+ var v = glideTarget || S.view, k = Math.min(4, Math.max(0.03, v.k * factor)); // quick clicks compound
206
+ glideTo({ k: k, x: cx - (cx - v.x) * (k / v.k), y: cy - (cy - v.y) * (k / v.k) }, ms);
76
207
  }
77
208
 
78
- function centerOn(id) {
209
+ function centerOn(id, animate) {
79
210
  var t = byId[id], p = S.pos[id];
80
211
  if (!t || !p) return;
81
212
  var g = Q.geometry(S.vm, S.mode)[id], sz = stageSize();
82
213
  if (!g) return;
83
214
  var k = Math.max(S.view.k, 0.9);
84
- S.view = { k: k, x: sz[0] / 2 - (p[0] + g.w / 2) * k, y: sz[1] / 2 - (p[1] + g.h / 2) * k };
85
- applyView();
215
+ glideTo({ k: k, x: sz[0] / 2 - (p[0] + g.w / 2) * k, y: sz[1] / 2 - (p[1] + g.h / 2) * k }, animate ? 350 : 0);
86
216
  var el = vp.querySelector('.t[data-id="' + cssEsc(id) + '"]');
87
217
  if (el) {
88
218
  el.classList.add("flash");
@@ -104,6 +234,10 @@
104
234
  }
105
235
 
106
236
  function applyFocus() {
237
+ applyFocusClasses();
238
+ light();
239
+ }
240
+ function applyFocusClasses() {
107
241
  vp.querySelectorAll(".ph").forEach(function (el) { el.classList.remove("ph"); });
108
242
  if (S.path && S.focus && S.path.steps) {
109
243
  var on = {}, edges = [];
@@ -161,7 +295,7 @@
161
295
  if (t) t.classList.add("hl");
162
296
  });
163
297
  });
164
- vp.addEventListener("mouseleave", function () { clearHover(); hideTip(); });
298
+ vp.addEventListener("mouseleave", function () { clearHover(); hideTip(); S.hover = null; scheduleLight(); });
165
299
 
166
300
  /* ------------------------------------------------------------- doc-comment tooltips */
167
301
  var tip = document.getElementById("tip");
@@ -209,17 +343,21 @@
209
343
  /* ------------------------------------------------------------- pointer: pan, click, drag */
210
344
  var drag = null;
211
345
  svg.addEventListener("pointerdown", function (ev) {
346
+ cancelGlide();
212
347
  if (ev.button !== 0) return;
213
348
  var tEl = ev.target.closest && ev.target.closest(".t");
214
- drag = { sx: ev.clientX, sy: ev.clientY, moved: false, table: tEl ? tEl.getAttribute("data-id") : null, el: tEl };
349
+ var hp = !tEl && ev.target.closest && ev.target.closest(".hit path");
350
+ drag = { sx: ev.clientX, sy: ev.clientY, moved: false, table: tEl ? tEl.getAttribute("data-id") : null, el: tEl,
351
+ edge: hp ? hp.getAttribute("data-k") : null };
215
352
  if (drag.table) { // tables are draggable everywhere; only `qdbd serve` saves the new position
216
353
  drag.start = S.pos[drag.table].slice();
354
+ dragPrep(drag);
217
355
  tEl.parentNode.appendChild(tEl); // bring the grabbed table to the front
218
356
  } else {
219
357
  drag.view = { x: S.view.x, y: S.view.y };
220
358
  }
221
359
  svg.classList.add("panning");
222
- svg.setPointerCapture(ev.pointerId);
360
+ try { svg.setPointerCapture(ev.pointerId); } catch (e) { /* synthetic events (the qdbd.dev tour) have no pointer to capture */ }
223
361
  });
224
362
  var raf = null;
225
363
  svg.addEventListener("pointermove", function (ev) {
@@ -231,19 +369,20 @@
231
369
  var p = [Math.round(drag.start[0] + dx / S.view.k), Math.round(drag.start[1] + dy / S.view.k)];
232
370
  S.pos[drag.table] = p;
233
371
  drag.el.setAttribute("transform", "translate(" + p[0] + "," + p[1] + ")");
234
- if (!raf) raf = requestAnimationFrame(function () { raf = null; renderEdges(); });
372
+ if (!raf) raf = requestAnimationFrame(function () { raf = null; if (drag) dragEdges(drag); });
235
373
  } else if (drag.view) {
236
374
  S.view.x = drag.view.x + dx;
237
375
  S.view.y = drag.view.y + dy;
238
376
  applyView();
239
377
  }
240
378
  });
241
- svg.addEventListener("pointerup", function () {
379
+ svg.addEventListener("pointerup", function (ev) {
242
380
  if (!drag) return;
243
381
  svg.classList.remove("panning");
244
382
  var d = drag;
245
383
  drag = null;
246
384
  if (!d.moved) {
385
+ if (d.edge) { if (d.edge === S.edge) select(null); else lockEdge(d.edge, ev.clientX, ev.clientY); return; }
247
386
  // Clicking the selected table toggles it off, unless it was selected quietly: then show its details.
248
387
  if (d.table && d.table === S.focus && !details.classList.contains("open")) showDetails(d.table);
249
388
  else select(d.table && S.focus !== d.table ? d.table : null);
@@ -251,14 +390,45 @@
251
390
  }
252
391
  if (d.start) {
253
392
  if (S.regions) render(); // regions are sized from table positions
393
+ else renderEdges();
254
394
  drawMinimap();
255
395
  var name = byId[d.table].name, body = {};
256
396
  body[name] = S.pos[d.table];
257
397
  if (window.qdbdServe) window.qdbdServe.saveLayout(body);
258
398
  }
259
399
  });
400
+ /* While a table is dragged only its own lines are redrawn (a full redraw per frame was slow at 200+ tables). */
401
+ function dragPrep(d) {
402
+ var on = function (k) { var p = k.split("|"); return p[0] === d.table || p[2] === d.table; };
403
+ d.geo = Q.geometry(S.vm, S.mode);
404
+ d.rels = S.vm.rels.filter(function (r) { return r.a.t === d.table || r.b.t === d.table; });
405
+ d.els = {};
406
+ d.hits = {};
407
+ vp.querySelectorAll(".edges .e").forEach(function (e) { var k = edgeKey(e); if (on(k)) d.els[k] = e; });
408
+ vp.querySelectorAll(".hit path").forEach(function (h) { var k = h.getAttribute("data-k"); if (on(k)) d.hits[k] = h; });
409
+ }
410
+ function dragEdges(d) {
411
+ var tmp = document.createElementNS("http://www.w3.org/2000/svg", "g"), fx = vp.querySelector(".fx");
412
+ tmp.innerHTML = Q.renderEdges({ style: S.vm.style, tables: S.vm.tables, rels: d.rels }, S.pos, S.mode, d.geo);
413
+ [].slice.call(tmp.children).forEach(function (e) {
414
+ var k = edgeKey(e), old = d.els[k], path = e.querySelector(".ep").getAttribute("d");
415
+ if (!old) return;
416
+ e.setAttribute("class", old.getAttribute("class")); // keep dim / lit / ph
417
+ old.replaceWith(e);
418
+ d.els[k] = e;
419
+ if (d.hits[k]) d.hits[k].setAttribute("d", path);
420
+ var items = fx ? fx.querySelectorAll('[data-k="' + cssEsc(k) + '"]') : [], labels = null, li = 0;
421
+ [].forEach.call(items, function (it) {
422
+ if (it.tagName === "path") { it.setAttribute("d", path); return; }
423
+ labels = labels || Q.edgeLabels(S.vm, S.pos, S.mode, S.vm.rels[relByKey[k]], d.geo) || [];
424
+ var l = labels[li++];
425
+ if (l) { it.setAttribute("x", l.x); it.setAttribute("y", l.y); it.setAttribute("text-anchor", l.anchor); }
426
+ });
427
+ });
428
+ }
260
429
  svg.addEventListener("wheel", function (ev) {
261
430
  ev.preventDefault();
431
+ cancelGlide();
262
432
  var r = svg.getBoundingClientRect();
263
433
  var factor = Math.exp(-ev.deltaY * (ev.ctrlKey ? 0.01 : 0.0015));
264
434
  zoomAt(factor, ev.clientX - r.left, ev.clientY - r.top);
@@ -269,9 +439,9 @@
269
439
 
270
440
  /* ------------------------------------------------------------- toolbar */
271
441
  function center() { var s = stageSize(); return [s[0] / 2, s[1] / 2]; }
272
- document.getElementById("zin").onclick = function () { var c = center(); zoomAt(1.25, c[0], c[1]); };
273
- document.getElementById("zout").onclick = function () { var c = center(); zoomAt(0.8, c[0], c[1]); };
274
- document.getElementById("zfit").onclick = function () { fit(); };
442
+ document.getElementById("zin").onclick = function () { var c = center(); zoomAt(1.25, c[0], c[1], 200); };
443
+ document.getElementById("zout").onclick = function () { var c = center(); zoomAt(0.8, c[0], c[1], 200); };
444
+ document.getElementById("zfit").onclick = function () { fit(null, true); };
275
445
  document.querySelectorAll("#modes button").forEach(function (b) {
276
446
  b.onclick = function () {
277
447
  setMode(b.getAttribute("data-mode"));
@@ -325,13 +495,14 @@
325
495
  /* opts.center: pan to it. opts.quiet: don't open the details panel (only refresh it if it's already open). */
326
496
  function select(id, opts) {
327
497
  if ((id || null) !== S.focus) S.path = null;
498
+ S.edge = null; // a table (or nothing) replaces the clicked line
328
499
  S.focus = id && byId[id] ? id : null;
329
500
  scheduleHash();
330
501
  applyFocus();
331
502
  if (S.focus && (!(opts && opts.quiet) || details.classList.contains("open"))) showDetails(S.focus);
332
503
  else closeDetails();
333
504
  markTree();
334
- if (S.focus && opts && opts.center) centerOn(S.focus);
505
+ if (S.focus && opts && opts.center) centerOn(S.focus, true);
335
506
  }
336
507
 
337
508
  /* ------------------------------------------------------------- explorer: search + table tree */
@@ -465,7 +636,7 @@
465
636
  drawTree();
466
637
  if (S.focus) showDetails(S.focus);
467
638
  else closeDetails();
468
- if (!keepView) fit();
639
+ if (!keepView) fit(null, true);
469
640
  scheduleHash();
470
641
  }
471
642
  finput.addEventListener("input", function () {
@@ -590,7 +761,7 @@
590
761
  S.path = { to: target, steps: Q.findPath(S.vm, S.focus, target) };
591
762
  showDetails(S.focus);
592
763
  applyFocus();
593
- if (S.path.steps && !applyingHash) fit([S.focus].concat(S.path.steps.map(function (st) { return st.to.t; })));
764
+ if (S.path.steps && !applyingHash) fit([S.focus].concat(S.path.steps.map(function (st) { return st.to.t; })), true);
594
765
  scheduleHash();
595
766
  return true;
596
767
  }
@@ -606,9 +777,7 @@
606
777
  // Selector syntax treats spaces, commas, + and a leading ! specially; match those characters with "?".
607
778
  applyFilter(names.map(function (n) { return n.replace(/[\s,+!]/g, "?"); }).join(" "));
608
779
  } else if (ev.target.id === "pathsql" && S.path && S.path.steps) {
609
- var sql = Q.pathSql(S.vm, S.focus, S.path.steps);
610
- if (navigator.clipboard && window.isSecureContext) navigator.clipboard.writeText(sql).then(function () { showToast("SQL copied"); });
611
- else window.prompt("Copy SQL:", sql);
780
+ copyText(Q.pathSql(S.vm, S.focus, S.path.steps), "SQL copied", dbody.querySelector("#pathres pre"));
612
781
  }
613
782
  });
614
783
 
@@ -649,6 +818,7 @@
649
818
  r.setAttribute("height", sz[1] / v.k);
650
819
  }
651
820
  function miniPan(ev) {
821
+ cancelGlide();
652
822
  var pt = mini.createSVGPoint();
653
823
  pt.x = ev.clientX;
654
824
  pt.y = ev.clientY;
@@ -658,7 +828,11 @@
658
828
  applyView();
659
829
  }
660
830
  var miniDrag = false;
661
- mini.addEventListener("pointerdown", function (ev) { miniDrag = true; mini.setPointerCapture(ev.pointerId); miniPan(ev); });
831
+ mini.addEventListener("pointerdown", function (ev) {
832
+ miniDrag = true;
833
+ try { mini.setPointerCapture(ev.pointerId); } catch (e) { /* synthetic event */ }
834
+ miniPan(ev);
835
+ });
662
836
  mini.addEventListener("pointermove", function (ev) { if (miniDrag) miniPan(ev); });
663
837
  mini.addEventListener("pointerup", function () { miniDrag = false; });
664
838
 
quickdbd_cli/html.py CHANGED
@@ -90,6 +90,7 @@ BODY = """
90
90
  <svg id="mini" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid meet"></svg>
91
91
  </div>
92
92
  <div class="tip" id="tip" role="tooltip"></div>
93
+ <div class="rcard" id="rcard" role="dialog" aria-label="Relationship"></div>
93
94
  <div class="toast" id="toast" role="status"></div>
94
95
  <div class="diffbar" id="diffbar"></div>
95
96
  <div class="hint">{hint}</div>
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: quickdbd-cli
3
- Version: 0.3.3
3
+ Version: 0.3.4
4
4
  Summary: Compile QuickDBD-syntax schema files into interactive HTML ER diagrams, with no table limit.
5
5
  Project-URL: Homepage, https://qdbd.dev/
6
6
  Project-URL: Documentation, https://qdbd.dev/guide/
@@ -186,7 +186,7 @@ jobs:
186
186
  permissions: { contents: read, pull-requests: write }
187
187
  steps:
188
188
  - uses: actions/checkout@v4
189
- - uses: MarkPhamm/quickdbd-cli@v0.3.3
189
+ - uses: MarkPhamm/quickdbd-cli@v0.3.4
190
190
  with:
191
191
  path: erd/ # schema file(s) or folder(s)
192
192
  output: site/index.html
@@ -1,4 +1,4 @@
1
- quickdbd_cli/__init__.py,sha256=8KcCYTXH99C2-gCLuPILJvtT9YftRWJsartIx6TQ2ZY,22
1
+ quickdbd_cli/__init__.py,sha256=oYLGMpySamd16KLiaBTfRyrAS7_oyp-TOEHmzmeumwg,22
2
2
  quickdbd_cli/__main__.py,sha256=E6Gls0DNz8GQK2K-kOUIx8cYhgANW_CH54VKrfCfs14,52
3
3
  quickdbd_cli/agent.py,sha256=LEuReTkwWDlRLs-GfQQMpMosHDFWbin0EAXRo73j-dA,37202
4
4
  quickdbd_cli/cli.py,sha256=h9pTnGnprYOhyuEpjdd32QyAqR-skJRTBvoipIyCZS0,29522
@@ -7,7 +7,7 @@ quickdbd_cli/dbml.py,sha256=tHJOtMm1PHx4-yqs6rncPqdDfbwPcW8F54j_kKlWkx0,59413
7
7
  quickdbd_cli/dbt.py,sha256=AMHzERmc8WfL1tBLX4bJdPVghIHrqrfx8nMDbZMrueY,23100
8
8
  quickdbd_cli/diff.py,sha256=UBiCE3dk_cN-xSRZFQoLiIIFh_FAVZUpLwsdtHG9eBo,9986
9
9
  quickdbd_cli/export.py,sha256=XbSvWqFmhQHPcBtPHPit1SmhJ6qXrPyGlHYt7FpIMyo,3539
10
- quickdbd_cli/html.py,sha256=fiMsBPkBiCpxz-8x4_SFaeejdnHCAAK0IVlrimbpS5I,7950
10
+ quickdbd_cli/html.py,sha256=pn4Jlcce5nniH7NtGoZjzrTH_dTw4qIhio4bWXGupVw,8031
11
11
  quickdbd_cli/jsrt.py,sha256=_sRkmUbgHDTwK272jeTNUwojfjUKAkv4UeD2E2u3H1A,3464
12
12
  quickdbd_cli/layout.py,sha256=2wrryJ-SSJbMlOdxmtCTzC2_zkTtvRVs-OuOBS1bGEQ,10856
13
13
  quickdbd_cli/mcp.py,sha256=ibUg1a50BBco-hZABdxTexGZi_gclFCdcBDalTvFar8,12350
@@ -28,15 +28,15 @@ quickdbd_cli/assets/editor.js,sha256=Gare_MeAOKlc2QZpTR9nD8Ty-U6JBY3HRWAeMnDkCsU
28
28
  quickdbd_cli/assets/elk-LICENSE.md,sha256=Y36B9KG2tAeVNcSZ_KBeI4z3YFwf9bdtYNfafOlnAMk,14443
29
29
  quickdbd_cli/assets/elk.bundled.js,sha256=EiLkT5U853Rq8jgB5yNwj45vQ2uLN3pqX8dVLzSjB7M,1609707
30
30
  quickdbd_cli/assets/font-metrics.json,sha256=sXSiElxQSK8md8IEePLwh16TTdAqKt9rV140pvMlVS8,8654
31
- quickdbd_cli/assets/render.js,sha256=3Jv7On2X9MghnP-GInfkkfsBFgMC1vg6VkpYfOzgjwM,28944
32
- quickdbd_cli/assets/style.css,sha256=aKIzW8z2V_khDSH7gJ42jisNxnFOKQTKBOuN1YiPt28,16283
31
+ quickdbd_cli/assets/render.js,sha256=WzxX_sUgdz0O4GgiaupKl8J_LI4YCqF9PUxzRbwebwo,31891
32
+ quickdbd_cli/assets/style.css,sha256=ttyGKLti1sU-NMp9CBTDgaznfeU14XjWquFJr9XGsaA,18877
33
33
  quickdbd_cli/assets/syntax-reference.md,sha256=WERcsWjyrfeHQPgxQriWKBsqFJnmWTQ2oRZjKYAHhKc,5322
34
- quickdbd_cli/assets/viewer.js,sha256=iKZBevQusb--Ysm3VPO3zaNAtesYOGjFWbevQ1JwBGU,40057
35
- quickdbd_cli-0.3.3.dist-info/METADATA,sha256=SgSNj1SP_ZSB2LAQFWN4P01B2cGYPKJ8SDouUAZtsnA,17195
36
- quickdbd_cli-0.3.3.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
37
- quickdbd_cli-0.3.3.dist-info/entry_points.txt,sha256=nRhiYYug-XOY95nWUmi4oanico3Zjkfqk2OHtIE6SQ0,47
38
- quickdbd_cli-0.3.3.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
39
- quickdbd_cli-0.3.3.dist-info/licenses/NOTICE,sha256=pyv3BfIYiGzKLU4w-GcBe7GXoXxHqZ97Mf6Zpx_3_i0,2067
40
- quickdbd_cli-0.3.3.dist-info/licenses/src/quickdbd_cli/assets/Inter-LICENSE.txt,sha256=JiSB6ERSGzJvXs0FPlm5jIstp4yO4b27boF0MF5Uk1o,4380
41
- quickdbd_cli-0.3.3.dist-info/licenses/src/quickdbd_cli/assets/elk-LICENSE.md,sha256=Y36B9KG2tAeVNcSZ_KBeI4z3YFwf9bdtYNfafOlnAMk,14443
42
- quickdbd_cli-0.3.3.dist-info/RECORD,,
34
+ quickdbd_cli/assets/viewer.js,sha256=3rKXI80TNv9jF1dJNQa3Gpy0042yTcGfTz_8qg4h5WU,49676
35
+ quickdbd_cli-0.3.4.dist-info/METADATA,sha256=ubcoaxo_WRsI_-5TAdjNoR1G9kWcfsRTT_ibmIXdAzA,17195
36
+ quickdbd_cli-0.3.4.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
37
+ quickdbd_cli-0.3.4.dist-info/entry_points.txt,sha256=nRhiYYug-XOY95nWUmi4oanico3Zjkfqk2OHtIE6SQ0,47
38
+ quickdbd_cli-0.3.4.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
39
+ quickdbd_cli-0.3.4.dist-info/licenses/NOTICE,sha256=pyv3BfIYiGzKLU4w-GcBe7GXoXxHqZ97Mf6Zpx_3_i0,2067
40
+ quickdbd_cli-0.3.4.dist-info/licenses/src/quickdbd_cli/assets/Inter-LICENSE.txt,sha256=JiSB6ERSGzJvXs0FPlm5jIstp4yO4b27boF0MF5Uk1o,4380
41
+ quickdbd_cli-0.3.4.dist-info/licenses/src/quickdbd_cli/assets/elk-LICENSE.md,sha256=Y36B9KG2tAeVNcSZ_KBeI4z3YFwf9bdtYNfafOlnAMk,14443
42
+ quickdbd_cli-0.3.4.dist-info/RECORD,,