Skip to content

feat(ui-banner): add Banner component with locally vendored design tokens (testing only) - #2722

Draft
adamlobler wants to merge 2 commits into
masterfrom
feature/prompt-to-code-banner-component
Draft

adamlobler wants to merge 2 commits into
masterfrom
feature/prompt-to-code-banner-component

Conversation

@adamlobler

Copy link
Copy Markdown
Collaborator

⚠️ Testing-only branch — do not merge

This PR is for review and discussion, not merging. It's a proof-of-concept exercise: draft an entirely new InstUI component (Banner) from a plain-language description, with no Figma source, using only the design tokens already released in instructure-design-tokens v1.5.0.

Why the tokens are vendored into this repo instead of bumped to a newer release: Banner has no token set in the pinned v1.5.0 release. A newer upstream release (v1.8.0) already ships an official Banner token set, discovered midway through this exercise — but using it would have meant designing against someone else's already-made decisions instead of deriving a design independently from v1.5.0's existing vocabulary. So this branch:

  • Copies the v1.5.0 token source (JSON + loader) byte-for-byte into design-tokens/ in this repo, and proves that vendoring step is a no-op (regenerated newThemeTokens output is identical to before vendoring)
  • Points packages/ui-scripts at that local copy via a link: dependency instead of the real github:...#v1.5.0 pin
  • Authors Banner's tokens directly in that local copy

This is not how token changes should ship for real. The same additions (see below) could be made directly in the instructure-design-tokens repo, tagged as a real release, and then this repo would just bump its pin — no vendoring, no link: dependency. That's the path to actually shipping this component; this branch exists to test the component design without needing write access to that repo or waiting on a release.

Process

  1. Explored existing patterns: Alert's v2 implementation (withStyleNew, close-button/icon conventions) and researched how other design systems (Polaris, Spectrum, NewsKit, Primer) distinguish a promotional "Banner" from a status "Alert".
  2. Checked what instructure-design-tokens v1.5.0 actually contains before designing anything — confirmed no Banner set exists, and inventoried the existing background.accent.<hue> family as the right non-status semantic home for a promotional color.
  3. Avoided the hues already claimed by something else: violet/sea turned out to be reserved for AI theming in v1.5.0, blue/green/red/orange are the status colors, navy is brand. Landed on plum + sky — the second color was originally honey, then stone, retuned twice after visual review flagged honey as too close to warning's orange.
  4. Added one new alias family (below) where nothing existing covered the need, authored Banner's full component token set against Alert's file as a structural reference, then registered it in $metadata.json/$themes.json.
  5. Built the component itself functional + hooks (forwardRef, useStyleNew, useDeterministicId), matching the majority prop-naming convention in the codebase (renderIcon, renderTitle, onDismiss, screenReaderLabel) rather than the specific names that had appeared in the discarded v1.8.0 preview.
  6. Verified accessibility contrast by hand (relative luminance formula) against getComputedStyle output in a running docs instance, across all four themes independently, rather than trusting the generated docs table.
  7. Wired the package into packages/ui, the docs app, and the regression-test app; added unit tests and a regression page.

New tokens

Semantic — one new alias family, added to all four themes (light, dark, legacyCanvas, legacyCanvasHighContrast):

Token Description
semantic.color.background.accentMuted.plum A washed-back, low-saturation tint of the existing accent.plum hue — used for Banner's card surface, distinct per theme so it isn't a fixed alpha overlay.
semantic.color.background.accentMuted.sky Same, for the sky hue.

Component (tokensStudio/{rebrand,canvas}/component/Banner.json, one set per brand family):

Token Description
plumBackground / skyBackground Card surface color for each color treatment (accentMuted.<hue>).
plumIconBackground / skyIconBackground Icon chip background, full-strength (accent.<hue>) — distinct from the washed card surface.
iconColor Icon glyph color (icon.onColor).
color Body text color (text.base).
titleColor Title text color (text.basetext.title doesn't exist in v1.5.0).
borderRadius Card corner radius (borderRadius.xl) — deliberately rounder than Alert's lg.
iconContainerBorderRadius Icon chip corner radius (borderRadius.lg).
borderWidth / borderStyle / borderColor Card border — decorative only; the tinted surface, not the border, conveys the boundary.
paddingVertical / paddingHorizontal Card inner padding.
iconGap Gap between the icon chip and the content column.
stackGap Vertical gap between title, message, and actions.
actionGap Gap between action buttons in the actions row.
closeButtonMarginTop / closeButtonMarginEnd Close button offset from the card's top/end edge.
titleFontFamily / titleFontSize / titleFontWeight / titleLineHeight Title typography (heading family/weight).
contentFontFamily / contentFontSize / contentFontWeight / contentLineHeight Body typography (base family/weight).

Component properties

Prop Type Description
color 'plum' | 'sky' Color treatment. Not a status color like Alert's variants — pick whichever reads best against surrounding content.
renderIcon Renderable Icon at the start of the Banner. Defaults to a megaphone icon so one is always present; treated as decorative.
renderTitle Renderable Optional title. When present, labels the Banner's landmark for assistive technology.
children ReactNode Body content.
renderActions Renderable Call-to-action content (e.g. one or more Buttons).
renderCloseButtonLabel Renderable Accessible label for the close button. The close button only renders when this is provided.
onDismiss () => void Fired when the user dismisses the Banner. The component doesn't manage its own visibility.
screenReaderLabel string Accessible label for the landmark, used when there's no renderTitle.
elementRef (el: Element | null) => void Ref to the underlying element.
margin Spacing Spacing shorthand around the Banner.

Test Plan

  • Open the docs app at /#Banner and check all four themes (canvas, canvas-high-contrast, light, dark) — the two color treatments should read as promotional, not as status colors.
  • Confirm Banner never sets role="alert" or aria-live, unlike Alert.
  • /regression-test's /banner page for the six example states (both colors, dismissible, with actions, screen-reader-label-only).

🤖 Generated with Claude Code

…mponent with locally vendored design tokens

Add Banner, a proactive promotional/announcement component distinct from
Alert's reactive system-status messaging: no live-region semantics, a
labelled section landmark, an icon, optional title, optional close button,
and optional CTA actions in two color treatments (plum, sky).

Banner's design tokens don't exist in the pinned instructure-design-tokens
v1.5.0 release, so this branch vendors that release's token source locally
into design-tokens/ and points ui-scripts at it via a link: dependency
instead of bumping to a newer release. This keeps the new component's
tokens independent of newer, not-yet-reviewed upstream token work. See the
PR description and design-tokens/README-VENDORED.md for details and the
path back to the real instructure-design-tokens repo.

New tokens added on top of vendored v1.5.0:
- semantic.color.background.accentMuted.{plum,sky} (all four themes) - a
  washed-back surface variant of the existing accent.{plum,sky} family
- tokensStudio/{rebrand,canvas}/component/Banner.json - the component's
  full token set (colors, spacing, radii, borders, typography)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@adamlobler adamlobler self-assigned this Sep 17, 2026
CI failed with ERR_MODULE_NOT_FOUND: Cannot find package 'glob' imported
from design-tokens/src/index.js. The vendored token loader used glob to
enumerate tokensStudio/**/*.json, but design-tokens is consumed via a
link: dependency rather than a normal package install, so pnpm never
installs its declared dependencies - a fresh checkout has no glob for it
to resolve. Locally this was papered over with an ad hoc npm install
inside design-tokens/, which never got captured in git (node_modules is
gitignored, and its incidental package-lock.json shouldn't have been
committed either).

Replace globSync with Node's built-in recursive readdirSync instead of
vendoring glob too - the repo already requires Node >=22.18, well past
when that API landed. Verified the regenerated newThemeTokens output is
still byte-identical to before this change.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://instructure.design/pr-preview/pr-2722/

Built to branch gh-pages at 2026-09-17 11:22 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

github-actions Bot pushed a commit that referenced this pull request Sep 17, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Visual regression report

Cypress suite: ✅ Passing

Visual diff:No changes.

Status Count
Unchanged 96
Changed 0
New 3
Removed 0

Accessibility (axe): ✅ No violations.

📊 View full report — click a screenshot's ⚠ badge to see each violation boxed on the image, with the offending element named and contrast failures shown as color swatches.

Baselines come from the visual-baselines branch. They refresh on every merge to master. The Cypress suite line covers the a11y and console-error assertions — a ❌ there means the suite found real issues even if the visual diff is clean.

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