mcpbeat Sign in

Self Hiding Child Cannot Hide Its Slot Agent Skill

A component that renders nothing when its feature is unavailable cannot remove the container someone else wrapped it in — gate the slot on the same availability predicate, publish that predicate beside the component, and branch the slot's clickable and non-clickable forms properly. Use when an empty cell, gap or stray divider appears where an optional control should be, when a group's rounded end caps land on the wrong item, or when a wrapper swallows the taps meant for the control inside it.

1k tokens
context cost
the whole folder, loaded on every use
1
files
instructions only
0
copies elsewhere
how many repositories repackaged it
100
stars on the repo
on the repository, not the skill itself

Install

one command, takes just this skill from the repository
npx skills add https://github.com/maxrave-dev/kotlin-footguns --skill self-hiding-child-cannot-hide-its-slot

The instruction itself

3 sections, as written by the author

A self-hiding child cannot hide its slot

Optional controls commonly hide themselves: no platform support, no permission, nothing to connect

to — render nothing. That is correct and it is not enough. A caller that gave the control a

container, a weight or a divider still draws all three around an empty box.

The component's own contract has to say so, in the same file that declares it:

// adapted
/**
 * Whether [OptionalControl] would render anything. Layouts that give it a container of its own
 * must hide the container too when this is false — the control hides itself, but it cannot hide
 * a wrapper it does not own.
 */
expect fun isOptionalControlAvailable(): Boolean

Traps

Publish the predicate next to the component, not at the call sites. Two call sites re-deriving

"is this available" from platform checks will disagree eventually — and the one that disagrees is the

one drawing the empty box. One exported predicate, backed by the same platform code the component

uses, is the whole fix.

The gate is needed exactly when the caller adds a container, and not otherwise. In this tree the

same control appears twice: inside a segmented group it is wrapped in if (available), and in a

plain row it is called bare, because there the control's own emptiness is invisible. Both are right.

Auditing for "every call site must be gated" would produce a wrong finding.

Removing a slot from a segmented group changes its neighbours' shapes. End caps, corner radii and

dividers are assigned by position. Drop a middle slot and the arithmetic still works; drop the first

or last and the cap has to move, or the group ends up with two square outer corners. Derive the

shapes from the built list rather than hardcoding them per index.

A slot with no click of its own needs a real branch, not a no-op lambda. The clickable and

non-clickable forms of a container are different composables with different semantics. Passing

onClick = {} keeps the ripple, keeps the accessibility click action and keeps consuming the tap —

so the control inside, which owns its own click, never receives it. Branch on onClick != null and

emit the plain container in the else.

Declare the predicate and the component as one platform pair, in one file. A target that stubs

the control to draw nothing must stub the predicate to false in the same edit; split across files

they drift, and the drift is invisible until someone builds that target. Declaring both together

also puts the contract — "callers with a container must gate it" — where both implementers read it.

Keep the predicate cheap and pure. It is called during composition, on every recomposition, from

inside a layout loop. A constant-returning platform function is fine; anything doing I/O, reading a

store or touching a service must be hoisted into state and passed down, or the group stutters.

Decide whether the slot disappears or reserves space, and say which. In a weighted row a removed

slot redistributes width to its siblings, which visibly resizes every remaining control. That is

usually right for a permanently unavailable feature and wrong for a transiently unavailable one,

where the group would twitch each time. Reserve for transient, remove for permanent.

"It renders nothing, so it costs nothing" is false for anything with a weight or a background. An

empty container with weight(1f) still claims its share of the row and still paints its container

colour; that is precisely the artefact this rule exists to prevent.

Verifying it

  • The availability predicate exists and is declared beside the component it describes:
   grep -rn --include='*.kt' --exclude-dir=build "expect fun is[A-Za-z]*Available(): Boolean" .
  • Every call site of that component, each shown with the gate above it if there is one. Read the

ungated ones and confirm the caller adds no container:

   F=$(grep -rl --include='*.kt' --exclude-dir=build "expect fun is[A-Za-z]*Available(): Boolean" .)
   C=$(grep -oE "^expect fun [A-Za-z]+" "$F" | awk '{print $3}' | head -1)
   grep -rn --include='*.kt' --exclude-dir=build -B8 "^ *$C(" . | grep -E "$C\(|Available\(\)"
  • The slot really branches instead of taking a no-op click — expect two container emissions in the

helper, one with a click parameter and one without:

   grep -rn --include='*.kt' --exclude-dir=build -A6 "onClick: (() -> Unit)?" .
  • On a device or target where the feature is unavailable: open the screen and count the cells in the

group. An extra one, a wider gap, or an outer corner that is square instead of rounded all mean

the slot outlived the control.

How to use it

Copy the folder

Take maxrave-dev/self-hiding-child-cannot-hide-its-slot from the repository into ~/.claude/skills for personal use, or into .claude/skills inside a project.

Check the name does not clash

The agent identifies a skill by the name field in its header. Two skills with the same name cannot sit side by side — one of them will be ignored.