[6.x] Documentation pass - #19506
Draft
brianjhanson wants to merge 90 commits into
Draft
Conversation
`.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
📚 Storybook previews@craftcms/ui — open Storybook Changed components:
resources/js — open Storybook No changed components detected in this Storybook. |
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
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Related issues