@ryadavwebdev/ui
v0.2.0
Published
Design tokens and presentational Angular components shared across the portfolio ecosystem.
Maintainers
Readme
@ryadavwebdev/ui
Design tokens and presentational Angular components shared across the portfolio
ecosystem (portfolio, folio, design-system).
Presentational only — no HTTP, no router, no app domain knowledge. Everything
public is exported from src/public-api.ts; nothing is deep-imported.
1. Load the tokens once, globally
The design tokens are a stylesheet, not a TypeScript export. Every app loads it exactly once. It must come first, before the app's own global styles.
Installed from npm (portfolio, design-system) — in angular.json:
"styles": [
"node_modules/@ryadavwebdev/ui/styles/tokens.css",
"src/styles.css"
]Inside this repo (form-builder, board) the libraries resolve to source,
so point at the workspace path instead — and note it belongs on the esbuild
target, which is where Native Federation moved the real build options:
"styles": [
"projects/ui/styles/tokens.css",
"projects/form-builder/src/styles.css"
]Under the Native Federation host: a remote loaded into the host does not serve its own
index.html, and the host's global styles win. The host must loadtokens.csstoo, or federated remotes render unstyled.
Once loaded, components reference the custom properties and never hardcode a value:
.thing {
padding: var(--space-16);
border-radius: var(--radius-md);
background: var(--color-surface-elevated);
color: var(--color-text);
}2. Switching themes
Themed tokens are declared once with CSS light-dark(), which resolves against
the element's computed color-scheme. That covers both requirements with one
mechanism:
| <html> | Result |
| -------------------- | ------------------------------------- |
| no attribute | Follows the OS prefers-color-scheme |
| data-theme="light" | Pinned light, overriding the OS |
| data-theme="dark" | Pinned dark, overriding the OS |
light-dark() is Baseline: Chrome 123+, Safari 17.5+, Firefox 120+.
Drive it from @ryadavwebdev/core, which owns the state and the persistence:
import { ThemeService } from '@ryadavwebdev/core';
export class Shell {
protected readonly theme = inject(ThemeService);
// theme.isDark() theme.toggle() theme.set('dark') theme.useSystem()
}Or call the framework-free helpers directly:
import { setTheme, applyStoredTheme } from '@ryadavwebdev/core';
setTheme('dark'); // sets <html data-theme> and persists
applyStoredTheme(); // re-applies the saved choiceAvoiding a flash of the wrong theme. ThemeService can only run once Angular
has booted. Add this to index.html so the attribute is set before first paint
(the key is `${storagePrefix}.theme`, default prefix folio):
<script>
(function () {
try {
var theme = localStorage.getItem('folio.theme');
if (theme === 'light' || theme === 'dark') {
document.documentElement.setAttribute('data-theme', theme);
document.documentElement.style.colorScheme = theme;
}
} catch (error) {}
})();
</script>Both lines are required — the
color-schemeone is not optional. Angular's critical-CSS inliner drops:root[data-theme="light"|"dark"]from the inlined block, because those selectors match nothing in the served HTML (Angular has not run yet). Those rules therefore arrive with the deferred stylesheet, after first paint. Set the attribute alone and the first paint falls back to the OS preference, then visibly flips — the exact flash this snippet exists to prevent. An inline style outranks any stylesheet and applies immediately.
3. Token reference
| Group | Tokens |
| ------------- | ------------------------------------------------------------------------------------------------------------------- |
| Surfaces | --color-bg --color-surface --color-surface-elevated --color-border --color-border-strong |
| Text | --color-ink --color-text --color-text-secondary --color-text-muted |
| Brand | --color-primary --color-purple --color-blue — each with -hover, --color-on-*, -text, -soft |
| Semantic | --color-success --color-warning --color-danger --color-info — each with --color-on-*, -text, -soft |
| Gradient | --gradient-brand --color-on-gradient |
| Type | --font-display --font-sans --font-mono; --text-12…--text-48; --leading-*; --weight-*; --tracking-wide |
| Space | --space-4 8 12 16 20 24 32 40 48 64 (4px base, named by px) |
| Radius | --radius-sm md lg xl full |
| Elevation | --shadow-sm --shadow-md --shadow-lg |
| Focus | --focus-ring-width --focus-ring-offset --focus-ring-color |
| Motion | --dur-fast --dur-base --ease-standard |
Naming convention for colour:
--color-x— the fill or accent--color-on-x— text that sits on that fill (AA verified)--color-x-text— the hue used as text on a background (darkened where needed)--color-x-soft— low-alpha tint for ghost and hover backgrounds
Fonts
Inter, Space Grotesk and JetBrains Mono are fetched from Google Fonts by an
@import at the top of tokens.css, so they travel with the stylesheet and need
no index.html step. Every font token carries a full system fallback stack, so
the UI degrades cleanly if the request fails.
4. Accessibility
Contrast is verified, not assumed. Every pairing below was computed against WCAG 2.1 (4.5:1 normal text, 3:1 non-text/interactive boundaries), light / dark:
| Pairing | Light | Dark |
| -------------------------------------- | --------------- | ------- |
| ink on bg | 19.64:1 | 18.82:1 |
| text-secondary on bg | 7.73:1 | 7.66:1 |
| text-muted on bg | 4.83:1 | 6.60:1 |
| on-primary on primary | 4.70:1 | 6.07:1 |
| on-purple on purple | 5.70:1 | 7.22:1 |
| on-blue on blue | 5.17:1 | 7.73:1 |
| on-success on success | 5.96:1 | 11.27:1 |
| on-warning on warning | 6.17:1 | 11.77:1 |
| on-danger on danger | 4.83:1 | 7.10:1 |
| on-gradient on both gradient ends | 4.70:1 / 5.70:1 | — |
| focus-ring-color vs background (3:1) | 5.17:1 | 7.73:1 |
| border-strong vs background (3:1) | 3.80:1 | 3.73:1 |
Two of these drove real decisions:
- White text fails on
success(3.30:1) andwarning(3.19:1). Rather than change the hues,--color-on-success/--color-on-warningare dark ink, which passes comfortably. Used as text on a light background those same hues also fail, so--color-success-text/--color-warning-textare darkened (#15803D,#B45309, both 5.02:1). --color-borderis decorative (1.27:1) and must never be the only boundary of a control.--color-border-strongexists for that — it is what thesecondary(outline) button uses.
Focus. tokens.css applies a 2px ring at 2px offset in brand blue to every
interactive element via a zero-specificity :where() rule, so components inherit
it rather than each reinventing one.
Motion. Under prefers-reduced-motion: reduce the duration tokens collapse to
0ms at source, so nothing needs to opt out individually. A blanket safety net
covers anything animating without the tokens.
5. Components
| Export | Selector | Notes |
| --------------- | ------------------- | ------------------------------------------------------------------- |
| Button | ui-button | variant · size · disabled · loading · block · ariaLabel |
| Card | ui-card | heading · subheading · [card-header] / [card-footer] slots |
| TokenShowcase | ui-token-showcase | The living reference — every token and component state |
Button variants: primary (brand red), secondary (outline), ghost,
gradient (the signature hero variant — one per view at most), danger.
Sizes: sm md lg.
The showcase
TokenShowcase renders the palette, type scale, spacing, radii, elevation and
every component state on one page. Mount it on a route:
{
path: 'design',
title: 'Design system',
loadComponent: () => import('@ryadavwebdev/ui').then((m) => m.TokenShowcase),
}In this repo it is live at :4201/design and :4202/design. It is purely
presentational — it holds no theme state, so it re-paints from the tokens when
the shell's theme toggle flips data-theme.
6. Versioning
The public API is a semver contract — and for this package the tokens are part of that contract, not just the TypeScript exports. Renaming a custom property breaks every consumer's stylesheet exactly as loudly as renaming a component.
| Change | Bump | | -------------------------------------------------------------------------------------------------------------- | --------- | | Adding a component, an optional input, a new token, a new variant | minor | | Removing or renaming an export or a token; changing a type incompatibly; making an optional input required | major | | Bug fix, docs, a token value corrected without renaming it | patch |
While the package is 0.x, a breaking change bumps the minor — that is the
standard 0.x convention, and it is why the API is not yet declared stable.
Bump projects/ui/package.json before every publish. npm versions are
immutable: a published version can never be re-uploaded, only deprecated.
Deep imports are not supported. The package exports map exposes the root entry
point plus ./styles/* only, so @ryadavwebdev/ui/lib/... fails at build time
by design.
7. Build, test, publish
ng build ui # → dist/ui (tokens.css ships at dist/ui/styles/)
ng test ui --watch=false
cd dist/ui && npm publish --access publicPublish from dist/ui, never from projects/ui — the built folder is the real
package (it has the FESM bundle, the flattened .d.ts and the generated
exports map).
