ScreenNode.svelte 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. <script lang="ts">
  2. /**
  3. * One screen on the Screens view: its path, and the component that renders
  4. * it. An origin — a function that navigates but belongs to no screen (a
  5. * store action after login) — draws dashed, so it reads as a trigger rather
  6. * than a place. The entry screen (`/`) carries a mark.
  7. *
  8. * Hidden handles along the top and bottom, one per port, exactly as the
  9. * Map's module box does — except that here a side may hold both kinds. The
  10. * layout routed every transition (`directional` ports): a return trip
  11. * leaves the TOP of this box and arrives at the BOTTOM of the screen it
  12. * returns to, so the line runs around the boxes instead of through them.
  13. * This only draws the ports the layout decided.
  14. */
  15. import { Handle, Position, type NodeProps } from '@xyflow/svelte';
  16. import type { MapNodeLayout } from '../../lib/map-model';
  17. import type { ScreenNodeInfo } from '../../lib/screens-model';
  18. let { data }: NodeProps = $props();
  19. const node = $derived(
  20. data as unknown as {
  21. layout: MapNodeLayout;
  22. info: ScreenNodeInfo;
  23. selected: boolean;
  24. dimmed: boolean;
  25. onSelect: (id: string) => void;
  26. /** Open what happens from this screen (the Steps picture) — a double-click. */
  27. onOpen?: (id: string) => void;
  28. }
  29. );
  30. const layout = $derived(node.layout);
  31. const info = $derived(node.info);
  32. function portStyle(index: number, total: number): string {
  33. return `left:${((index + 1) / (total + 1)) * 100}%`;
  34. }
  35. </script>
  36. {#each layout.ports.top as port, i (`${port.type}:${port.id}`)}
  37. <Handle
  38. type={port.type}
  39. id={`${port.type === 'source' ? 's' : 't'}:${port.id}`}
  40. position={Position.Top}
  41. style={portStyle(i, layout.ports.top.length)}
  42. isConnectable={false}
  43. />
  44. {/each}
  45. <button
  46. class="snode"
  47. class:sel={node.selected}
  48. class:dimmed={node.dimmed}
  49. class:origin={info.origin}
  50. class:entry={info.entry}
  51. class:unreached={info.unreached}
  52. style={`width:${layout.width}px;height:${layout.height}px`}
  53. onclick={() => node.onSelect(info.id)}
  54. ondblclick={() => node.onOpen?.(info.id)}
  55. aria-pressed={node.selected}
  56. title={(info.origin
  57. ? `${info.label} — navigates, but no screen reaches it within the walk. In ${info.sub}.`
  58. : `${info.label} — rendered by ${info.sub}${info.entry ? '. The entry screen.' : ''}${
  59. info.unreached ? '. No transition in the graph reaches it from the entry.' : ''
  60. }`) + (node.onOpen ? ' Double-click for what happens here.' : '')}
  61. >
  62. <span class="name">{#if info.entry}<span class="mark" aria-hidden="true">●</span>{/if}{info.label}</span>
  63. <span class="sub">{info.sub}</span>
  64. </button>
  65. {#each layout.ports.bottom as port, i (`${port.type}:${port.id}`)}
  66. <Handle
  67. type={port.type}
  68. id={`${port.type === 'source' ? 's' : 't'}:${port.id}`}
  69. position={Position.Bottom}
  70. style={portStyle(i, layout.ports.bottom.length)}
  71. isConnectable={false}
  72. />
  73. {/each}
  74. <style>
  75. .snode {
  76. display: flex;
  77. flex-direction: column;
  78. justify-content: center;
  79. gap: 1px;
  80. box-sizing: border-box;
  81. padding: 0 9px;
  82. border: 1px solid var(--ink);
  83. border-radius: 0;
  84. background: var(--paper);
  85. text-align: left;
  86. cursor: pointer;
  87. font: inherit;
  88. color: var(--ink);
  89. transition: background 90ms linear;
  90. }
  91. .snode:hover,
  92. .snode.sel {
  93. border-width: 2px;
  94. padding: 0 8px;
  95. background: var(--press);
  96. }
  97. .snode.dimmed {
  98. border-color: var(--ink-4);
  99. color: var(--ink-4);
  100. }
  101. .snode.dimmed .sub {
  102. color: var(--ink-4);
  103. }
  104. .snode.origin {
  105. border-style: dashed;
  106. border-color: var(--ink-3);
  107. }
  108. .snode.unreached {
  109. border-color: var(--ink-4);
  110. color: var(--ink-2);
  111. }
  112. .snode:focus-visible {
  113. outline: 2px solid var(--accent);
  114. outline-offset: 1px;
  115. }
  116. .name {
  117. font: 500 13px var(--mono);
  118. line-height: 15px;
  119. white-space: nowrap;
  120. overflow: hidden;
  121. text-overflow: ellipsis;
  122. }
  123. .mark {
  124. color: var(--accent);
  125. margin-right: 5px;
  126. font-size: 9px;
  127. vertical-align: 1px;
  128. }
  129. .sub {
  130. font: 400 11px var(--sans);
  131. line-height: 13px;
  132. color: var(--ink-3);
  133. white-space: nowrap;
  134. overflow: hidden;
  135. text-overflow: ellipsis;
  136. }
  137. </style>