App.svelte 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. <script lang="ts">
  2. import { untrack } from 'svelte';
  3. import TopBar from './components/TopBar.svelte';
  4. import TrailBar from './components/TrailBar.svelte';
  5. import HomeView from './views/HomeView.svelte';
  6. import SymbolView from './views/SymbolView.svelte';
  7. import FileView from './views/FileView.svelte';
  8. import FileCodeView from './views/FileCodeView.svelte';
  9. import MapView from './views/MapView.svelte';
  10. import FlowView from './views/FlowView.svelte';
  11. import NotFoundView from './views/NotFoundView.svelte';
  12. import { router, navigate, back, mapHref, flowHref } from './lib/router.svelte';
  13. import { trail, resolveTrailNames } from './lib/trail.svelte';
  14. import { project } from './lib/project.svelte';
  15. // One `/api/stats` for the whole app: the top bar's counts and the Symbol
  16. // view's blast-radius denominator come out of the same payload.
  17. $effect(() => {
  18. void project.ensure();
  19. });
  20. let topbar: TopBar | null = $state(null);
  21. let route = $derived(router.route);
  22. // Keep the in-memory trail and the `t` param in step. untrack() because the
  23. // body writes the same store it would otherwise read itself into a loop.
  24. $effect(() => {
  25. const current = router.route;
  26. const encoded = router.params.get('t');
  27. untrack(() => {
  28. trail.hydrate(encoded);
  29. if (current.view === 'symbol' && trail.current?.id !== current.id) {
  30. trail.push({ id: current.id });
  31. }
  32. });
  33. });
  34. // Hops restored from a URL carry ids and nothing else; one batched request
  35. // turns the bar back into names. Runs after every trail change, and does
  36. // nothing when every hop already has one.
  37. $effect(() => {
  38. void trail.hops.length;
  39. void resolveTrailNames();
  40. });
  41. function isTypingTarget(target: EventTarget | null): boolean {
  42. if (!(target instanceof HTMLElement)) return false;
  43. return (
  44. target.isContentEditable ||
  45. target instanceof HTMLInputElement ||
  46. target instanceof HTMLTextAreaElement ||
  47. target instanceof HTMLSelectElement
  48. );
  49. }
  50. function onkeydown(event: KeyboardEvent) {
  51. if (event.defaultPrevented) return;
  52. // Cmd/Ctrl+K reaches the search box even from inside another field.
  53. if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'k') {
  54. event.preventDefault();
  55. topbar?.focusSearch();
  56. return;
  57. }
  58. if (event.metaKey || event.ctrlKey || event.altKey) return;
  59. if (isTypingTarget(event.target)) return;
  60. switch (event.key) {
  61. case '/':
  62. event.preventDefault();
  63. topbar?.focusSearch();
  64. break;
  65. case 'm':
  66. event.preventDefault();
  67. navigate(mapHref());
  68. break;
  69. case 'f':
  70. event.preventDefault();
  71. navigate(flowHref());
  72. break;
  73. case 'Backspace':
  74. case '[':
  75. event.preventDefault();
  76. back();
  77. break;
  78. }
  79. }
  80. </script>
  81. <svelte:window {onkeydown} />
  82. <TopBar bind:this={topbar} project={project.name} stats={project.summary} />
  83. <TrailBar />
  84. <main>
  85. {#if route.view === 'symbol'}
  86. <SymbolView id={route.id} line={route.line} />
  87. {:else if route.view === 'file' && route.source}
  88. <FileCodeView path={route.path} line={route.line} />
  89. {:else if route.view === 'file'}
  90. <FileView path={route.path} line={route.line} />
  91. {:else if route.view === 'map'}
  92. <MapView root={route.root} depth={route.depth} tests={route.tests} />
  93. {:else if route.view === 'flow'}
  94. <FlowView
  95. from={route.from}
  96. to={route.to}
  97. symbols={route.symbols}
  98. trailParam={route.trail}
  99. />
  100. {:else if route.view === 'unknown'}
  101. <NotFoundView path={route.path} />
  102. {:else}
  103. <HomeView project={project.name} />
  104. {/if}
  105. </main>
  106. <style>
  107. /* The shell grid lives on #app (index.html's mount host) in app.css —
  108. Svelte's scoped styles cannot reach an element this component does not
  109. render. Only <main>, which it does render, is styled here. */
  110. main {
  111. /* min-height:0 lets the row shrink so the view, not the page, scrolls. */
  112. min-height: 0;
  113. overflow: hidden;
  114. }
  115. </style>