@cronos-labs/ui
v0.11.6
Published
Shared Header, Footer, Seo, locale and theme primitives for Cronos web properties.
Keywords
Readme
@cronos-labs/ui
Shared Header, Footer, SEO, locale and theme primitives for Cronos web
properties. Consumed by cronos-landingpage-webui, cronos-bridge-webui
and cronos-launch-webui so the three surfaces stay visually and
behaviourally identical without copying code between them.
The package is published publicly on npm; this repository is private.
Install
npm install @cronos-labs/uiPeer dependencies
Required — the package will not work without them:
| Package | Range |
| ------------------- | --------- |
| react | ^19.1.0 |
| react-dom | ^19.1.0 |
| styled-components | ^6.1.15 |
| react-router-dom | ^7.1.5 |
Optional — only needed if you consume the shared ESLint config
(@cronos-labs/ui/eslint-config):
@eslint/js, eslint, eslint-config-prettier, eslint-plugin-react-hooks,
eslint-plugin-react-refresh, globals, typescript-eslint.
Entry points
The package ships compiled ESM plus type declarations. Four subpath patterns are exported:
| Import | Contents |
| --------------------------------- | -------------------------------------------------------------- |
| @cronos-labs/ui | The barrel — every component, token and helper below |
| @cronos-labs/ui/<path> | Any single module, e.g. locale/config, locale/localization |
| @cronos-labs/ui/eslint-config | createReactEslintConfig |
| @cronos-labs/ui/prettier-config | The shared Prettier config, as a default export |
When to use a deep import instead of the barrel
This is the most common way to break a build. The barrel re-exports
components that depend on react-router-dom and styled-components. Any
context that evaluates modules with Node's own ESM loader rather than a
bundler — an Astro config file, a content-collection schema, a sitemap
script — cannot load it, and fails at import time with
styled.div is not a function.
Reach for the single module you actually need in those places:
// astro.config.mjs — evaluated by Node, not the bundler
import { localeConfig } from '@cronos-labs/ui/locale/config';// src/content/config.ts — content collection schema
import { Locale } from '@cronos-labs/ui/locale/localization';Application code that goes through the bundler uses the barrel normally:
import { Header, Footer, tokens, useCurrentLocale } from '@cronos-labs/ui';Bundler configuration
Vite treats the package as external during SSR by default and loads it
with Node's native ESM loader, which does not apply the __esModule
CJS-interop convention styled-components relies on. Force it through
the same transform pipeline as styled-components itself:
// astro.config.mjs / vite.config.ts
ssr: {
noExternal: ['styled-components', '@cronos-labs/ui'],
}For Vitest, inline the package so component tests resolve it the same way:
test: {
server: { deps: { inline: ['@cronos-labs/ui'] } },
}What's in the package
Components
| Export | Purpose |
| ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Header | Site header: brand wordmark, nav with dropdowns, mobile drawer, locale selector, waitlist CTA |
| Footer | Site footer: link columns, legal links, cookie-preferences hook, disclaimer copy |
| HeaderStatic / FooterStatic | Router-free variants for statically rendered pages (Astro). Take locale and pathname as props, force document navigation, and never import react-router-dom, so the island needs no <MemoryRouter> |
| Seo | Renders nothing; imperatively upserts <title>, meta and canonical tags. Creates tags that are absent rather than skipping them |
| LocaleSelector | Locale dropdown, shared by Header and Footer |
| SectionAnchorNav | In-page anchor nav that tracks the active section |
| SearchField | Labelled search input with optional clear button and status text |
| PillButton, FilterPillRow | Filter pill primitives |
Header and Footer are driven entirely by props — no data fetching, no
router coupling beyond react-router-dom. Their content strings arrive
through HeaderContent / FooterContent so each app supplies its own
localized copy.
Exported types: HeaderPresentationVariant, HeaderPresentationState,
HeaderNavItem, HeaderNavChild, HeaderContent, HeaderWaitlistCta,
FooterLinkItem, FooterLinkGroup, FooterLegalLinkItem,
FooterContent, SeoMeta, RouteSeoMeta, SearchFieldProps,
SectionAnchorNavItem.
Design tokens
tokens groups every shared value: colors, sizes, mediaQueries,
borderRadius, fontWeight, typography, shadow, effects, motion,
layout, zIndex. desktopViewportQuery is the min-width: 1024px
media query the components use to switch to their desktop layout.
import { tokens, desktopViewportQuery } from '@cronos-labs/ui';Typography
Ready-made styled-components css blocks, so type scale stays
consistent across apps: displayHeroTitleTypography,
pageHeroTitleTypography, articleHeroTitleTypography,
sectionTitleTypography, articleSectionTitleTypography,
cardTitleTypography, leadBodyTypography, articleBodyTypography,
compactBodyTypography, eyebrowTypography, metaTypography.
Locale
Nine locales are supported: en, en-in, en-ng, en-ph, id-id,
ko-kr, pt-br, es-mx, vi-vn. The prefix strategy is as-needed —
the default locale has no path prefix, the rest do.
| Export | Purpose |
| ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| Locale | Enum of supported locale codes |
| localeConfig | Full config: default, fallback, supported list, per-locale labels and text direction |
| isSupportedLocale | Type guard for an unknown string |
| resolveLocale | Resolves a value to a locale, falling back when unrecognised |
| stripLocalePrefix | Removes the locale segment from a pathname |
| buildLocalizedPath | Prefixes a path for a locale, honouring as-needed |
| useCurrentLocale | Reads the active locale from the router |
| useApplyLocaleDocumentAttributes | Keeps <html lang> and dir in sync |
| setStoredLocalePreference, getStoredLocalePreference, hasStoredLocalePreference, localePreferenceStorageKey | Persisted locale preference (cronos.locale) |
Types: AppLocale, LocaleConfig, LocaleDefinition,
LocalePrefixStrategy, TextDirection, LocalizedContentDictionary,
LocalizedPageMetadata.
Utilities
| Export | Purpose |
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| navigationContent | Shared nav structure, with the NavigationContent type |
| normalizePathname | Trailing-slash-insensitive pathname comparison used for nav active state |
| interpolateMessageTemplate | Fills {placeholder} tokens in localized strings |
| scrollToSectionHeading | Smooth-scrolls to a section's first h1/h2 (or [data-section-scroll-target]), offset 128px for the fixed header |
Shared lint and format config
// eslint.config.mjs
import { createReactEslintConfig } from '@cronos-labs/ui/eslint-config';
export default createReactEslintConfig({
tsconfigRootDir: import.meta.dirname,
});createReactEslintConfig takes tsconfigRootDir (required) and optional
ignores and allowDefaultProject. It composes the recommended,
type-checked and stylistic typescript-eslint sets with the React Hooks
and React Refresh plugins, and disables rules Prettier owns.
// package.json
"prettier": "@cronos-labs/ui/prettier-config"Local development
The toolchain is pinned, so every machine and CI resolve identically:
| Pinned by | File | Value |
| --------- | ---------------------------------- | -------------------- |
| Node | .nvmrc | 24.21.0 (active LTS) |
| npm | packageManager in package.json | 11.19.0 |
Use a version manager that reads .nvmrc (nvm, fnm, asdf) and enable Corepack
once — it ships with Node and resolves the npm named above.
This package deliberately publishes no engines field. The Node floor the
dev toolchain needs comes from a dev dependency, not from anything the library
does at runtime — it ships as bundled ESM and runs wherever its consumer runs.
Publishing that floor would impose a constraint that is not real, and every
consuming site sets engine-strict=true, so a wrong floor here would hard-fail
their npm ci. Pin the toolchain for contributors, impose nothing on
consumers.
nvm use # or: fnm use
corepack enable
npm ci
npm run lint
npm run typecheck
npm run test
npm run build
npm run format:checkUse npm ci, not npm install, unless you are deliberately changing a
dependency: packageManager fixes the npm version because two npm majors
disagree about which optional peer entries belong in package-lock.json, and
a lockfile written by one and installed by the other fails npm ci. The
release workflow publishes with the same pinned npm.
build compiles src/ to dist/ with tsc, then rewrites every relative
specifier in dist/**/*.js and dist/**/*.d.ts to point at the concrete
emitted file. tsc emits extensionless specifiers under
moduleResolution: "bundler", which plain Node ESM cannot resolve at
runtime. dist/ is generated and not committed.
The repo formats itself with the same Prettier config it exports. A unit
test asserts .prettierrc and src/config/prettierConfig.ts stay in
sync, so the two copies cannot drift.
Structure rule
Styled React units use ComponentName/index.tsx for the component and
ComponentName/styles.ts for its styled definitions.
Releasing
Releases are staged by tag and promoted by a human. The tarball is always built on the pinned runner rather than whatever happens to be on a laptop, and no credential in this repository can make a version live on its own: the CI token is a granular stage-only token, so the most it can do is put a build in front of a maintainer.
Land your change on
mainthrough a PR.Bump
versioninpackage.json(semver).Tag the merge commit and push:
git tag ui-v<version> git push origin ui-v<version>The workflow refuses to continue if the tag and
package.jsondisagree on the version.prepackrebuildsdist/as part of packing, so a stale build cannot ship.CI runs
npm stage publish .. The version is now staged and not yet installable —npm view @cronos-labs/ui versionstill reports the previous release.A maintainer with 2FA promotes it. The run log prints the stage id:
npm stage list @cronos-labs/ui npm stage view <stage-id> # inspect before shipping npm stage download <stage-id> # or pull the tarball and check it npm stage approve <stage-id> # publish npm stage reject <stage-id> # discard
Consumers then bump their range and run npm install. A patch or minor
inside their existing caret range needs no bump — npm install picks it up.
License
UNLICENSED. Published publicly on npm for installation convenience; not open source.
