npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@waynestate/wayne-ui-vue

v2.0.1

Published

Wayne State University Vue 3 component library.

Readme

@waynestate/wayne-ui-vue

Wayne State University's design system, as Vue 3 components.

These components carry no styles of their own. Every class they emit is defined in @waynestate/wayne-ui-css, and the markup is held to contract/markup-contract.json. Extracted from the two canonical templates in apps/examples/static-html. That is the only reason the static-HTML, raw-PHP, Blade and Vue paths render identically rather than merely similarly.

Install

pnpm add @waynestate/wayne-ui-vue @waynestate/wayne-ui-css

Import the stylesheet once, in your application entry. This one line is required for both templates:

import '@waynestate/wayne-ui-css'

If the public can reach the page, also import the official chrome overlay, after the line above and never instead of it. /official layers the wayne.edu masthead and footer on top of the base stylesheet; on its own it renders almost no styling at all:

import '@waynestate/wayne-ui-css'
import '@waynestate/wayne-ui-css/official'

Copy icons.svg and the marks/ directory from node_modules/@waynestate/wayne-ui-css/dist into your app's public/ directory, at public/assets/wayne-ui, so a Vite build serves them at /assets/wayne-ui/icons.svg and /assets/wayne-ui/marks/*.svg unchanged. That is also the components' default sprite path, so no further configuration is needed if you copy them there. Somewhere else instead, pass that URL as the sprite option shown below, or as the mark prop on AppBar for a mark. The <use> elements the icon components render resolve against whatever URL you pass, not against where the npm package itself lives, so the path has to match where you actually copied the files.

Use

Per-file imports tree-shake and are what a Vite application should prefer:

<script setup>
import { AppShell, AppBar, Sidebar, SidebarNav, PageHeader, Footer } from '@waynestate/wayne-ui-vue'
</script>

<template>
  <AppShell>
    <template #bar>
      <AppBar variant="compact" title="Course Registration" mark="/assets/wayne-ui/marks/wsu-shield.svg" :nav="nav" search user="ee6515" />
    </template>
    <template #sidebar>
      <Sidebar><SidebarNav heading="Registration" :items="items" /></Sidebar>
    </template>
    <template #footer><Footer :links="links" /></template>

    <PageHeader title="Select courses" subtitle="Winter 2027" />
  </AppShell>
</template>

Or register everything globally. Useful in Blade-adjacent setups where no bundler analyzes the templates:

import WayneUI from '@waynestate/wayne-ui-vue'

app.use(WayneUI, { sprite: '/assets/wayne-ui/icons.svg' })
// <WsuButton>, <WsuAppShell>, …. Prefixed because Footer, Dialog, Input and
// Select all collide with something. Pass `prefix: ''` to opt out.

Two templates, not two design systems

Both are application templates. The difference is who can reach them, and therefore how much university identity they carry.

| | internal application | publicly accessible | |---|---|---| | chrome | <AppBar variant="compact">. Shield only, full-bleed | <OfficialHeader> + <OfficialFooter> | | search | inline field, collapsing to a row on narrow viewports | search-variant="expandable" | | footer | <Footer> | <OfficialFooter> |

A public page's masthead already has a wayne.edu search box; two search fields side by side, doing different jobs and looking identical, is a confusing thing to put in front of someone. Hence the expanding variant.

OfficialHeader and OfficialFooter render @waynestate/wsuheader and @waynestate/wsufooter themselves. They are not ours to fork, so they are consumed as dependencies; their scoped styles are stripped at build time so the vendored header.css/footer.css in wayne-ui-css/official remains the single source of their appearance, for every consumer path.

Behavior is shared, not reimplemented

Theme switching, the mobile drawer, the expanding search and the Cmd/Ctrl+K shortcut all come from @waynestate/wayne-ui-css/theme. The same module the no-build paths load. Wired through data-wsu-* attributes. A drawer therefore behaves identically in a Blade page and an Inertia page, and a bug fixed in one is fixed in all four.

AppShell initializes that runtime for markup Vue has just mounted, and only once the document has finished loading; before that the runtime's own bootstrap handles it. Wiring both would give every toggle two click listeners, and a drawer that opens and immediately closes.

import { useTheme, initChrome } from '@waynestate/wayne-ui-vue'

const { theme, isDark, toggle } = useTheme()

Forms and Laravel

Field components read validation errors from any of the three shapes Laravel hands a front end. An Inertia page prop, a MessageBag, or a JSON envelope, so no adapter is needed at the call site:

<script setup>
import { provideFormErrors } from '@waynestate/wayne-ui-vue'
const form = useForm({ course: '', reason: '' })
provideFormErrors(() => form.errors)
</script>

<template>
  <ErrorSummary :errors="form.errors" />
  <Input label="Course number" name="course" v-model="form.course" required />
  <Textarea label="Reason" name="reason" v-model="form.reason" />
</template>

Each field finds its own message by name. An error is always three things at once. A message in text, an icon, and aria-invalid. Never the red border alone (1.4.1, 3.3.1). ErrorSummary takes focus when it appears, so a failed submit is announced rather than hunted for.

Give each field an explicit id matching its name when you use ErrorSummary, so its links land on the right control.

Accessibility

WCAG 2.2 AA is the floor, AAA where the palette allows, and it is measured rather than asserted. Every component here is audited with axe-core in the test suite, and the whole system is audited again in a real browser at three viewports in both themes. pnpm verify at the repository root runs both.

What that means in practice for these components:

  • Styling is driven off the accessible state, aria-current, aria-invalid, aria-sort, [open]. Never a parallel .active class, so the two cannot drift apart.
  • Nothing is hidden by width, opacity or off-screen positioning. A collapsed drawer and a collapsed search row use hidden, so their controls are genuinely out of the tab order (2.4.3).
  • Dialog is a native <dialog>: focus trapping, Escape and an inert backdrop come from the browser, which is the implementation screen readers already agree with.
  • Tabs implements the APG roving tabindex; DataTable puts sort state in aria-sort, not only in an arrow.
  • Icon-only controls take their name from the label prop, rendered for screen readers only. Note that label is not enforced here: <Button icon> with no label renders an unnamed control. The Blade component throws in that case and the Vue one does not, which is a gap rather than a decision.
  • Toasts never disappear while they are being read: hovering or focusing the region pauses the countdown, and duration: 0 pins one open (2.2.1).

Motion

Subtle motion is part of the design and lives in wayne-ui-css, not in these components. The Blade and static paths have to move the same way. Durations come from --wsu-duration-fast and --wsu-duration-base; the global prefers-reduced-motion rule in base.css collapses them all.

Where Vue is needed at all it drives CSS classes rather than animating properties itself: Toasts uses <TransitionGroup name="wsu-toast">, whose wsu-toast-* classes are defined alongside the toast in components.css.

Development

pnpm --filter @waynestate/wayne-ui-vue test    # vitest + axe + the markup contract
pnpm --filter @waynestate/wayne-ui-vue build   # es + umd, vue external

test/contract.test.js reads contract/markup-contract.json and asserts the rendered output matches it, class for class, attribute for attribute. If it fails, the component is wrong. The templates are the contract, and changes go in that direction. Regenerate the contract with node scripts/extract-contract.mjs only when a template changed on purpose, and expect every other consumer path to need the same change.