@besaitech/ng-design-system

中文文档
0.0.84发布于 2026/11/05TypeScriptESM + CJS近 30 天下载 4495

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).

原始使用文档
文档目录10 节
  1. Requirements
  2. 1. Install
  3. 2. Tailwind setup
  4. 3. Tailwind v4 setup
  5. 4. Use a component
  6. Component catalog
  7. Theme & cross-app preferences
  8. Labels & languages
  9. Troubleshooting
  10. Versioning

@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-system

Public 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/postcss

3. 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 ignores node_modules by default. The explicit @source re-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

ComponentSelectorNotes
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)
SdsDateRangePicker / SdsRangeCalendar<sds-date-range-picker> / <sds-range-calendar>Range field: presets column + two-month range calendar, pending until Apply; [(value)] is an SdsDateRange | null ({ start, end }, end inclusive); (presetSelect) emits the preset id; helpers sdsLastDays(n), sdsLastMonths(n), formatDateRange()
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

ComponentSelectors
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)

ComponentSelectorNotes
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>

Charts (D3 does the maths, Angular renders the SVG, so they are OnPush, SSR-safe and themed by tokens) — a secondary entry point, @besaitech/ng-design-system/charts:

import { SdsLineChart, SdsKpi, SdsChartSeries } from '@besaitech/ng-design-system/charts'; // components + chart data types
import { provideSdsCharts, SdsChartLabels } from '@besaitech/ng-design-system';            // config + strings (root)

The root entry is ONE module: whatever an app uses from it anywhere — even on a lazy route — lands in the app's initial chunk. Keeping the charts (and d3) in their own entry lets the bundler leave them in the lazy chunk of the page that draws them. The configuration (provideSdsCharts, SDS_CHART_CONFIG, SdsChartConfig, SdsChartLabels, SDS_CHART_LABELS_EN and the label text types) stays in the root — it is tiny and d3-free — so app.config can provide it eagerly without pulling the charts in. Since 0.0.68 the root no longer re-exports the chart components or chart types.

ComponentSelectorNotes
SdsLineChart<sds-line-chart>Time / numeric / category x; multi-series, area, stack (stacked · percent), curve, log yScale, annotations, dashed forecast series, endLabels, crosshair tooltip
SdsBarChart<sds-bar-chart>orientation, stack, negatives, showValues, sort, barColor, highlight, combo type: 'line' series
SdsScatterChart<sds-scatter-chart>x/y, bubble size, group, trendline, log/time axes, nearest-point hover
SdsDonutChart<sds-donut-chart>Donut · pie · half; tail folds into "Other" past maxSlices; centre total / hovered slice (centerLabel="" hides the centre readout)
SdsHeatmap<sds-heatmap>Two categorical dims × a measure; scheme sequential · diverging, showValues
SdsTreemap<sds-treemap>Nested { name, value?, children? }; branch colours, click-to-zoom breadcrumb
SdsSparkline<sds-sparkline>Word-sized line · area · bar; band, reference, showMinMax
SdsKpi<sds-kpi>Stat tile: value, change vs previous (good/bad via upIsGood), optional sparkline trend

Every chart reads plain rows through accessors (a property name or (row, i) => value), so API data plots without reshaping; has an interactive legend (colour follows the series, never its rank), a keyboard-navigable tooltip, a data-table twin (tableToggle), loading / empty states, and a (pointClick) output. Colours come from the colour-blind-safe --sds-chart-1…8 slots in base.css (override any slot in your preset). Set app-wide defaults once:

providers: [provideSdsCharts({ locale: 'fr-FR', labels: CHART_LABELS_FR })]
// or, switching live with the app language (both functions are read inside computed()s):
providers: [provideSdsCharts({ locale: () => lang(), labels: () => chartLabelsFor(lang()) })]

Chart strings. Every built-in string — empty state, table toggle and headers, tooltip rows, the spoken summaries, role descriptions, the KPI's change sentence — is an SdsChartLabels key with an English default (SDS_CHART_LABELS_EN). Translate them once through provideSdsCharts({ labels }) (a Partial<SdsChartLabels>, merged over the English defaults); parameterised ones are functions (sliceCount(n), gridSize(rows, columns), labelValue(label, value), zoomedTo(path), versus(y, x), sparklineSummary({ label, first, last, min, max, count }), kpiChange({ direction, delta, period, good }), …). Per-chart inputs (emptyText, otherLabel, centerLabel, valueLabel, rootLabel, sizeLabel) still win for one chart; left unset (null) they fall back to the configured labels.

<sds-line-chart
  heading="Admissions"
  [data]="rows"
  x="date"
  [series]="[{ key: 'urgences', label: 'Urgences' }, { key: 'programmees', label: 'Programmées' }]"
  [annotations]="[{ type: 'line', value: 150, label: 'Capacité' }]"
/>

Custom tooltip body: <ng-template sdsChartTooltip let-tip>…</ng-template> inside any chart.

Rich text (Tiptap / ProseMirror) — a secondary entry point, @besaitech/ng-design-system/rich-text, for the same reason as the charts: ProseMirror stays in the lazy chunk of the page that edits rich text.

import { SdsRichTextEditor, SDS_RICH_TEXT_LABELS } from '@besaitech/ng-design-system/rich-text';
ComponentSelectorNotes
SdsRichTextEditor<sds-rich-text-editor>CVA over an HTML string ('' when empty). [tools] (headings, bold, italic, underline, strike, lists, quote, link, table, divider, clear, undo/redo) is the toolbar AND the schema; rows / maxRows, maxLength + counter, label, hint, error, invalid, required, readonly, disabled, appearance (default·filled), contentLanguage (spell-check language); focus(), insertText(text)
SdsLocalizedRichTextEditor<sds-localized-rich-text-editor>CVA over an SdsLocalizedValue ({ fr: '<p>…</p>', de: … }), like sds-localized-input: languages, currentLanguage, requiredLanguages, switcher at the end of the toolbar with filled / required / missing dots, empty translations pruned. One editor per language underneath — undo, caret and spell-check stay per language. Same rich-text inputs as above (maxLength is per language)
SdsRichTextToolbarEnd[sdsRichTextToolbarEnd]Marks content projected at the end of the toolbar (kept in readonly): a language switcher, "insert variable" buttons calling editor.insertText(…)

The emitted HTML is clean by construction: no inline styles, nothing outside the enabled tools, and links limited to http(s), mailto, tel, site-relative paths and anchors (sdsSafeHref) with rel="noopener noreferrer nofollow" — still sanitise stored HTML when you display it. Tables are saved bare (no <colgroup>, no inline widths). When loaded HTML holds content the tools cannot keep (<img>, <iframe>, a table without the table tool…), a warning under the field names it before anyone edits — saving an edit would drop it. ⌘/Ctrl+K opens the link field and is not passed on to an app-wide ⌘K palette. Strings are English by default: translate them per instance with [labels] (a Partial<SdsRichTextLabels>, follows a live language switch) or app-wide with { provide: SDS_RICH_TEXT_LABELS, useValue: { bold: 'Gras', … } }.

<sds-rich-text-editor
  label="Présentation"
  [tools]="['heading2', 'bold', 'italic', 'bulletList', 'orderedList', 'link', 'undo', 'redo']"
  [maxLength]="2000"
  formControlName="description"
/>

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=abc

Never 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

SymptomCause / fix
403 insufficient_scopeToken is missing read_package_registry scope
Components render unstyledMissing/incorrect @source line, or no theme preset imported (step 3b)
Brand colours are wrong / absentThe theme preset is imported BEFORE @import "tailwindcss", so it is overridden
sds-icon shows nothingThe 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.

版本与发布记录

@besaitech/ng-design-system 共发布 56 个版本,首次发布于 2026/09/01;过去 12 个月发布了 56 个版本。

版本发布日期距今
0.0.84latest2026/11/052 天前
0.0.832026/11/025 天前
0.0.822026/11/016 天前
0.0.812026/11/016 天前
0.0.802026/11/016 天前
0.0.792026/11/016 天前

关于 @besaitech/ng-design-system 的常见问题

@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).

如何安装 @besaitech/ng-design-system?

在项目目录运行 npm install @besaitech/ng-design-system,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:@angular/core(^20.3.0 || ^21.0.0 || ^22.0.0)、@angular/forms(^20.3.0 || ^21.0.0 || ^22.0.0)、@angular/common(^20.3.0 || ^21.0.0 || ^22.0.0)、@angular/router(^20.3.0 || ^21.0.0 || ^22.0.0)、@angular/platform-browser(^20.3.0 || ^21.0.0 || ^22.0.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@besaitech/ng-design-system 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@besaitech/ng-design-system 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@besaitech/ng-design-system 可以商用吗?

@besaitech/ng-design-system 使用 未声明 许可。package.json 没有声明许可证。没有许可证的代码默认保留全部权利,商用前请联系作者或查看仓库中的 LICENSE 文件。以上为概括,不构成法律意见,请以许可原文为准。

@besaitech/ng-design-system 的下载量有多少?

根据 npm 官方下载统计,@besaitech/ng-design-system 在 2026/10/08 – 2026/11/06 这 30 天内被下载 4,495 次,日均约 150 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@besaitech/ng-design-system 还在维护吗?

最新版本 0.0.84 发布于 2026/11/05(2 天前);至今共发布 56 个版本,其中过去 12 个月发布了 56 个;首次发布于 2026/09/01。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。