@norstat/ui
v0.2.1
Published
Norstat's shared Vue 3 component library: form controls, feedback, layout and design tokens as one ES/CJS module with types and a single stylesheet.
Maintainers
Readme
@norstat/ui
Norstat's shared Vue 3 component library. It ships framework-agnostic building blocks
(BaseButton, BaseInput, …) and composables as a single ES module with rolled-up type
declarations and one stylesheet driven by CSS custom properties.
- Stack: Vue 3 (
<script setup>, Composition API), TypeScript, Vite library mode - Zero runtime dependencies — Vue is a peer dependency (
^3.4.0) - Distribution:
@norstat/uion npm, published under a proprietary licence for use by Norstat and its affiliates (see LICENSE) - Every component is prefixed
Baseto avoid collisions in consuming apps
What's in the box
Twenty-five components, grouped the same way as in Storybook and in
src/index.ts, plus one composable. Every prop, emit and slot is documented in
Storybook (pnpm storybook); this list is only the map.
Form controls
BaseButton— call to action; renders an<a>whenhrefis set. Variantsprimary,gradient,accent,secondary,ghost,text,danger;loading,full, icon slots.BaseIconButton— icon-only button with a required accessiblelabel.BaseInput— text field withlabel,hint,error,prefix/suffix, sizesmd/sm.BasePasswordInput—BaseInputwith a show/hide toggle.BaseSelect—optionsarray in the same field skin. Custom listbox on desktop (keyboard, type-ahead, flips above when there is no room, token-styled hover and selected states); native<select>on touch devices, or everywhere withnative.BaseCheckbox— labelled checkbox withhintanderror.BaseChip/BaseChipGroup— toggle chip and a single-choice group of chips (v-model).BaseOtpInput— one-time code split into digit boxes; emitscompletewith the full code.
Feedback and overlay
BaseAlert— inline message withvarianttone, live-regionrole, optional dismiss.BaseStatusIcon— tinted circle around a status icon (neutral,success,warning,danger).BaseProgressBar— determinate progress bar.BaseStepDots— segmented step indicator (total,current).BaseStrengthMeter— password strength segments driven by ascore.BaseLoadingDots— three-dot typing / loading indicator.BaseOverlay— modal on a native<dialog>: Escape and optional backdrop close, body scroll lock, widthsscreen,form,full. Lives in the browser's top layer, so fixed third-party widgets (the Freshdesk Help launcher) are covered and inert while it is open.
Layout and content
BaseCard— surface with variantstint,solid,mint,accent,subtle,darkand radiuslg/md.BaseHeading—eyebrow+title+subtitleblock with headinglevel,size,align,tone.BaseBadge— small pill (primary,accent,neutral).BaseStat— big figure with a caption; sizesmd,lg,display.BaseScreen— onboarding step shell: back button, step progress, primary and secondary CTA.BaseAvatarBubble— artwork holder incircle,sceneorbareshape.BaseIconText— icon + text row,boxedorplain.BaseLogo— Norstatmark(80×32) or outlinedwordmark(140×72); has an intrinsic size, so size it withheightonly.BaseFooter— footer shell with logo,taglineand a labelled<nav>oflinks;layoutwrap(default) orrow.
Composables
useDebounce(source, delayMs = 300)— read-only ref that mirrorssourceonce it has been stable fordelayMs:import { ref, watch } from 'vue' import { useDebounce } from '@norstat/ui' const query = ref('') const debouncedQuery = useDebounce(query, 300) watch(debouncedQuery, (q) => search(q))
Installing in an app
Install the package from the public npm registry. No
.npmrcor token is needed:pnpm add @norstat/ui # or: npm install @norstat/ui / yarn add @norstat/uiMake sure the app has the peer dependency.
@norstat/uidoes not bundle Vue and works with anyvue@^3.4.0; the package manager warns if it is missing:pnpm add vueImport the stylesheet once in your app entry, then use components where needed:
// main.ts import '@norstat/ui/style.css'<script setup lang="ts"> import { ref } from 'vue' import { BaseButton, BaseInput } from '@norstat/ui' import type { BaseButtonVariant } from '@norstat/ui' const email = ref('') const variant: BaseButtonVariant = 'primary' </script> <template> <BaseInput v-model="email" label="Email" type="email" /> <BaseButton :variant="variant" @click="submit">Save</BaseButton> </template>
Components are tree-shaken: an app that imports BaseButton and BaseInput bundles only those
two. The stylesheet is one file for the whole library (tokens + every component, ~44 kB, ~7 kB
gzipped) and is imported once.
The package ships an ES module (import) and a CommonJS build (require), each with its own type
declarations, so it also works in CJS-only tooling such as Jest or older SSR setups:
const { BaseButton } = require('@norstat/ui')Theming
All visual values are CSS custom properties prefixed --ns- (see
src/styles/tokens.css). Override them on :root or any ancestor
after importing the stylesheet:
:root {
--ns-color-primary: #0a7d5c;
--ns-radius-md: 0;
}The stylesheet also ships one container theme, .ns-theme-light
(src/styles/theme-light.css), for the white surfaces on the dark
pages (the signup card, the thank-you panel). Put the class on the white container and every
component inside switches to grey text and borders with a teal accent; nothing outside it changes:
<div class="ns-theme-light">
<BaseInput label="Email" />
</div>Every token is listed with its value under Foundations → Design tokens in Storybook.
The base font stack starts with SeroOffcPro, Norstat's licensed brand font. The package does not
ship font files; apps that have the font load it themselves, everything else falls back to the
system UI font. Override --ns-font-family-base to use a different typeface.
Browser support
The build targets Vite's baseline-widely-available set: the current and previous versions of
Chrome, Edge, Firefox and Safari, concretely Chrome 107+, Edge 107+, Firefox 104+, Safari 16+
(desktop and mobile). Vue 3 itself requires native ES2015 (no Internet Explorer), and
BaseOverlay uses the native <dialog> element, which all supported browsers ship. No polyfills
are included.
Developing locally
Requirements: Node 22.12+ (.nvmrc) and pnpm 12 (corepack enable picks the pinned version
from package.json).
pnpm install
pnpm dev # empty playground at http://localhost:5173 for ad-hoc local testing
pnpm storybook # Storybook at http://localhost:6006 — docs, controls, a11y checks
pnpm test # Vitest + @vue/test-utils (jsdom); pnpm test:watch for watch mode
pnpm lint # ESLint (flat config: eslint-plugin-vue + typescript-eslint); lint:fix to autofix
pnpm format # Prettier (format:check is what CI runs)
pnpm typecheck # vue-tsc for src/ + playground/ + .storybook/, tsc for vite.config.ts
pnpm build # typecheck + Vite library build → dist/
pnpm build-storybook # static Storybook → storybook-static/ (also run in CI)Two surfaces render the components, with different jobs:
- Playground (playground/) — an empty page for ad-hoc local testing. Drop
the component you are working on into
App.vue, iterate with hot reload, and clear the file before committing: nothing is stored there. - Storybook (.storybook/) — the documentation surface for designers and
developers. Each component has a
BaseThing.stories.tsnext to it with controls for every prop, autogenerated docs from thetypes.tsJSDoc, and the a11y addon flagging violations. Stories are the browsable, shareable reference; the playground is scratch space.
Neither is part of the published package: files ships dist/ only, and the library build
excludes *.stories.ts and .storybook/.
CI (.github/workflows/ci.yml) runs the same steps on every PR and
push to main: install with a frozen lockfile, lint, format:check, typecheck, test,
build and build-storybook. There is no hosted Storybook yet, so run pnpm storybook locally
to browse it.
Adding a component
- Create
src/components/BaseThing/with four files, mirroringBaseButton:BaseThing.vue—<script setup lang="ts">,withDefaults(defineProps<BaseThingProps>()),<style scoped>using onlyvar(--ns-*)tokenstypes.ts— exported prop/emit/variant typesBaseThing.spec.ts— at least one meaningful test (props, events, a11y attributes)index.ts— re-exports the component and its types
- Re-export it explicitly from src/index.ts. Nothing is auto-globbed.
- Add
BaseThing.stories.ts(copy the shape of BaseButton.stories.ts). - Run
pnpm changesetand describe the change for consumers.
The full checklist is in CONTRIBUTING.md.
Releasing
Versioning and the changelog are handled by Changesets; publishing to npm is triggered by a version tag.
Every PR that changes the public package includes a changeset (
pnpm changeset).On merge to
main, the Release workflow opens (or updates) a "chore: version packages" PR that bumpspackage.jsonandCHANGELOG.md.Merge that PR, then tag the resulting commit and push the tag:
git checkout main && git pull git tag "v$(node -p "require('./package.json').version")" git push origin --tagsThe Publish workflow (
.github/workflows/publish.yml) runs on everyv*tag: it installs, lints, typechecks, tests, builds and runsnpm publish --access public. It authenticates through npm's trusted publishing (the package on npmjs.com trusts this repository, workflow file and thenpmenvironment), so there is no token secret to store or rotate. The publish does not use--provenance: npm only accepts provenance attestations from public source repositories and this repository is private.
Before publishing, a package maintainer must configure Trusted publishing in the npm settings
for @norstat/ui, selecting GitHub Actions with these exact, case-sensitive values:
- Organization or user:
Norstat - Repository:
norstatpanel-ui - Workflow filename:
publish.yml(notrelease.yml, and without.github/workflows/) - Environment:
npm
The trusted publisher must allow direct npm publish, not only staged publishing.
An npm E404 on the publish PUT can mean missing publishing permission, not a missing package.
Check the verbose publish logs for an OIDC token exchange failure and verify the trusted publisher
settings above.
If a new trust configuration expired before its first successful publish, recreate it on npm.
Do not add NODE_AUTH_TOKEN or token-based .npmrc configuration to this tokenless workflow.
Re-running an old Actions run uses the workflow from its original tagged commit, not this fix;
ensure the release tag points to a commit containing the updated workflow before publishing.
The tag must match the version in package.json (v0.1.0 ↔ 0.1.0), otherwise the workflow fails
before publishing. A version can never be published twice, and npm only allows unpublishing within
72 hours, so check the "Version Packages" PR carefully before merging it.
Manual publish (first release, or if the workflow is unavailable): npm login, then
npm publish --access public from a clean main checkout. prepublishOnly runs lint, typecheck,
tests and the build first.
Package layout
dist/
norstat-ui.js ES module, Vue externalised
norstat-ui.cjs CommonJS build, Vue externalised
*.map source maps for both
index.d.ts rolled-up type declarations (ESM)
index.d.cts the same declarations for the CJS entry
style.css tokens + all component stylespackage.json exposes . (import → ESM, require → CJS, each with types), ./style.css and
./package.json. Only dist/, README.md, LICENSE and CHANGELOG.md are published; run
npm pack --dry-run to see the exact file list.
Further reading
- CONTRIBUTING.md — workflow, component checklist, conventions.
- CHANGELOG.md — what each release changed for consumers.
- LICENSE — proprietary; the package is public on npm so Norstat projects can install it without registry credentials, but it is licensed to Norstat and its affiliates only.
- docs/signup-inventory.md — the norstatpanel_signup component inventory the library was ported from.
- docs/campaigns-inventory.md — the norstatpanel-campaigns
inventory and the design decisions behind the
gradient/darkvariants.
