Skip to content

Contributing

Most of what follows exists because breaking it has already caused a real defect. None of it is a style preference.

Running it

bash
pnpm install && composer install

pnpm build            # tokens, css, vue, registry
pnpm test             # unit tests across every package
pnpm lint             # eslint and stylelint
pnpm verify           # lint, build, test, and the accessibility gates
pnpm dev              # this site, with live component previews

The token package has to build before anything else, because the CSS build reads packages/tokens/dist/tokens.css and fails if it is missing. pnpm build gets the order right.

The two templates are the contract

apps/examples/static-html/index.html and public-app.html are the source of truth for what WayneUI markup looks like. Vue components, Blade components and the framework-free PHP helpers all have to emit the same class names, the same ARIA and the same data attributes. That is the only reason the four consumer paths render identically rather than merely similarly.

"Match the templates" is worth nothing if the only thing enforcing it is someone remembering, so the templates are extracted:

bash
node scripts/extract-contract.mjs            # regenerate
node scripts/extract-contract.mjs --check    # fail if stale

Every package tests against contract/markup-contract.json. If you change a template on purpose, every other path needs the same change, which is exactly what the failing check is telling you.

Adding a component

In order, because each step depends on the one before it:

  1. The CSS, in packages/css/src/components.css.
  2. Behavior in packages/css/src/theme.js, if it needs any, found by a data-wsu-* attribute. Only if the behavior can be shared. A Vue component with its own implementation opts out with data-wsu-thing="component".
  3. The Vue component, exported from the package index.
  4. The Blade component and the framework-free function. Both, not one.
  5. Both static templates, then regenerate the contract.
  6. A registry item. For a plain component, nothing by hand: the generator reads apps/site/src/gallery/examples.mjs, so once the component has a gallery entry, build.mjs writes its wrapper and its registry.json entry on the next run, and --check fails if either is missing. Chrome, the multi-file assemblies wired to Wayne State's structure and colors, is still curated by hand. See packages/registry/README.md for which of the two a given component is.
  7. Tests, and both browser suites: pnpm test:showcase and pnpm test:a11y. They cover different pages and either can be green while the other is not.
  8. An entry in apps/docs/.vitepress/data/components.mjs, with the three code samples, every prop the component declares, and whatever a reader has to know that the markup does not say. A component nobody can find is one nobody uses.
  9. node scripts/check-docs-coverage.mjs, which fails if a prop exists and no row describes it, or if a documented signature disagrees with the source.

This list stopped at seven for a while, and ten components were built against it before anyone noticed they were all undocumented. A step that is not written down does not happen, however obvious it seems.

Step 9 exists because step 8 was written down and still did not happen. On the day it was added it found 24 props across 8 components with no row, and 6 raw PHP signatures printing fewer parameters than the function takes, most of them added long after somebody read that list and believed they had followed it. A step nobody can skip beats a step nobody means to skip.

What it cannot do is tell you whether a row is true. It compares names and signatures, not meaning, so a prop documented as doing the opposite of what it does passes cleanly. That part is still on whoever operates the thing.

Three mistakes have each been made more than once here, so they are worth naming rather than leaving to be rediscovered:

Say only what you checked. Grep both templates yourself before writing that a component is on both. A commit message claiming a coverage it does not have is worse than silence, because it is where the next reader stops looking.

A component reaching only some paths is fine, when there is a reason. Say the reason. The keyboard key is on the internal template only, because the expandable search on public pages never shows the glyph. That is a decision. Absence with no explanation reads as an oversight and gets "fixed" later.

Track a child element when it carries meaning the parent cannot imply. A role, a state, or simply being the element the parent requires. A timeline's items are list items and its dates are time elements; an accordion's items are details and its summaries are summary. Add those to COMPONENTS in scripts/extract-contract.mjs, or nothing notices when a path stops emitting them. Decorative children do not need it, and saying which is which is part of the work.

Tokens

Components reference the semantic layer only. Never a raw ramp value. The CSS build fails on var(--wsu-green-400) inside a component or the chrome. Reaching through is how a system stops being themeable: the value is right in light, wrong in dark, and invisible to the contrast gate because no semantic token declares it.

Color ramps match waynestate/base-site stop for stop. green-400 has to mean the same thing to a C&IT developer as it does to a marketing developer. Extension stops are allowed only on numbers base-site does not define, and each one has to justify itself in the token source.

Contrast minima are declared on the tokens themselves, and the test suite reads those declarations back. Adding a color without a declaration is how a pairing escapes the gate, which is what the coverage test exists to catch.

Accessibility

WCAG 2.2 AA is the floor, AAA where the palette allows. It is a build gate, not an aspiration: nothing in CI is allow_failure.

  • State must never rest on color alone (1.4.1). The current-page item carries a heavier weight as well as a fill. Status messages carry an icon and text.
  • Drive styling off the accessible state. Select on aria-current, aria-invalid and [open] rather than a parallel .active class, so the two cannot drift.
  • Anything hidden has to be genuinely out of the tab order. A zero-width or zero-opacity control is still focusable, and landing on an invisible thing is a 2.4.3 failure that is easy to ship and hard to notice.
  • hidden is an HTMLElement property. On an SVGElement, assigning el.hidden = true can create an expando that reflects nothing. Use setAttribute and removeAttribute. This shipped a theme toggle whose icon and accessible name disagreed.

Motion

Subtle animation is encouraged. It is part of the design rather than a finishing touch, and it should be present in anything that opens, closes, expands or changes state.

The bar for whether motion earns its place is whether it explains a relationship or a state change. The expanding search in the public template animates its width because the growth is what tells you the icon and the field are the same control. Decoration that explains nothing does not belong.

  • Use the duration tokens. --wsu-duration-fast at 120ms for hovers, presses and small reveals. --wsu-duration-base at 200ms for anything entering or leaving. Never invent a duration.
  • prefers-reduced-motion is handled globally in base.css. Do not add per-component media queries for it. Keeping durations in tokens is what lets the global rule collapse them.
  • Top-layer entry and exit (dialog, popover, toast) needs @starting-style and transition-behavior: allow-discrete together. Without both, display flips instantly and there is nothing to transition.
  • Never transition visibility on an element that script focuses when it opens. A discrete property with allow-discrete does not commit its flip synchronously, so focus() runs against a still-hidden element and is silently ignored. The same applies to a transition-delay on that property. This cost an afternoon. See the expanding search in chrome.css.
  • Buttons take a 1px press on :active. Navigation items transition background-color only.

CSS

Cascade layers, not specificity fights. The order is wsu.base, wsu.components, wsu.chrome. If a rule is losing, it belongs in a later layer. Do not escalate the selector.

Never !important inside a layer. It inverts the layer order, so a "stronger" rule in base silently outranks one in chrome.

Do not reach into components you do not own. The base reset is scoped away from .wsuheader and .wsufooter. Forcing 44px targets onto the official search button distorted its background-image glyph. 1.x made the same mistake in the opposite direction, applying a global * reset from inside its header partial.

The official components

The university masthead and footer are @waynestate/wsuheader and @waynestate/wsufooter, consumed as npm dependencies. They are not ours to fork or restyle. Adaptations live in src/adapters/official.css, mostly layout and focus, plus two rules that close a gap in the dependency's own stylesheet rather than change its identity: a color on the footer's bare copyright text, which the dependency never sets one for, and a fix for the login label, which the dependency's Vue component names differently than its own CSS hides. See Theming for both, with the numbers.

1.x pasted a 2017 snapshot into a Blade template and never updated it. The official component has since renamed its wrapper and nobody noticed for nine years. A dependency cannot do that.

WayneUI is pure Tailwind. waynestate/styleguide is Zurb Foundation 6 based and is deliberately unused. The build fails rather than ships if Foundation ever appears in the official chrome.

Scope

WayneUI serves C&IT and DSOE internal applications and dashboards. waynestate/base-site serves marketing's public-facing sites. Both WayneUI templates are application templates. The only difference is who can reach them, and therefore how much university identity they carry:

internal application    compact bar, shield only, full bleed
publicly reachable      official masthead and footer, 1200px

Verifying

Look at it in a browser, and measure. Every visual defect found so far, the dark-on-dark wordmark, the squashed icon, the doubled focus ring, the 0px gap under the breadcrumbs, was invisible in the source and obvious on screen. Several were only obvious in one theme.

Working on this site

bash
pnpm --filter @waynestate/wayne-ui-docs dev

Three things about it are worth knowing before you edit anything.

The Tokens page is generated. It reads packages/tokens/dist/tokens.json and measures every contrast pairing with the same module the token tests use. There is nothing to edit on that page: change the token source and rebuild.

Component pages are generated too. The source is apps/docs/.vitepress/data/components.mjs. Adding a component there adds its page, its sidebar entry and its card on the index. Editing apps/docs/components/*.md directly is editing a file that is overwritten on the next build.

Every class name in a code sample is checked. When the site builds, each wsu- class in each of the three samples is looked up in the markup contract and in packages/css/dist/wayne-ui.css. A name that is in neither fails the build.

The site also serves three things that are not documentation:

/v2/*      the stylesheet bundle, copied from packages/css/dist
/r/*.json  the shadcn-vue registry, copied from packages/registry/dist/r
/brand/*   the WayneUI mark, copied from brand/

All three are copied into apps/docs/public/ by scripts/collect-assets.mjs before every dev and build run, so the URL that works locally is the URL that ships. The same script lifts the @font-face rules out of the built stylesheet into .vitepress/theme/generated/fonts.css, which is why this site is set in the same Lato the package ships rather than in a copy of it that can drift.

This site has to meet the bar it documents. It is the most-copied code in the project, and the axe gate runs over it exactly as it runs over the example applications.

Questions

Open an issue for anything that is reproducible: a defect, a component that does not match the contract, a gap in these pages. An issue is visible to whoever hits the same thing next, which an email is not.

For anything that does not fit that, including questions about adopting WayneUI in an application, requests for a component that does not exist yet, and accessibility findings you would rather not file in public, write to adammpkins@wayne.edu.