quickdbd-cli 0.1.13__tar.gz → 0.1.15__tar.gz

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.
Files changed (61) hide show
  1. {quickdbd_cli-0.1.13 → quickdbd_cli-0.1.15}/NOTICE +2 -1
  2. {quickdbd_cli-0.1.13 → quickdbd_cli-0.1.15}/PKG-INFO +2 -2
  3. {quickdbd_cli-0.1.13 → quickdbd_cli-0.1.15}/README.md +1 -1
  4. {quickdbd_cli-0.1.13 → quickdbd_cli-0.1.15}/pyproject.toml +1 -1
  5. quickdbd_cli-0.1.15/src/quickdbd_cli/__init__.py +1 -0
  6. {quickdbd_cli-0.1.13 → quickdbd_cli-0.1.15}/src/quickdbd_cli/assets/render.js +9 -9
  7. {quickdbd_cli-0.1.13 → quickdbd_cli-0.1.15}/src/quickdbd_cli/assets/style.css +91 -17
  8. {quickdbd_cli-0.1.13 → quickdbd_cli-0.1.15}/src/quickdbd_cli/assets/viewer.js +20 -0
  9. {quickdbd_cli-0.1.13 → quickdbd_cli-0.1.15}/src/quickdbd_cli/cli.py +31 -1
  10. {quickdbd_cli-0.1.13 → quickdbd_cli-0.1.15}/src/quickdbd_cli/html.py +12 -0
  11. {quickdbd_cli-0.1.13 → quickdbd_cli-0.1.15}/tests/test_gallery.py +5 -0
  12. {quickdbd_cli-0.1.13 → quickdbd_cli-0.1.15}/tests/test_pipeline.py +51 -0
  13. quickdbd_cli-0.1.13/src/quickdbd_cli/__init__.py +0 -1
  14. {quickdbd_cli-0.1.13 → quickdbd_cli-0.1.15}/.gitignore +0 -0
  15. {quickdbd_cli-0.1.13 → quickdbd_cli-0.1.15}/LICENSE +0 -0
  16. {quickdbd_cli-0.1.13 → quickdbd_cli-0.1.15}/examples/quickdbd-sample.txt +0 -0
  17. {quickdbd_cli-0.1.13 → quickdbd_cli-0.1.15}/examples/synthetic/finance.txt +0 -0
  18. {quickdbd_cli-0.1.13 → quickdbd_cli-0.1.15}/examples/synthetic/hr.txt +0 -0
  19. {quickdbd_cli-0.1.13 → quickdbd_cli-0.1.15}/examples/synthetic/inventory.txt +0 -0
  20. {quickdbd_cli-0.1.13 → quickdbd_cli-0.1.15}/examples/synthetic/marketing.txt +0 -0
  21. {quickdbd_cli-0.1.13 → quickdbd_cli-0.1.15}/examples/synthetic/qdbd.layout.json +0 -0
  22. {quickdbd_cli-0.1.13 → quickdbd_cli-0.1.15}/examples/synthetic/sales.txt +0 -0
  23. {quickdbd_cli-0.1.13 → quickdbd_cli-0.1.15}/scripts/build_gallery.py +0 -0
  24. {quickdbd_cli-0.1.13 → quickdbd_cli-0.1.15}/scripts/gen_font_metrics.py +0 -0
  25. {quickdbd_cli-0.1.13 → quickdbd_cli-0.1.15}/scripts/gen_schema.py +0 -0
  26. {quickdbd_cli-0.1.13 → quickdbd_cli-0.1.15}/scripts/gen_thumbnails.py +0 -0
  27. {quickdbd_cli-0.1.13 → quickdbd_cli-0.1.15}/scripts/release.sh +0 -0
  28. {quickdbd_cli-0.1.13 → quickdbd_cli-0.1.15}/src/quickdbd_cli/__main__.py +0 -0
  29. {quickdbd_cli-0.1.13 → quickdbd_cli-0.1.15}/src/quickdbd_cli/assets/Inter-Bold.ttf +0 -0
  30. {quickdbd_cli-0.1.13 → quickdbd_cli-0.1.15}/src/quickdbd_cli/assets/Inter-Bold.woff2 +0 -0
  31. {quickdbd_cli-0.1.13 → quickdbd_cli-0.1.15}/src/quickdbd_cli/assets/Inter-LICENSE.txt +0 -0
  32. {quickdbd_cli-0.1.13 → quickdbd_cli-0.1.15}/src/quickdbd_cli/assets/Inter-Regular.ttf +0 -0
  33. {quickdbd_cli-0.1.13 → quickdbd_cli-0.1.15}/src/quickdbd_cli/assets/Inter-Regular.woff2 +0 -0
  34. {quickdbd_cli-0.1.13 → quickdbd_cli-0.1.15}/src/quickdbd_cli/assets/editor.js +0 -0
  35. {quickdbd_cli-0.1.13 → quickdbd_cli-0.1.15}/src/quickdbd_cli/assets/elk-LICENSE.md +0 -0
  36. {quickdbd_cli-0.1.13 → quickdbd_cli-0.1.15}/src/quickdbd_cli/assets/elk.bundled.js +0 -0
  37. {quickdbd_cli-0.1.13 → quickdbd_cli-0.1.15}/src/quickdbd_cli/assets/font-metrics.json +0 -0
  38. {quickdbd_cli-0.1.13 → quickdbd_cli-0.1.15}/src/quickdbd_cli/dbt.py +0 -0
  39. {quickdbd_cli-0.1.13 → quickdbd_cli-0.1.15}/src/quickdbd_cli/diff.py +0 -0
  40. {quickdbd_cli-0.1.13 → quickdbd_cli-0.1.15}/src/quickdbd_cli/export.py +0 -0
  41. {quickdbd_cli-0.1.13 → quickdbd_cli-0.1.15}/src/quickdbd_cli/jsrt.py +0 -0
  42. {quickdbd_cli-0.1.13 → quickdbd_cli-0.1.15}/src/quickdbd_cli/layout.py +0 -0
  43. {quickdbd_cli-0.1.13 → quickdbd_cli-0.1.15}/src/quickdbd_cli/measure.py +0 -0
  44. {quickdbd_cli-0.1.13 → quickdbd_cli-0.1.15}/src/quickdbd_cli/model.py +0 -0
  45. {quickdbd_cli-0.1.13 → quickdbd_cli-0.1.15}/src/quickdbd_cli/parser.py +0 -0
  46. {quickdbd_cli-0.1.13 → quickdbd_cli-0.1.15}/src/quickdbd_cli/project.py +0 -0
  47. {quickdbd_cli-0.1.13 → quickdbd_cli-0.1.15}/src/quickdbd_cli/serve.py +0 -0
  48. {quickdbd_cli-0.1.13 → quickdbd_cli-0.1.15}/src/quickdbd_cli/share.py +0 -0
  49. {quickdbd_cli-0.1.13 → quickdbd_cli-0.1.15}/src/quickdbd_cli/tags.py +0 -0
  50. {quickdbd_cli-0.1.13 → quickdbd_cli-0.1.15}/tests/fixtures/jaffle/LICENSE +0 -0
  51. {quickdbd_cli-0.1.13 → quickdbd_cli-0.1.15}/tests/fixtures/jaffle/README.md +0 -0
  52. {quickdbd_cli-0.1.13 → quickdbd_cli-0.1.15}/tests/fixtures/jaffle/catalog.json +0 -0
  53. {quickdbd_cli-0.1.13 → quickdbd_cli-0.1.15}/tests/fixtures/jaffle/manifest.json +0 -0
  54. {quickdbd_cli-0.1.13 → quickdbd_cli-0.1.15}/tests/test_dbt.py +0 -0
  55. {quickdbd_cli-0.1.13 → quickdbd_cli-0.1.15}/tests/test_diff.py +0 -0
  56. {quickdbd_cli-0.1.13 → quickdbd_cli-0.1.15}/tests/test_parser.py +0 -0
  57. {quickdbd_cli-0.1.13 → quickdbd_cli-0.1.15}/tests/test_path.py +0 -0
  58. {quickdbd_cli-0.1.13 → quickdbd_cli-0.1.15}/tests/test_select.py +0 -0
  59. {quickdbd_cli-0.1.13 → quickdbd_cli-0.1.15}/tests/test_serve.py +0 -0
  60. {quickdbd_cli-0.1.13 → quickdbd_cli-0.1.15}/tests/test_share.py +0 -0
  61. {quickdbd_cli-0.1.13 → quickdbd_cli-0.1.15}/tests/test_tags.py +0 -0
@@ -16,7 +16,8 @@ This product bundles third-party components, distributed unmodified:
16
16
 
17
17
  The demo gallery (gallery/, not part of the Python package) includes schemas derived from:
18
18
 
19
- * Northwind and AdventureWorks (gallery/northwind, gallery/adventureworks), converted from
19
+ * Northwind, pubs, AdventureWorks and AdventureWorks DW (gallery/northwind, gallery/pubs,
20
+ gallery/adventureworks, gallery/adventureworks-dw), converted from
20
21
  microsoft/sql-server-samples. Copyright (c) Microsoft Corporation. MIT License.
21
22
  * Sakila (gallery/sakila), converted from sakila-schema.sql. Copyright (c) 2006, 2026, Oracle and/or
22
23
  its affiliates. BSD-3-Clause.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: quickdbd-cli
3
- Version: 0.1.13
3
+ Version: 0.1.15
4
4
  Summary: Compile QuickDBD-syntax schema files into interactive HTML ER diagrams, with no table limit.
5
5
  Project-URL: Homepage, https://github.com/MarkPhamm/quickdbd-cli
6
6
  Author: Minh (Mark) Pham
@@ -77,7 +77,7 @@ Every demo is a single HTML page written by `qdbd build`, from a QuickDBD schema
77
77
  | [![200 tables](https://raw.githubusercontent.com/MarkPhamm/quickdbd-cli/main/docs/assets/img/gallery/synthetic.png)](https://qdbd.dev/demo/synthetic/)<br>**[200 tables](https://qdbd.dev/demo/synthetic/)**<br>Five business domains, 200 tables and every relationship type. Search, filter, click a table to focus it. | [![Schema diff](https://raw.githubusercontent.com/MarkPhamm/quickdbd-cli/main/docs/assets/img/gallery/diff.png)](https://qdbd.dev/demo/diff/)<br>**[Schema diff](https://qdbd.dev/demo/diff/)**<br>What a branch changed, coloured in: new tables in green, changed columns in amber, removals listed. |
78
78
  | [![AdventureWorks](https://raw.githubusercontent.com/MarkPhamm/quickdbd-cli/main/docs/assets/img/gallery/adventureworks.png)](https://qdbd.dev/demo/adventureworks/)<br>**[AdventureWorks](https://qdbd.dev/demo/adventureworks/)**<br>Microsoft's bicycle-company OLTP database: 68 tables in five schemas, one region each, with column descriptions. | [![OMOP CDM on Synthea](https://raw.githubusercontent.com/MarkPhamm/quickdbd-cli/main/docs/assets/img/gallery/synthea.png)](https://qdbd.dev/demo/synthea/#t=concept)<br>**[OMOP CDM on Synthea](https://qdbd.dev/demo/synthea/#t=concept)**<br>OHDSI's dbt-synthea: synthetic patient records mapped to the OMOP Common Data Model. 145 tables, 235 relationships from tests. |
79
79
 
80
- [View all 14 demos →](https://qdbd.dev/demo/)
80
+ [View all 18 demos →](https://qdbd.dev/demo/)
81
81
  <!-- gallery:end -->
82
82
 
83
83
  ## Quickstart
@@ -49,7 +49,7 @@ Every demo is a single HTML page written by `qdbd build`, from a QuickDBD schema
49
49
  | [![200 tables](https://raw.githubusercontent.com/MarkPhamm/quickdbd-cli/main/docs/assets/img/gallery/synthetic.png)](https://qdbd.dev/demo/synthetic/)<br>**[200 tables](https://qdbd.dev/demo/synthetic/)**<br>Five business domains, 200 tables and every relationship type. Search, filter, click a table to focus it. | [![Schema diff](https://raw.githubusercontent.com/MarkPhamm/quickdbd-cli/main/docs/assets/img/gallery/diff.png)](https://qdbd.dev/demo/diff/)<br>**[Schema diff](https://qdbd.dev/demo/diff/)**<br>What a branch changed, coloured in: new tables in green, changed columns in amber, removals listed. |
50
50
  | [![AdventureWorks](https://raw.githubusercontent.com/MarkPhamm/quickdbd-cli/main/docs/assets/img/gallery/adventureworks.png)](https://qdbd.dev/demo/adventureworks/)<br>**[AdventureWorks](https://qdbd.dev/demo/adventureworks/)**<br>Microsoft's bicycle-company OLTP database: 68 tables in five schemas, one region each, with column descriptions. | [![OMOP CDM on Synthea](https://raw.githubusercontent.com/MarkPhamm/quickdbd-cli/main/docs/assets/img/gallery/synthea.png)](https://qdbd.dev/demo/synthea/#t=concept)<br>**[OMOP CDM on Synthea](https://qdbd.dev/demo/synthea/#t=concept)**<br>OHDSI's dbt-synthea: synthetic patient records mapped to the OMOP Common Data Model. 145 tables, 235 relationships from tests. |
51
51
 
52
- [View all 14 demos →](https://qdbd.dev/demo/)
52
+ [View all 18 demos →](https://qdbd.dev/demo/)
53
53
  <!-- gallery:end -->
54
54
 
55
55
  ## Quickstart
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "quickdbd-cli"
7
- version = "0.1.13"
7
+ version = "0.1.15"
8
8
  description = "Compile QuickDBD-syntax schema files into interactive HTML ER diagrams, with no table limit."
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -0,0 +1 @@
1
+ __version__ = "0.1.15"
@@ -63,7 +63,7 @@
63
63
  }
64
64
 
65
65
  function keyIcon(x, cy) {
66
- return '<g stroke="' + C.key + '" stroke-width="1.5" fill="none">' +
66
+ return '<g class="ki" stroke="' + C.key + '" stroke-width="1.5" fill="none">' +
67
67
  '<circle cx="' + n(x + 3.5) + '" cy="' + n(cy) + '" r="3"/>' +
68
68
  '<path d="M' + n(x + 6.5) + " " + n(cy) + "h7m-2 0v3m-2.5-3v2" + '"/></g>';
69
69
  }
@@ -84,7 +84,7 @@
84
84
  out.push('<text x="' + (w - st.padX) + '" y="' + n(st.headerH / 2 + 4) + '" text-anchor="end" fill="#fff" font-family="' + FONT +
85
85
  '" font-size="10" font-weight="bold">' + (state === "added" ? "NEW" : "CHANGED") + "</text>");
86
86
  }
87
- out.push('<text x="' + st.padX + '" y="' + n(st.headerH / 2 + st.headerSize * 0.36) + '" fill="' + C.headerText +
87
+ out.push('<text class="tn" x="' + st.padX + '" y="' + n(st.headerH / 2 + st.headerSize * 0.36) + '" fill="' + C.headerText +
88
88
  '" font-family="' + FONT + '" font-size="' + st.headerSize + '">' + esc(t.name) + "</text>");
89
89
  if (docMarks && t.doc) {
90
90
  out.push(docMark(st.padX, st.headerH / 2 + st.headerSize * 0.36 + 3, t.nameW, "rgba(255,255,255,.7)"));
@@ -92,24 +92,24 @@
92
92
  g.cols.forEach(function (c, i) {
93
93
  var y = st.headerH + i * st.rowH, cy = y + st.rowH / 2, base = n(cy + st.rowSize * 0.36);
94
94
  var weight = c.pk || c.fk ? ' font-weight="bold"' : "";
95
- var color = c.pk ? C.pk : c.fk ? C.fk : C.text;
95
+ var color = c.pk ? C.pk : c.fk ? C.fk : C.text, rn = c.pk ? "rn rpk" : c.fk ? "rn rfk" : "rn";
96
96
  out.push('<g class="r" data-c="' + esc(c.id) + '">');
97
97
  out.push('<rect class="rb" y="' + y + '" width="' + w + '" height="' + st.rowH + '" fill="' + C.body + '" fill-opacity="0"/>');
98
98
  var cstate = t.diff && t.diff.cols && t.diff.cols[c.id];
99
99
  if (cstate) out.push('<rect y="' + (y + 3) + '" width="3" height="' + (st.rowH - 6) + '" fill="' + C[cstate] + '"/>');
100
- out.push('<text x="' + st.padX + '" y="' + base + '" fill="' + color + '" font-family="' + FONT + '" font-size="' +
100
+ out.push('<text class="' + rn + '" x="' + st.padX + '" y="' + base + '" fill="' + color + '" font-family="' + FONT + '" font-size="' +
101
101
  st.rowSize + '"' + weight + ">" + esc(c.name) + "</text>");
102
102
  if (docMarks && c.doc) out.push(docMark(st.padX, cy + st.rowSize * 0.36 + 2.5, c.nameW, color));
103
103
  var bx = st.padX + c.nameW + 6;
104
104
  c.badges.forEach(function (b) {
105
105
  var bh = st.badgeSize + 5;
106
- out.push('<rect x="' + n(bx) + '" y="' + n(cy - bh / 2) + '" width="' + n(b.w) + '" height="' + bh + '" rx="3" fill="' + C.badgeBg + '"/>');
107
- out.push('<text x="' + n(bx + st.badgePad) + '" y="' + n(cy + st.badgeSize * 0.36) + '" fill="' + C.badgeText +
106
+ out.push('<rect class="bb" x="' + n(bx) + '" y="' + n(cy - bh / 2) + '" width="' + n(b.w) + '" height="' + bh + '" rx="3" fill="' + C.badgeBg + '"/>');
107
+ out.push('<text class="bt" x="' + n(bx + st.badgePad) + '" y="' + n(cy + st.badgeSize * 0.36) + '" fill="' + C.badgeText +
108
108
  '" font-family="' + FONT + '" font-size="' + st.badgeSize + '">' + esc(b.t) + "</text>");
109
109
  bx += b.w + st.badgeGap;
110
110
  });
111
111
  if (c.pk) out.push(keyIcon(w - st.padX - c.typeW - st.keyW, cy));
112
- out.push('<text x="' + (w - st.padX) + '" y="' + base + '" text-anchor="end" fill="' + C.type + '" font-family="' + FONT +
112
+ out.push('<text class="rt" x="' + (w - st.padX) + '" y="' + base + '" text-anchor="end" fill="' + C.type + '" font-family="' + FONT +
113
113
  '" font-size="' + st.rowSize + '">' + esc(c.type) + "</text>");
114
114
  out.push("</g>");
115
115
  });
@@ -150,7 +150,7 @@
150
150
  s.push('<path d="M' + n(x + dir * 11) + " " + n(y) + "L" + n(x) + " " + n(y - 6) + "M" + n(x + dir * 11) + " " + n(y) + "L" + n(x) + " " + n(y + 6) + '"' + st + "/>");
151
151
  }
152
152
  function circle(o) {
153
- s.push('<circle cx="' + n(x + dir * o) + '" cy="' + n(y) + '" r="3.5" fill="' + C.bg + '" stroke="' + C.edge + '" stroke-width="1.2"/>');
153
+ s.push('<circle class="mc" cx="' + n(x + dir * o) + '" cy="' + n(y) + '" r="3.5" fill="' + C.bg + '" stroke="' + C.edge + '" stroke-width="1.2"/>');
154
154
  }
155
155
  if (card === "one") { bar(7); }
156
156
  else if (card === "many") { crow(); }
@@ -252,7 +252,7 @@
252
252
  var x0 = b[0] - pad - edgePad, y0 = b[1] - pad, w = b[2] - b[0] + 2 * (pad + edgePad), h = b[3] - b[1] + 2 * pad;
253
253
  var style = opts.fontCss ? "<style>" + opts.fontCss + "</style>" : "";
254
254
  return '<svg xmlns="http://www.w3.org/2000/svg" width="' + n(w) + '" height="' + n(h) + '" viewBox="' + n(x0) + " " + n(y0) + " " + n(w) + " " + n(h) + '">' +
255
- style + '<rect x="' + n(x0) + '" y="' + n(y0) + '" width="' + n(w) + '" height="' + n(h) + '" fill="' + C.bg + '"/>' +
255
+ style + '<rect class="bgr" x="' + n(x0) + '" y="' + n(y0) + '" width="' + n(w) + '" height="' + n(h) + '" fill="' + C.bg + '"/>' +
256
256
  renderInner(m, pos, mode, { regions: opts.regions }) + "</svg>";
257
257
  }
258
258
 
@@ -10,10 +10,71 @@
10
10
  --sel: #eaf0f8;
11
11
  --amber: #f0a020;
12
12
  --shadow: 0 1px 2px rgba(16, 24, 40, .05), 0 8px 24px rgba(16, 24, 40, .07);
13
+ --on-ink: #fff;
14
+ --pick: #fde3b0;
15
+ --scroll: #cfd4da;
16
+ --chip-hover: #b8c2d0;
17
+ --tag-bg: #eef1f5;
18
+ --tag-ink: #6b7a8c;
19
+ --tag-pk-bg: #dfe8f6;
20
+ --tag-fk-bg: #ece6f7;
21
+ --tag-fk: #6a4fa3;
22
+ --mini-bg: rgba(255, 255, 255, .94);
23
+ --tip-bg: #15181c;
24
+ /* diagram: the same colours render.js writes as SVG attributes (exports keep those) */
25
+ --t-body: #ffffff;
26
+ --t-border: #dfe3e8;
27
+ --t-text: #333333;
28
+ --t-pk: #2b5797;
29
+ --t-fk: #222222;
30
+ --t-type: #9a9a9a;
31
+ --t-key: #b0b0b0;
32
+ --t-badge-bg: #eef1f5;
33
+ --t-badge-ink: #7a8899;
34
+ --t-edge: #b8bcc2;
35
+ --row-hl: #dbe6f6;
36
+ --row-hover: #eef3fa;
13
37
  }
14
- ::selection { background: #fde3b0; color: var(--ink); }
38
+ /* Dark theme. viewer.js sets data-theme from the toggle, or from prefers-color-scheme when it's on auto. */
39
+ :root[data-theme="dark"] {
40
+ color-scheme: dark;
41
+ --bg: #121418;
42
+ --blue: #7aa7ff;
43
+ --ink: #e6e8eb;
44
+ --ink-2: #b4bac3;
45
+ --muted: #8a929e;
46
+ --panel: #1b1e23;
47
+ --line: #2c313a;
48
+ --hover: #252930;
49
+ --sel: #23324a;
50
+ --shadow: 0 1px 2px rgba(0, 0, 0, .3), 0 8px 24px rgba(0, 0, 0, .35);
51
+ --on-ink: #121418;
52
+ --pick: #5a4210;
53
+ --scroll: #3a404a;
54
+ --chip-hover: #4a5260;
55
+ --tag-bg: #2a2f37;
56
+ --tag-ink: #9aa5b3;
57
+ --tag-pk-bg: #22324a;
58
+ --tag-fk-bg: #2f2840;
59
+ --tag-fk: #b7a2e6;
60
+ --mini-bg: rgba(27, 30, 35, .94);
61
+ --tip-bg: #2a2f37;
62
+ --t-body: #1e2228;
63
+ --t-border: #333944;
64
+ --t-text: #d5d9df;
65
+ --t-pk: #8fb4ff;
66
+ --t-fk: #eceef1;
67
+ --t-type: #7d8693;
68
+ --t-key: #6b7380;
69
+ --t-badge-bg: #2a2f37;
70
+ --t-badge-ink: #9aa5b3;
71
+ --t-edge: #5b6472;
72
+ --row-hl: #24344d;
73
+ --row-hover: #23282f;
74
+ }
75
+ ::selection { background: var(--pick); color: var(--ink); }
15
76
  :focus-visible { outline: 2px solid var(--amber); outline-offset: 1px; }
16
- * { scrollbar-width: thin; scrollbar-color: #cfd4da transparent; }
77
+ * { scrollbar-width: thin; scrollbar-color: var(--scroll) transparent; }
17
78
  * { box-sizing: border-box; }
18
79
  html, body { margin: 0; height: 100%; background: var(--bg); color: var(--ink);
19
80
  font-family: Inter, "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 13px; overflow: hidden; }
@@ -22,13 +83,26 @@ html, body { margin: 0; height: 100%; background: var(--bg); color: var(--ink);
22
83
  #canvas { width: 100%; height: 100%; display: block; cursor: grab; touch-action: none; user-select: none; }
23
84
  #canvas.panning { cursor: grabbing; }
24
85
 
86
+ /* diagram colours: CSS beats the SVG presentation attributes, so the page can theme them */
87
+ .tb { fill: var(--t-body); stroke: var(--t-border); }
88
+ .rn { fill: var(--t-text); }
89
+ .rn.rpk { fill: var(--t-pk); }
90
+ .rn.rfk { fill: var(--t-fk); }
91
+ .r .dm { stroke: var(--t-text); }
92
+ .rt { fill: var(--t-type); }
93
+ .ki { stroke: var(--t-key); }
94
+ .bb { fill: var(--t-badge-bg); }
95
+ .bt { fill: var(--t-badge-ink); }
96
+ .e path { stroke: var(--t-edge); }
97
+ .e .mc { stroke: var(--t-edge); fill: var(--bg); }
98
+
25
99
  /* states */
26
100
  .t, .e { transition: opacity .15s; }
27
101
  .focus .t.dim, .focus .e.dim { opacity: .12; }
28
102
  .e.hl .ep { stroke: var(--blue); stroke-width: 2.2; }
29
103
  .e.hl path, .e.hl circle { stroke: var(--blue); }
30
- .r.hl .rb { fill: #dbe6f6; fill-opacity: 1; }
31
- .r:hover .rb { fill: #eef3fa; fill-opacity: 1; }
104
+ .r.hl .rb { fill: var(--row-hl); fill-opacity: 1; }
105
+ .r:hover .rb { fill: var(--row-hover); fill-opacity: 1; }
32
106
  .t.flash .tb { stroke: #f0a020; stroke-width: 3; }
33
107
  .t { cursor: grab; }
34
108
  #canvas.panning .t { cursor: grabbing; }
@@ -39,13 +113,13 @@ html, body { margin: 0; height: 100%; background: var(--bg); color: var(--ink);
39
113
  .bar button, .seg button { border: 0; background: transparent; color: var(--ink-2); font: inherit; font-weight: 500;
40
114
  cursor: pointer; padding: 5px 9px; border-radius: 6px; transition: background .12s, color .12s; }
41
115
  .bar button:hover, .seg button:hover { background: var(--hover); color: var(--ink); }
42
- .bar button.on { background: var(--ink); color: #fff; }
116
+ .bar button.on { background: var(--ink); color: var(--on-ink); }
43
117
  .zoomval { min-width: 46px; text-align: center; color: var(--muted); font-variant-numeric: tabular-nums; }
44
118
  .seg { display: flex; background: var(--hover); border-radius: 7px; padding: 2px; }
45
119
  .seg button.on { background: var(--panel); color: var(--ink); box-shadow: 0 1px 2px rgba(16, 24, 40, .12); }
46
120
  .sep { width: 1px; height: 18px; background: var(--line); margin: 0 4px; }
47
121
  .bar button svg, .exhead button svg, .details .close svg { display: block; }
48
- #zin, #zout, #zfit { padding: 5px 6px; }
122
+ #zin, #zout, #zfit, #xtheme { padding: 5px 6px; }
49
123
 
50
124
  /* explorer (left): search + table tree grouped by file/folder */
51
125
  .explorer { position: absolute; top: 14px; left: 16px; width: 270px; max-height: calc(100% - 60px); display: flex;
@@ -62,7 +136,7 @@ html, body { margin: 0; height: 100%; background: var(--bg); color: var(--ink);
62
136
  .exfilter { padding: 6px 6px 4px; border-bottom: 1px solid var(--line); }
63
137
  .explorer.collapsed .exfilter { display: none; }
64
138
  .exfilter input { width: 100%; padding: 5px 8px; border: 1px solid var(--line); border-radius: 7px; font: 12px ui-monospace, Menlo, Consolas, monospace;
65
- color: var(--ink); outline: none; }
139
+ color: var(--ink); background: var(--panel); outline: none; }
66
140
  .exfilter input:focus { border-color: var(--ink-2); }
67
141
  .exfilter input.bad { border-color: #e57373; }
68
142
  .fstat { font-size: 11px; color: var(--muted); padding: 3px 2px 0; min-height: 14px; }
@@ -70,10 +144,10 @@ html, body { margin: 0; height: 100%; background: var(--bg); color: var(--ink);
70
144
  .fstat a { color: var(--blue); cursor: pointer; margin-left: 6px; }
71
145
  .tagbar { display: flex; flex-wrap: wrap; gap: 4px; padding: 4px 0 2px; }
72
146
  .tchip { display: inline-flex; align-items: center; gap: 4px; font: 11px/1.6 inherit; padding: 0 7px 0 5px; border: 1px solid var(--line);
73
- border-radius: 10px; background: #fff; color: var(--text, #333); cursor: pointer; }
147
+ border-radius: 10px; background: var(--panel); color: var(--ink); cursor: pointer; }
74
148
  .tchip i { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
75
149
  .tchip small { color: var(--muted); }
76
- .tchip:hover { border-color: #b8c2d0; }
150
+ .tchip:hover { border-color: var(--chip-hover); }
77
151
  .tchip.on { border-color: var(--ink-2); background: var(--sel); }
78
152
  .tdot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; margin: 0 6px 1px -13px; vertical-align: middle; }
79
153
  .tree.flat .tdot { margin-left: 0; }
@@ -117,10 +191,10 @@ html, body { margin: 0; height: 100%; background: var(--bg); color: var(--ink);
117
191
  .details td.cn { font-weight: 600; word-break: break-word; }
118
192
  .details td.ct { color: var(--muted); text-align: right; white-space: nowrap; }
119
193
  .details .cd { color: var(--ink-2); font-weight: normal; margin-top: 2px; white-space: pre-wrap; }
120
- .details .tag { display: inline-block; font-size: 10px; padding: 0 5px; border-radius: 3px; background: #eef1f5;
121
- color: #6b7a8c; margin: 0 3px 0 0; font-weight: normal; }
122
- .details .tag.pk { background: #dfe8f6; color: var(--blue); }
123
- .details .tag.fk { background: #ece6f7; color: #6a4fa3; }
194
+ .details .tag { display: inline-block; font-size: 10px; padding: 0 5px; border-radius: 3px; background: var(--tag-bg);
195
+ color: var(--tag-ink); margin: 0 3px 0 0; font-weight: normal; }
196
+ .details .tag.pk { background: var(--tag-pk-bg); color: var(--blue); }
197
+ .details .tag.fk { background: var(--tag-fk-bg); color: var(--tag-fk); }
124
198
  .details ul { list-style: none; margin: 0; padding: 0; }
125
199
  .details li { padding: 6px 0; border-top: 1px solid var(--line); }
126
200
  .details a { color: var(--blue); text-decoration: none; cursor: pointer; }
@@ -130,19 +204,19 @@ html, body { margin: 0; height: 100%; background: var(--bg); color: var(--ink);
130
204
  .details .hidden { color: var(--muted); }
131
205
 
132
206
  /* minimap (bottom right) */
133
- .minimap { position: absolute; right: 16px; bottom: 16px; width: 200px; height: 140px; background: rgba(255, 255, 255, .94);
207
+ .minimap { position: absolute; right: 16px; bottom: 16px; width: 200px; height: 140px; background: var(--mini-bg);
134
208
  border: 1px solid var(--line); border-radius: 10px; box-shadow: var(--shadow); overflow: hidden; cursor: pointer; }
135
209
  .minimap svg { width: 100%; height: 100%; display: block; }
136
210
  .minimap .vpr { fill: rgba(43,87,151,.12); stroke: var(--blue); stroke-width: 1.5; vector-effect: non-scaling-stroke; }
137
211
 
138
- .toast { position: absolute; left: 50%; bottom: 24px; transform: translateX(-50%); background: var(--ink); color: #fff;
212
+ .toast { position: absolute; left: 50%; bottom: 24px; transform: translateX(-50%); background: var(--tip-bg); color: #fff;
139
213
  padding: 8px 14px; border-radius: 8px; font-size: 12px; opacity: 0; transition: opacity .2s; pointer-events: none; z-index: 6; }
140
214
  .toast.open { opacity: 1; }
141
215
 
142
216
  /* path finder (in the details panel) */
143
217
  .details .pathbox { display: flex; gap: 6px; margin: 10px 0 4px; }
144
218
  .details .pathbox input { flex: 1; min-width: 0; padding: 6px 9px; border: 1px solid var(--line); border-radius: 7px; font: inherit;
145
- color: var(--ink); outline: none; }
219
+ color: var(--ink); background: var(--panel); outline: none; }
146
220
  .details .pathbox input:focus { border-color: var(--ink-2); }
147
221
  .details .pathres ol { margin: 6px 0 8px 18px; padding: 0; }
148
222
  .details .pathres li { border: 0; padding: 2px 0; }
@@ -173,7 +247,7 @@ html, body { margin: 0; height: 100%; background: var(--bg); color: var(--ink);
173
247
  .details .state.changed { background: #d48806; }
174
248
 
175
249
  /* doc-comment tooltip */
176
- .tip { position: absolute; display: none; max-width: 340px; background: var(--ink); color: #eef0f2; border-radius: 8px;
250
+ .tip { position: absolute; display: none; max-width: 340px; background: var(--tip-bg); color: #eef0f2; border-radius: 8px;
177
251
  padding: 8px 10px; font-size: 12px; line-height: 1.45; pointer-events: none; box-shadow: 0 4px 14px rgba(0,0,0,.25);
178
252
  z-index: 5; }
179
253
  .tip.open { display: block; }
@@ -287,6 +287,26 @@
287
287
  }
288
288
  regionBtn.onclick = function () { setRegions(!S.regions); };
289
289
 
290
+ /* Theme: auto (follow the OS) -> dark -> light. A viewer preference, so localStorage, not the URL hash.
291
+ * The <head> script applies it before paint; exports always use render.js's light colours. */
292
+ var themeBtn = document.getElementById("xtheme"), darkQuery = window.matchMedia("(prefers-color-scheme: dark)");
293
+ function themePref() {
294
+ try { var t = localStorage.getItem("qdbd-theme"); return t === "light" || t === "dark" ? t : "auto"; } catch (e) { return "auto"; }
295
+ }
296
+ function applyTheme() {
297
+ var pref = themePref(), t = pref === "auto" ? (darkQuery.matches ? "dark" : "light") : pref;
298
+ document.documentElement.setAttribute("data-theme", t);
299
+ themeBtn.title = "Theme: " + pref;
300
+ }
301
+ themeBtn.onclick = function () {
302
+ var next = { auto: "dark", dark: "light", light: "auto" }[themePref()];
303
+ try { if (next === "auto") localStorage.removeItem("qdbd-theme"); else localStorage.setItem("qdbd-theme", next); } catch (e) { /* private mode */ }
304
+ applyTheme();
305
+ showToast("Theme: " + next);
306
+ };
307
+ if (darkQuery.addEventListener) darkQuery.addEventListener("change", applyTheme);
308
+ applyTheme();
309
+
290
310
  function setMode(mode) {
291
311
  S.mode = ["full", "keys", "header"].indexOf(mode) >= 0 ? mode : "full";
292
312
  document.querySelectorAll("#modes button").forEach(function (o) { o.classList.toggle("on", o.getAttribute("data-mode") === S.mode); });
@@ -3,11 +3,13 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import argparse
6
+ import dataclasses
7
+ import json
6
8
  import sys
7
9
  from pathlib import Path
8
10
 
9
11
  from . import __version__
10
- from .model import Schema
12
+ from .model import Diagnostic, Schema
11
13
  from .project import Project, SelectionError, selector_expr
12
14
 
13
15
  OK = "No errors found. Good job!"
@@ -36,9 +38,31 @@ def _title(project: Project) -> str:
36
38
  return "diagram"
37
39
 
38
40
 
41
+ def _github_annotation(d: Diagnostic, level: str) -> str:
42
+ """A GitHub Actions workflow command, which shows `d` inline on the PR's changed lines."""
43
+
44
+ def esc(s: str) -> str:
45
+ return s.replace("%", "%25").replace("\r", "%0D").replace("\n", "%0A")
46
+
47
+ def esc_prop(s: str) -> str:
48
+ return esc(s).replace(":", "%3A").replace(",", "%2C")
49
+
50
+ return f"::{level} file={esc_prop(d.path)},line={d.line},title=qdbd::{esc(d.message)}"
51
+
52
+
39
53
  def cmd_check(args) -> int:
40
54
  project = _open(args)
41
55
  schema = project.compile()
56
+ if args.format == "json":
57
+ ok = not schema.errors and not (args.strict and schema.warnings)
58
+ diags = [dataclasses.asdict(d) for d in schema.diagnostics]
59
+ report = {"ok": ok, "errors": len(schema.errors), "warnings": len(schema.warnings), "diagnostics": diags}
60
+ print(json.dumps(report, indent=2))
61
+ return 0 if ok else 1
62
+ if args.format == "github":
63
+ # Under --strict a warning fails the job, so it's annotated as an error.
64
+ for d in schema.diagnostics:
65
+ print(_github_annotation(d, "error" if args.strict else d.severity))
42
66
  ok = _print_diagnostics(schema, args.strict)
43
67
  if ok and not schema.diagnostics:
44
68
  print(OK)
@@ -345,6 +369,12 @@ def build_parser() -> argparse.ArgumentParser:
345
369
  sp = sub.add_parser("check", help="validate schema files")
346
370
  paths(sp)
347
371
  sp.add_argument("--strict", action="store_true", help="treat warnings (QuickDBD incompatibilities) as errors")
372
+ sp.add_argument(
373
+ "--format",
374
+ choices=["text", "github", "json"],
375
+ default="text",
376
+ help="github: also emit GitHub Actions annotations (inline on PR lines); json: a machine-readable report",
377
+ )
348
378
  sp.set_defaults(func=cmd_check)
349
379
 
350
380
  sp = sub.add_parser("build", help="write an interactive HTML diagram")
@@ -39,6 +39,7 @@ ICONS = {
39
39
  "icon_minus": _icon("M5 12h14"),
40
40
  "icon_plus": _icon("M12 5v14M5 12h14"),
41
41
  "icon_fit": _icon("M4 9V4h5M15 4h5v5M20 15v5h-5M9 20H4v-5"),
42
+ "icon_theme": _icon("M12 3a9 9 0 1 0 0 18a9 9 0 1 0 0-18zM12 3v18M12 8h4.5M12 12h6M12 16h4.5"),
42
43
  }
43
44
 
44
45
 
@@ -82,6 +83,8 @@ BODY = """
82
83
  <span class="zoomval" id="zoomval">100%</span>
83
84
  <button id="zin" title="Zoom in" aria-label="Zoom in">{icon_plus}</button>
84
85
  <button id="zfit" title="Fit to screen" aria-label="Fit to screen">{icon_fit}</button>
86
+ <div class="sep"></div>
87
+ <button id="xtheme" title="Theme: auto" aria-label="Switch light / dark theme">{icon_theme}</button>
85
88
  </div>
86
89
  <div class="minimap">
87
90
  <svg id="mini" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid meet"></svg>
@@ -111,6 +114,14 @@ FILTER_HELP = (
111
114
  )
112
115
 
113
116
 
117
+ # Runs in <head> so the page paints in the right theme. viewer.js owns the toggle; keep the key in sync.
118
+ THEME_JS = (
119
+ "(function(){var t;try{t=localStorage.getItem('qdbd-theme');}catch(e){}"
120
+ "if(t!=='light'&&t!=='dark')t=matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light';"
121
+ "document.documentElement.setAttribute('data-theme',t);})();"
122
+ )
123
+
124
+
114
125
  # Read-only live view: refetch the model whenever the server's version changes. Relative URLs, so it
115
126
  # also works behind a tunnel or a path prefix.
116
127
  LIVE_JS = (
@@ -151,6 +162,7 @@ def render_page(model: dict, positions: dict, title: str, serve: bool = False, l
151
162
  '<meta name="viewport" content="width=device-width, initial-scale=1">'
152
163
  '<meta name="generator" content="qdbd">'
153
164
  f"<title>{_escape(title)}</title>"
165
+ f"<script>{THEME_JS}</script>"
154
166
  # The font CSS lives in its own <style> so the viewer can reuse it for SVG/PNG export
155
167
  # instead of embedding the fonts a second time.
156
168
  f'<style id="qdbd-font">{font_css()}</style>'
@@ -91,6 +91,11 @@ def test_sample_databases_and_dbt_projects(built):
91
91
  assert sizes["adventureworks"][0] == 68 and sizes["adventureworks"][1] >= 90
92
92
  assert sizes["synthea"] == (145, 235)
93
93
  assert sizes["shopify"][0] == 155 and sizes["shopify"][1] > 150 # every link inferred
94
+ for d, n in (("pubs", 11), ("adventureworks-dw", 28), ("saas-billing", 13), ("courses", 14)):
95
+ m = _model(built / d / "index.html")
96
+ assert len(m["tables"]) == n and m["rels"], d
97
+ dw = _model(built / "adventureworks-dw" / "index.html")
98
+ assert {"dim", "fact"} <= {t["name"] for t in dw["tags"]} # coloured by the Dim/Fact name prefixes
94
99
  aw = _model(built / "adventureworks" / "index.html")
95
100
  assert aw["regions"] and any(t.get("doc") for t in aw["tables"]) # MS_Description -> tooltips
96
101
 
@@ -136,6 +136,47 @@ def test_check_strict_warnings(tmp_path):
136
136
  assert main(["check", "--strict", str(f)]) == 1
137
137
 
138
138
 
139
+ def test_check_format_github(tmp_path, capsys):
140
+ bad = tmp_path / "bad.txt"
141
+ bad.write_text("T\n-\nA int foo\nB decimal(10, 2)\n")
142
+ assert main(["check", "--format", "github", str(bad)]) == 1
143
+ out = capsys.readouterr().out.splitlines()
144
+ assert f"::error file={bad},line=3,title=qdbd::Unrecognized keyword 'foo'" in out
145
+ assert any(line.startswith(f"::warning file={bad},line=4,title=qdbd::QuickDBD would reject this") for line in out)
146
+
147
+
148
+ def test_check_format_github_strict_promotes_warnings(tmp_path, capsys):
149
+ f = tmp_path / "w.txt"
150
+ f.write_text("T\n-\nA decimal(10, 2)\n")
151
+ assert main(["check", "--format", "github", "--strict", str(f)]) == 1
152
+ assert capsys.readouterr().out.startswith(f"::error file={f},line=3,")
153
+
154
+
155
+ def test_github_annotation_escaping():
156
+ from quickdbd_cli.cli import _github_annotation
157
+ from quickdbd_cli.model import Diagnostic
158
+
159
+ d = Diagnostic("a,b:c.txt", 2, "50% bad\nnext", "warning")
160
+ assert _github_annotation(d, "warning") == "::warning file=a%2Cb%3Ac.txt,line=2,title=qdbd::50%25 bad%0Anext"
161
+
162
+
163
+ def test_check_format_json(tmp_path, capsys):
164
+ bad = tmp_path / "bad.txt"
165
+ bad.write_text("T\n-\nA int foo\n")
166
+ assert main(["check", "--format", "json", str(bad)]) == 1
167
+ data = json.loads(capsys.readouterr().out)
168
+ assert data["ok"] is False and data["errors"] == 1 and data["warnings"] == 0
169
+ assert data["diagnostics"] == [
170
+ {"path": str(bad), "line": 3, "severity": "error", "message": "Unrecognized keyword 'foo'"}
171
+ ]
172
+
173
+
174
+ def test_check_format_json_ok(capsys):
175
+ assert main(["check", "--format", "json", str(SAMPLE)]) == 0
176
+ data = json.loads(capsys.readouterr().out)
177
+ assert data == {"ok": True, "errors": 0, "warnings": 0, "diagnostics": []}
178
+
179
+
139
180
  def test_build_is_deterministic_and_does_not_write_sidecar(tmp_path):
140
181
  work = tmp_path / "s"
141
182
  shutil.copytree(SYNTH, work)
@@ -240,6 +281,16 @@ def test_page_has_share_link_controls():
240
281
  assert 'id="xlink"' in html and 'id="toast"' in html
241
282
 
242
283
 
284
+ def test_page_has_theme_toggle_and_exports_stay_light():
285
+ m = model_json(compile_text(SMALL))
286
+ pos = {"Customer": [0, 0], "Order": [300, 0]}
287
+ html = render_page(m, pos, "t")
288
+ assert 'id="xtheme"' in html and ':root[data-theme="dark"]' in html
289
+ assert html.index("qdbd-theme") < html.index("</head>") # theme applied before first paint
290
+ svg = export.render_svg(m, pos, embed_font=False)
291
+ assert "data-theme" not in svg and 'fill="#ffffff"' in svg and "#1e2228" not in svg
292
+
293
+
243
294
  def test_drawio_export(tmp_path):
244
295
  import xml.etree.ElementTree as ET
245
296
 
@@ -1 +0,0 @@
1
- __version__ = "0.1.13"
File without changes
File without changes