Skip to content

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

Open
iisa wants to merge 10 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 10 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 is merged into loading. It only meant "reserve space, draw nothing". Gone from the type, but the component still renders that placeholder for any unrecognised mode — ia-otp-form depends 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.
  • ready removed from LoadingStatus. Offshoot's duplicated ValidationStatus needs narrowing. No runtime change — ia-signup-input.ts:98 already guards it.

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.

Known duplication: this branches off main, so the mask rendering is inline rather than importing maskedIcon() from #64. Once #64 lands, a follow-up moves the helper somewhere shared and retires ia-itemnav-loading-view.

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. 213 pass repo-wide. madge --circular clean, build clean, 0 ESLint errors. The 10 new SVGs land in dist/.

Prettier drift in demo/story-template.test.ts is pre-existing on main (WEBDEV-8971).

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

ia-status-indicator rendered a ring with three animated dots and nothing
else — the middle was not addressable. PR #64 then brought in 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 bespoke
item-nav loader a path to retirement.

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

New API:
  - mediatype: renders one of 10 bundled glyphs in the ring's center
  - icon slot: escape hatch for arbitrary glyphs (e.g. item-nav's book)
  - hideDots: bare-ring escape hatch, 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.

Glyphs are drawn with a CSS mask rather than <img> so they recolor with
the ring, and ship as separate .svg assets to keep path data out of the
JS bundle. The mask url() is quoted: bundlers inline small SVGs as data:
URIs containing apostrophes, and an unquoted url() silently drops the
whole declaration and paints a solid block.

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 outright would shift its input row.

Unmapped mediatypes (account, data, movies) fall back to the dots rather
than a broken mask.

40 component tests at 100% coverage; verified in-browser that the mask
resolves, the glyph stays static while the ring spins, all 10 glyphs are
distinct and unclipped, 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>
@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-08-27 23:50 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 50.00000% with 47 lines in your changes missing coverage. Please review.
✅ Project coverage is 78.57%. Comparing base (8c054a6) to head (0565272).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
...s/ia-status-indicator/ia-status-indicator-story.ts 15.38% 21 Missing and 1 partial ⚠️
demo/story-components/story-styles-settings.ts 53.57% 13 Missing ⚠️
demo/story-components/story-prop-settings.ts 26.66% 10 Missing and 1 partial ⚠️
demo/story-template.ts 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #91      +/-   ##
==========================================
- Coverage   81.12%   78.57%   -2.55%     
==========================================
  Files          27       27              
  Lines         927      999      +72     
  Branches      227      250      +23     
==========================================
+ Hits          752      785      +33     
- Misses        113      152      +39     
  Partials       62       62              

☔ 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.

iisa and others added 9 commits August 27, 2026 15:59
… story

The story only exposed a raw Width text field and the theme colour vars, so
checking the mediatype glyph at a realistic size meant typing values by hand,
and there was no way to see it white-on-black the way the theater and
bookreader render it.

- Size presets for the widths consumers actually use: 1.25rem (default),
  3rem (otp-form), 4rem (home, details router), 5rem (theater, bookreader),
  6rem (account settings). Each button shows its width and names the consumer
  in its tooltip.
- Dark surface toggle: white icon and ring on black, matching the theater's
  --primary-text-color override.
- Reset, returning size, surface, mode, mediatype and hideDots to defaults.

Size and surface are applied inline on the demo element so they take
precedence over the Styles panel. The panel's own radios don't follow Reset,
so they can read stale until re-applied.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The presets belong next to the value they set, so they now render under their
own input in the Styles panel rather than in a separate settings form.

Adds two optional fields to StyleInputSettings, both generic enough for any
story to use:

- `presets`, rendered as buttons below the input. Choosing one fills the input
  and applies in a single click, so the value also flows into the generated
  STYLING snippet.
- `section`, which groups consecutive inputs under a heading. The three colour
  inputs are now "Loading" / "Success" / "Error" under a "Color" heading
  instead of repeating "Color - " in every label.

Width gets the widths consumers actually use; Loading gets White and Black.

The dark-surface toggle now sets only the background. It previously forced the
text colour white inline, which overrode the panel — the same way the inline
width did before the presets moved. Anything the panel owns has to stay out of
the demo element's inline style, or the panel's own controls look broken.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two fixes to the Styles panel:

- Presets rendered beside their input rather than below it. The cause was the
  pre-existing `.style-input-cell { display: flex }`, which laid the input and
  the preset group out as a row. The input and its range readout now share a
  `.style-input-row`, and the cell itself stacks, so presets sit under the
  value they set.
- The panel had no way back to defaults. `Reset` now restores every input to
  its `defaultValue` and applies, sitting beside `Apply`.

The story's own reset is relabelled "Reset props & surface" — with a Reset in
the Styles panel, two unqualified Reset buttons in one panel were ambiguous
about scope.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Drops the Surface section. A dark background is not an independent choice —
it is what a light indicator needs to be visible at all — so it now follows
the Color > Loading value instead of being toggled by hand. Picking the White
preset puts the indicator on black; going back to Black or hitting the Styles
panel's Reset takes it off.

`stylesApplied` now bubbles and is composed, so a story can react to the vars
its own panel applies. Without that the event stopped at story-template's
shadow boundary and the story could not see the colour at all.

Light is measured by perceived luminance rather than matching #ffffff, so a
hand-typed near-white behaves the same as the preset. Unparseable colours
leave the surface alone rather than guessing.

The story's reset is now just "Reset props" — the surface is no longer
something it owns.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
White earns its place because it is the theater's treatment and pairs with the
dark surface. Black was just a second way to reach a dark-on-light default the
colour picker and the Styles panel's Reset already cover.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three changes to the props panel, all generic to any story:

- `section` groups consecutive inputs under a heading, mirroring the Styles
  panel. The three accessible-title fields now sit together at the bottom
  under "Accessible titles" as Loading / Success / Error, instead of being
  interleaved with the behavioural props and repeating the prefix in every
  label.
- Radios apply on change, so choosing a mode or mediatype updates the demo and
  the usage example without a trip to Apply. Text fields still need it.
- Props sitting at their default are left out of the usage example. Consumers
  only need to see what they are actually changing, so the example collapses
  back to a bare tag once everything is default. They are still assigned to the
  component, otherwise selecting a default would not reset it.

That last one needed story-template to stop treating an empty stringifiedProps
as "nothing to do" — an empty string is the meaningful case where every prop is
at its default.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Whether presets belong under an input or beside it depends on how many there
are. Width's five buttons need their own row; the loading colour's single
swatch-and-button pair reads better on one line.

Adds `presetsInline` to StyleInputSettings, defaulting to the stacked layout,
and sets it on the loading colour.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A reflecting property is most naturally written as an attribute in markup, so
the example should show it that way rather than as a Lit property binding.

Adds `reflects` to PropInputSettings. Marked props render as `name="value"`,
or as a bare attribute when boolean, since a reflected boolean is present or
absent rather than ="false". Everything else keeps the `.prop=${value}` form,
so the two mix in one example: mode, mediatype and hidedots as attributes,
the accessible titles as bindings.

The attribute name is the lowercased property name, matching what Lit actually
reflects -- hence `hidedots`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every other icon in elements sits flat in its component's directory --
ia-combo-box/caret-closed.svg, ia-dropdown-search-bar/search.svg,
ia-snow/flake.svg -- so the mediatype glyphs follow rather than inventing an
icons/ subdirectory for themselves.

These are reusable domain iconography rather than component chrome, and
elements has no shared asset location the way offshoot's src/assets/img does.
Where they should ultimately live is WEBDEV-8817's call (migrating offshoot's
ia-icon into elements), and offshoot still holds its own copy of all ten.

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.

2 participants