Skip to content

[6.x] Documentation pass - #19506

Draft
brianjhanson wants to merge 90 commits into
6.xfrom
feature/documentation-pass
Draft

[6.x] Documentation pass#19506
brianjhanson wants to merge 90 commits into
6.xfrom
feature/documentation-pass

Conversation

@brianjhanson

Copy link
Copy Markdown
Contributor

Description

Related issues

brianjhanson and others added 7 commits August 27, 2026 15:30
`.storybook/preview.ts` imports `dist/custom-elements.json` statically, so a
running Storybook keeps whatever manifest it booted with. Editing a component's
JSDoc then had no effect until the server was restarted — and because the
helpers turn any arg without a matching argType into a literal attribute, a
newly documented slot would render as an escaped `*-slot="<p>…"` string
instead of slotted content.

Run the analyzer's watch mode alongside the dev server so the manifest, the
controls, and the generated API tables follow the source.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HY6JVxWjJjmZUMuSiBUW9p
`selectable` rendered a bare `<input type="checkbox">`, which axe flags as
"Form elements must have labels" and which blocked any story exercising it
from passing the a11y gate.

Name it with the new `select-label` attribute, falling back to a translated
"Select". A list of chips should set it to the entity each chip stands for,
so the checkboxes do not all read alike.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HY6JVxWjJjmZUMuSiBUW9p
Callout had three undescribed attributes, no documented slots despite having
four, placeholder descriptions on the rest, and a `@TODO` sitting inside a
public JSDoc block. Rewrite the annotations so the manifest carries the whole
surface, convert the stories to derive their args and argTypes from it, and
add a docs page.

Fixes found while writing it: the `variant` control was populated with
appearance values, the default args set a `variant` that does not exist, a
dead `flash` arg had a control, and `craft-button` was never imported so the
action button never upgraded.

The `padding` attribute no longer accepts unitless numbers or arbitrary CSS
lengths — only the `sm`/`md`/`lg`/`xl` steps and `0`/`none`. An off-scale
value now writes nothing and leaves the component's own default standing;
consumers who need one set the padding custom properties instead. This lands
in the shared `Paddable` mixin, so craft-pane behaves the same way.

Without a title, the callout grid is now a single row, rather than a two-row
template with the body borrowing the title's area.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HY6JVxWjJjmZUMuSiBUW9p
The `thumbnail` slot was undocumented despite being rendered, and
`thumb-alignment` had no description, so neither reached the manifest. Add
both, and move the CP-specific note about spreading an element's card
attributes out of the public summary into a comment beside the code.

Convert the stories to derive their args and argTypes from the manifest, so
the controls drive every story rather than only the ones with a matching
custom render, and add a docs page.

Two fixes found while writing it: the header action buttons set
`appearance="plain"`, which craft-button has no such attribute for — the
property is `variant`. And `craft-button`'s `accessible-name` reports the
name it computed for its own error check rather than setting one, so an
icon-only button still needs an `aria-label`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HY6JVxWjJjmZUMuSiBUW9p
`accessible-name` looked like a way to name an icon-only button, but it only
recorded the name the component computed for its own nameless-button warning.
Setting it never put anything in the DOM — it just silenced the warning, so
the Matrix "Remove" buttons that used it were nameless and unflagged.

Make it `_accessibleName` state, and point the callers that were naming
buttons with it at `aria-label`: the PHP Button builder (so the existing
`accessibleName()` API keeps working and now emits a real name), the Matrix
Vue control, and the matrix-input custom element.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HY6JVxWjJjmZUMuSiBUW9p
Twelve of its thirteen attributes had no description, and none of its four
slots or its `action:change-state` event were documented. Annotate the whole
surface, convert the stories to derive their args and argTypes from the
manifest, and add a docs page.

Three fixes along the way. The `confirm` attribute was never read — the CP
already passes confirmation inside the action descriptor, which `runAction`
handles — so it is gone. `active` styled itself from `:host([active])` but did
not reflect, so setting the property did nothing; it reflects now. And the
component rendered `craft-icon` and `craft-spinner` without importing either,
leaving them unupgraded anywhere the stories did not happen to pull them in.

The stories had disabled the a11y gate wholesale (`a11y: {test: 'todo'}` on
the meta). It is on now, scoped off for the two stories whose violations are
inherent: an interactive `suffix` nests a control inside the item's button,
and the full-palette grid necessarily includes low-contrast colors. The
nested-interactive finding is a real structural issue, called out in the docs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HY6JVxWjJjmZUMuSiBUW9p
@github-actions

github-actions Bot commented Aug 28, 2026

Copy link
Copy Markdown

brianjhanson and others added 22 commits August 28, 2026 15:30
None of its nineteen attributes had a description, its slot was undocumented,
and it had no stories at all. Annotate the whole surface, add stories driven
from the manifest, and add a docs page.

The submission contract is the part worth writing down: a `name` posts
`name[date]`, `name[time]`, `name[timezone]`, and `name[locale]`, so the
server receives one coherent value. The locale always travels with it, and the
timezone rides along in a hidden input whenever it is not shown as a control.
So is the asymmetry between the visibility flags: `show-date` and `show-time`
are default-on and only the exact string "false" turns them off, while
`show-timezone` is an ordinary boolean attribute.

Also fixes `disabled-time-ranges`, whose converter guarded `null` but not an
empty attribute, so `disabled-time-ranges=""` threw "Unexpected end of JSON
input" before the element could render.

The a11y gate is surfaced rather than enforced for these stories: the date and
time inputs have no accessible name of their own, since each is a field with
an empty label slot and a wrapping craft-field names the pair instead of the
parts. That needs a labelling decision, and is called out on the docs page.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HY6JVxWjJjmZUMuSiBUW9p
Story-level decorators are additive rather than overriding, so the story's
own `(story) => story()` did not opt out of the shared field wrapper — it
added a pass-through and the meta decorator still wrapped the result, giving
two nested `craft-field`s with the inner one outside the outer's `input` slot.

Make opting out a parameter the shared decorator checks instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HY6JVxWjJjmZUMuSiBUW9p
`show-date` and `show-time` are default-on string flags, and the storybook
helpers map a `"false"` arg onto boolean `false`, which removes the attribute
— an absent attribute then reads as on, so both stories rendered both inputs.

Write the attribute directly in those two stories, and disable the controls
for the pair rather than offering a knob that cannot express the value. Spell
out on the docs page that a boolean binding cannot turn these off either.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HY6JVxWjJjmZUMuSiBUW9p
A patch in the previous commit truncated the story off the end of the file,
leaving the docs page pointing at it with `of={undefined}` and failing to
render at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HY6JVxWjJjmZUMuSiBUW9p
`<Canvas of={ChipStories.Icon}>` resolving to `undefined` does not fail the
build — Storybook renders the entire page as an error instead, so a renamed
or dropped story silently costs a component its documentation until someone
opens the page. That has now happened twice.

Cross-check every MDX page's story references against its story file's
exports.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HY6JVxWjJjmZUMuSiBUW9p
None of the three had a class summary, neither date nor time had any stories,
and craft-input-time's three own attributes were undescribed. Annotate all
three, add stories driven from the manifest, and add docs pages. This also
gives craft-input-date-time's docs page the two links it was already making
to pages that did not exist.

craft-input is the base the other `craft-input-*` controls extend, so its page
carries the shared surface — labelling, the width inference around `maxlength`,
and Lion's eight slots — and the others link back to it rather than restating
it.

Two things worth recording while writing them. `min` and `max` are declared
`attribute: false` on the base, so they are properties rather than attributes
and have to be bound as such. And `disabled-time-ranges` takes `[start, end]`
tuples, not `{start, end}` objects as craft-input-date-time's page claimed;
that page is corrected, and the ranges are enforced by validation after a time
is chosen rather than by the picker.

Also applies the empty-attribute fix from craft-input-date-time to
craft-input-time's identical converter, where `disabled-time-ranges=""` threw
before the element could render.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HY6JVxWjJjmZUMuSiBUW9p
Adds docs pages for money, handle, color, copy, password, and file, which
completes the family: all ten `craft-input*` elements now carry a summary,
described attributes, stories, and a page.

craft-input-money had seven undescribed attributes, and craft-input-handle
had two plus no summary and no stories at all. Password and file had neither
summary nor slots recorded, because both extend Lion directly rather than
craft-input — which is worth knowing, since it means the base control's
`maxlength`, `size`, `width`, `monospace`, `center`, and `hidden-input` are
not available on them. craft-input-color does the same. Each page says so
rather than leaving a reader to find out.

Two behaviours worth recording. craft-input-money's `show-currency` and
`clearable` are default-on string flags — the same shape as
craft-input-date-time's `show-date`, where only the literal "false" turns
them off and a boolean binding cannot. And craft-input-handle's `autocorrect`
serialises as `on`/`off` rather than as a bare boolean attribute.

Also renames the file input's story title from `Controls/InputFile` to
`Controls/Input File`, so it sorts and reads with the rest of the family and
its docs id matches the pattern.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HY6JVxWjJjmZUMuSiBUW9p
Both extended Lion's input directly, so neither picked up the base control's
`maxlength`, `size`, `small`, `width`, `center`, `monospace`, or
`hidden-input` — and the PHP builders stripped all seven back out of the host
attributes to match, with a comment naming that limitation as the reason.

Extending craft-input closes the gap in both directions: the elements gain the
base surface, and the builders can stop deleting attributes their parent had
already prepared. `type` is still kept off the host for both, since the
password field toggles it and the colour field pairs its own text input with a
native swatch.

The reveal button, the colour swatch, the presets datalist, and the model
values are unchanged, and the help text renders identically — the styles the
two already imported were the ones doing that work.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HY6JVxWjJjmZUMuSiBUW9p
Three of its five attributes had no description, and its slot was unrecorded.
Annotate the surface, convert the stories to derive their args from the
manifest, and add a docs page.

The page leads on the two things a consumer has to get right: icons take their
size and colour from surrounding text rather than being sized directly, and an
icon is `aria-hidden` unless given a `label` — which is correct beside text
that already names it, and wrong on an icon-only control.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HY6JVxWjJjmZUMuSiBUW9p
It had no summary, none of its four attributes described, and its slot
unrecorded. Annotate the surface, convert the stories to derive their args
from the manifest, and add a docs page.

Also removes a placeholder announcement. On open, the component set a
`role="status"` live region to the literal string "Some new status" after a
200ms timeout — untranslated debug text that a screen reader would read out
every time a tooltip was opened. The live region was never finished (nothing
else ever wrote to it), so it goes along with the string.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HY6JVxWjJjmZUMuSiBUW9p
It had no summary, its one attribute undescribed, and neither its slot nor its
two events recorded. Annotate the surface, convert the stories to derive their
args from the manifest, and add a docs page.

The page makes the two decisions explicit: reach for a progress component when
progress can actually be measured, and slot text saying what is loading, since
the animation alone conveys nothing to a screen reader.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HY6JVxWjJjmZUMuSiBUW9p
Converts the stories to derive their args from the manifest and adds a docs
page.

Also stops an unlabelled avatar claiming to be an image. The disc was always
`role="img"`, so without a `label` it was an image with no accessible name —
which the a11y gate flags as soon as a story exercises it. There is nothing to
announce in that state, so it is now `aria-hidden`, matching how craft-icon
treats an unlabelled icon.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HY6JVxWjJjmZUMuSiBUW9p
Adds a docs page. The annotations were already complete, so this is the page
alone: what the box is for, the default-true string booleans behind
`checkered` and `rounded`, why the box is sized rather than the image, and the
`alt` decision — which is nearly always `alt=""` inside a chip or card, where
the entity is already named in text.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HY6JVxWjJjmZUMuSiBUW9p
Adds a docs page. The element is a controller with no visual output, so the
page carries no stories — it explains what the wrapper does to the
server-rendered thumb placeholders inside it, why it exists as a declarative
alternative to an imperative boot call, and when to reach for craft-thumbnail
instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HY6JVxWjJjmZUMuSiBUW9p
The component still carried the scaffolding template's placeholder JSDoc, so
the manifest advertised a slot named "example", a `base` part, an `--example`
CSS property, and a `craft-event-name` event — none of which exist. All four
were being served as public API. Replaced with what the component actually is.

Two fixes alongside. The fallback accessible name was a hardcoded English
`Status: live`, now translated. And an unset status left `role="img"` with no
name; a dot with neither `status` nor `label` is decorative, so it is no
longer exposed as an image.

The page also draws the line between this, craft-indicator, and craft-badge,
which all render a small coloured mark for different reasons.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HY6JVxWjJjmZUMuSiBUW9p
It had no summary, its one attribute undescribed, its slot unrecorded, and no
stories. Annotate the surface, add stories, and add a docs page.

Also drops a dead import: the component imported CraftInfoIcon and never used
it, which was half of a circular dependency between the two.

The page is explicit that hidden is not removed — `display: none` takes
content out of the accessibility tree where this keeps it in — and cautions
against reaching for it in place of a visible label.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HY6JVxWjJjmZUMuSiBUW9p
It had no summary and its slot was unrecorded. Annotate it and add a docs
page explaining why the wrapper exists at all: a run of links in a div is
announced one at a time, where a list is announced with a count and position.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HY6JVxWjJjmZUMuSiBUW9p
It had no summary, no stories, and its slot unrecorded. Annotate it, add
stories, and add a docs page.

The point worth writing down is that its breakpoints are container queries
rather than media queries, so the same group stacks inside a narrow pane on a
wide screen — which is what makes it safe to drop a form into a slideout or a
sidebar without configuring anything.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HY6JVxWjJjmZUMuSiBUW9p
Moves everything that was under `Controls/` into a `Form Controls/` group with
three subgroups — Choice Controls, Select Controls, and Text Controls —
covering the checkbox/radio/switch family, the select family, and the inputs.

Field, Field Group, Input File, Slide Picker, and Slide Rule sit directly
under Form Controls: they are form controls, but none of them is a choice, a
select, or a text field.

Regrouping changes every docs id under the group, which silently breaks every
cross-link written against the old ones, so the docs-reference guard now
validates `?path=/docs/…--docs` links against the ids the story titles
actually declare. That immediately found a dead link in craft-button's page,
pointing at `concepts-colorable` for a page that has been `tokens-colorable`
for as long as the guard can see — fixed here too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HY6JVxWjJjmZUMuSiBUW9p
It had no summary and none of its slots recorded. Annotate the surface and add
a docs page.

The page leads on choosing the right control — checkboxes for any number of
answers, radios for exactly one — and explains why the component preserves a
server-rendered input's checked/disabled/name rather than resetting them on
upgrade.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HY6JVxWjJjmZUMuSiBUW9p
It had no summary and none of its four slots recorded. Annotate the surface
and add a docs page.

Records why the group adopts a server-rendered name: Lion syncs every
registered child's name to the group's, so a group that upgraded without one
would strip the names off its inputs and break native posting.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HY6JVxWjJjmZUMuSiBUW9p
It had no summary and no slots recorded. Annotate the surface and add a docs
page.

The page draws the distinction that matters: this derives the mixed state from
its children, where a plain craft-checkbox exposes `indeterminate` for you to
drive yourself — so the parent here cannot fall out of step with the set it
summarises.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HY6JVxWjJjmZUMuSiBUW9p
brianjhanson and others added 30 commits September 1, 2026 09:33
Adds a docs page; the annotations were already complete.

Explains why it exists alongside craft-copy-button: the value is already on an
element in server-rendered markup, so reading the attribute avoids serialising
it a second time. For a value held in script, the copy button is simpler.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HY6JVxWjJjmZUMuSiBUW9p
Adds a docs page; the annotations were already complete.

The distinction worth writing down is that `name` changes what the group
means: without it the buttons are actions, with it they are options, and the
group is announced as a radio group with arrow-key navigation. That is a
semantic difference rather than a styling one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HY6JVxWjJjmZUMuSiBUW9p
It had no summary, its one attribute undescribed, and none of its slots
recorded. Annotate the surface and add a docs page.

Notes that a field's size is a promise about the length of the answer, and
asks that the resize handle be left alone — it is one of the few pieces of
interface a person can adjust for themselves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HY6JVxWjJjmZUMuSiBUW9p
Adds a docs page; the annotations were already complete.

No stories: the component takes a whole permission structure as data, and a
representative one is large enough that an example would obscure rather than
explain. The page covers the three data properties, why nesting is enforced by
the component rather than left to the caller, and what locked permissions mean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HY6JVxWjJjmZUMuSiBUW9p
It had no summary and neither attribute described. Annotate the surface and
add a docs page.

The page explains what the component is protecting against — a page that would
otherwise fail to render or show an unexplained gap — and why naming the
plugin is the useful part: it turns a dead end into an action.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HY6JVxWjJjmZUMuSiBUW9p
An MDX page importing a stories file that is not there fails the Storybook
build rather than degrading quietly, which is how craft-permission-tree's page
was caught — but the build takes a minute where this takes milliseconds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HY6JVxWjJjmZUMuSiBUW9p
Its page hand-authored HTML tables for the properties and the appearance
values, which is why all four of its attributes reached the manifest with no
description — the documentation existed, it just lived somewhere the manifest,
editor IntelliSense, and the generated controls could not see it.

Move the descriptions onto the properties and let `<ArgTypes>` render the
table. That takes the package to 306/306 attributes described.

Also sharpens the summary to say when to reach for this over craft-status,
which covers the fixed vocabulary of object states.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HY6JVxWjJjmZUMuSiBUW9p
Three components each defined their own default-true boolean converter, two
defined the same JSON converter, the two choice groups shared a name-adoption
method character for character, and card, pane, and chip each ran their own
light-DOM MutationObserver. All of it is now written once.

`defaultTrueBoolean` and `jsonAttribute` move to `utilities/converters.ts`,
`SsrChoiceGroupMixin` joins `SsrChoiceInputMixin` alongside it, and
`LightDomController` — a reactive controller, so pane can still layer its
scroll measuring on the same mutations — lands in a new `controllers/`
directory with a vitest project of its own.

Two bugs fall out of the consolidation:

`craft-thumbnail`'s checkered default was inverted. The server renders the
attribute as an ordinary HTML boolean — `Html::tag()` writes a bare
`checkered` or omits it — but the component initialised the property to
`true`, and Lit reflects that initialiser onto the element. Every element
whose `hasCheckeredThumb()` is false was rendering the chequerboard anyway.
It is now a plain presence-based boolean defaulting to false, which is also
what the base `HasThumbnails` trait means by returning false. Its docs page
claimed both `checkered` and `rounded` were default-true string booleans;
neither is.

`craft-card` checked slot presence with `querySelector()` across its whole
subtree, so a nested component's own `footer` or `actions` slot lit up the
card's chrome. Pane already checked direct children only and carried a comment
warning about exactly this; `hasSlotted()` now does it for all three.

The callout padding test asserted an inline default of `md`, but e9522b6
deliberately moved that spacing onto the icon, title, and description when the
grid went single-row. The test and the stylesheet comment were the only things
still describing the old shape.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HY6JVxWjJjmZUMuSiBUW9p
The documentation pass found six components describing themselves wrongly and
found them all by hand: craft-status shipped the scaffold's placeholder slot,
part, CSS property, and event as though they were real API, and five more
carried a full class comment that reached nothing because it was missing an
`@summary` tag.

Both are now checked. `manifest-coverage.test.ts` reads the built manifest and
requires every element to say what it is, every attribute, slot, event, part,
and CSS property to be described, and no scaffold placeholder to survive into
it — 203 checks that run in milliseconds against the artifact the docs site is
generated from. A `pretest` hook rebuilds the manifest so the guard never
passes on a stale one.

Holding the line at fully described turned up five events with no description.
Two were real and are now documented — combobox's and select-color's
`model-value-changed`. The other three were artifacts: the analyzer reads the
first argument of `dispatchEvent()` literally, so slide-rule's `#emit(type)`
helper was recorded as an event named `type`. The manifest config now treats
`@fires` as the whole story once a class uses it, which drops the inferred
extras without hiding anything from the guard.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HY6JVxWjJjmZUMuSiBUW9p
Twenty-four component pages ended without a `<ArgTypes>` block, so their API
was either hand-written or simply absent — craft-badge still carried three
hand-maintained HTML tables restating what its JSDoc already said. Every page
now ends in a generated table, including the eight that deliberately have no
stories: `<ArgTypes of="craft-radio" />` resolves a bare tag against the
manifest, so a page without a story still documents its element.

Four pages had a prose "Properties" section that predated the table and now
sits above it or has been dropped as redundant. craft-badge's page was
rewritten around the generated table.

Filling those tables in exposed what they had been hiding. The analyzer
records every class member it finds, so 45 elements were publishing 408
internals as public API — craft-permission-tree advertised a `#treeId`, and
craft-copy-button its element refs. The manifest config now drops anything
`private`, `protected`, `#`- or `_`-prefixed, and 22 members that were
internal in everything but their modifier are marked `protected` to match.

Of the members that remain genuinely public, 75 had no description. Forty-seven
now do — `show()`/`hide()` on the overlays, `turnOn()`/`turnOff()` on the
switch, the Lion slot and scoped-element overrides, and the `type` each input
subclass fixes, which is real information a consumer needs. The manifest guard
covers members as well now, with the three Lion constructor assignments that
cannot be redeclared named in an allowlist rather than silently tolerated.

Also normalised three pages onto the conventions the other 64 follow: the
namespace stories import, `name="Docs"` on the meta, and a filename matching
the component — `button/Docs.mdx` was the only page not named for its
component.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HY6JVxWjJjmZUMuSiBUW9p
An audit of the manifest turned up a handful of places where the package does
not name things the way it names them everywhere else. Attribute names, class
names, file names, and the camelCase-to-kebab-case mapping are all consistent;
these were the exceptions.

`craft-slide-picker` spelled its attribute `read-only`. HTML spells it
`readonly`, and so does `craft-input-date-time` — the package's only other
readonly control. Renamed, along with its one caller in the field layout
designer.

`craft-avatar` published `--color-start`, `--color-end`, and `--color-text`:
three unprefixed custom properties in a package where the other 53 are
`--c-<component>-*`. Since custom properties inherit, a `--color-text` set
anywhere up the tree reached inside every avatar. They are now `--c-avatar-*`.

`--size` stays as it is. It looked like the same mistake, but avatar, spinner,
status, indicator, action-item, and button all read it deliberately, so one
declaration can size a row of them together — and the CP relies on that in a
dozen places. Only avatar had ever documented it; spinner and status now do
too, and all three say that it is shared.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HY6JVxWjJjmZUMuSiBUW9p
The pane rendered `label` as an `<h1>`, and CP pages routinely stack two or
three panes — ModalForm and ProjectConfigDiff render three each. A run of
`<h1>`s leaves the heading list a screen reader navigates by completely flat,
which is the one thing headings exist to prevent.

`heading-level` now sets it, defaulting to `2`: the page around the pane owns
the `<h1>`. A pane that really is the page's main heading can ask for `1`, a
nested one for more, and the value is clamped to the six real heading elements
so a bad attribute can't emit an `<h0>`. Slotting your own heading into `title`
still overrides all of it.

The docs page already described this problem and told people to work around it
by slotting a heading; it now describes the attribute instead.

The manifest guard also checks that the manifest is newer than the components
it describes. A stale one fails every check at once and blames the components,
which is a confusing way to find out that `npx vitest` skips the `pretest`
rebuild that `npm test` runs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HY6JVxWjJjmZUMuSiBUW9p
craft-chip's `size` was documented as accepting `large` "but has no styles of
its own". All three steps have set the label padding and the prefix thumbnail
size for some time — the comment described an older state, and the docs page
never showed the two size stories that were sitting in the file. Both are on
the page now.

craft-truncate measured in `firstUpdated()` as well as from its ResizeObserver,
whose `observe()` already delivers an initial callback once the element is laid
out. The redundant pass set state immediately after an update completed, which
rendered the component twice on mount and logged Lit's change-in-update warning
on every story that used it. It is now the fallback for when there is no
ResizeObserver to seed the first measurement.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HY6JVxWjJjmZUMuSiBUW9p
Every component had a page, but a developer arriving from outside landed on an
alphabetical component list with nothing telling them what the package is, how
to install it, or how to make it match their colours. Three pages now sit above
the components, and the sidebar is ordered so they come first.

**Introduction** covers what is in the library, why it is custom elements —
which is what explains the attribute-shaped API and the server-rendered
adoption behaviour — and the conventions that hold across every component, so
each page does not have to restate them.

**Installation** covers the CP case (nothing to install; prefer the PHP
component classes over writing tags by hand) and the external case: the two
peer dependencies, side-effect imports versus named class exports, the token
stylesheet that components inherit their colours from, binding rich values in
Vue, and pointing an editor at the manifest for completion inside markup.

**Theming** lays out the three levels — palette, `--c-*` tokens, per-component
properties — in the order to reach for them, plus how `data-theme` and
`data-color` work and what not to do.

The `Functional/` group is folded into `Components/`. It held four components
against Components' fifty, the distinction was never written down anywhere,
and the sidebar read as though someone had started a taxonomy and stopped. The
cross-links into it were rewritten.

Widening the link guard to every MDX in the package, rather than only the ones
beside a story file, immediately caught two dead links in the pages this commit
adds — the token colours page is `Color Palette`, not `Colors`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HY6JVxWjJjmZUMuSiBUW9p
Both were written up as known problems on their pages and left for a design
call. Neither needed one.

craft-input-date-time's date and time inputs had no accessible names. A
wrapping craft-field names the pair, not the parts, so a screen reader reached
two controls announced as edit fields inside a labelled group with nothing to
say which half was which. Each input the component owns now carries its own
translated name — Date, Time, Time zone.

craft-action-item's `suffix` slot sits inside the item's own button, so a
control there nests a control inside a control and swallows the click meant for
the item. The page said not to do it; nothing said so at the point it happened.
The item now marks the suffix the way craft-button marks itself when it has no
accessible name — the shared rule moved to `styles/a11y-error.styles.ts` rather
than being copied, since two components use it.

Detection runs off `LightDomController` rather than `slotchange`: a menu built
at runtime is exactly where this mistake gets made, and content slotted after
mount never fires a slot's own event.

Both files needed their new module constants moved above the class docblock —
declared between the comment and the class, they took the `@summary` with them
and the elements went undescribed. The manifest guard added earlier today
caught it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HY6JVxWjJjmZUMuSiBUW9p
Card and Pane are not redundant — one is an entity tile, the other page
structure — but three differences made them read as the same component built
twice. The heading one was fixed separately; these are the other two.

Card exposed a single CSS part, `label`, against Pane's seven, so styling any
other region meant reaching for internals that are not API. It now exposes
`base`, `header`, `label`, `actions`, `body`, `thumbnail`, and `footer`.

Card also had no `padding` attribute, so it was the only one of the three
padded surfaces — Pane, Callout, Card — where spacing could not come from the
shared scale. It takes the `Paddable` mixin now, nominating the public
`--c-card-padding-*` properties: with `padding` unset the mixin writes nothing
and each region keeps its own default, so nothing renders differently until
someone asks for a value.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HY6JVxWjJjmZUMuSiBUW9p
Nine story files hand-rolled their `args` and `argTypes` instead of taking them
from `getStorybookHelpers`, so their controls and API tables were maintained
separately from the components they document. They now derive both, which is
the standard the rest of the package already follows.

Hand-rolling let stories document attributes that do not exist.
craft-indicator's used `variant`, `volume`, `swatch`, and `color` — none of
them on the component, which has only `size`, `fill`, `label`, and
`appearance`. Three of its six stories were demonstrating inert markup, and its
own docs page describes the real behaviour: `fill` resolves a status variant or
a palette swatch to the matching token. The stories now show that.

Story names the docs pages reference are unchanged, apart from Truncate's
`Playground`, which is now `Default` like every other args-driven story; its
page was updated. The link guard caught craft-badge-indicator's `Primary` and
`Secondary` mid-conversion.

Grid stories that render many instances keep their own markup and disable the
controls that no longer reach them, rather than offering knobs that do nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HY6JVxWjJjmZUMuSiBUW9p
Seven more story files take their args and argTypes from the manifest. These
are the Lion-derived controls, where two things do not go through the generated
template and are commented where they apply:

`label` and `help-text` are slots rather than attributes, so they are set with
`label-slot` and `help-text-slot`. And a named slot's content is wrapped in a
`<span>`, which is fine for text but not for craft-select's `input` slot —
Lion adopts that span as the control and the real `<select>` never lands. Select
and Select Rich keep an explicit template for that reason, as do the two groups,
whose options carry a `choiceValue` property that no markup string can express.

craft-textarea's story rendered `<craft-textarea …></craft-uinput>`, a closing
tag for an element that does not exist.

Two stories now scope off one axe rule each, with a comment naming why: the
checkbox with a visually hidden label reports `label-title-only`, which is the
thing the story demonstrates, and the disabled textarea's help text reports
`color-contrast`, which WCAG 1.4.3 exempts for inactive controls.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HY6JVxWjJjmZUMuSiBUW9p
Seven more story files take their args and argTypes from the manifest. Each
keeps an explicit template, for a reason now stated in the file: the value is
Lion's `modelValue`, which is a property rather than an attribute, and the
label is a slot.

Converting them turned up a contrast bug that reaches the whole CP. Lion styles
a disabled field's label, help text, and value with `--disabled-text-color`,
falling back to its own `#767676` — 4.09:1 on our default surface, under the
4.5:1 that help text still has to meet. The package never set the property, so
every disabled Lion field in the control panel has been using Lion's fallback.
It now points at `--c-status-disabled-text`, which was already defined, and
lands at 5.05:1 — the same as the enabled help text.

That also removed the need for the rule scoping the textarea story picked up in
the previous commit: it now passes on its own merits.

The `tabs > Overflow` story is the one that could not be reproduced when it was
reported flaky. Running the full browser suite after this change reproduced it
once in four, and the cause is `requestAnimationFrame` starvation: the story
waited a fixed three frames for a measurement that had not finished when
several browser tests run at once. It now waits for the measurement to land.
Three consecutive full runs since, all green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HY6JVxWjJjmZUMuSiBUW9p
The last fifteen story files take their args and argTypes from
`getStorybookHelpers`. Every story file in the package now does, which was the
point: a control or an API table can no longer drift from the component it
documents, because neither is written down twice.

Most of these keep their existing templates — they are composition and grid
stories, where the markup is the thing being shown — but their hand-written
argTypes are gone. Several had been maintaining a `select` control with its
options spelled out beside a union type the manifest already knows, which is
exactly the duplication the type parser was wired up to remove.

Two exceptions are commented where they apply: craft-tabs declares
`selected-index` alongside the generated set because it is Lion's rather than
the component's, and craft-select-color binds `modelValue` as a property so an
empty selection can be `null`.

craft-input-handle's story set `autocorrect="on"`. The property is a boolean
whose converter serialises to `on`/`off`, so the string was the serialised form
going back in — the type-check caught it once the arg was typed from the
manifest.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HY6JVxWjJjmZUMuSiBUW9p
Eight components had no unit test: avatar, status, indicator, spinner,
shortcut, visually-hidden, thumbnail, and empty. Each now has a small one
covering what it renders, the attributes that change it, and — for the four
that carry an accessible name — the rule they all follow, that an unlabelled
decorative element is hidden rather than announced as an unnamed image.

craft-thumbnail's cover the corrected `checkered` contract in both directions,
since the server relies on an absent attribute meaning off.

Also folds the test files added earlier in this pass onto the package's
conventions: `nested-control.test.ts` becomes `action-item.test.ts`, matching
every other component, and the six that imported from `vitest` now import from
`vite-plus/test` like the other 47.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HY6JVxWjJjmZUMuSiBUW9p
Six more components have unit tests: progress, progress-bar, badge,
badge-indicator, nav-list, and breadcrumbs.

The progress pair are tested through what they report rather than what they
draw. Both are canvas or CSS fills with nothing a screen reader can read, so
the tests hold the ARIA range, the accessible name, and — for craft-progress —
the visually hidden text that carries the percentage, the loading state, and
the failure.

The badges cover the state that is easy to get wrong: a zero count stays a dot,
a count past 99 becomes `99+`, and `data-color` follows `fill`, since that is
what scopes the tokens the badge's own surface reads.

nav-list and breadcrumbs cover their landmark semantics — the list element that
makes a run of items countable, the named navigation landmark, and the
separator kept out of the accessibility tree.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HY6JVxWjJjmZUMuSiBUW9p
Five more: craft-input, craft-textarea, craft-select, craft-copy-button, and
craft-copy-attribute.

craft-input matters most of the five — every other `craft-input-*` extends it,
and it was the largest component in the package with no test at all. Its tests
cover what the subclasses inherit: the native input Lion renders, the label
wiring that makes a click focus the field, `maxlength` reaching the control,
the reflected presentation flags, and the properties that are synced onto the
native input after render rather than rendered with it.

The label assertions check the `for`/`id` pairing rather than
`aria-labelledby`, which Lion fills in later than happy-dom settles — the
`for` attribute is the mechanism that actually makes the label clickable.

The copy components stub `navigator.clipboard`, which happy-dom does not
provide, and cover what a caller depends on: the value written, the events
fired on success and failure, and the two cases that must not copy — disabled,
and a second press while a copy is still running.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HY6JVxWjJjmZUMuSiBUW9p
Every component in the package now has a unit test except craft-select-color,
which cannot have one: it renders a craft-select-rich, and Lion's listbox
throws in happy-dom as soon as an option registers. Its behaviour is asserted
in its Storybook stories instead, which run in a real browser — the label and
name it forwards, the model value, and the opt-in transparent option.

craft-select-rich is tested for what does survive happy-dom and says in the
file why the options are not: registering one runs the same Lion wiring.

craft-checkbox-indeterminate is scoped the same way. The tri-state sync is
Lion's, and runs off registration events that need a real browser to settle, so
what is checked here is this package's own contribution and that the three
states are addressable.

Writing the disclosure tests turned up behaviour worth recording: with no
`state` in the markup it opens, overriding whatever `data-state` the target
already had. That is deliberate — the comment in `__setupExternalMode` says so
— but it is the opposite of what the markup suggests, so the test now states
it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HY6JVxWjJjmZUMuSiBUW9p
The timing fix earlier in this pass made the story reliably reach the state it
was testing, which turned an intermittent failure into a consistent one — and
the consistent failure is a different thing entirely. With the overflow menu
open, the a11y scan reaches Lion's overlay wrapper: a `<dialog role="none">`
that `OverlayController` creates to position the content without wrapping it in
a second dialog. axe reports the role as one a `<dialog>` may not take.

The element is Lion's and the role is deliberate on its side, so the rule is
scoped off for this one story with a comment saying why. It is the only story
that leaves an overlay open while it is scanned.

Three consecutive full browser runs since, all green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HY6JVxWjJjmZUMuSiBUW9p
`<craft-slide-picker>` and `<craft-slide-rule>` post with a form the way a
native input does — `new FormData(form)` reads them, `form.reset()` resets
them — instead of leaning on a hidden input the factory had to keep in sync.

The shared surface lives in a new `FormAssociated` mixin: `name`, `disabled`,
the validity members, and the four user-agent callbacks, with the host
supplying `_formValue()`/`_restoreFormValue()`.

Following Web Awesome, form controls emit native events rather than prefixed
ones, so `value-change` and slide-rule's `start`/`change`/`end` give way to
`input` (every alteration), `change` (on commit), and `craft-drag-start`/
`craft-drag-end` for the pointer gesture. Slide-rule's `change` used to fire on
every drag tick; the image editor now straightens off `input`, which is what
that behaviour actually was.

Three bugs turned up on the way:

- Slide-rule's `updated()` never chained to `super.updated()`, so once the
  mixin posted from there the value froze at its initial one.
- The factory and `cp-compat.js` both set a `read-only` attribute that Lit
  binds as `readonly`, so read-only slide pickers were never read-only.
- Button-group's multiple mode posts under `name[]` behind an empty sentinel,
  which its test had never covered.

happy-dom ships no `ElementInternals` in any release, so the unit tests run
against `element-internals-polyfill`. The polyfill does not perform the user
agent's own side — excluding a disabled control, calling `formResetCallback` —
so that is covered by story play functions in chromium instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HY6JVxWjJjmZUMuSiBUW9p
Custom events carried four conventions at once — bare (`change`, `open`),
prefixed (`craft-copy`), prefixed with the element name repeated
(`craft-text-expander-select`), and colon-namespaced (`action:change-state`,
which Angular templates cannot even bind).

Web Awesome's rule settles all of them: a form control emits the native event,
everything else is prefixed, and the name never repeats the element it comes
from. So:

  craft-action-item     action:change-state           -> craft-state-change
  craft-action-menu     change                        -> craft-select
  craft-disclosure      open / close                  -> craft-show / craft-hide
  craft-progress(-bar)  complete                      -> craft-complete
  craft-reorder-button  reorder                       -> craft-reorder
  craft-spinner         show / hide                   -> craft-show / craft-hide
  craft-tabs            selected-changed              -> craft-tab-show
  craft-text-expander   craft-text-expander-select    -> craft-select
                        craft-text-expander-error     -> craft-error

`craft-tabs` is not a rename: `selected-changed` is Lion's protocol name, fired
by its own setter, so the public event is now dispatched under our name from
`updated()` — guarded so the initial render stays quiet.

`model-value-changed` on the combobox, colour select, and permission tree stays
put for the same reason, and each now says so. Renaming it would fight Lion's
form system, which both dispatches and listens for it. Giving those three a
native `change`/`input` surface is the real fix and is left for its own change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HY6JVxWjJjmZUMuSiBUW9p
A default chip stamps `data-color="white"`, whose palette is built from
`--color-white` and `--color-static-gray-*` — static colours that do not follow
the theme. The chip stayed literally white in dark mode, with static dark text
on it.

The two filled tiers now take `--c-surface-raised` instead, paired with the
text and border tokens `craft-pane` already uses on that surface, so the three
move together: 12.29:1 in light and 10.61:1 in dark, where before dark mode was
a white chip in a dark interface.

Scoped to the stamped colour and the filled tiers, so `outline` and `plain`
stay transparent and an author-chosen `data-color` still fills with its own.
`--c-chip-fill`, `--c-chip-text`, and `--c-chip-border-color` are documented
overrides.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HY6JVxWjJjmZUMuSiBUW9p
The combobox, colour select, and permission tree were the last three
components whose only event was `model-value-changed` — Lion's internal
protocol name, which the event pass could not rename because Lion's form
system both dispatches and listens for it.

Following Web Awesome, they now emit the native events as their public
contract: `input` on every alteration, `change` on commit. `model-value-changed`
stays for Lion and says so in the docs.

Selecting a colour or toggling a permission is a commit, so the pair fires
together. The combobox distinguishes the two: `input` per keystroke, `change`
when an option is selected or the field is left after typing, and nothing at
all when a field is left unedited.

Lion keeps its textbox in the light DOM, so that input's own `input` and
`change` reached consumers with the textbox as `event.target` rather than the
component — and the blur produced a second, differently-targeted `change`. The
combobox now claims both and re-emits from the host; listeners bound to the
textbox itself still run.

Also folds the two dispatches into `utilities/form-events.ts`, and fixes a
duplicate `## Properties` heading on the permission tree's page.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HY6JVxWjJjmZUMuSiBUW9p
Half the package had no pages: the components were documented and nothing
beside them was. A third-party developer had no way to discover that the
factory exists, let alone that it is the supported way to build a control
imperatively.

Adds a JavaScript API section, sitting between the components and the tokens
that explain them:

- Factory — the six builders, what each returns, and the form-value story.
- Utilities — the root exports grouped by what they are for, with the
  subpath-only ones named as such.
- Services — ConfigService, AssetIndexer, and Csrf.
- Reactive Controllers — LightDomController and `hasSlotted()`.
- Vue Wrappers — one page on why seventeen controls have wrappers, what the
  bridge does, and why nothing else needs one.

Writing them turned up twelve undocumented events. `wireOverlayLifecycleEvents`
gives Dialog, Popover, and Tooltip the same four-event lifecycle Web Awesome
uses — `craft-show`, `craft-after-show`, `craft-hide`, `craft-after-hide` — but
none of the three declared them, so they never reached the manifest and no page
mentioned them. Tooltip described them in prose that the analyzer cannot read.

Mixins and constants are deliberately left undocumented; they are composition
internals rather than surface a consumer builds against.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HY6JVxWjJjmZUMuSiBUW9p
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