quickdbd-cli 0.3.3__tar.gz → 0.3.5__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 (260) hide show
  1. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/PKG-INFO +2 -2
  2. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/README.md +1 -1
  3. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/pyproject.toml +1 -1
  4. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/scripts/build_gallery.py +2 -1
  5. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/scripts/gen_thumbnails.py +17 -0
  6. quickdbd_cli-0.3.5/src/quickdbd_cli/__init__.py +1 -0
  7. quickdbd_cli-0.3.5/src/quickdbd_cli/assets/icon.svg +1 -0
  8. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/src/quickdbd_cli/assets/render.js +52 -0
  9. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/src/quickdbd_cli/assets/style.css +38 -2
  10. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/src/quickdbd_cli/assets/viewer.js +200 -26
  11. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/src/quickdbd_cli/html.py +8 -0
  12. quickdbd_cli-0.3.5/tests/test_home_hook.py +52 -0
  13. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/test_pipeline.py +2 -0
  14. quickdbd_cli-0.3.5/tests/test_render.py +109 -0
  15. quickdbd_cli-0.3.5/tests/test_viewer_contract.py +22 -0
  16. quickdbd_cli-0.3.3/src/quickdbd_cli/__init__.py +0 -1
  17. quickdbd_cli-0.3.3/tests/test_render.py +0 -20
  18. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/.gitignore +0 -0
  19. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/LICENSE +0 -0
  20. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/NOTICE +0 -0
  21. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/examples/quickdbd-sample.txt +0 -0
  22. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/examples/synthetic/finance.txt +0 -0
  23. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/examples/synthetic/hr.txt +0 -0
  24. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/examples/synthetic/inventory.txt +0 -0
  25. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/examples/synthetic/marketing.txt +0 -0
  26. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/examples/synthetic/qdbd.layout.json +0 -0
  27. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/examples/synthetic/sales.txt +0 -0
  28. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/scripts/gen_font_metrics.py +0 -0
  29. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/scripts/gen_schema.py +0 -0
  30. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/scripts/import_dbml_corpus.py +0 -0
  31. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/scripts/release.sh +0 -0
  32. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/src/quickdbd_cli/__main__.py +0 -0
  33. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/src/quickdbd_cli/agent.py +0 -0
  34. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/src/quickdbd_cli/assets/Inter-Bold.ttf +0 -0
  35. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/src/quickdbd_cli/assets/Inter-Bold.woff2 +0 -0
  36. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/src/quickdbd_cli/assets/Inter-LICENSE.txt +0 -0
  37. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/src/quickdbd_cli/assets/Inter-Regular.ttf +0 -0
  38. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/src/quickdbd_cli/assets/Inter-Regular.woff2 +0 -0
  39. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/src/quickdbd_cli/assets/editor.js +0 -0
  40. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/src/quickdbd_cli/assets/elk-LICENSE.md +0 -0
  41. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/src/quickdbd_cli/assets/elk.bundled.js +0 -0
  42. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/src/quickdbd_cli/assets/font-metrics.json +0 -0
  43. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/src/quickdbd_cli/assets/syntax-reference.md +0 -0
  44. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/src/quickdbd_cli/cli.py +0 -0
  45. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/src/quickdbd_cli/convert.py +0 -0
  46. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/src/quickdbd_cli/dbml.py +0 -0
  47. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/src/quickdbd_cli/dbt.py +0 -0
  48. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/src/quickdbd_cli/diff.py +0 -0
  49. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/src/quickdbd_cli/export.py +0 -0
  50. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/src/quickdbd_cli/jsrt.py +0 -0
  51. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/src/quickdbd_cli/layout.py +0 -0
  52. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/src/quickdbd_cli/mcp.py +0 -0
  53. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/src/quickdbd_cli/measure.py +0 -0
  54. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/src/quickdbd_cli/model.py +0 -0
  55. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/src/quickdbd_cli/parser.py +0 -0
  56. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/src/quickdbd_cli/project.py +0 -0
  57. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/src/quickdbd_cli/serve.py +0 -0
  58. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/src/quickdbd_cli/share.py +0 -0
  59. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/src/quickdbd_cli/tags.py +0 -0
  60. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/src/quickdbd_cli/targets.py +0 -0
  61. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml/team.dbml +0 -0
  62. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/LICENSE +0 -0
  63. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/README.md +0 -0
  64. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/binder/alias_of_duplicated_names.dbml +0 -0
  65. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/binder/alias_of_duplicated_names.json +0 -0
  66. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/binder/dep.dbml +0 -0
  67. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/binder/dep.json +0 -0
  68. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/binder/duplicate_columns.dbml +0 -0
  69. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/binder/duplicate_columns.json +0 -0
  70. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/binder/duplicate_enum_field.dbml +0 -0
  71. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/binder/duplicate_enum_field.json +0 -0
  72. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/binder/duplicate_name.dbml +0 -0
  73. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/binder/duplicate_name.json +0 -0
  74. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/binder/duplicate_names.dbml +0 -0
  75. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/binder/duplicate_names.json +0 -0
  76. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/binder/duplicate_table_partial_injections.dbml +0 -0
  77. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/binder/duplicate_table_partial_injections.json +0 -0
  78. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/binder/enum_as_default_column_value.dbml +0 -0
  79. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/binder/enum_as_default_column_value.json +0 -0
  80. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/binder/enum_name.dbml +0 -0
  81. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/binder/enum_name.json +0 -0
  82. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/binder/erroneous.dbml +0 -0
  83. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/binder/erroneous.json +0 -0
  84. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/binder/multiple_notes_in_table_group.dbml +0 -0
  85. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/binder/multiple_notes_in_table_group.json +0 -0
  86. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/binder/nonexisting_column_in_index.dbml +0 -0
  87. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/binder/nonexisting_column_in_index.json +0 -0
  88. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/binder/nonexisting_column_in_index_table_partial.dbml +0 -0
  89. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/binder/nonexisting_column_in_index_table_partial.json +0 -0
  90. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/binder/nonexisting_inline_ref_column_in_table.dbml +0 -0
  91. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/binder/nonexisting_inline_ref_column_in_table.json +0 -0
  92. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/binder/nonexisting_inline_ref_column_in_table_partial.dbml +0 -0
  93. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/binder/nonexisting_inline_ref_column_in_table_partial.json +0 -0
  94. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/binder/ref_validator.dbml +0 -0
  95. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/binder/ref_validator.json +0 -0
  96. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/binder/sticky_notes_validator.dbml +0 -0
  97. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/binder/sticky_notes_validator.json +0 -0
  98. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/binder/table_partial.dbml +0 -0
  99. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/binder/table_partial.json +0 -0
  100. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/binder/unknown_table_group_field.dbml +0 -0
  101. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/binder/unknown_table_group_field.json +0 -0
  102. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/array_type.dbml +0 -0
  103. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/array_type.json +0 -0
  104. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/checks.dbml +0 -0
  105. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/checks.json +0 -0
  106. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/circular_ref_1_inline_1_element.dbml +0 -0
  107. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/circular_ref_1_inline_1_element.json +0 -0
  108. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/circular_ref_2_elements.dbml +0 -0
  109. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/circular_ref_2_elements.json +0 -0
  110. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/circular_ref_2_inlines.dbml +0 -0
  111. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/circular_ref_2_inlines.json +0 -0
  112. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/column_caller_type.dbml +0 -0
  113. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/column_caller_type.json +0 -0
  114. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/comment.dbml +0 -0
  115. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/comment.json +0 -0
  116. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/composite_pk.dbml +0 -0
  117. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/composite_pk.json +0 -0
  118. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/default_tables.dbml +0 -0
  119. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/default_tables.json +0 -0
  120. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/dep.dbml +0 -0
  121. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/dep.json +0 -0
  122. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/dep_color.dbml +0 -0
  123. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/dep_color.json +0 -0
  124. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/dep_duplicate.dbml +0 -0
  125. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/dep_duplicate.json +0 -0
  126. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/dep_pipeline.dbml +0 -0
  127. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/dep_pipeline.json +0 -0
  128. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/enum_as_default_column_value.dbml +0 -0
  129. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/enum_as_default_column_value.json +0 -0
  130. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/enum_tables.dbml +0 -0
  131. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/enum_tables.json +0 -0
  132. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/erroneous.dbml +0 -0
  133. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/erroneous.json +0 -0
  134. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/general_schema.dbml +0 -0
  135. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/general_schema.json +0 -0
  136. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/header_color_tables.dbml +0 -0
  137. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/header_color_tables.json +0 -0
  138. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/index_table_partial.dbml +0 -0
  139. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/index_table_partial.json +0 -0
  140. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/index_tables.dbml +0 -0
  141. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/index_tables.json +0 -0
  142. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/metadata.dbml +0 -0
  143. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/metadata.json +0 -0
  144. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/metadata_errors.dbml +0 -0
  145. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/metadata_errors.json +0 -0
  146. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/multi_notes.dbml +0 -0
  147. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/multi_notes.json +0 -0
  148. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/multiline_string.dbml +0 -0
  149. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/multiline_string.json +0 -0
  150. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/negative_number.dbml +0 -0
  151. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/negative_number.json +0 -0
  152. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/note_normalize.dbml +0 -0
  153. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/note_normalize.json +0 -0
  154. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/note_normalize_with_top_empty_lines.dbml +0 -0
  155. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/note_normalize_with_top_empty_lines.json +0 -0
  156. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/old_undocumented_syntax.dbml +0 -0
  157. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/old_undocumented_syntax.json +0 -0
  158. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/primary_key.dbml +0 -0
  159. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/primary_key.json +0 -0
  160. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/project.dbml +0 -0
  161. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/project.json +0 -0
  162. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/ref_name_and_color_setting.dbml +0 -0
  163. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/ref_name_and_color_setting.json +0 -0
  164. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/ref_settings.dbml +0 -0
  165. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/ref_settings.json +0 -0
  166. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/referential_actions.dbml +0 -0
  167. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/referential_actions.json +0 -0
  168. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/self_referential_ref_in_tablepartial.dbml +0 -0
  169. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/self_referential_ref_in_tablepartial.json +0 -0
  170. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/sticky_notes.dbml +0 -0
  171. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/sticky_notes.json +0 -0
  172. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/table_element.dbml +0 -0
  173. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/table_element.json +0 -0
  174. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/table_group.dbml +0 -0
  175. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/table_group.json +0 -0
  176. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/table_group_element.dbml +0 -0
  177. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/table_group_element.json +0 -0
  178. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/table_group_settings.dbml +0 -0
  179. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/table_group_settings.json +0 -0
  180. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/table_partial.dbml +0 -0
  181. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/table_partial.json +0 -0
  182. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/table_reappear_tablegroup.dbml +0 -0
  183. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/table_reappear_tablegroup.json +0 -0
  184. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/table_settings.dbml +0 -0
  185. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/table_settings.json +0 -0
  186. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/tablepartial_causing_circular_ref.dbml +0 -0
  187. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/tablepartial_causing_circular_ref.json +0 -0
  188. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/two_inline_refs.dbml +0 -0
  189. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/interpreter/two_inline_refs.json +0 -0
  190. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/parser/call_expression.dbml +0 -0
  191. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/parser/call_expression.json +0 -0
  192. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/parser/element_in_simple_body.dbml +0 -0
  193. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/parser/element_in_simple_body.json +0 -0
  194. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/parser/erroneous_setting.dbml +0 -0
  195. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/parser/erroneous_setting.json +0 -0
  196. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/parser/expression.dbml +0 -0
  197. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/parser/expression.json +0 -0
  198. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/parser/last_invalid_number.dbml +0 -0
  199. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/parser/last_invalid_number.json +0 -0
  200. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/parser/list_expression.dbml +0 -0
  201. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/parser/list_expression.json +0 -0
  202. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/parser/trailing_comments.dbml +0 -0
  203. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/parser/trailing_comments.json +0 -0
  204. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/parser/tuple_expression.dbml +0 -0
  205. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/parser/tuple_expression.json +0 -0
  206. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/validator/checks.dbml +0 -0
  207. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/validator/checks.json +0 -0
  208. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/validator/complex_names.dbml +0 -0
  209. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/validator/complex_names.json +0 -0
  210. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/validator/dep.dbml +0 -0
  211. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/validator/dep.json +0 -0
  212. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/validator/enum.dbml +0 -0
  213. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/validator/enum.json +0 -0
  214. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/validator/invalid_args.dbml +0 -0
  215. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/validator/invalid_args.json +0 -0
  216. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/validator/public_schema.dbml +0 -0
  217. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/validator/public_schema.json +0 -0
  218. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/validator/redefined_note.dbml +0 -0
  219. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/validator/redefined_note.json +0 -0
  220. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/validator/ref_error_setting.dbml +0 -0
  221. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/validator/ref_error_setting.json +0 -0
  222. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/validator/ref_in_table.dbml +0 -0
  223. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/validator/ref_in_table.json +0 -0
  224. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/validator/schema_nested_tablegroup.dbml +0 -0
  225. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/validator/schema_nested_tablegroup.json +0 -0
  226. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/validator/table_group_settings.dbml +0 -0
  227. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/validator/table_group_settings.json +0 -0
  228. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/validator/table_partial_check.dbml +0 -0
  229. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/validator/table_partial_check.json +0 -0
  230. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/validator/table_partial_settings_general.dbml +0 -0
  231. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/validator/table_partial_settings_general.json +0 -0
  232. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/validator/table_settings_check.dbml +0 -0
  233. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/validator/table_settings_check.json +0 -0
  234. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/validator/table_settings_general.dbml +0 -0
  235. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/validator/table_settings_general.json +0 -0
  236. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/validator/wrong_sub_element_declarations.dbml +0 -0
  237. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/validator/wrong_sub_element_declarations.json +0 -0
  238. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/validator/wrong_table_partial_injection_syntax.dbml +0 -0
  239. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/dbml-ref/validator/wrong_table_partial_injection_syntax.json +0 -0
  240. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/jaffle/LICENSE +0 -0
  241. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/jaffle/README.md +0 -0
  242. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/jaffle/catalog.json +0 -0
  243. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/jaffle/manifest.json +0 -0
  244. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/fixtures/targets/cases.json +0 -0
  245. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/test_agent.py +0 -0
  246. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/test_convert.py +0 -0
  247. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/test_dbml.py +0 -0
  248. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/test_dbml_pipeline.py +0 -0
  249. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/test_dbt.py +0 -0
  250. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/test_diff.py +0 -0
  251. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/test_gallery.py +0 -0
  252. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/test_mcp.py +0 -0
  253. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/test_packaging.py +0 -0
  254. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/test_parser.py +0 -0
  255. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/test_path.py +0 -0
  256. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/test_select.py +0 -0
  257. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/test_serve.py +0 -0
  258. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/test_share.py +0 -0
  259. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/test_tags.py +0 -0
  260. {quickdbd_cli-0.3.3 → quickdbd_cli-0.3.5}/tests/test_targets.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: quickdbd-cli
3
- Version: 0.3.3
3
+ Version: 0.3.5
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.5
190
190
  with:
191
191
  path: erd/ # schema file(s) or folder(s)
192
192
  output: site/index.html
@@ -148,7 +148,7 @@ jobs:
148
148
  permissions: { contents: read, pull-requests: write }
149
149
  steps:
150
150
  - uses: actions/checkout@v4
151
- - uses: MarkPhamm/quickdbd-cli@v0.3.3
151
+ - uses: MarkPhamm/quickdbd-cli@v0.3.5
152
152
  with:
153
153
  path: erd/ # schema file(s) or folder(s)
154
154
  output: site/index.html
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "quickdbd-cli"
7
- version = "0.3.3"
7
+ version = "0.3.5"
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"
@@ -19,7 +19,7 @@ from pathlib import Path
19
19
  from quickdbd_cli import __version__, diff
20
20
  from quickdbd_cli.dbt import import_dbt
21
21
  from quickdbd_cli.export import font_css
22
- from quickdbd_cli.html import _escape, _json_for_script, render_page
22
+ from quickdbd_cli.html import _escape, _json_for_script, favicon, render_page
23
23
  from quickdbd_cli.parser import compile_sources
24
24
  from quickdbd_cli.project import Project
25
25
 
@@ -175,6 +175,7 @@ def gallery_page(catalog: Catalog, stats: dict[str, dict]) -> str:
175
175
  html = TEMPLATE.read_text(encoding="utf-8")
176
176
  return (
177
177
  html.replace("/*FONT_CSS*/", font_css())
178
+ .replace("<!--FAVICON-->", favicon())
178
179
  .replace("__VERSION__", _escape(__version__))
179
180
  .replace("__DATA__", _json_for_script(data))
180
181
  )
@@ -3,6 +3,7 @@
3
3
  python scripts/gen_thumbnails.py # every demo
4
4
  python scripts/gen_thumbnails.py diff tags # just these
5
5
  python scripts/gen_thumbnails.py hero # the README hero (docs/assets/img/hero.png)
6
+ python scripts/gen_thumbnails.py path-sql # the home page's path-to-SQL shot (docs/assets/img/path-sql.png)
6
7
 
7
8
  Dev-only: needs Playwright (`uv pip install -p .venv playwright`) and drives the system Chrome, so no
8
9
  browser download. Each demo is opened at its `view` + `shot` URL hash; the screenshot is framed in a card
@@ -24,6 +25,8 @@ from quickdbd_cli.html import _escape
24
25
 
25
26
  W, H = 1200, 630
26
27
  SHOT_W, SHOT_H = 1280, 820
28
+ # The home page's "Every relationship is a JOIN" section; hooks/home.py asks join_path for the same two tables.
29
+ PATH_SHOT = ("sakila", "t=payment&p=country")
27
30
  # Hidden in the screenshot: the table list, toolbar, minimap and hint. The details panel and diff bar stay.
28
31
  CLEAN = ".explorer, .bar, .minimap, .hint { display: none !important; }"
29
32
 
@@ -98,6 +101,20 @@ def main(argv: list[str]) -> int:
98
101
  dst = ROOT / "docs" / "assets" / "img" / "hero.png"
99
102
  page.screenshot(path=str(dst))
100
103
  print(f"Wrote {dst.relative_to(ROOT)}")
104
+ if "path-sql" in (set(argv) or {"path-sql"}):
105
+ page.set_viewport_size({"width": 1440, "height": 860})
106
+ page.goto((out / PATH_SHOT[0] / "index.html").as_uri() + f"#{PATH_SHOT[1]}")
107
+ page.wait_for_timeout(800)
108
+ page.get_by_text("Show only this path").click()
109
+ # the home page shows the SQL beside the shot, so the panel goes and the whole path shows
110
+ page.add_style_tag(content=".details, .hint { display: none !important; }")
111
+ page.click("#zfit")
112
+ page.click("#zout") # fit counts tables only; the payment edge loops out to the right
113
+ page.mouse.move(700, 820)
114
+ page.wait_for_timeout(800)
115
+ dst = ROOT / "docs" / "assets" / "img" / "path-sql.png"
116
+ page.screenshot(path=str(dst))
117
+ print(f"Wrote {dst.relative_to(ROOT)}")
101
118
  browser.close()
102
119
  return 0
103
120
 
@@ -0,0 +1 @@
1
+ __version__ = "0.3.5"
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><rect width="128" height="128" rx="28" fill="#2563eb"/><rect x="22" y="22" width="42" height="36" rx="9" fill="#fff"/><path d="M44 58v19a10 10 0 0 0 10 10h12" fill="none" stroke="#fff" stroke-width="8"/><rect x="64" y="70" width="42" height="33" rx="8" fill="#a8c1f7"/></svg>
@@ -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
 
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import json
6
6
  from importlib import resources
7
+ from urllib.parse import quote
7
8
 
8
9
  from .export import font_css
9
10
 
@@ -14,6 +15,11 @@ def _asset(name: str) -> str:
14
15
  return resources.files("quickdbd_cli").joinpath(f"assets/{name}").read_text(encoding="utf-8")
15
16
 
16
17
 
18
+ def favicon() -> str:
19
+ """The VS Code extension's icon as an inline SVG data URL, so pages stay single-file."""
20
+ return f'<link rel="icon" href="data:image/svg+xml,{quote(_asset("icon.svg").strip(), safe=" /=:")}">'
21
+
22
+
17
23
  def _json_for_script(obj) -> str:
18
24
  # Safe inside <script type="application/json">: no "</" sequences.
19
25
  return json.dumps(obj, sort_keys=True, separators=(",", ":"), ensure_ascii=False).replace("</", "<\\/")
@@ -90,6 +96,7 @@ BODY = """
90
96
  <svg id="mini" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid meet"></svg>
91
97
  </div>
92
98
  <div class="tip" id="tip" role="tooltip"></div>
99
+ <div class="rcard" id="rcard" role="dialog" aria-label="Relationship"></div>
93
100
  <div class="toast" id="toast" role="status"></div>
94
101
  <div class="diffbar" id="diffbar"></div>
95
102
  <div class="hint">{hint}</div>
@@ -170,6 +177,7 @@ def render_page(
170
177
  '<!doctype html>\n<html lang="en"><head><meta charset="utf-8">'
171
178
  '<meta name="viewport" content="width=device-width, initial-scale=1">'
172
179
  '<meta name="generator" content="qdbd">'
180
+ f"{favicon()}"
173
181
  f"<title>{_escape(title)}</title>"
174
182
  f"<script>{THEME_JS}</script>"
175
183
  # The font CSS lives in its own <style> so the viewer can reuse it for SVG/PNG export