StepNode.svelte 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. <script lang="ts">
  2. /**
  3. * One step on the Steps view. The box is the Screens view's screen box with
  4. * a kind: a screen is drawn exactly as there; a handler is a plain box; a
  5. * native call or a native event carries an accent rule on its left, where
  6. * the language changes under the code — and so does an endpoint the code
  7. * crosses to (`⇢ POST /api/users`) or a job, an event, a message arriving;
  8. * a store action sits on `--paper-2`;
  9. * a call that leaves the index is dashed, like a trigger no screen reaches
  10. * on the Screens view — a place the graph cannot follow into. The anchor
  11. * carries the entry mark. A step the walk was cut at ends its name with an
  12. * ellipsis, and its tooltip says which cap.
  13. *
  14. * Hidden handles along the top and bottom, one per port the layout decided
  15. * (`directional` ports), exactly as the screen box. A click selects the step;
  16. * a double-click starts the picture there (the panel's *Start here →*) — an
  17. * endpoint or another screen reached as a boundary opens as its own chapter.
  18. */
  19. import { Handle, Position, type NodeProps } from '@xyflow/svelte';
  20. import type { MapNodeLayout } from '../../lib/map-model';
  21. import { kindWord, type ProjectKind, type StepNodeInfo } from '../../lib/steps-model';
  22. let { data }: NodeProps = $props();
  23. const node = $derived(
  24. data as unknown as {
  25. layout: MapNodeLayout;
  26. info: StepNodeInfo;
  27. project: ProjectKind;
  28. selected: boolean;
  29. dimmed: boolean;
  30. onSelect: (id: string) => void;
  31. /** Re-anchor the picture on this step — a double-click; absent for a step with no symbol. */
  32. onStart?: (id: string) => void;
  33. }
  34. );
  35. const layout = $derived(node.layout);
  36. const info = $derived(node.info);
  37. const step = $derived(info.step);
  38. const cutNote = $derived.by(() => {
  39. switch (step.cut) {
  40. case 'depth':
  41. return ' More happens past the depth of this picture — start here to see it.';
  42. case 'fan-out':
  43. return ' It reaches more than the walk follows from one node.';
  44. case 'folded':
  45. return ' The walk folded as much plumbing as it allows from one step.';
  46. case 'steps':
  47. return ' The picture reached its size limit here.';
  48. case 'screen':
  49. return ` Another ${kindWord('screen', node.project, step)} — a chapter of its own. Start here to see what happens on it.`;
  50. case 'component':
  51. return ' The event lands in a component of another screen — a picture of its own. Start here to see it.';
  52. default:
  53. return '';
  54. }
  55. });
  56. function portStyle(index: number, total: number): string {
  57. return `left:${((index + 1) / (total + 1)) * 100}%`;
  58. }
  59. </script>
  60. {#each layout.ports.top as port, i (`${port.type}:${port.id}`)}
  61. <Handle
  62. type={port.type}
  63. id={`${port.type === 'source' ? 's' : 't'}:${port.id}`}
  64. position={Position.Top}
  65. style={portStyle(i, layout.ports.top.length)}
  66. isConnectable={false}
  67. />
  68. {/each}
  69. <button
  70. class={`snode k-${step.kind}`}
  71. class:sel={node.selected}
  72. class:dimmed={node.dimmed}
  73. class:anchor={step.anchor}
  74. style={`width:${layout.width}px;height:${layout.height}px`}
  75. onclick={() => node.onSelect(info.id)}
  76. ondblclick={() => node.onStart?.(info.id)}
  77. aria-pressed={node.selected}
  78. title={`${info.label} — ${step.anchor ? 'where this picture starts; ' : ''}${kindWord(step.kind, node.project, step)}. ${info.sub}.${cutNote}${node.onStart && !step.anchor ? ' Double-click to start here.' : ''}`}
  79. >
  80. <span class="name"
  81. >{#if step.anchor}<span class="mark" aria-hidden="true">●</span>{/if}{info.label}{#if step.cut !== null}<span
  82. class="more"
  83. aria-hidden="true"> …</span
  84. >{/if}</span
  85. >
  86. <span class="sub">{info.sub}</span>
  87. </button>
  88. {#each layout.ports.bottom as port, i (`${port.type}:${port.id}`)}
  89. <Handle
  90. type={port.type}
  91. id={`${port.type === 'source' ? 's' : 't'}:${port.id}`}
  92. position={Position.Bottom}
  93. style={portStyle(i, layout.ports.bottom.length)}
  94. isConnectable={false}
  95. />
  96. {/each}
  97. <style>
  98. .snode {
  99. display: flex;
  100. flex-direction: column;
  101. justify-content: center;
  102. gap: 1px;
  103. box-sizing: border-box;
  104. padding: 0 9px;
  105. border: 1px solid var(--ink);
  106. border-radius: 0;
  107. background: var(--paper);
  108. text-align: left;
  109. cursor: pointer;
  110. font: inherit;
  111. color: var(--ink);
  112. transition: background 90ms linear;
  113. }
  114. .snode:hover,
  115. .snode.sel {
  116. border-width: 2px;
  117. padding: 0 8px;
  118. background: var(--press);
  119. }
  120. .snode.dimmed {
  121. border-color: var(--ink-4);
  122. color: var(--ink-4);
  123. }
  124. .snode.dimmed .sub {
  125. color: var(--ink-4);
  126. }
  127. /* The language changes under the code: a rule where it does. */
  128. .snode.k-bridge,
  129. .snode.k-event {
  130. border-left: 3px solid var(--accent);
  131. padding-left: 7px;
  132. }
  133. .snode.k-bridge:hover,
  134. .snode.k-bridge.sel,
  135. .snode.k-event:hover,
  136. .snode.k-event.sel {
  137. border-left-width: 3px;
  138. padding-left: 7px;
  139. }
  140. .snode.k-bridge.dimmed,
  141. .snode.k-event.dimmed {
  142. border-left-color: var(--accent-line);
  143. }
  144. .snode.k-store {
  145. background: var(--paper-2);
  146. }
  147. .snode.k-store:hover,
  148. .snode.k-store.sel {
  149. background: var(--press);
  150. }
  151. /* Outside the index: a place the graph cannot follow into. */
  152. .snode.k-effect {
  153. border-style: dashed;
  154. border-color: var(--ink-3);
  155. }
  156. .snode:focus-visible {
  157. outline: 2px solid var(--accent);
  158. outline-offset: 1px;
  159. }
  160. .name {
  161. font: 500 13px var(--mono);
  162. line-height: 15px;
  163. white-space: nowrap;
  164. overflow: hidden;
  165. text-overflow: ellipsis;
  166. }
  167. .mark {
  168. color: var(--accent);
  169. margin-right: 5px;
  170. font-size: 9px;
  171. vertical-align: 1px;
  172. }
  173. .more {
  174. color: var(--ink-3);
  175. }
  176. .sub {
  177. font: 400 11px var(--sans);
  178. line-height: 13px;
  179. color: var(--ink-3);
  180. white-space: nowrap;
  181. overflow: hidden;
  182. text-overflow: ellipsis;
  183. }
  184. </style>