Skip to content

WEBDEV-8951 Add a mediatype center icon to ia-status-indicator - #91

Open
iisa wants to merge 5 commits into
mainfrom
WEBDEV-8951-status-indicator-mediatype-icon
Open

WEBDEV-8951 Add a mediatype center icon to ia-status-indicator#91
iisa wants to merge 5 commits into
mainfrom
WEBDEV-8951-status-indicator-mediatype-icon

Conversation

@iisa

@iisa iisa commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

WEBDEV-8951

Preview: https://internetarchive.github.io/elements/pr/pr-91/#elem-ia-status-indicator

Extend and upgrade the status indicator so that it can carry a mediatype icon in the centre of its ring, not just the three animated dots it draws today.

API

export type LoadingStatus = 'loading' | 'success' | 'error';

export type MediaTypeIcon =
  | 'audio' | 'collection' | 'etree' | 'images' | 'search'
  | 'software' | 'texts' | 'tv' | 'video' | 'web';
Property Type Default Reflects
mode LoadingStatus loading yes
mediatype MediaTypeIcon unset yes
hideDots boolean false yes
loadingStyle removed

Slot: icon — for glyphs with no mediatype, e.g. item-nav's book.

Behaviour

The centre is a mediatype icon or the dots, never both. In loading, first match wins:

  1. Slotted icon
  2. mediatype glyph — static, monochrome with the ring
  3. Nothing, when hideDots is set
  4. Three animated dots — today's default, so existing consumers see no change

Two ring geometries, both kept. 120x120 for dots, a roomier 100x100 for a glyph. Unifying them would shift every existing call site for no gain.

success / error ignore mediatype — they replace the whole SVG, and error has no ring.

ready renders a placeholder that reserves the indicator's space without drawing, so a consumer's layout doesn't shift once the indicator appears. ia-otp-form relies on it for its idle state.

Unmapped mediatypes degrade to dots. account, data, movies have no glyph here.

Breaking changes

Consumer updates are separate tickets.

  • loadingStyle removed. Four offshoot sites pass "ring-dots" (the default) — drop the attribute. home-page.ts:215 passes "ring"hideDots.

Implementation

Glyphs are CSS masks, not <img><img> can't be recoloured. They ship as separate .svg files to keep path data out of the bundle. The mask url() stays quoted: bundlers inline these as data: URIs containing apostrophes, and an unquoted url() silently drops the declaration and paints a solid block.

MediaTypeIcon is a local union, not @internetarchive/field-parsers — no runtime dep for a type, and it can't promise a glyph we don't ship.

maskedIcon moved from ia-item-navigator to src/util so both components share one copy of the helper and its quoted-url() fix, rather than each carrying its own. Only import paths changed for the navigator. Retiring ia-itemnav-loading-view in favour of this component is WEBDEV-8980.

Demo

Checking a glyph at a realistic size meant typing values by hand, so the story gained real controls. The additions are generic and available to any story:

  • StyleInputSettings.presets — one-click values, rendered under the input, or beside it with presetsInline. Width carries the widths consumers actually use; Loading carries White.
  • StyleInputSettings.section / PropInputSettings.section — groups consecutive inputs under a heading, giving the Styles panel a Color group and the Settings panel an Accessible titles group instead of repeating the prefix in every label.
  • A Reset in the Styles panel, which had no way back to defaults.
  • Radios apply on change, so the demo and the usage example follow a selection without a trip to Apply.
  • Props at their default are left out of the usage example, and PropInputSettings.reflects renders reflecting props as attributes rather than property bindings — closer to how a consumer writes them.
  • The dark surface is derived from the loading colour rather than toggled: a light indicator needs a dark background to be visible at all. This needed stylesApplied to bubble and be composed, since it previously stopped at story-template's shadow boundary.

Testing

40 component tests, 100% coverage on ia-status-indicator. 333 pass repo-wide. madge --circular clean, build clean, lint clean. The 10 new SVGs land in dist/.

QA

Open the preview at #elem-ia-status-indicator.

Component

  • Regression first: all defaults → 120x120 ring with three animated dots, unchanged from the deployed demo
  • Each of the 10 mediatypes → glyph centred, sized, not clipped; dots gone; ring becomes 100x100
  • Glyph is static while the ring spins
  • Hide dots true with mediatype none → bare ring
  • Mode success, then error, with a mediatype set → mode glyph wins, no centre glyph
  • Shadow root: glyph is aria-hidden, exactly one <title>

Styles panel

  • Width presets sit under the input; one click applies, no Apply
  • Presets match real consumers: Default 1.25rem, OTP form 3rem, Page 4rem, Theater 5rem, Account settings 6rem
  • Glyph scales with width, stays centred
  • Color heading groups Loading / Success / Error
  • Loading's White preset sits beside the swatch, not under it
  • White → glyph and ring both white, and the demo gains a black background so they stay visible
  • Set Loading back to a dark colour → black background goes away
  • If the glyph paints as a solid block or won't recolour, the mask url() quoting has regressed — check computed mask-image, not just the visual
  • Reset → all four inputs back to defaults, background off

Settings panel

  • Any radio updates the demo and the usage example immediately, without Apply
  • Accessible titles heading groups Loading / Success / Error at the bottom; text fields, still need Apply
  • Usage example omits props at their default — all defaults collapses to <ia-status-indicator></ia-status-indicator>
  • Reflecting props show as attributes, non-reflecting as bindings:
<ia-status-indicator
  mode="error"
  mediatype="texts"
  hidedots
  .loadingTitle=${'Fetching book...'}
></ia-status-indicator>
  • Reset props → mode, mediatype, hideDots back to defaults; example back to the bare tag

Elsewhere

  • #elem-ia-otp-form — flip Validation Status idle → loading, the input row must not jump
  • Safari — ring still spins (the explicit 100% keyframe exists because Safari needs it)

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Aug 27, 2026

Copy link
Copy Markdown
PR Preview Action v1.8.1

🚀 View preview at
https://internetarchive.github.io/elements/pr/pr-91/

Built to branch ghpages at 2026-09-04 23:32 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@codecov-commenter

codecov-commenter commented Aug 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 48.23529% with 44 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.48%. Comparing base (d6888bb) to head (25e9c7d).

Files with missing lines Patch % Lines
...s/ia-status-indicator/ia-status-indicator-story.ts 13.79% 24 Missing and 1 partial ⚠️
demo/story-components/story-prop-settings.ts 26.66% 10 Missing and 1 partial ⚠️
demo/story-components/story-styles-settings.ts 68.18% 7 Missing ⚠️
demo/story-template.ts 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #91      +/-   ##
==========================================
- Coverage   84.69%   82.48%   -2.21%     
==========================================
  Files          38       38              
  Lines        1274     1342      +68     
  Branches      303      325      +22     
==========================================
+ Hits         1079     1107      +28     
- Misses        115      154      +39     
- Partials       80       81       +1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@property({ type: String, reflect: true }) mediatype?: MediaTypeIcon;

/* Renders a bare ring, with no dots in the middle. Ignored when a center icon is present. */
@property({ type: Boolean, reflect: true }) hideDots = false;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How come you're reflecting these 3 properties?

* consumers with an idle/pre-validation state (see `ia-otp-form`), which keeps
* their layout from shifting when the indicator appears.
*/
export type LoadingStatus = 'loading' | 'success' | 'error';

@jbuckner jbuckner Aug 31, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did we remove the ready status? Since this is a typed value on the property, it should be validating the type that it gets set to

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good call, restored in 25e9c7d. i'd taken it out thinking nothing used it, which was wrong — ia-otp-form renders the indicator unconditionally and relies on ready to reserve the row's width so it doesn't jump when validation starts. back in the union as a real case, and otp-form is back to plain LoadingStatus.

— Claude Code, o/b/o Isa HV

],
// Anything unrecognised (e.g. an idle 'ready' state) reserves space
// without drawing. See the note on LoadingStatus.
() => this.placeholderTemplate,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When would this get reached? this.mode is of type LoadingStatus so it should only ever be one of those types.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it wouldn't. that branch only existed to catch ready after i'd removed it from the type, so the type and the runtime disagreed. restoring ready made it an explicit case in choose and the fallback is gone — along with the type widening and comments that were propping it up. net -24 lines.

— Claude Code, o/b/o Isa HV

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would cropping the whitespace around this icon fix the vertical alignment issue?

interface HTMLElementTagNameMap {
'ia-status-indicator': IAStatusIndicator;
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't need this

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we organize the icons in a subdirectory so we don't have a bunch of icons at the top-level of the component?

iisa and others added 4 commits September 4, 2026 14:39
`maskedIcon` arrived with the item navigator in #64, but nothing about it is
navigator-specific: it renders a standalone `.svg` as a CSS-masked span so the
glyph stays recolorable and its path data stays out of the JS bundle, and it
carries the quoted-`url()` fix that keeps bundler-inlined `data:` URIs from
silently dropping the whole `mask-image` declaration.

The status indicator needs exactly that, so the helper and its test move to
`src/util/` rather than a second copy carrying the same fix. Only import paths
change for the navigator; the helper itself is untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ia-status-indicator rendered a ring with three animated dots and nothing
else — the middle was not addressable. #64 then landed a second loader,
ia-itemnav-loading-view, differing only in putting a static book glyph inside
a spinning ring. This closes that gap so the details-page theater can show
what kind of item is loading, and gives the duplicate loader a path out.

The ticket's original framing was wrong: ia-status-indicator has always lived
in elements (WEBDEV-8019, #17) and offshoot only consumes it. Nothing needed
moving — the gap was capability.

New API:
  - mediatype: one of 10 bundled glyphs in the ring's center
  - icon slot: escape hatch for glyphs with no mediatype
  - hideDots: bare ring, replacing loadingStyle="ring"
  - mode and mediatype now reflect, for CSS attribute hooks

Center resolution in loading mode: slotted icon, then mediatype glyph, then
nothing when hideDots is set, then the default dots. Two ring geometries are
kept deliberately — the original 120x120 when the center is dots or empty, the
roomier 100x100 when a glyph is present — so existing call sites see no visual
change. Unmapped mediatypes (account, data, movies) fall back to the dots.

Glyphs sit flat beside the component, matching ia-combo-box and
ia-dropdown-search-bar, and render through the shared maskedIcon.

Breaking changes:
  - loadingStyle is removed. Consumers passing "ring-dots" can drop the
    attribute; "ring" becomes hideDots.
  - 'ready' is removed from LoadingStatus. The component still renders its
    space-reserving placeholder for any unrecognised mode, and that fallback is
    now documented — ia-otp-form relies on it for its idle state and renders
    the indicator unconditionally, so removing the render would shift its row.

40 component tests at 100% coverage. Verified in-browser that all 10 glyphs are
distinct and unclipped, the glyph stays static while the ring spins, recolouring
drives glyph and ring together, and the otp-form row measures 60px in both idle
and loading.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Checking a component at a realistic size meant typing values by hand, and the
usage example only ever showed property bindings. These are generic additions
to the shared panels, available to any story.

Styles panel:
  - `presets`: one-click values rendered under an input, or beside it with
    `presetsInline`. The input and its range readout now share a row so the
    presets can stack beneath them.
  - `section`: groups consecutive inputs under a heading.
  - `stylesApplied` now bubbles and is composed, so a story can react to the
    variables its own panel applies. It previously stopped at story-template's
    shadow boundary.

Settings panel:
  - `section`, as above.
  - Radios apply on change, so the demo and the usage example follow a
    selection without a trip to Apply. Text fields still need it.
  - Props sitting at their default are left out of the usage example — a
    consumer only needs to see what they are actually changing — while still
    being assigned, otherwise selecting a default would not reset anything.
  - `reflects` renders a reflecting property as an attribute rather than a
    binding, bare when boolean, since a reflected boolean is present or absent
    rather than ="false".

That last one needed story-template to stop treating an empty stringifiedProps
as "nothing to do": an empty string is the meaningful all-defaults case.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Uses the new panel affordances to make the demo answer real questions.

  - Width presets are the widths consumers actually render at: 1.25rem default,
    3rem otp-form, 4rem page, 5rem theater, 6rem account settings. Each button
    names its consumer in a tooltip.
  - Colour inputs group under a `Color` heading, so the labels are Loading /
    Success / Error rather than repeating the prefix. Loading carries a White
    preset inline beside its swatch — one swatch and one button read fine on a
    line, unlike Width's five.
  - The accessible titles group at the bottom under their own heading.
  - The dark surface is derived from the loading colour rather than toggled: a
    light indicator needs a dark background to be visible at all. Revert sends
    an empty styles string, which is treated as "back to defaults" and clears
    the surface, rather than leaving it stuck on.
  - Mediatype uses a 'none' sentinel because the panel skips empty values, and
    the component degrades any unmapped mediatype to the dots anyway.
  - `revertable` opts into the panel's own Revert; the story's remaining button
    resets only the props, which Revert does not cover.

Width and colour are deliberately left out of the demo element's inline style —
the panel applies those to the demo's container, and setting either inline
would override it and make the panel's own controls look broken.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@iisa
iisa force-pushed the WEBDEV-8951-status-indicator-mediatype-icon branch from fbdb22c to bec1e01 Compare September 4, 2026 21:40
Removing `ready` from `LoadingStatus` while `ia-otp-form` still passed it left
the type and the runtime disagreeing: the union said three states, the render
carried a fallback for a fourth, and otp-form widened its own property back to
`LoadingStatus | 'ready'` to compile. The fallback was scaffolding for a
removal that nothing wanted.

`ready` goes back in the union and becomes an explicit case in `choose`, so the
placeholder is a real state rather than a default branch. That also removes an
unreachable-by-type arm: with `mode` typed, `choose` needs no fallback.
otp-form goes back to plain `LoadingStatus`.

Net -24 lines.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

3 participants