Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions .fork/customizations.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -938,8 +938,16 @@
inflate scrollHeight on the editor, so upstream's max-h + overflow-y:auto
there painted a phantom thumb on a single line. The wrapper owns the
12.5rem cap (upstream max-h-50); the editor stays unclamped and clips its
ink. Scoped to the same >=40rem media as the line box — below that the
prompt stays on leading-relaxed and upstream's editor scroll is left alone.
ink. The editor's siblings — Lexical's placeholder and spacer — carry
overflow clip on both axes (clip, not hidden, so the pure-paint
placeholder never becomes a scroll box; both axes, so an unbreakable
approval-detail placeholder can neither eat its own 16px line with a
horizontal scrollbar nor propagate one onto the wrapper), because the
placeholder's overhanging line box otherwise reaches the wrapper's
scrollable overflow through its abspos box and paints the thumb on an
empty prompt only. Scoped to the same >=40rem media as the
line box — below that the prompt stays on leading-relaxed and upstream's
editor scroll is left alone.
The prompt and its placeholder are set by one rule and must stay that way:
they are two elements stacked exactly on top of each other, and any
disagreement about size or leading puts the caret beside the text it
Expand Down
30 changes: 29 additions & 1 deletion apps/web/src/__fork_guards__/forkComposerShell.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,16 +210,44 @@ describe("fork guard: fork-composer-shell", () => {
expect(scrollport!.body).toMatch(/max-height:\s*12\.5rem/u);
expect(scrollport!.body).toMatch(/overflow-y:\s*auto/u);

// The `~ div` exclusion is load-bearing: the sibling clip rule below
// satisfies the other three predicates too, so without it this `find`
// resolves to the unclamp rule only because that rule happens to sit
// earlier in the file. Reorder the two blocks and `max-height: none`
// starts interrogating the sibling rule and fails for a reason unrelated
// to the invariant — the same class of wrong-thing-satisfied-it bug the
// @media prefix match already cost this file.
const editor = rules.find(
(rule) =>
inDesktopLineBoxMedia(rule) &&
rule.selector.includes("[data-fork-composer-prompt]") &&
rule.selector.includes('[data-testid="composer-editor"]'),
rule.selector.includes('[data-testid="composer-editor"]') &&
!/~\s*div/u.test(rule.selector),
);
expect(editor, "desktop editor unclamp rule missing or unscoped").toBeDefined();
expect(editor!.body).toMatch(/max-height:\s*none/u);
expect(editor!.body).toMatch(/overflow-y:\s*hidden/u);

// The editor's clip does not cover its siblings: the placeholder's line
// box overhangs its inset-0 box by a pixel at 14/16, and abspos overflow
// propagates to the wrapper's scrollable overflow — a thumb that appears
// only while the prompt is empty. Clip specifically, not hidden: hidden
// computes the other axis's visible to auto, and an unbreakable
// approval-detail placeholder would then hand the 16px line to a
// horizontal scrollbar. Both axes, because a visible x would propagate
// that same text sideways into the wrapper's auto overflow-x instead.
// (The editor tolerates hidden only because pre-wrap + wrap-break-word
// forecloses horizontal overflow.)
const siblings = rules.find(
(rule) =>
inDesktopLineBoxMedia(rule) &&
rule.selector.includes("[data-fork-composer-prompt]") &&
/\[data-testid="composer-editor"\]\s*~\s*div/u.test(rule.selector),
);
expect(siblings, "desktop placeholder clip rule missing or unscoped").toBeDefined();
expect(siblings, "the unclamp and clip rules must stay distinct").not.toBe(editor);
expect(siblings!.body).toMatch(/overflow:\s*clip/u);

// An imperative attribute toggle is the failure mode this replaces — see
// .fork/notes/FORK-CUSTOMIZATION-DECISIONS.md#fork-composer-shell.
expect(theme).not.toContain("data-composer-prompt-scrollable");
Expand Down
25 changes: 25 additions & 0 deletions apps/web/src/theme.custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,31 @@
max-height: none;
overflow-y: hidden;
}

/* The placeholder (and Lexical's zero-height spacer) are the editor's
siblings, outside its clip. At 14/16 the placeholder's line box is a pixel
taller than its inset-0 box, and that pixel propagates through the
absolutely-positioned box into the wrapper's scrollable overflow — a
phantom thumb that shows only while the prompt is empty, because typing
unmounts the placeholder. Typed text already paints without that pixel.

`clip`, not `hidden`: setting an axis to hidden computes the other
axis's `visible` to `auto`, which turns this pure-paint element into a
scroll box — and the approval-state placeholder is server-supplied text
(paths, URLs, SHAs) with no break opportunity, so the reserved horizontal
scrollbar would eat the entire 16px line. And both axes, not just y: left
visible, that same unbreakable text propagates sideways overflow into the
wrapper — whose overflow-x computes to auto alongside its overflow-y —
and paints a 16px horizontal scrollbar there instead. clip contains the
element on both axes without creating a scroll container anywhere. The
editor above gets away with hidden only because pre-wrap +
wrap-break-word means it can never overflow horizontally. */
:root[data-fork="noahhendrickson-t3code"]
[data-fork-composer-prompt]
[data-testid="composer-editor"]
~ div {
overflow: clip;
}
Comment on lines +419 to +424

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fourth selector is unnecessary. The media block already uses :is([data-testid="composer-editor"], [data-testid="composer-editor"] ~ div) for the type ramp — reuse that shape for the clip.

Fold the editor unclamp and this sibling-only rule into one prompt-scoped :is(...):

:root[data-fork="noahhendrickson-t3code"]
  [data-fork-composer-prompt]
  :is([data-testid="composer-editor"], [data-testid="composer-editor"] ~ div) {
  max-height: none;
  overflow-y: hidden;
}

max-height: none on the abspos spacer/placeholder is harmless (initial value; they are sized by inset). Delete this sibling-only rule and most of the comment; keep a short note that siblings must share the clip because abspos overflow bypasses the editor. Update the fork guard to assert the unified :is(...) rule instead of locking in a fourth near-duplicate.

Net: −1 rule, reuse the existing selector shape, and stop growing an already >1k theme.custom.css for a duplicate.

}

/* The in-box pills and the control row below it share one 24px ghost treatment:
Expand Down
Loading