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
27 changes: 25 additions & 2 deletions skills/leaf/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -937,6 +937,28 @@ or draft heading remain quotable while runtime controls stay outside the
passage. `relabel` writes the said marker; `offer` writes the control marker.
They are independent facts and neither clears the other.

A label copied off another element on the page is a route to those words rather
than a second place the page says them. Say it once: a contents link, a roster
row naming a worker, any generated index entry stays chrome, and the passage
lives where the page speaks it. Two copies of one label carry the same text and,
being fenced, the same empty context, so neither can be told from the other and
a drag across either detaches.

A route a widget builds outside any control needs nothing further: chrome carries
no offer marker, so no medium takes its words away (`lf-toc`'s rows). A route that
is a control declares itself with `says: "echo"`, `relabel`'s third answer, and
this is the one place the two questions the marker pair answers come apart. An
echo is no passage, and it is still what its row is about: a roster row is a name
and a chip, and a sheet that dropped the name would print the chip alone.
`data-lf-echo` therefore strikes the paper bargain `data-lf-said` strikes — the
press goes, the words stay — without entering the `says` reading. Paper is the
medium that bargain holds in. A copy still divides on the value `offer` wrote: an
echoed route is empty-valued and stays a live fragment link, but an echo on a
`button` would be removed with its words, because the pass that keeps a press's
words in a copy reads `data-lf-said` alone. The first widget to echo a label off a
real press is what makes that reachable, and what teaches those two passes the
third answer.

### Data projections

The page has three kinds of visible words:
Expand Down Expand Up @@ -3011,8 +3033,9 @@ inside it.

Print asks a stricter question than export because nothing on paper is
interactive. `data-lf-offer` identifies injected controls to remove, while
`data-lf-said` preserves a decision word the page speaks through a control. What
`data-lf-said` keeps is the word and not the shape: a control that survives paper
`data-lf-said` preserves a decision word the page speaks through a control and
`data-lf-echo` a name a control copies off the row it routes to. What the two word
markers keep is the word and not the shape: a control that survives paper
gives up its ground, corner, border, underline, marker and pointer hand, because
nothing on a sheet can answer the press they promise. Colour stays, being part of
what the control says.
Expand Down
10 changes: 6 additions & 4 deletions skills/leaf/assets/leaf.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,12 @@
* the diff and in reach of the anchor pass. CLAUDE.md carries why.
*
* Paper reads both: a control a widget injected (data-lf-offer) has nothing on paper to
* be pressed, so it goes, unless its own label is one of the page's words. Keying print
* on .lf-ui instead cost a printed decision the only words that stated it (see
* CLAUDE.md), because a pick mark is a control and a statement at once. render_version
* compares the two media and reports what a page says on screen and not on paper.
* be pressed, so it goes, unless its own label is words the page keeps — ones it speaks
* (data-lf-said) or ones it echoes off the element a route points at (data-lf-echo,
* which anchoring skips and paper keeps). Keying print on .lf-ui instead cost a printed
* decision the only words that stated it (see CLAUDE.md), because a pick mark is a
* control and a statement at once. render_version compares the two media and reports
* what a page says on screen and not on paper.
*
* Native controls are the default. A control that also says selectable page words uses
* the explicit selectable-offer exception, because Chrome starts no pointer selection
Expand Down
31 changes: 19 additions & 12 deletions skills/leaf/assets/runtime/chrome-style.js
Original file line number Diff line number Diff line change
Expand Up @@ -617,32 +617,39 @@ ${MARK_RULES}
the chrome that would have to chase it down every scroll, reflow and drag. */
[${PAGE_PAINT_ATTRIBUTE.decision}] { outline: var(--here-ring); --lf-here-ring: decision; outline-offset: var(--here-ring-gap); }
/* Paper takes no input, so what a widget injects to be worked goes: the control,
and the box that holds controls. What stays is a control whose label is one of
the page's own words — a pick mark reading "chosen" is the only place the page
says which option it carries — which is why this keys on the declaration each
label makes (see relabel) rather than on .lf-ui, whose question is anchoring's.
and the box that holds controls. What stays is a control whose label is words the
page keeps — one it speaks (data-lf-said: a pick mark reading "chosen" is the only
place the page says which option it carries), or one it echoes off another element
(data-lf-echo: a roster row is nothing but its worker's name and a chip) — which is
why this keys on the declaration each label makes (see relabel) rather than on
.lf-ui, whose question is anchoring's. The two markers part on that question alone:
an echo is no passage, and on a sheet the distinction has nothing to stand on.
Asked of the control itself, not of what it holds: a settled group's disclosure
names the chosen card, and that word is worth keeping on screen where the row is
the only place it stands and worth dropping on paper, where the cards are open
underneath saying it themselves. An exported copy strikes the same bargain on the
same two markers, and takes the control out of the document rather than hiding it,
which paper cannot do (BAKE). The runtime's own layer hides as one thing, in the
underneath saying it themselves. An exported copy strikes the same bargain, and
takes the control out of the document rather than hiding it, which paper cannot do
(BAKE) — it asks the marker's value there, so an echoed route is a link the browser
still owns and keeps working. The runtime's own layer hides as one thing, in the
@scope block below. */
@media print {
[data-lf-offer]:not([data-lf-said]) { display: none !important; }
[data-lf-offer][data-lf-said] {
[data-lf-offer]:not([data-lf-said], [data-lf-echo]) { display: none !important; }
[data-lf-offer]:is([data-lf-said], [data-lf-echo]) {
background: none !important; border: 0 !important; border-radius: 0 !important;
box-shadow: none !important; padding-inline: 0 !important;
cursor: default !important; text-decoration: none !important;
list-style: none !important;
}
[data-lf-offer][data-lf-said]::-webkit-details-marker { display: none !important; }
[data-lf-offer]:is([data-lf-said], [data-lf-echo])::-webkit-details-marker {
display: none !important;
}
/* And the ones that stay give up the promise of a press. Twenty of the corpus's 236
injected controls survive that first rule, every one of them because it speaks —
and each was arriving on paper still dressed as a control: a chip background and a
999px radius, a pointer cursor, a summary's marker, a link's underline. The words
are what data-lf-said keeps; the shape was never part of the bargain, and on a
sheet where nothing can be pressed it is a promise the page cannot answer.
are what the two word markers keep; the shape was never part of the bargain,
and on a sheet where nothing can be pressed it is a promise the page cannot
answer.
Colour is left alone, because a chip that is red for a reason is saying something
too and that is the same declaration's business. */
}
Expand Down
36 changes: 28 additions & 8 deletions skills/leaf/assets/runtime/widget-elements.js
Original file line number Diff line number Diff line change
Expand Up @@ -322,13 +322,31 @@ export function installReachedForWordsGuard() {
// once the strip exists. One element wears both over its life, so the kind is
// restated on every write rather than settled at birth.
//
// This writes one marker and one only: data-lf-said, the page speaking. Anchoring
// takes it over the `.lf-ui` box around it — that box is a look, the chrome face, and
// it was standing in for a permission the user has no category for — and paper
// reads it beside data-lf-offer to keep a control whose label is one of the page's own
// words. data-lf-gen goes on either way, because the diff parses the base version
// This writes the page-speaking marker, data-lf-said. Anchoring takes it over the
// `.lf-ui` box around it — that box is a look, the chrome face, and it was standing
// in for a permission the user has no category for — and paper reads it beside
// data-lf-offer to keep a control whose label is one of the page's own words.
// data-lf-gen goes on either way, because the diff parses the base version
// unupgraded and would read any label as text that version lacked.
//
// Those are two questions with one answer until a label is a copy of words the page
// says somewhere else — a roster row naming a worker, a generated index entry, any
// route built out of its target's own words. Such a label must not anchor, because two
// passages carrying the same text and the same empty context cannot be told apart and
// both detach; and it must still print, because it is the only thing naming the row it
// stands in. `says: "echo"` is that third answer, and it writes data-lf-echo: no
// passage, and the same bargain on paper that data-lf-said strikes — the press goes,
// the words stay. Paper is the medium that bargain holds in. A copy divides on the
// marker's *value* instead, which is a fact about the tag and not about this
// declaration: bake removes a press by the value `offer` wrote, so an echoed route is
// empty-valued, slips that pass and stays a real fragment link — while an echo on an
// `offer("button", …)` would go out of the copy with its words inside it, and nothing
// would report the loss, because the static-ising pass that would have kept them reads
// data-lf-said alone. Unreachable while `button()` is the only caller and builds an `a`.
// The second widget to echo a label off a real press is what makes it reachable, and
// what has to teach standalone.js's two passes the third answer; it does not belong
// here, where the label is only being worded.
//
// It leaves data-lf-offer alone, which it used to clear. That attribute is what `offer`
// made: this is a control a widget injected, true for the mark's whole life however it
// is worded, and four passes ask it (print, the drag guard above, the render gate, and
Expand All @@ -343,13 +361,15 @@ export function installReachedForWordsGuard() {
// upgrades, which the console reports and the render gate reads back as a finding
// — the loud direction, in front of whoever wrote the label.
export function relabel(node, label, { says } = {}) {
if (typeof says !== "boolean")
if (typeof says !== "boolean" && says !== "echo")
throw new TypeError(
`relabel(${label}): say whether this label is the page speaking`,
`relabel(${label}): say whether this label is the page speaking — ` +
`true, false, or "echo" for a copy of words it says elsewhere`,
);
node.textContent = label;
node.dataset.lfGen = "1";
node.toggleAttribute("data-lf-said", says);
node.toggleAttribute("data-lf-said", says === true);
node.toggleAttribute("data-lf-echo", says === "echo");
}

// Room for a word not yet said, taken from the words themselves. A control that will
Expand Down
16 changes: 15 additions & 1 deletion skills/leaf/packages/command-hub/widgets/lf-command.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,22 @@ function speakingOffer(tag, label, cls = "") {
return node;
}

// A route to a row, not a second place the page says its name. The label is copied off
// the target's own <strong>, and both copies stand fenced — the roster item is generated
// words, and the row's name sits between the state chip and the remit the widget builds —
// so two passages carried the same text and the same empty context, and neither could be
// told from the other: a drag across a worker's name detached instead of anchoring, on the
// name and on the row alike. Chrome, like the contents sidebar's links, which name every
// heading on the page and say none of them. The words stay quotable where the page says
// them, which is the row this points at.
//
// An echo rather than an unsaid label, because paper reads the same declaration: a
// stopped row is its goal's title and an age, a fleet row a worker's name and its state,
// and a sheet that drops the name prints "· 12d — awaiting review" with no subject at
// all. The words are not this row's to be pointed at; they are still what it is about.
function button(label, target, cls = "") {
const node = speakingOffer("a", label, cls);
const node = offer("a", cls);
relabel(node, label, { says: "echo" });
node.href = `#${target.id}`;
node.addEventListener("click", () => {
if (commandRole(target, "worker")) {
Expand Down
8 changes: 5 additions & 3 deletions tests/test_render_controls.py
Original file line number Diff line number Diff line change
Expand Up @@ -3870,12 +3870,14 @@ def offered(page, selector):
# The window a scope's own surface stands in, where that is not the walk's own. Both
# entries are a floor the layer states rather than a preference: the Map control is drawn
# under the margin's breakpoint and nowhere else, and a Thread Button builds its card only
# where the document leaves room beside the source and opens Threads otherwise. Every
# other scope is read at the width the page opened at.
# where the document leaves room beside the source and opens Threads otherwise. That room
# is the wider of the two floors here, because the card's walk is ship review and ship
# review stands a contents map: a page with a sidebar waits for 1472px of shell rather
# than 1208px (theme.css). Every other scope is read at the width the page opened at.
RING_WALK_VIEWPORT = (1200, 900)
# The one scope whose surface the standing panel takes the place of.
RING_SCOPES_WITHOUT_PANEL = {"a thread card"}
RING_SCOPE_WIDTH = {"a thread card": 1440, "the page map sheet": 760}
RING_SCOPE_WIDTH = {"a thread card": 1600, "the page map sheet": 760}
# Focus put back at the document's start. `document.body.focus()` and not a blur: a blur
# leaves the sequential focus navigation starting point where the blurred control stood,
# so the next Tab carries on from the chrome, runs off the end of the order and never
Expand Down
18 changes: 17 additions & 1 deletion tests/test_render_margin.py
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,9 @@ def test_ask_addresses_follow_the_feature_gallery_s_visible_margin_controls(
resized(page, width, 900)
margins_laid_out(page)

# Twice: the gallery's core surfaces open on a decision, which is the page's first
# ask and carries no address of its own, and the suggestions this case is about
# begin after it.
page.keyboard.press("a")
expect(page.locator("#bg-choice-ask")).to_be_focused()
page.keyboard.press("a")
Expand Down Expand Up @@ -2475,7 +2478,12 @@ def test_a_shared_passage_keeps_all_of_its_threads_in_one_quiet_card(browser, se
def test_the_shipped_long_thread_opens_beside_its_source_in_the_right_margin(
browser, serve
):
"""The shipped exchange fits beside its source and the contents sidebar."""
"""The shipped exchange fits beside its source and the contents sidebar.

Ship review now stands a contents map, and a sidebar claims the opposite strip: the
thread margin waits for 1472px of shell there rather than 1208px (theme.css), so
1440 is a window this page opens Threads in rather than the one this case is
about."""
example = next(page for page in EXAMPLES if page.stem == "ship-review")
page, errors = open_page(browser, serve(example))
resized_shell(page, 1536, 900)
Expand Down Expand Up @@ -2523,6 +2531,10 @@ def test_the_shipped_long_thread_opens_beside_its_source_in_the_right_margin(
assert geometry["cardLeft"] >= geometry["markerRight"] - 0.5, geometry
assert geometry["cardLeft"] >= geometry["mainRight"], geometry
assert geometry["cardRight"] <= geometry["shellWidth"], geometry
# Narrower than the 460px --thread-card the pages without a sidebar get, and it is
# the strip's arithmetic rather than this window: a sidebar page keeps the document
# exactly --thread-margin (520px) from the right edge at every width, and the marker,
# the gutter beside it and the card's own 8px inset all come out of that 520.
assert geometry["cardWidth"] >= 439, geometry
assert geometry["cardTop"] >= geometry["bannerBottom"] + 7, geometry
assert geometry["cardBottom"] <= 892, geometry
Expand Down Expand Up @@ -2579,6 +2591,7 @@ def test_the_shipped_long_thread_opens_beside_its_source_in_the_right_margin(
expect(preview).to_be_visible()
expect(preview.locator("textarea")).to_be_focused()

# 1208 plus the sidebar's 264: the floor a page standing a contents map waits for.
resized_shell(page, 1472, 900)
beside = page.evaluate(
"""() => {
Expand All @@ -2594,6 +2607,9 @@ def test_the_shipped_long_thread_opens_beside_its_source_in_the_right_margin(
assert beside["mainRight"] <= beside["cardLeft"] + 0.5, beside
assert beside["cardLeft"] == pytest.approx(beside["markerRight"] + 8, abs=0.5)
assert beside["cardRight"] <= beside["shellWidth"] - 8 + 0.5, beside
# At the sidebar floor the document is down to its own 640px floor and the strip is
# exactly --thread-margin, so the card takes what the marker, the gutter, main's
# 24px padding and its own 8px inset leave of that 520.
assert beside["cardWidth"] >= 423, beside

resized(page, 1471, 900)
Expand Down
49 changes: 49 additions & 0 deletions tests/test_render_projection.py
Original file line number Diff line number Diff line change
Expand Up @@ -4589,6 +4589,55 @@ def test_command_hub_derives_the_operator_reading_from_its_goal_tree(browser, se
page.close()


def test_a_roster_row_names_its_target_without_saying_it_twice(browser, serve):
"""A roster row names its target in the target's own words, which makes the name a
route rather than a second place the page says it: two fenced passages carrying the
same text and the same empty context cannot be told apart, and a drag across either
detaches. The row is also nothing but that name and a chip, so a sheet that drops it
prints "· 12d — awaiting review" with no subject at all.

`says: "echo"` is both answers at once — no passage, and the words survive the
medium that takes the press away. Read on paper because the loss is silent
everywhere else: the rows say the same thing on screen either way, and `paperWords`
reads no text inside a declared offer, so the gate cannot report a word that only
ever stood in one."""
page, errors = open_page(browser, serve(COMMAND_HUB_EXAMPLE))
fleet = page.locator("#hub-plan > .lf-fleet-view")
stopped = page.locator("#hub-plan > .lf-stopped-view")
fleet.locator(":scope > summary").click()
stopped.locator(":scope > summary").click()
names = page.locator("#hub-plan > :is(.lf-fleet-view, .lf-stopped-view) li > a")
expect(names).to_have_count(10)
expect(fleet.locator("li > a").first).to_have_text("atlas-lead")
expect(stopped.locator("li > a").first).to_have_text("Choose the additive schema")
rows = """() => [...document.querySelectorAll(
'#hub-plan > :is(.lf-fleet-view, .lf-stopped-view) li')]
.map((row) => row.innerText.trim())"""
on_screen = page.evaluate(rows)
assert on_screen[0].startswith("Choose the additive schema · "), on_screen
page.emulate_media(media="print")
assert page.evaluate(rows) == on_screen, "paper dropped a row's only subject"
assert (
page.evaluate(
"""() => getComputedStyle(
document.querySelector('#hub-plan > .lf-fleet-view li > a'),
).textDecorationLine"""
)
== "none"
), "the words stay on the sheet; the promise of a press does not"
page.emulate_media(media="screen")

assert names.evaluate_all(
"""links => links.every(
(link) =>
link.hasAttribute("data-lf-echo") && !link.hasAttribute("data-lf-said"),
)"""
), "a roster name declares itself an echo of the words its target says"

assert errors == []
page.close()


def test_command_hub_goal_metadata_wraps_on_a_phone(browser, serve):
long_when = "handoff-" + "unbroken" * 40
markup = COMMAND_HUB_PAGE.replace('when="week 3"', f'when="{long_when}"', 1)
Expand Down
Loading