@besaitech/ng-design-system
v0.0.64
Published
Reusable **Angular 20** component library (`sds-` prefix) styled with **Tailwind CSS v4**, based on the Saitech visual language. Buttons, inputs, selects, tables, tabs, accordions, a date picker, phone input, plus the app shell (top bar + sidebar).
Readme
@besaitech/ng-design-system
Reusable Angular 20 component library (sds- prefix) styled with Tailwind CSS v4, based on
the Saitech visual language. Buttons, inputs, selects, tables, tabs, accordions, a date picker,
phone input, plus the app shell (top bar + sidebar).
Important: components are styled with Tailwind utility classes that live in their templates — the package does not ship compiled CSS. Your app must run Tailwind v4, scan this package, and define the design tokens (see Tailwind setup). Without that step the components render unstyled.
Requirements
- Angular 20.3+, 21.x or 22.x (
@angular/common,core,forms,platform-browser,router) - Tailwind CSS v4 in the consuming app
1. Install
npm install @besaitech/ng-design-systemPublic npm registry — no .npmrc, no scope mapping and no read token required.
2. Tailwind setup
The Angular peer dependencies (@angular/common, core, forms, …) are already present in any
Angular 20.3+ / 21 / 22 app. If you don't have Tailwind yet, add it as a dev dependency:
npm install -D tailwindcss @tailwindcss/postcss3. Tailwind v4 setup
This is the step that makes the components look right. Tailwind v4 is CSS-first — there is no
tailwind.config.js.
a. Register the PostCSS plugin. Create .postcssrc.json at your project root:
{
"plugins": {
"@tailwindcss/postcss": {}
}
}b. Import Tailwind, scan the library, and define the tokens in your global stylesheet
(src/styles.css). The @source line is what tells Tailwind to generate the utility classes used
inside the library, and the @theme block defines the colors/radii those classes refer to —
both are required:
@import "tailwindcss";
/* Generate the utilities used inside @besaitech/ng-design-system templates.
Path is relative to THIS file (src/styles.css → ../node_modules). */
@source "../node_modules/@besaitech/ng-design-system";
/* Tokens. BOTH imports are required, and both must come AFTER the tailwindcss import —
Tailwind v4 processes @theme in imported files and later declarations win.
base.css = surfaces, ink, accents, status, radius, layout vars (15 tokens).
The brand preset = the 11 --color-brand-* steps, and nothing else. */
@import "@besaitech/ng-design-system/themes/base.css";
@import "@besaitech/ng-design-system/themes/saitech.css"; /* blues */
/* @import "@besaitech/ng-design-system/themes/belvigo.css"; black */
/* @import "@besaitech/ng-design-system/themes/belvigo-emerald.css"; emerald (Belvigo merchant);
keeps green primary fills in dark mode through the --sds-dark-* hooks listed in base.css */
/* Override any base default in your own @theme afterwards, e.g. the typeface. */
@theme {
/* Neutrals / surfaces (already provided by base.css — shown for reference) */
--color-canvas: #f5f6f8;
--color-surface: #ffffff;
--color-muted: #f8f9fb;
--color-border: #e6e9f0;
--color-ink: #1b2440;
--color-ink-soft: #5b6577;
--color-ink-faint: #8a93a6;
/* Accent icon-tiles (foreground + soft background) */
--color-accent-blue: #2f6bd4;
--color-accent-blue-soft: #e8effb;
--color-accent-green: #1f9d57;
--color-accent-green-soft: #e6f5ec;
--color-accent-amber: #e0902a;
--color-accent-amber-soft: #fdf0dd;
--color-accent-red: #df4f6b;
--color-accent-red-soft: #fde7ec;
--color-accent-indigo: #6c5ce7;
--color-accent-indigo-soft: #ece9fb;
/* Status */
--color-success: #27c06b;
--color-warning: #f3b73c;
--color-danger: #df4f6b;
/* Radius */
--radius-control: 0.5rem;
--radius-card: 0.75rem;
/* Typography */
--font-sans: "Inter", "Segoe UI", system-ui, -apple-system, sans-serif;
}
/* Layout dimensions used by <sds-shell> / <sds-sidebar> / <sds-topbar>.
Only needed if you use the layout components. */
@layer base {
:root {
--sds-topbar-h: 64px;
--sds-sidebar-w: 256px;
--sds-sidebar-w-collapsed: 76px;
}
body {
margin: 0;
font-family: var(--font-sans);
background-color: var(--color-canvas);
color: var(--color-ink);
}
}Why
@source? Tailwind v4 ignoresnode_modulesby default. The explicit@sourcere-includes this package so the classes baked into its compiled templates produce real CSS. If components show up unstyled, this line (or a wrong relative path) is almost always the cause.
4. Use a component
All components are standalone — import the ones you need directly into a component's imports:
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { SdsButton, SdsInput, SdsIcon } from '@besaitech/ng-design-system';
@Component({
selector: 'app-member-form',
imports: [FormsModule, SdsButton, SdsInput, SdsIcon],
template: `
<sds-input label="Nom" [(ngModel)]="name" leadingIcon="search" clearable />
<sds-button variant="primary" (click)="save()">
<sds-icon sdsIconLeading name="user-plus" [size]="18" />
Créer un membre
</sds-button>
`,
})
export class MemberForm {
name = '';
save() {/* ... */}
}Form controls (sds-input, sds-textarea, sds-select, sds-multi-select, sds-toggle-switch,
sds-date-picker, sds-phone-input) implement ControlValueAccessor, so they work with both
template-driven ([(ngModel)]) and reactive (formControlName) forms.
Component catalog
Import names are the Sl… classes; the markup selectors are sds-….
Controls & primitives
| Component | Selector | Notes |
| --- | --- | --- |
| SdsButton | <sds-button> | variant (primary·secondary·tertiary·ghost·danger), size, loading, block, iconOnly; [sdsIconLeading]/[sdsIconTrailing] slots |
| SdsInput | <sds-input> | CVA. label, hint, error, size, appearance (default·on-brand), leading/trailingIcon, clearable, required, invalid, inputId |
| SdsTextarea | <sds-textarea> | CVA. Auto-grow, label, hint, error, required |
| SdsTimeInput | <sds-time-input> | CVA time of day, 24 h, value "HH:mm" | null; minuteStep, clearable, withSeconds |
| SdsChipGroup / SdsChip | <sds-chip-group> / <sds-chip> | Filter/choice chips: one or multiple (array), counts, [(value)] or CVA; single toggle chip [(selected)] |
| SdsToggleSwitch | <sds-toggle-switch> | CVA boolean toggle |
| SdsSelect | <sds-select> | CVA single-select dropdown |
| SdsMultiSelect | <sds-multi-select> | CVA multi-select with chips; (opened) / (closed) (emits the selection) |
| SdsDatePicker / SdsCalendar | <sds-date-picker> / <sds-calendar> | CVA date field + popover calendar (date-utils helpers) |
| SdsPhoneInput | <sds-phone-input> | CVA phone field with country selector (phone-countries) |
| SdsIcon | <sds-icon> | Inline-SVG icon; name, size, strokeWidth, filled |
| SdsAvatar | <sds-avatar> | Image, initials or fallbackIcon + status dot; shape (circle·rounded·square), decorative; broken image falls back |
| SdsBadge | <sds-badge> | Status pill; variant, dot |
| SdsCard | <sds-card> | Surface container; variant, interactive, padding |
Composite
| Component | Selectors |
| --- | --- |
| Tabs | <sds-tabs> · <sds-tab-list> · <sds-tab> · <sds-tab-panels> · <sds-tab-panel> — controlled + (tabSelect) for query-param/guarded tabs; panels optional; <sds-tab-list [ariaLabel]> |
| Accordion | <sds-accordion> · <sds-accordion-panel> · <sds-accordion-header> · <sds-accordion-content> |
| Table | <sds-table> · ng-template[sdsCell] · [sdsHeader] · [sdsRowExpansion] · [sdsTableEmpty] — rowClickable, [rowClass] |
| Paginator | <sds-paginator> |
Layout (use the design tokens / layout vars from step 3b)
| Component | Selector | Notes |
| --- | --- | --- |
| SdsShell | <sds-shell> | Top bar + sidebar + scrollable content; [sdsTopbar]/[sdsSidebar] slots |
| SdsTopbar | <sds-topbar> | appName, user, (logout); [sdsLogo] slot |
| SdsSidebar + SdsNavGroup + SdsNavItem | <sds-sidebar> … | Collapsible nav rail; collapse state via root-provided SidebarStore; items render routerLink or <button> |
Theme & cross-app preferences
SdsThemeService holds the person's mode — 'system' | 'light' | 'dark', persisted to localStorage
under sds-theme — and resolves it to the painted theme (light/dark); system follows the OS live.
toggle() / setTheme() set an explicit theme, setMode('system') goes back to following the OS.
Apps of one product can carry the person's language and theme in the URL when they link to each other, so the page that opens matches. Sending side:
import { appendUiPrefs } from '@besaitech/ng-design-system';
appendUiPrefs('https://auth.example/continue#token=abc', { lang: translate.getCurrentLang(), theme: theme.mode() });
// → https://auth.example/continue?lang=fr&theme=dark#token=abcNever put them inside a nested redirect a server matches exactly (return_to, redirect_uri) — append them to
the outer URL. Receiving side: an inline script in <head> of index.html applies, persists and strips them
before first paint (no flash, and no import of this library at the root). LANG_KEY is the app's own
language storage key; keep LANGS in step with the app's languages. readUiPrefs / stripUiPrefs are the same
logic for code and tests.
<script>
(function () {
var LANG_KEY = 'app.lang', LANGS = ['en', 'fr', 'nl'], THEME_KEY = 'sds-theme', MODES = ['system', 'light', 'dark'];
var url = new URL(location.href), q = url.searchParams, lang = q.get('lang'), mode = q.get('theme');
try {
if (LANGS.indexOf(lang) >= 0) localStorage.setItem(LANG_KEY, lang);
if (MODES.indexOf(mode) >= 0) localStorage.setItem(THEME_KEY, mode);
} catch (e) {}
try { lang = localStorage.getItem(LANG_KEY) || lang; mode = localStorage.getItem(THEME_KEY) || mode; } catch (e) {}
if (LANGS.indexOf(lang) >= 0) document.documentElement.lang = lang;
var dark = mode === 'dark' || (mode !== 'light' && matchMedia('(prefers-color-scheme: dark)').matches);
document.documentElement.classList.toggle('dark', dark);
document.documentElement.style.colorScheme = dark ? 'dark' : 'light';
if (q.has('lang') || q.has('theme')) {
q.delete('lang'); q.delete('theme');
history.replaceState(history.state, '', url.pathname + url.search + url.hash);
}
})();
</script>Labels & languages
The library ships no translations of its own. Every visible or spoken string — placeholders,
empty states, validation messages and the accessible names of icon-only buttons — is an input with
an English default (since 0.0.54; earlier releases defaulted some components to French). An app
serving more than one language binds its translations, e.g.
<sds-select [placeholder]="'common.select' | translate" [clearLabel]="'common.clear' | translate" />.
Locale-aware parts take a locale input: sds-date-picker/sds-calendar (month and weekday names,
default en-GB), sds-input-number and sds-table (number/currency formatting), sds-phone-input
(country names via Intl.DisplayNames; empty = built-in English names).
Troubleshooting
| Symptom | Cause / fix |
| --- | --- |
| 403 insufficient_scope | Token is missing read_package_registry scope |
| Components render unstyled | Missing/incorrect @source line, or no theme preset imported (step 3b) |
| Brand colours are wrong / absent | The theme preset is imported BEFORE @import "tailwindcss", so it is overridden |
| sds-icon shows nothing | The name isn't in the icon registry; check available names via the SdsIconName type |
Versioning
Semver. Each release is published from the workspace with npm run release:lib (bump → build →
publish). Pin a version in your app and bump deliberately.
