Skip to content

The holographic layer, <os-switch>, and the Components tab - #480

Merged
AllTerrainDeveloper merged 4 commits into
trunkfrom
feat/holographic-controls
Aug 3, 2026
Merged

The holographic layer, <os-switch>, and the Components tab#480
AllTerrainDeveloper merged 4 commits into
trunkfrom
feat/holographic-controls

Conversation

@AllTerrainDeveloper

@AllTerrainDeveloper AllTerrainDeveloper commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

The brand ships five mesh gradients and one instruction about them — "meshes reserved for hero surfaces" — and nothing in the component kit was wearing them. This puts them in, as a moment rather than a skin: a control paints the mesh when it is on, selected, primary or filled, and wears ordinary Obsidian the rest of the time. A panel where every surface is iridescent has no identity moments left to spend, which is the failure mode the whole shape of this is built to avoid.

Four commits: the layer, its motion, a pile of Components-tab bugs the review turned up, and two stale labels.


The meshes, in CSS

--os-mesh-holo / -pulse / -auro / -star / -mio are the brand's SVGs transcribed stop-for-stop — Holomesh is its base linear plus all eight radial glows, at the artboard's own offsets, radii, colours and alphas, converted from 1440×960 into percentages.

Gradient stacks rather than url()s, and that is the point rather than an optimisation. background-position can slide a gradient, and that slide IS the holographic effect — a foil card shifting hue as you tilt it. An SVG could not be animated, could not be tinted, and would rasterise one flat corner of a 1440×960 artboard onto a 44 px switch track.

src/ui/holo.ts

Eleven css fragments so no component re-decides what holographic means. Three surfaces — holoEdge (an iridescent hairline), holoSheen (a 10%-alpha film), holoFill (the mesh itself, Void ink on top) — and four motions:

glint a specular band crossing once on hover. Foil does not glow, it catches.
ring a press response that expands and fades, so a click reads as received before its result paints.
shimmer the mesh travelling, for waits of unknown length.
enter scale-and-fade arrival for menus, dialogs and popovers.

Plus the ones that belong to a single component: the <os-segmented> thumb that slides between segments (the group owns one lit pill that moves, rather than each child owning a fill that appears — so the selection travels and the eye follows it), the <os-tabs> underline growing from the centre, <os-toast> arriving from above and leaving sideways, and the <os-avatar> presence ring, which pulses only on online so "who is here" stops depending on telling three dot colours apart.

Every fragment honours prefers-reduced-motion by stopping the tilt — never by removing the fill. A control that lost its mesh would lose its state, not just its animation.

<os-switch>

New component. role="switch" on a real <button>, so Space, Enter, the disabled semantics and the focus ring come from the platform. Off is a lifted well; on is the Holomesh with a Pulse glow and Void ink. Tap, drag (the knob tracks the pointer live and snaps past the midpoint), plus ArrowLeft/Home and ArrowRight/End for absolute off/on — the pair a screen-reader user can use without tracking the current state.

It emits os-checkbox-change alongside os-switch-change, so swapping a checkbox for a switch is a tag change and nothing else.

Dimming Pulse

Per a designer note, and the measurement disagrees with the premise. Pulse #f252fc is not a contrast problem — it carries 6.2:1 against Obsidian. It is saturated (HSL 296, 97, 65), and what hurts is where it is spread rather than stated.

So --os-ui-accent stays #f252fc (that one is the brand's, not ours, and brand-palette.test.ts pins it) and --os-ui-accent-dim #d92ee3 takes over everything ambient — glows, washes, focus blooms. Turning the station up or down is now one edit. The focus ring deliberately does not follow it; only the bloom behind it does, because a focus indicator is the last place to trade legibility for calm.


Three contrast fixes, all measured

The switch changed size between states. A 1px border plus the default border-box background clip: off drew a grey ring over the fill's outer edge, on (border-color: transparent) let the mesh through it. The visible pill grew a pixel per side as it turned on. The border is gone; the off edge is an inset shadow, which occupies no layout.

The unlit half was not drawn. --os-ui-holo-track was rgba( 12, 11, 15, 0.55 ), which composites to #121017 on an Obsidian panel: 1.07:1. An off switch was, to a good approximation, invisible — and the same token is the unchecked checkbox and every empty slider half. The well is now lifted rather than sunk, and the 3:1 WCAG 1.4.11 asks of a control boundary comes from a new --os-ui-holo-track-edge: Pewter #66636b, the first step on the Shade ramp that reaches it (Silver manages 2.03, Astro 1.37).

The knob vanished on its own mesh. Holomesh's white glow is #fffdff, so the Starlight knob measured 1.01:1 where the lit track is brightest. Not dim — absent. Its ring was Void at 12% alpha; at 55% it carries the knob at 3.5:1 there and 6.9:1 over the mesh's darkest stop.

Text selection had no rule at all. The shell shipped no ::selection, so every selection was the browser default — a colour chosen for a white page landing on a near-black one. --os-ui-selection-bg is Lagoon at 60%: the one primary colour in the guidelines the shell had no job for, and violet-blue is what every OS has trained people to read as "selected". The alpha is squeezed from both ends — the selected text must stay readable (Starlight over it, 5.1:1 on Obsidian) and the highlight must separate from the unselected background (3.4:1). An accent wash cannot do this at all: the Pulse family only separates above 75%, by which point it is a fill rather than a highlight. Both halves are set, because leaving the text colour to the UA is how a selection ends up black-on-violet.


The Components tab was showing empty examples

Seventeen of them, in three ways, none of which fails loudly — a blank example is not an error, it is an empty <div>.

  1. A <script> that could never run. <os-crumb-chain> set its segments from an inline script. The html tag compiles by assigning to a <template>'s innerHTML; the fragment-parsing algorithm sets a parsed script's already-started flag, and the cloning steps copy it. Inert in the template, inert in every clone.
  2. Property-driven components with nothing to render. segments, data, columns, entries, ratings are JS properties — no markup can fill them, so <os-table> and <os-log> rendered their empty states. OsHelp gains exampleInit( root ), run after the example renders and scoped to its own container. It calls customElements.upgrade() first, and that is not belt-and-braces: on the first paint the section is still detached, so assigning to an un-upgraded element defines an own property that permanently shadows the accessor the upgrade installs — the setter never runs and the component sits there empty holding data it cannot see.
  3. Twelve classes with no example at all. Overlays are display: none until opened, so they now ship a trigger; children like <os-tab> have no shape outside their parent, so each example is the parent in miniature.

Two more from the same review: <os-tag-input> rendered "++ Add" (the default label was literally '+ Add' and the button draws its own SVG plus — and since the icon is aria-hidden, a screen reader was announcing "plus Add" too), and the detail pane kept its scrollTop when switching components, so a long entry opened the next one halfway down.

Two labels that had stopped telling the truth

since was a version-history annotation rendered into the UI, which AGENTS.md already bans in docs and comments for the reason it is wrong here too. Removed from all 63 declarations, from OsHelp, from the panel and from the stylesheet.

experimental was on 27 components that several windows already depend on. A badge saying "experimental" on a shipped component is not a caveat, it is stale metadata — it tells plugin authors not to rely on something they safely can. All promoted to stable; OsHelpStatus still accepts the value for a component that genuinely is one.


Guards

  • holo-layer.test.ts — the mesh transcriptions against the brand's own hexes (a typo in a nine-layer gradient is invisible in review and wrong forever), alias privacy, the :where() specificity below, the dim routing, and prefers-reduced-motion on every fragment.
  • help-examples.test.ts — every class with static help has an example, no example contains a <script>, every property-driven component has an exampleInit, every overlay ships a trigger, no exampleInit accumulates listeners, and no descriptor carries a version stamp or an experimental badge.
  • css-template-hygiene.test.ts — a backtick inside a css template closes it early and the toolchain reports "expected a semicolon" pointing at prose. This one says what is actually wrong; it caught a live defect in <os-progress-bar> on its first run.

Two traps worth knowing about

holoField uses bare input / select / textarea selectors — safe inside a shadow root. Spelled honestly with :not() they weigh (0,3,1), heavier than input[aria-invalid='true']:focus at (0,2,1) — so the shared fragment would have outranked every component's own error ring and an invalid field would focus in Pulse instead of red. The exclusions are wrapped in :where() to hold the selector at (0,1,1). No test that renders a valid form would have caught this.

The pseudo-element budget is spent. holoSheen owns ::before and holoEdge owns ::after. Glint and ring are therefore element-based, driven from the parent's state through the child combinator — :active matches an activated element and every ancestor of it, so :active .os-holo-ring would fire every ring on the page.


Green: lint, typecheck, 3634 vitest, build. No PHP touched.

Docs: a holographic layer section in docs/components-reference.md (which treatment goes where, the token tables, how to use it in your own component), the new themeable tokens and motion scale in docs/desktop-themes.md, and rules in AGENTS.md for the holo module, the accent-dim knob and the backtick trap.

Worth looking at on a real install: OS Settings (the sliding segmented thumb, switches off and on), any dialog (spring entry), a Posts table (selection marker, checkboxes), the Components tab end to end, and drag-selecting some text anywhere.

🤖 Generated with Claude Code

Open WordPress Playground Preview

AllTerrainDeveloper and others added 4 commits August 3, 2026 22:04
The brand ships five mesh gradients and one instruction about them:
"meshes reserved for hero surfaces." This puts them in the component
kit as a moment rather than a skin — a control wears the mesh at the
instant it is on, selected, primary or filled, and ordinary Obsidian
the rest of the time. A panel where every surface is iridescent has no
identity moments left to spend, which is the failure mode the whole
shape of this is built to avoid.

The meshes, in CSS
------------------
`--os-mesh-holo` / `-pulse` / `-auro` / `-star` / `-mio` in
`variables.css` are the brand's SVGs transcribed stop-for-stop:
Holomesh is its base linear plus all eight radial glows, at the
artboard's own offsets, radii, colours and alphas, converted from
1440x960 into percentages so they scale to any box.

Gradient stacks rather than `url()`s, and that is the point rather
than an optimisation. `background-position` can slide a gradient, and
that slide IS the holographic effect — a foil card shifting hue as you
tilt it. An SVG could not be animated, could not be tinted, and would
rasterise one flat corner of a 1440x960 artboard onto a 44px switch
track.

src/ui/holo.ts
--------------
Seven `css` fragments so no component re-decides what holographic
means: `holoFill` (the mesh), `holoSheen` (a ~10%-alpha hover film),
`holoEdge` (a mask-composited iridescent hairline, since `border-color`
takes a colour and this is a gradient), `holoField`, `holoCheck`,
`holoDrift`, and `holoTokens`, which reads every public token into a
private `--_holo-*` alias so the palette and every desktop theme stay
reachable.

Two rings, not one. `--os-ui-focus-ring` is for TARGETS — buttons,
switches, checkboxes, swatches — and carries a Void spacer plus a
bloom so it survives landing on a bright mesh, where a flat 2px line
vanishes into the pink. `--os-ui-focus-ring-field` is quieter and
tightens a field's own border: a form of twelve inputs should not look
alarmed. Around twenty `outline: 2px solid var(--wp-admin-theme-color)`
rules collapse onto the two.

<os-switch>
-----------
`role="switch"` on a real `<button>`, so Space, Enter, the disabled
semantics and the focus ring come from the platform. Off is a sunken
Void well; on is the Holomesh with a Pulse glow and Void ink. Tap,
drag (the knob tracks the pointer live and snaps past the midpoint),
and ArrowLeft/Home + ArrowRight/End for absolute off/on, which a
screen-reader user can use without tracking the current state.

Travel is `clientWidth - clientHeight` — the stylesheet's
`w - knob - 2*pad` with the pads cancelled — deliberately measured
from the track and never from the knob, which widens to 1.28x under
`:active`. Whether `:active` has landed by the time `pointerdown`
dispatches is not something the spec pins down, so measuring the knob
would have shortened travel by a quarter on some presses and not
others: a switch that flips early, intermittently.

It emits `os-checkbox-change` alongside `os-switch-change`, so swapping
a checkbox for a switch is a tag change and nothing else.

The sweep
---------
`accent-color` had to go from the checkbox and the range slider. It
takes a COLOUR, and checked here is a GRADIENT — so both are repainted
from scratch. The checkbox tick is two borders of a rotated box (CSS,
not an inline SVG, so it inherits `--_holo-ink` and follows a theme
that re-points it); the slider paints the mesh under an opaque wedge
whose boundary is one custom property the component writes per input,
so the mesh never rescales mid-drag.

Retinted in the palette rather than in the components — which keeps
each component's literal as the no-stylesheet floor and keeps Legacy
able to revert them: `--os-ui-progress-fill` and `--os-ui-step-chip-bg`.

`<os-button variant="holo">` is the hero CTA. `primary` deliberately
did NOT become the mesh: it is three-to-a-row in OS Settings, and a
mesh three-to-a-row is wallpaper. `danger` keeps its red edge and a red
focus ring all the way through the hover, because that border is the
only warning the user gets and an iridescent one says the wrong thing.

Also: an iridescent tab underline that animates out from the centre,
the hairline on selected chips / cards / swatches, the mesh on checked
menu items and the selected segment, a leading-edge marker on selected
table rows (the 10%-alpha wash is correct and is not there at all for
anyone who cannot separate those two greys), and the shared field
chrome across text / textarea / number / select / user-search /
multiselect / tag-input.

Two specificity traps
---------------------
`holoField` uses bare `input` / `select` / `textarea` selectors, which
are safe inside a shadow root. Spelled honestly with `:not()` they
weigh (0,3,1) — heavier than `input[aria-invalid='true']:focus` at
(0,2,1) — so a shared fragment would have outranked every component's
own error ring and an invalid field would focus in Pulse instead of
red. `:where()` contributes nothing, so the exclusions are wrapped in
it and every component keeps the last word about its own states. No
test that renders a valid form would have caught this.

`--_holo-fill` is a nine-layer gradient list, so a trailing
`center / 220% 220%` in a `background` shorthand binds to the LAST
layer only and the other eight fall back to `auto`. Every consumer
uses longhands.

Reduced motion stops the tilt, never the fill: a control that lost its
mesh would lose its STATE, not just its animation.

Tests pin the mesh transcriptions against the brand's own hexes (a hex
typo in a nine-layer gradient is invisible in review and wrong
forever), the alias privacy, the `:where()` specificity, and the drag
arithmetic including its independence from the knob's laid-out width.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Round two on the holographic kit: the four shared motions the surfaces
were missing, the mesh on the surfaces that had earned it, and one
knob for how loud the station is.

Then three defects the review found, two of them accessibility
failures that measured far worse than they looked.

Motion
------
Four fragments in `src/ui/holo.ts`, all `prefers-reduced-motion`-aware
and all timed from one scale:

  glint    a specular band crossing once on hover. This is the gesture
           people mean by "holographic" far more than any static
           gradient is — foil does not glow, it CATCHES.
  ring     a press response that expands and fades, so a click reads
           as received before its result paints.
  shimmer  the mesh travelling, for waits of unknown length.
  enter    scale-and-fade arrival for menus, dialogs and popovers.

Plus the ones that belong to a single component: the `<os-segmented>`
thumb that SLIDES between segments (the group owns one lit pill that
moves, rather than each child owning a fill that appears — so the
selection travels and the eye follows it), the `<os-tabs>` underline
growing from the centre, `<os-toast>` arriving from above and leaving
sideways, `<os-modal>`'s scrim fading while its dialog springs in, and
`<os-avatar>`'s presence ring — which pulses ONLY on `online`, so "who
is here" stops depending on telling three dot colours apart.

`--os-ui-motion-{fast,slow,ambient}` + `--os-ui-ease-{spring,out,loop}`
join `--os-ui-holo-transition`. Not for tidiness: a panel where the
switch settles in 220ms, the thumb in 300 and the underline in 150
does not read as three well-tuned controls, it reads as one surface
that cannot keep time.

The pseudo-element budget
-------------------------
`holoSheen` owns `::before` and `holoEdge` owns `::after`, which is the
whole budget for a control wearing both — as `<os-button>` does. So
glint and ring are ELEMENT-based: the component stamps a `<span>` and
the fragment styles it. Both are driven from the parent's state
through the CHILD combinator, which is load-bearing: `:active` matches
an activated element AND every ancestor of it, so `:active .os-holo-ring`
would fire every ring on the page the moment anything in the panel was
pressed.

Dimming Pulse
-------------
Per the designer's note, and the measurement says the fix is not where
the note pointed. Pulse #f252fc carries 6.2:1 against Obsidian — it is
not a contrast problem. It is SATURATED (HSL 296, 97, 65), and what
hurts is the places it is spread rather than stated: a bloom behind a
focused control, an 18% wash under a selected row.

So `--os-ui-accent` stays #f252fc — that one is the brand's, not ours,
and `brand-palette.test.ts` pins it — and `--os-ui-accent-dim` #d92ee3
(same hue, S and L pulled down together) takes over everything
ambient. Turning the station up or down is now one edit. The focus
RING deliberately does not follow it; only the bloom behind the ring
does, because a focus indicator is the last place to trade legibility
for calm.

The switch changed size between states
--------------------------------------
A 1px border plus the default border-box background clip: the off
state drew a grey ring OVER the fill's outer edge, and the on state
(`border-color: transparent`) let the mesh through it. The visible
pill therefore grew a pixel on each side as it turned on. The border
is gone and the off edge is an inset shadow, which occupies no layout
— so both states are the same box, and the swap for the lit glow moves
nothing.

The unlit half was not drawn
----------------------------
`--os-ui-holo-track` was `rgba( 12, 11, 15, 0.55 )`, which composites
to #121017 on an Obsidian panel: **1.07:1**. An off switch was, to a
good approximation, invisible — and the same token is the unchecked
checkbox and the empty half of every slider. WCAG 1.4.11 asks 3:1 of a
control's boundary.

The well is now LIFTED rather than sunk (a Starlight wash, which also
tracks whatever surface a theme puts behind it), and the 3:1 comes
from a new `--os-ui-holo-track-edge`: Pewter #66636b, the first step on
the Shade ramp that reaches it — Silver manages 2.03 and Astro 1.37.
The checkbox border moved off `--os-ui-border-strong` for the same
reason.

The knob vanished on its own mesh
---------------------------------
Holomesh's white glow is #fffdff, so the Starlight knob measured
**1.01:1** where the lit track is brightest. Not dim: absent. Its ring
was Void at 12% alpha, nowhere near enough to rescue it. At 55% the
ring composites to ~#7d7c7f over that glow and carries the knob at
3.5:1 there, 6.9:1 over the mesh's darkest stop — one declaration
covering both ends of the gradient and the unlit track too.

Text selection had no rule at all
---------------------------------
The shell shipped no `::selection`, so every selection in it was the
browser default — a colour chosen for a white page landing on a
near-black one, around 1.3:1 against the surface. The one rule that
did exist (inside form fields) read `--os-ui-accent-soft`, a 14% hover
tint, at ~1.2:1.

`--os-ui-selection-bg` is Lagoon at 60%: the one primary colour in the
guidelines the shell had no job for, and violet-blue is what every OS
has trained people to read as "selected". The alpha is squeezed from
both ends — the selected text has to stay readable (Starlight over it
is 5.1:1 on Obsidian, 5.5:1 on Void) AND the highlight has to separate
from the unselected background (3.4:1 and 3.5:1). Raising the alpha
helps the second and hurts the first; 60% is the middle of the band
where both clear on both surfaces. An accent wash cannot do this at
all: the Pulse family only separates above 75%, by which point it is a
fill rather than a highlight.

Both halves are set. Setting only `background` is the common half-fix
— the UA then keeps its own selected-text colour, frequently forced to
black, landing on this violet at ~2:1. The text colour is Starlight,
the colour body text already is: a selection that RECOLOURS text
destroys every distinction the text was carrying.

Scoped to `body.os-active`, so an admin page inside a window keeps the
selection it has outside one. `::-moz-selection` is its own rule
because an unrecognised pseudo-element invalidates the whole selector
list, which would have left Chrome unstyled too.

A guard for the trap that caused three of these builds to fail
--------------------------------------------------------------
A backtick inside a `css``` template closes the template early, and the
error the toolchain reports points at prose and says "expected a
semicolon". `tests/vitest/css-template-hygiene.test.ts` scans every
template under `src/ui` and says what is actually wrong. It caught a
live one in `<os-progress-bar>` on its first run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Seventeen of the kit's live examples rendered nothing, in three
distinct ways, none of which fails loudly — a blank example is not an
error, it is an empty div.

A <script> that could never run
------------------------------
`<os-crumb-chain>` populated its `segments` from an inline `<script>`
in the help template. `html``` compiles by assigning to a
`<template>`'s `innerHTML`; the HTML fragment-parsing algorithm sets a
parsed script's *already started* flag, and the cloning steps copy that
flag to every clone. The script was inert in the template and inert in
the rendered output. It had never run once.

Property-driven components with nothing to render
-------------------------------------------------
Half the kit takes its data through a JS property rather than an
attribute — `segments`, `data`, `columns`, `entries`, `ratings` — so no
markup can fill them. `<os-table>` and `<os-log>` were rendering their
empty states; the vestigial `id="sample-table"` / `id="sample-log"`
attributes are the fossil of a script that was meant to populate them
and never did.

`OsHelp` gains `exampleInit?: ( root: HTMLElement ) => void`, run by
the panel after the example renders and scoped to the example's own
container. `customElements.upgrade( host )` first, and that is not
belt-and-braces: on the first paint the section is still detached, so
assigning `.segments` to an un-upgraded element would define an OWN
property that permanently shadows the accessor the upgrade installs on
the prototype — the setter never runs and the component sits there
empty, holding data it cannot see.

The hook must be idempotent; the panel re-runs it on every keystroke in
the filter box. Examples wire triggers with `onclick =` rather than
`addEventListener`, and a test enforces it.

Twelve classes with no example at all
-------------------------------------
The tab lists every entry in `OS_COMPONENT_TAGS`, so a file defining
three components is three rows — and only the parent had an example.

  - Overlays (`<os-modal>`, `<os-confirm-dialog>`, `<os-context-menu>`)
    are `display: none` until opened, so mounting one demonstrates
    nothing. They now ship a trigger; the context menu is pinned back
    into normal flow with an inline `position: relative` so it can be
    looked at rather than floating over the settings window at
    coordinates nothing set.
  - Children (`<os-tab>`, `<os-tabpanel>`, `<os-segment>`,
    `<os-menu-item>`, `<os-context-menu-option>`) have no shape outside
    their parent — an unpadded row on the panel background — so each
    example is the parent in miniature, showing every modifier the
    child actually has.
  - `<os-option>` paints nothing BY DESIGN (`:host { display: none }`,
    it is a data carrier). Its example shows what the parent builds out
    of it, because a blank Example section reads as a bug rather than
    as the deliberate choice it is.
  - `<os-tile>` is the one light-DOM component, so it only looks like a
    tile where `desktop-files.css` is loaded. Shown on a dark strip,
    because tiles live on the wallpaper and their labels are Starlight
    — on the settings panel's own surface they would be white on white.
  - `<os-role-picker>` passes `roles` explicitly rather than leaning on
    the global config, which is empty on a site that has never shared a
    folder.

Two more from the same review
-----------------------------
`<os-tag-input>` rendered "++ Add": the default `add-label` was the
literal string `'+ Add'` AND the button draws its own SVG plus. The
icon is `aria-hidden`, so the label is also the accessible name — a
screen reader was announcing "plus Add", the same duplication in the
other modality. The default is now `'Add'`.

The Components detail pane is its own scroll container and `render()`
diffs, so the same `<div>` survived every repaint and kept its
`scrollTop`. Picking a component while scrolled down through a long one
opened the next one halfway down, which reads as a component with
nothing at the top. It resets on selection change only — resetting on
every repaint would yank the pane to the top on each keystroke in the
filter box.

Guards
------
`tests/vitest/help-examples.test.ts` pins all of it: every class with
`static help` has an `example`, no example contains a `<script>`, every
property-driven component has an `exampleInit`, every overlay ships a
trigger, and no `exampleInit` accumulates listeners. The
addEventListener check strips comments first — every one of these files
explains in prose why it does not use it, and a scan that read the
explanation as the offence would fail on exactly the files that got it
right.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two labels in the Components tab that had stopped telling the truth.

`since` was a version-history annotation rendered into the UI, which
`AGENTS.md` already bans in docs and comments for the reason it is
wrong here too: git is the changelog, and a help descriptor's job is
to say what a component does NOW. Removed from all 63 declarations,
from `OsHelp`, from the panel, and from the stylesheet. A change big
enough that "since when?" is a real question for a plugin author is a
breaking change and wants a `docs/migration-*.md` note instead.

`experimental` was on 27 components that several windows already
depend on. A badge saying "experimental" on a shipped component is not
a caveat, it is stale metadata — it tells plugin authors not to rely
on something they safely can. All promoted to stable. `OsHelpStatus`
still accepts the value for a component that genuinely is one.

Guarded in `help-examples.test.ts` so neither creeps back.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@AllTerrainDeveloper
AllTerrainDeveloper merged commit ba98428 into trunk Aug 3, 2026
5 checks passed
@AllTerrainDeveloper
AllTerrainDeveloper deleted the feat/holographic-controls branch August 3, 2026 22:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant