Ver Fonte

feat(steps): an early exit reads as a guard clause, not as a branch

A fork with nothing on one side is `if (!user) return` — a reader takes it as a
guard, not as a decision with two sides. Drawn as a branch it costs a column
and a step right, and a handler with four guards (every server handler) read as
four nested branches with three-quarters of the width holding the words
"returns here". It is now one line — the condition and where the code leaves —
with everything below it running because it did not, and the rail stays on its
own hairline. next-saas-starter's `signIn` goes from not fitting the screen to
fitting it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01REFyW9hmNrxhwN5wxRoAkC
Colby McHenry há 1 semana atrás
pai
commit
75686502e3

+ 5 - 2
docs/design/codegraph-ui-design-spec.md

@@ -623,8 +623,11 @@ layout engine, a column of boxes with a hairline down its left and a fork as a r
 `StepBox.svelte`, the canvas's box exactly (the canvas wraps it in handles; the rail lets it size to its words), and so
 are the click, the double-click-to-start-here and the panel. The fork's head says the decision once and its arms say
 only which side they are — **WHEN** / **WHEN NOT** — except a `switch`, whose arms each have a case to say, and a
-`try`, which says `on error` once. `StepsKey.svelte` is the key: floating over the canvas, last in the document on the
-rail, which scrolls and cannot have things sitting on it.
+`try`, which says `on error` once. **An early exit is drawn as a guard clause, not as a branch** — a fork with nothing on
+one side (`if (!user) return`) is one line, the condition and where the code leaves (`userWithTeam.length === 0
+· returns here`), with everything below it running because it did not; a rail of guard clauses would otherwise step
+right once per guard and a handler with four of them would read as four nested branches. `StepsKey.svelte` is the key:
+floating over the canvas, last in the document on the rail, which scrolls and cannot have things sitting on it.
 
 **Which reading opens** travels in the URL (`&view=order` / `&view=tree`) and the summary offers both; without one the
 answer's own `defaultView` decides — the code's order for a handler, an endpoint or any function, the tree for a

+ 45 - 13
ui/src/components/steps/RailBlock.svelte

@@ -58,20 +58,38 @@
         </div>
       {/if}
     {:else if item.kind === 'fork'}
-      <div class="fork">
-        <div class="cond mono">{@render words(item.words)}</div>
-        <div class="arms">
-          {#each item.arms as arm, a (a)}
-            <div class="arm">
-              <div class="armh mono">{@render words(arm.words)}</div>
-              {#if arm.body.length > 0}
-                <Self items={arm.body} {project} {selected} {lit} {onSelect} {onStart} {canStart} />
-              {/if}
-              {#if arm.ends}<div class="ends">{arm.ends}</div>{/if}
-            </div>
-          {/each}
+      {@const guard = item.arms.length <= 2 && item.arms[0]?.body.length === 0 && item.arms[0]?.ends !== null}
+      {#if guard}
+        <!--
+          An early exit is a fork with nothing on one side: `if (!user) return`.
+          A reader takes it as a guard, not as a branch — one line saying where
+          the code leaves, and everything below it running when it did not — so
+          it is drawn as one, and the rail does not step right for it.
+        -->
+        <div class="guard">
+          <span class="cond mono">{@render words(item.words)}</span>
+          <span class="ends inline">{item.arms[0]!.ends}</span>
         </div>
-      </div>
+        {#if item.arms[1] && item.arms[1].body.length > 0}
+          <Self items={item.arms[1].body} {project} {selected} {lit} {onSelect} {onStart} {canStart} />
+        {/if}
+        {#if item.arms[1]?.ends}<div class="ends">{item.arms[1].ends}</div>{/if}
+      {:else}
+        <div class="fork">
+          <div class="cond mono">{@render words(item.words)}</div>
+          <div class="arms">
+            {#each item.arms as arm, a (a)}
+              <div class="arm">
+                <div class="armh mono">{@render words(arm.words)}</div>
+                {#if arm.body.length > 0}
+                  <Self items={arm.body} {project} {selected} {lit} {onSelect} {onStart} {canStart} />
+                {/if}
+                {#if arm.ends}<div class="ends">{arm.ends}</div>{/if}
+              </div>
+            {/each}
+          </div>
+        </div>
+      {/if}
     {:else if item.kind === 'group'}
       <div class="group" class:again={item.again}>
         <div class="label">
@@ -178,6 +196,20 @@
     padding-top: 4px;
     align-self: stretch;
   }
+  /* An early exit: the condition and where it leaves, on one line. */
+  .guard {
+    display: flex;
+    align-items: baseline;
+    gap: 8px;
+    max-width: 100%;
+    min-width: 0;
+  }
+  .ends.inline {
+    border-top: 0;
+    padding-top: 0;
+    align-self: auto;
+    white-space: nowrap;
+  }
   .kw {
     font-weight: 600;
   }