From a62c2b9dd3c2e67250995ed16b04211e1eae58b7 Mon Sep 17 00:00:00 2001
From: sjoerdbeentjes <11621275+sjoerdbeentjes@users.noreply.github.com>
Date: Tue, 30 Jun 2026 15:25:15 +0200
Subject: [PATCH 1/6] feat(angular): add data-table built on
@tanstack/angular-table
Mirror the React data-table for @surfnet/angular. Ships a headless
injectDataTable helper plus presentational pieces (HlmDataTableContent,
HlmDataTablePagination, hlmDataTableToolbar) for sorting, filtering,
pagination, column visibility and row selection on top of the existing
helm table.
Uses @tanstack/angular-table v8 (stable, version-matched to React's
@tanstack/react-table) and reuses the existing dataTableContract.
---
packages/angular/ng-package.json | 1 +
packages/angular/package.json | 1 +
.../src/lib/ui/data-table/src/index.ts | 14 +
.../src/lib/hlm-data-table-content.ts | 85 +++++
.../src/lib/hlm-data-table-pagination.ts | 62 ++++
.../src/lib/hlm-data-table-toolbar.ts | 16 +
.../src/lib/hlm-data-table.stories.ts | 322 ++++++++++++++++++
.../data-table/src/lib/inject-data-table.ts | 82 +++++
packages/angular/src/public-api.ts | 1 +
packages/angular/tsconfig.json | 1 +
pnpm-lock.yaml | 15 +
11 files changed, 600 insertions(+)
create mode 100644 packages/angular/src/lib/ui/data-table/src/index.ts
create mode 100644 packages/angular/src/lib/ui/data-table/src/lib/hlm-data-table-content.ts
create mode 100644 packages/angular/src/lib/ui/data-table/src/lib/hlm-data-table-pagination.ts
create mode 100644 packages/angular/src/lib/ui/data-table/src/lib/hlm-data-table-toolbar.ts
create mode 100644 packages/angular/src/lib/ui/data-table/src/lib/hlm-data-table.stories.ts
create mode 100644 packages/angular/src/lib/ui/data-table/src/lib/inject-data-table.ts
diff --git a/packages/angular/ng-package.json b/packages/angular/ng-package.json
index 7a7d2af..2bcb9cc 100644
--- a/packages/angular/ng-package.json
+++ b/packages/angular/ng-package.json
@@ -10,6 +10,7 @@
"@angular/router",
"@ng-icons/phosphor-icons",
"@spartan-ng/brain",
+ "@tanstack/angular-table",
"class-variance-authority",
"clsx",
"tailwind-merge",
diff --git a/packages/angular/package.json b/packages/angular/package.json
index 63773e5..df14110 100644
--- a/packages/angular/package.json
+++ b/packages/angular/package.json
@@ -27,6 +27,7 @@
"@angular/router": ">=21.0.0 <23.0.0",
"@ng-icons/phosphor-icons": ">=32.0.0 <34.0.0",
"@spartan-ng/brain": "0.0.1-alpha.715",
+ "@tanstack/angular-table": "^8.21.4",
"class-variance-authority": "0.7.1",
"clsx": "2.1.1",
"tailwind-merge": "3.6.0",
diff --git a/packages/angular/src/lib/ui/data-table/src/index.ts b/packages/angular/src/lib/ui/data-table/src/index.ts
new file mode 100644
index 0000000..c9238ff
--- /dev/null
+++ b/packages/angular/src/lib/ui/data-table/src/index.ts
@@ -0,0 +1,14 @@
+import { HlmDataTableContent } from './lib/hlm-data-table-content';
+import { HlmDataTablePagination } from './lib/hlm-data-table-pagination';
+import { HlmDataTableToolbar } from './lib/hlm-data-table-toolbar';
+
+export * from './lib/inject-data-table';
+export * from './lib/hlm-data-table-content';
+export * from './lib/hlm-data-table-pagination';
+export * from './lib/hlm-data-table-toolbar';
+
+export const HlmDataTableImports = [
+ HlmDataTableContent,
+ HlmDataTablePagination,
+ HlmDataTableToolbar,
+] as const;
diff --git a/packages/angular/src/lib/ui/data-table/src/lib/hlm-data-table-content.ts b/packages/angular/src/lib/ui/data-table/src/lib/hlm-data-table-content.ts
new file mode 100644
index 0000000..8d34581
--- /dev/null
+++ b/packages/angular/src/lib/ui/data-table/src/lib/hlm-data-table-content.ts
@@ -0,0 +1,85 @@
+import { ChangeDetectionStrategy, Component, input } from '@angular/core';
+import { HlmTableImports } from '@spartan-ng/helm/table';
+import { FlexRenderDirective, type ColumnDef, type Table } from '@tanstack/angular-table';
+
+/**
+ * Renders a TanStack table instance into the styled helm `table` directives. Mirrors React's
+ * `DataTableContent`: header groups, body rows (with `data-state="selected"` per selected
+ * row), and an empty-state row when there are no rows.
+ *
+ * String / HTML header and cell defs flow through `*flexRender` + `[innerHTML]`; component
+ * cell defs created with `flexRenderComponent()` are mounted in place by `*flexRender`.
+ */
+@Component({
+ selector: 'hlm-data-table-content',
+ imports: [HlmTableImports, FlexRenderDirective],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ host: {
+ 'data-slot': 'data-table-content',
+ class: 'block overflow-hidden rounded-md border',
+ },
+ template: `
+
+
+
+ @for (headerGroup of table().getHeaderGroups(); track headerGroup.id) {
+
+ @for (header of headerGroup.headers; track header.id) {
+
+ @if (!header.isPlaceholder) {
+
+
+
+ }
+
+ }
+
+ }
+
+
+ @if (table().getRowModel().rows.length) {
+ @for (row of table().getRowModel().rows; track row.id) {
+
+ @for (cell of row.getVisibleCells(); track cell.id) {
+
+
+
+
+
+ }
+
+ }
+ } @else {
+
+
+ {{ noResultsLabel() }}
+
+
+ }
+
+
+
+ `,
+})
+export class HlmDataTableContent {
+ /** The TanStack table instance, e.g. from `injectDataTable`. */
+ public readonly table = input.required>();
+
+ /** The column defs — used to span the empty-state row across all columns. */
+ public readonly columns = input.required[]>();
+
+ /** Content shown in the body when there are no rows. */
+ public readonly noResultsLabel = input('No results.');
+}
diff --git a/packages/angular/src/lib/ui/data-table/src/lib/hlm-data-table-pagination.ts b/packages/angular/src/lib/ui/data-table/src/lib/hlm-data-table-pagination.ts
new file mode 100644
index 0000000..3b1ca8b
--- /dev/null
+++ b/packages/angular/src/lib/ui/data-table/src/lib/hlm-data-table-pagination.ts
@@ -0,0 +1,62 @@
+import { ChangeDetectionStrategy, Component, input } from '@angular/core';
+import { HlmButton } from '@spartan-ng/helm/button';
+import { type Table } from '@tanstack/angular-table';
+
+/**
+ * Selection summary plus Previous / Next paging controls. Mirrors React's
+ * `DataTablePagination`.
+ */
+@Component({
+ selector: 'hlm-data-table-pagination',
+ imports: [HlmButton],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ host: {
+ 'data-slot': 'data-table-pagination',
+ class: 'flex items-center justify-end gap-2 py-4',
+ },
+ template: `
+
+ {{
+ selectionLabel()(
+ table().getFilteredSelectedRowModel().rows.length,
+ table().getFilteredRowModel().rows.length
+ )
+ }}
+
+
+
+ {{ previousLabel() }}
+
+
+ {{ nextLabel() }}
+
+
+ `,
+})
+export class HlmDataTablePagination {
+ /** The TanStack table instance, e.g. from `injectDataTable`. */
+ public readonly table = input.required>();
+
+ /** Label for the "previous page" button. */
+ public readonly previousLabel = input('Previous');
+
+ /** Label for the "next page" button. */
+ public readonly nextLabel = input('Next');
+
+ /** Builds the selection-summary text from the selected and total row counts. */
+ public readonly selectionLabel = input<(selectedCount: number, totalCount: number) => string>(
+ (selectedCount, totalCount) => `${selectedCount} of ${totalCount} row(s) selected.`,
+ );
+}
diff --git a/packages/angular/src/lib/ui/data-table/src/lib/hlm-data-table-toolbar.ts b/packages/angular/src/lib/ui/data-table/src/lib/hlm-data-table-toolbar.ts
new file mode 100644
index 0000000..a0c6159
--- /dev/null
+++ b/packages/angular/src/lib/ui/data-table/src/lib/hlm-data-table-toolbar.ts
@@ -0,0 +1,16 @@
+import { Directive } from '@angular/core';
+import { classes } from '@spartan-ng/helm/utils';
+
+/**
+ * Layout wrapper for data-table controls (filter inputs, column toggles, actions). Mirrors
+ * React's `DataTableToolbar` — a styled flex row above the table.
+ */
+@Directive({
+ selector: '[hlmDataTableToolbar]',
+ host: { 'data-slot': 'data-table-toolbar' },
+})
+export class HlmDataTableToolbar {
+ constructor() {
+ classes(() => 'flex items-center gap-2 py-4');
+ }
+}
diff --git a/packages/angular/src/lib/ui/data-table/src/lib/hlm-data-table.stories.ts b/packages/angular/src/lib/ui/data-table/src/lib/hlm-data-table.stories.ts
new file mode 100644
index 0000000..d17e4d3
--- /dev/null
+++ b/packages/angular/src/lib/ui/data-table/src/lib/hlm-data-table.stories.ts
@@ -0,0 +1,322 @@
+import { ChangeDetectionStrategy, Component, input, signal } from '@angular/core';
+import { NgIcon, provideIcons } from '@ng-icons/core';
+import { phosphorArrowsDownUp, phosphorDotsThreeVertical } from '@ng-icons/phosphor-icons/regular';
+import { HlmButton, HlmButtonImports } from '@spartan-ng/helm/button';
+import { HlmCheckbox } from '@spartan-ng/helm/checkbox';
+import { HlmDropdownMenuImports } from '@spartan-ng/helm/dropdown-menu';
+import { HlmInput } from '@spartan-ng/helm/input';
+import { type Meta, type StoryObj } from '@storybook/angular';
+import { dataTableContract } from '@surfnet/contracts';
+import {
+ flexRenderComponent,
+ type Column,
+ type ColumnDef,
+ type Row,
+ type Table,
+} from '@tanstack/angular-table';
+
+import { HlmDataTableContent } from './hlm-data-table-content';
+import { HlmDataTablePagination } from './hlm-data-table-pagination';
+import { HlmDataTableToolbar } from './hlm-data-table-toolbar';
+import { injectDataTable } from './inject-data-table';
+
+// ---------------------------------------------------------------------------
+// Shared fixtures
+// ---------------------------------------------------------------------------
+
+type Payment = {
+ id: string;
+ amount: number;
+ status: 'pending' | 'processing' | 'success' | 'failed';
+ email: string;
+};
+
+const data: Payment[] = [
+ { id: 'm5gr84i9', amount: 630.44, status: 'success', email: 'michael.mitc@example.com' },
+ { id: '3u1reuv4', amount: 767.5, status: 'success', email: 'felicia.reid@example.com' },
+ { id: 'derv1ws0', amount: 396.84, status: 'processing', email: 'georgia.young@example.com' },
+ { id: '5kma53ae', amount: 475.22, status: 'success', email: 'alma.lawson@example.com' },
+ { id: 'bhqecj4p', amount: 275.43, status: 'failed', email: 'dolores.chambers@example.com' },
+];
+
+const currency = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' });
+
+// ---------------------------------------------------------------------------
+// Interactive cell components (mounted by FlexRender via flexRenderComponent)
+// ---------------------------------------------------------------------------
+
+@Component({
+ selector: 'data-table-select-all',
+ imports: [HlmCheckbox],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `
+
+ `,
+})
+class DataTableSelectAll {
+ public readonly table = input.required>();
+}
+
+@Component({
+ selector: 'data-table-select-row',
+ imports: [HlmCheckbox],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `
+
+ `,
+})
+class DataTableSelectRow {
+ public readonly row = input.required>();
+}
+
+@Component({
+ selector: 'data-table-email-header',
+ imports: [HlmButton, NgIcon],
+ providers: [provideIcons({ phosphorArrowsDownUp })],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `
+
+ Email
+
+
+ `,
+})
+class DataTableEmailHeader {
+ public readonly column = input.required>();
+}
+
+@Component({
+ selector: 'data-table-row-actions',
+ imports: [HlmButtonImports, HlmDropdownMenuImports, NgIcon],
+ providers: [provideIcons({ phosphorDotsThreeVertical })],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `
+
+
+
+
+
+
+ Actions
+ Copy payment ID
+
+
+ View customer
+ View payment details
+
+
+ `,
+})
+class DataTableRowActions {
+ public readonly payment = input.required();
+
+ protected copyId(): void {
+ void navigator.clipboard?.writeText(this.payment().id);
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Shared columns
+// ---------------------------------------------------------------------------
+
+const columns: ColumnDef[] = [
+ {
+ id: 'select',
+ header: ({ table }) => flexRenderComponent(DataTableSelectAll, { inputs: { table } }),
+ cell: ({ row }) => flexRenderComponent(DataTableSelectRow, { inputs: { row } }),
+ enableSorting: false,
+ enableHiding: false,
+ },
+ {
+ accessorKey: 'status',
+ header: 'Status',
+ cell: ({ row }) => `${row.getValue('status')} `,
+ },
+ {
+ accessorKey: 'email',
+ header: ({ column }) => flexRenderComponent(DataTableEmailHeader, { inputs: { column } }),
+ cell: ({ row }) => `${row.getValue('email')} `,
+ },
+ {
+ accessorKey: 'amount',
+ header: () => `Amount
`,
+ cell: ({ row }) =>
+ `${currency.format(row.getValue('amount'))}
`,
+ },
+ {
+ id: 'actions',
+ enableHiding: false,
+ cell: ({ row }) =>
+ flexRenderComponent(DataTableRowActions, { inputs: { payment: row.original } }),
+ },
+];
+
+// ---------------------------------------------------------------------------
+// Host components (one per story, mirroring the React render functions)
+// ---------------------------------------------------------------------------
+
+@Component({
+ selector: 'payment-data-table',
+ imports: [
+ HlmDataTableContent,
+ HlmDataTablePagination,
+ HlmDataTableToolbar,
+ HlmButton,
+ HlmInput,
+ HlmDropdownMenuImports,
+ ],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ host: { class: 'block w-full' },
+ template: `
+
+
+
+ Columns
+
+
+
+ @for (column of hideableColumns(); track column.id) {
+
+ {{ column.id }}
+
+
+ }
+
+
+
+
+
+ `,
+})
+class PaymentDataTable {
+ protected readonly _data = signal(data);
+ protected readonly columns = columns;
+ protected readonly table = injectDataTable(() => ({ data: this._data(), columns: this.columns }));
+
+ protected readonly emailFilter = signal('');
+
+ protected hideableColumns() {
+ return this.table.getAllColumns().filter((column) => column.getCanHide());
+ }
+
+ protected setEmailFilter(event: Event): void {
+ const value = (event.target as HTMLInputElement).value;
+ this.emailFilter.set(value);
+ this.table.getColumn('email')?.setFilterValue(value);
+ }
+}
+
+@Component({
+ selector: 'empty-data-table',
+ imports: [HlmDataTableContent],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ host: { class: 'block w-full' },
+ template: `
+
+ `,
+})
+class EmptyDataTable {
+ protected readonly columns = columns;
+ protected readonly table = injectDataTable(() => ({ data: [], columns: this.columns }));
+}
+
+@Component({
+ selector: 'custom-toolbar-data-table',
+ imports: [HlmDataTableContent, HlmDataTablePagination, HlmDataTableToolbar, HlmButton],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ host: { class: 'block w-full' },
+ template: `
+
+ Recent payments
+ Export
+
+
+
+ `,
+})
+class CustomToolbarDataTable {
+ protected readonly _data = signal(data);
+ protected readonly columns = columns;
+ protected readonly table = injectDataTable(() => ({ data: this._data(), columns: this.columns }));
+}
+
+// ---------------------------------------------------------------------------
+// Meta + stories
+// ---------------------------------------------------------------------------
+
+const meta: Meta = {
+ title: 'Components/DataTable',
+ parameters: {
+ docs: {
+ description: {
+ component: dataTableContract.description,
+ },
+ },
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+/**
+ * Full table: filter input, column-visibility toggle, row selection, sortable Email column,
+ * right-aligned Amount, row actions, and Previous/Next pagination.
+ */
+export const Default: Story = {
+ render: () => ({
+ moduleMetadata: { imports: [PaymentDataTable] },
+ template: ' ',
+ }),
+};
+
+/** Empty state — no rows to display. */
+export const Empty: Story = {
+ render: () => ({
+ moduleMetadata: { imports: [EmptyDataTable] },
+ template: ' ',
+ }),
+};
+
+/** Custom toolbar layout: heading on the left, action button on the right. */
+export const CustomToolbar: Story = {
+ render: () => ({
+ moduleMetadata: { imports: [CustomToolbarDataTable] },
+ template: ' ',
+ }),
+};
diff --git a/packages/angular/src/lib/ui/data-table/src/lib/inject-data-table.ts b/packages/angular/src/lib/ui/data-table/src/lib/inject-data-table.ts
new file mode 100644
index 0000000..7c81ed0
--- /dev/null
+++ b/packages/angular/src/lib/ui/data-table/src/lib/inject-data-table.ts
@@ -0,0 +1,82 @@
+import { signal, type WritableSignal } from '@angular/core';
+import {
+ createAngularTable,
+ getCoreRowModel,
+ getFilteredRowModel,
+ getPaginationRowModel,
+ getSortedRowModel,
+ type ColumnDef,
+ type ColumnFiltersState,
+ type PaginationState,
+ type RowSelectionState,
+ type SortingState,
+ type Table,
+ type Updater,
+ type VisibilityState,
+} from '@tanstack/angular-table';
+
+interface InjectDataTableOptions {
+ data: TData[];
+ columns: ColumnDef[];
+ initialSorting?: SortingState;
+ initialColumnFilters?: ColumnFiltersState;
+ initialColumnVisibility?: VisibilityState;
+ initialPagination?: PaginationState;
+}
+
+/** Apply a TanStack `Updater` (value or `(old) => new`) to a writable signal. */
+function applyUpdater(state: WritableSignal): (updater: Updater) => void {
+ return (updater) =>
+ state.set(typeof updater === 'function' ? (updater as (old: T) => T)(state()) : updater);
+}
+
+/**
+ * Angular analog of React's `useDataTable`. Wraps `createAngularTable` with the core,
+ * sorted, filtered and paginated row models and manages sorting, column-filter,
+ * column-visibility, row-selection and pagination state with signals.
+ *
+ * Must be called from an injection context (a component constructor or field initializer),
+ * since `createAngularTable` injects. Pass an options factory so reactive `data` re-renders
+ * the table:
+ *
+ * ```ts
+ * readonly table = injectDataTable(() => ({ data: this.data(), columns }));
+ * ```
+ */
+function injectDataTable(options: () => InjectDataTableOptions): Table {
+ const initial = options();
+
+ const sorting = signal(initial.initialSorting ?? []);
+ const columnFilters = signal(initial.initialColumnFilters ?? []);
+ const columnVisibility = signal(initial.initialColumnVisibility ?? {});
+ const rowSelection = signal({});
+ const pagination = signal(
+ initial.initialPagination ?? { pageIndex: 0, pageSize: 10 },
+ );
+
+ return createAngularTable(() => {
+ const { data, columns } = options();
+ return {
+ data,
+ columns,
+ state: {
+ sorting: sorting(),
+ columnFilters: columnFilters(),
+ columnVisibility: columnVisibility(),
+ rowSelection: rowSelection(),
+ pagination: pagination(),
+ },
+ onSortingChange: applyUpdater(sorting),
+ onColumnFiltersChange: applyUpdater(columnFilters),
+ onColumnVisibilityChange: applyUpdater(columnVisibility),
+ onRowSelectionChange: applyUpdater(rowSelection),
+ onPaginationChange: applyUpdater(pagination),
+ getCoreRowModel: getCoreRowModel(),
+ getSortedRowModel: getSortedRowModel(),
+ getFilteredRowModel: getFilteredRowModel(),
+ getPaginationRowModel: getPaginationRowModel(),
+ };
+ });
+}
+
+export { injectDataTable, type InjectDataTableOptions };
diff --git a/packages/angular/src/public-api.ts b/packages/angular/src/public-api.ts
index d81e144..c9e9f41 100644
--- a/packages/angular/src/public-api.ts
+++ b/packages/angular/src/public-api.ts
@@ -10,6 +10,7 @@ export * from './lib/ui/breadcrumb/src';
export * from './lib/ui/button/src';
export * from './lib/ui/card/src';
export * from './lib/ui/checkbox/src';
+export * from './lib/ui/data-table/src';
export * from './lib/ui/dropdown-menu/src';
export * from './lib/ui/field/src';
export * from './lib/ui/input/src';
diff --git a/packages/angular/tsconfig.json b/packages/angular/tsconfig.json
index df8920d..efd4727 100644
--- a/packages/angular/tsconfig.json
+++ b/packages/angular/tsconfig.json
@@ -18,6 +18,7 @@
"@spartan-ng/helm/button": ["./src/lib/ui/button/src/index.ts"],
"@spartan-ng/helm/card": ["./src/lib/ui/card/src/index.ts"],
"@spartan-ng/helm/checkbox": ["./src/lib/ui/checkbox/src/index.ts"],
+ "@spartan-ng/helm/data-table": ["./src/lib/ui/data-table/src/index.ts"],
"@spartan-ng/helm/dropdown-menu": ["./src/lib/ui/dropdown-menu/src/index.ts"],
"@spartan-ng/helm/field": ["./src/lib/ui/field/src/index.ts"],
"@spartan-ng/helm/input": ["./src/lib/ui/input/src/index.ts"],
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index c3d58af..8a56e75 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -96,6 +96,9 @@ importers:
'@spartan-ng/brain':
specifier: 0.0.1-alpha.715
version: 0.0.1-alpha.715(@angular/cdk@21.0.0(@angular/common@22.0.1(@angular/core@22.0.1(@angular/compiler@22.0.1)(rxjs@7.8.2)(zone.js@0.16.2))(rxjs@7.8.2))(@angular/core@22.0.1(@angular/compiler@22.0.1)(rxjs@7.8.2)(zone.js@0.16.2))(rxjs@7.8.2))(@angular/common@22.0.1(@angular/core@22.0.1(@angular/compiler@22.0.1)(rxjs@7.8.2)(zone.js@0.16.2))(rxjs@7.8.2))(@angular/core@22.0.1(@angular/compiler@22.0.1)(rxjs@7.8.2)(zone.js@0.16.2))(@angular/forms@22.0.1(@angular/common@22.0.1(@angular/core@22.0.1(@angular/compiler@22.0.1)(rxjs@7.8.2)(zone.js@0.16.2))(rxjs@7.8.2))(@angular/core@22.0.1(@angular/compiler@22.0.1)(rxjs@7.8.2)(zone.js@0.16.2))(@angular/platform-browser@22.0.1(@angular/common@22.0.1(@angular/core@22.0.1(@angular/compiler@22.0.1)(rxjs@7.8.2)(zone.js@0.16.2))(rxjs@7.8.2))(@angular/core@22.0.1(@angular/compiler@22.0.1)(rxjs@7.8.2)(zone.js@0.16.2)))(rxjs@7.8.2))(clsx@2.1.1)(luxon@3.7.2)(rxjs@7.8.2)(tailwindcss@4.3.1)(tw-animate-css@1.4.0)
+ '@tanstack/angular-table':
+ specifier: ^8.21.4
+ version: 8.21.4(@angular/core@22.0.1(@angular/compiler@22.0.1)(rxjs@7.8.2)(zone.js@0.16.2))
class-variance-authority:
specifier: 0.7.1
version: 0.7.1
@@ -4541,6 +4544,12 @@ packages:
peerDependencies:
vite: ^5.2.0 || ^6 || ^7 || ^8
+ '@tanstack/angular-table@8.21.4':
+ resolution: {integrity: sha512-djyuJFvIB/pBuBIqny8b7+5ozeXJSElaxYRZyyKLpYrXn4xQSysBT//X51eDT/mi3kS0spcjIHhoObjk3iCjcQ==}
+ engines: {node: '>=12'}
+ peerDependencies:
+ '@angular/core': '>=17'
+
'@tanstack/react-table@8.21.3':
resolution: {integrity: sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==}
engines: {node: '>=12'}
@@ -15471,6 +15480,12 @@ snapshots:
tailwindcss: 4.3.1
vite: 8.0.16(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass-embedded@1.100.0)(sass@1.101.0)(terser@5.48.0)(yaml@2.9.0)
+ '@tanstack/angular-table@8.21.4(@angular/core@22.0.1(@angular/compiler@22.0.1)(rxjs@7.8.2)(zone.js@0.16.2))':
+ dependencies:
+ '@angular/core': 22.0.1(@angular/compiler@22.0.1)(rxjs@7.8.2)(zone.js@0.16.2)
+ '@tanstack/table-core': 8.21.3
+ tslib: 2.8.1
+
'@tanstack/react-table@8.21.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7)':
dependencies:
'@tanstack/table-core': 8.21.3
From 47e85a6c7b063dcd47066112ca6510a7ad1b761d Mon Sep 17 00:00:00 2001
From: sjoerdbeentjes <11621275+sjoerdbeentjes@users.noreply.github.com>
Date: Tue, 30 Jun 2026 20:18:19 +0200
Subject: [PATCH 2/6] fix(angular): render data-table cells as text, not
innerHTML
---
.../ui/data-table/src/lib/hlm-data-table-content.ts | 10 ++++++----
1 file changed, 6 insertions(+), 4 deletions(-)
diff --git a/packages/angular/src/lib/ui/data-table/src/lib/hlm-data-table-content.ts b/packages/angular/src/lib/ui/data-table/src/lib/hlm-data-table-content.ts
index 8d34581..b65c90b 100644
--- a/packages/angular/src/lib/ui/data-table/src/lib/hlm-data-table-content.ts
+++ b/packages/angular/src/lib/ui/data-table/src/lib/hlm-data-table-content.ts
@@ -7,8 +7,10 @@ import { FlexRenderDirective, type ColumnDef, type Table } from '@tanstack/angul
* `DataTableContent`: header groups, body rows (with `data-state="selected"` per selected
* row), and an empty-state row when there are no rows.
*
- * String / HTML header and cell defs flow through `*flexRender` + `[innerHTML]`; component
- * cell defs created with `flexRenderComponent()` are mounted in place by `*flexRender`.
+ * String / primitive header and cell defs are rendered as inert escaped text by `*flexRender`
+ * (matching React's `flexRender` of a string node); component cell defs created with
+ * `flexRenderComponent()` are mounted in place. To render markup, return a
+ * `flexRenderComponent()` rather than an HTML string — raw strings are never parsed as HTML.
*/
@Component({
selector: 'hlm-data-table-content',
@@ -34,7 +36,7 @@ import { FlexRenderDirective, type ColumnDef, type Table } from '@tanstack/angul
let headerContent
"
>
-
+ {{ headerContent }}
}
@@ -55,7 +57,7 @@ import { FlexRenderDirective, type ColumnDef, type Table } from '@tanstack/angul
let cellContent
"
>
-
+ {{ cellContent }}
}
From 967ecd9f0a8667e62848119443eb03262e43e343 Mon Sep 17 00:00:00 2001
From: sjoerdbeentjes <11621275+sjoerdbeentjes@users.noreply.github.com>
Date: Tue, 30 Jun 2026 20:18:22 +0200
Subject: [PATCH 3/6] refactor(angular): replace HTML-string cells with
flexRenderComponent in data-table story
---
.../src/lib/hlm-data-table.stories.ts | 55 +++++++++++++++++--
1 file changed, 50 insertions(+), 5 deletions(-)
diff --git a/packages/angular/src/lib/ui/data-table/src/lib/hlm-data-table.stories.ts b/packages/angular/src/lib/ui/data-table/src/lib/hlm-data-table.stories.ts
index d17e4d3..370a7f5 100644
--- a/packages/angular/src/lib/ui/data-table/src/lib/hlm-data-table.stories.ts
+++ b/packages/angular/src/lib/ui/data-table/src/lib/hlm-data-table.stories.ts
@@ -1,4 +1,4 @@
-import { ChangeDetectionStrategy, Component, input, signal } from '@angular/core';
+import { ChangeDetectionStrategy, Component, computed, input, signal } from '@angular/core';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { phosphorArrowsDownUp, phosphorDotsThreeVertical } from '@ng-icons/phosphor-icons/regular';
import { HlmButton, HlmButtonImports } from '@spartan-ng/helm/button';
@@ -136,6 +136,45 @@ class DataTableRowActions {
}
}
+// ---------------------------------------------------------------------------
+// Presentational cell components (the Angular equivalent of React's JSX cells)
+// ---------------------------------------------------------------------------
+
+@Component({
+ selector: 'data-table-status-cell',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `{{ status() }} `,
+})
+class DataTableStatusCell {
+ public readonly status = input.required();
+}
+
+@Component({
+ selector: 'data-table-email-cell',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `{{ email() }} `,
+})
+class DataTableEmailCell {
+ public readonly email = input.required();
+}
+
+@Component({
+ selector: 'data-table-amount-header',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `Amount
`,
+})
+class DataTableAmountHeader {}
+
+@Component({
+ selector: 'data-table-amount-cell',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ template: `{{ formatted() }}
`,
+})
+class DataTableAmountCell {
+ public readonly amount = input.required();
+ protected readonly formatted = computed(() => currency.format(this.amount()));
+}
+
// ---------------------------------------------------------------------------
// Shared columns
// ---------------------------------------------------------------------------
@@ -151,18 +190,24 @@ const columns: ColumnDef[] = [
{
accessorKey: 'status',
header: 'Status',
- cell: ({ row }) => `${row.getValue('status')} `,
+ cell: ({ row }) =>
+ flexRenderComponent(DataTableStatusCell, {
+ inputs: { status: row.getValue('status') },
+ }),
},
{
accessorKey: 'email',
header: ({ column }) => flexRenderComponent(DataTableEmailHeader, { inputs: { column } }),
- cell: ({ row }) => `${row.getValue('email')} `,
+ cell: ({ row }) =>
+ flexRenderComponent(DataTableEmailCell, { inputs: { email: row.getValue('email') } }),
},
{
accessorKey: 'amount',
- header: () => `Amount
`,
+ header: () => flexRenderComponent(DataTableAmountHeader),
cell: ({ row }) =>
- `${currency.format(row.getValue('amount'))}
`,
+ flexRenderComponent(DataTableAmountCell, {
+ inputs: { amount: row.getValue('amount') },
+ }),
},
{
id: 'actions',
From 9d826f591bf31374bd467845486a7ad29b30ae52 Mon Sep 17 00:00:00 2001
From: sjoerdbeentjes <11621275+sjoerdbeentjes@users.noreply.github.com>
Date: Tue, 30 Jun 2026 20:18:24 +0200
Subject: [PATCH 4/6] docs(angular): add manual source snippets to data-table
stories
---
.../src/lib/hlm-data-table.stories.ts | 112 ++++++++++++++++++
1 file changed, 112 insertions(+)
diff --git a/packages/angular/src/lib/ui/data-table/src/lib/hlm-data-table.stories.ts b/packages/angular/src/lib/ui/data-table/src/lib/hlm-data-table.stories.ts
index 370a7f5..4963ddd 100644
--- a/packages/angular/src/lib/ui/data-table/src/lib/hlm-data-table.stories.ts
+++ b/packages/angular/src/lib/ui/data-table/src/lib/hlm-data-table.stories.ts
@@ -344,6 +344,71 @@ type Story = StoryObj;
* right-aligned Amount, row actions, and Previous/Next pagination.
*/
export const Default: Story = {
+ parameters: {
+ docs: {
+ source: {
+ language: 'typescript',
+ code: `@Component({
+ selector: 'payment-data-table',
+ imports: [
+ HlmDataTableContent,
+ HlmDataTablePagination,
+ HlmDataTableToolbar,
+ HlmButton,
+ HlmInput,
+ HlmDropdownMenuImports,
+ ],
+ host: { class: 'block w-full' },
+ template: \`
+
+
+
+ Columns
+
+
+
+ @for (column of hideableColumns(); track column.id) {
+
+ {{ column.id }}
+
+
+ }
+
+
+
+
+
+ \`,
+})
+class PaymentDataTable {
+ protected readonly columns = columns;
+ protected readonly table = injectDataTable(() => ({ data, columns: this.columns }));
+ protected readonly emailFilter = signal('');
+
+ protected hideableColumns() {
+ return this.table.getAllColumns().filter((column) => column.getCanHide());
+ }
+
+ protected setEmailFilter(event: Event): void {
+ const value = (event.target as HTMLInputElement).value;
+ this.emailFilter.set(value);
+ this.table.getColumn('email')?.setFilterValue(value);
+ }
+}`,
+ },
+ },
+ },
render: () => ({
moduleMetadata: { imports: [PaymentDataTable] },
template: ' ',
@@ -352,6 +417,29 @@ export const Default: Story = {
/** Empty state — no rows to display. */
export const Empty: Story = {
+ parameters: {
+ docs: {
+ source: {
+ language: 'typescript',
+ code: `@Component({
+ selector: 'empty-data-table',
+ imports: [HlmDataTableContent],
+ host: { class: 'block w-full' },
+ template: \`
+
+ \`,
+})
+class EmptyDataTable {
+ protected readonly columns = columns;
+ protected readonly table = injectDataTable(() => ({ data: [], columns: this.columns }));
+}`,
+ },
+ },
+ },
render: () => ({
moduleMetadata: { imports: [EmptyDataTable] },
template: ' ',
@@ -360,6 +448,30 @@ export const Empty: Story = {
/** Custom toolbar layout: heading on the left, action button on the right. */
export const CustomToolbar: Story = {
+ parameters: {
+ docs: {
+ source: {
+ language: 'typescript',
+ code: `@Component({
+ selector: 'custom-toolbar-data-table',
+ imports: [HlmDataTableContent, HlmDataTablePagination, HlmDataTableToolbar, HlmButton],
+ host: { class: 'block w-full' },
+ template: \`
+
+ Recent payments
+ Export
+
+
+
+ \`,
+})
+class CustomToolbarDataTable {
+ protected readonly columns = columns;
+ protected readonly table = injectDataTable(() => ({ data, columns: this.columns }));
+}`,
+ },
+ },
+ },
render: () => ({
moduleMetadata: { imports: [CustomToolbarDataTable] },
template: ' ',
From 74c36febdb29c335f219b2bf72f866329c521a9c Mon Sep 17 00:00:00 2001
From: sjoerdbeentjes <11621275+sjoerdbeentjes@users.noreply.github.com>
Date: Wed, 1 Jul 2026 15:26:57 +0200
Subject: [PATCH 5/6] fix(angular): rewrite @spartan-ng/helm alias imports to
relative paths
ng-packagr doesn't resolve tsconfig path aliases when bundling, so vendored
helm-to-helm imports through @spartan-ng/helm/* were leaking into dist as
unresolved external imports (that package doesn't exist on npm), sometimes
alongside a correctly inlined duplicate of the same symbol. Add a codemod
(fix-helm-imports, run via jiti) to rewrite them to relative paths after
every `ng g` run.
---
.agents/skills/add-component/angular.md | 17 +++++-
packages/angular/package.json | 4 +-
.../angular/scripts/rewrite-helm-imports.ts | 60 +++++++++++++++++++
.../lib/ui/avatar/src/lib/hlm-avatar-badge.ts | 2 +-
.../ui/avatar/src/lib/hlm-avatar-fallback.ts | 2 +-
.../avatar/src/lib/hlm-avatar-group-count.ts | 2 +-
.../lib/ui/avatar/src/lib/hlm-avatar-group.ts | 2 +-
.../lib/ui/avatar/src/lib/hlm-avatar-image.ts | 2 +-
.../src/lib/ui/avatar/src/lib/hlm-avatar.ts | 2 +-
.../src/lib/hlm-breadcrumb-ellipsis.ts | 4 +-
.../breadcrumb/src/lib/hlm-breadcrumb-item.ts | 2 +-
.../breadcrumb/src/lib/hlm-breadcrumb-link.ts | 2 +-
.../breadcrumb/src/lib/hlm-breadcrumb-list.ts | 2 +-
.../breadcrumb/src/lib/hlm-breadcrumb-page.ts | 2 +-
.../src/lib/hlm-breadcrumb-separator.ts | 2 +-
.../src/lib/ui/button/src/lib/hlm-button.ts | 2 +-
.../lib/ui/card/src/lib/hlm-card-action.ts | 2 +-
.../lib/ui/card/src/lib/hlm-card-content.ts | 2 +-
.../ui/card/src/lib/hlm-card-description.ts | 2 +-
.../lib/ui/card/src/lib/hlm-card-footer.ts | 2 +-
.../lib/ui/card/src/lib/hlm-card-header.ts | 2 +-
.../src/lib/ui/card/src/lib/hlm-card-title.ts | 2 +-
.../src/lib/ui/card/src/lib/hlm-card.ts | 2 +-
.../lib/ui/checkbox/src/lib/hlm-checkbox.ts | 4 +-
.../src/lib/hlm-data-table-content.ts | 2 +-
.../src/lib/hlm-data-table-pagination.ts | 2 +-
.../src/lib/hlm-data-table-toolbar.ts | 2 +-
.../src/lib/hlm-data-table.stories.ts | 8 +--
.../hlm-dropdown-menu-checkbox-indicator.ts | 2 +-
.../src/lib/hlm-dropdown-menu-checkbox.ts | 2 +-
.../src/lib/hlm-dropdown-menu-group.ts | 2 +-
.../hlm-dropdown-menu-item-sub-indicator.ts | 2 +-
.../src/lib/hlm-dropdown-menu-item.ts | 2 +-
.../src/lib/hlm-dropdown-menu-label.ts | 2 +-
.../lib/hlm-dropdown-menu-radio-indicator.ts | 2 +-
.../src/lib/hlm-dropdown-menu-radio.ts | 2 +-
.../src/lib/hlm-dropdown-menu-separator.ts | 2 +-
.../src/lib/hlm-dropdown-menu-shortcut.ts | 2 +-
.../src/lib/hlm-dropdown-menu-sub.ts | 2 +-
.../src/lib/hlm-dropdown-menu.ts | 2 +-
.../lib/ui/field/src/lib/hlm-field-content.ts | 2 +-
.../ui/field/src/lib/hlm-field-description.ts | 2 +-
.../lib/ui/field/src/lib/hlm-field-error.ts | 2 +-
.../lib/ui/field/src/lib/hlm-field-group.ts | 2 +-
.../lib/ui/field/src/lib/hlm-field-label.ts | 4 +-
.../lib/ui/field/src/lib/hlm-field-legend.ts | 2 +-
.../ui/field/src/lib/hlm-field-separator.ts | 4 +-
.../src/lib/ui/field/src/lib/hlm-field-set.ts | 2 +-
.../lib/ui/field/src/lib/hlm-field-title.ts | 2 +-
.../src/lib/ui/field/src/lib/hlm-field.ts | 2 +-
.../src/lib/hlm-input-group-addon.ts | 2 +-
.../src/lib/hlm-input-group-button.ts | 4 +-
.../src/lib/hlm-input-group-input.ts | 4 +-
.../src/lib/hlm-input-group-text.ts | 2 +-
.../src/lib/hlm-input-group-textarea.ts | 4 +-
.../src/lib/hlm-input-group.stories.ts | 2 +-
.../ui/input-group/src/lib/hlm-input-group.ts | 2 +-
.../src/lib/ui/input/src/lib/hlm-input.ts | 2 +-
.../src/lib/ui/label/src/lib/hlm-label.ts | 2 +-
.../ui/select/src/lib/hlm-select-content.ts | 2 +-
.../lib/ui/select/src/lib/hlm-select-group.ts | 2 +-
.../lib/ui/select/src/lib/hlm-select-item.ts | 2 +-
.../lib/ui/select/src/lib/hlm-select-label.ts | 2 +-
.../ui/select/src/lib/hlm-select-multiple.ts | 2 +-
.../select/src/lib/hlm-select-placeholder.ts | 2 +-
.../select/src/lib/hlm-select-scroll-down.ts | 2 +-
.../ui/select/src/lib/hlm-select-scroll-up.ts | 2 +-
.../ui/select/src/lib/hlm-select-separator.ts | 2 +-
.../ui/select/src/lib/hlm-select-trigger.ts | 2 +-
.../lib/ui/select/src/lib/hlm-select-value.ts | 2 +-
.../src/lib/hlm-select-values-content.ts | 2 +-
.../src/lib/ui/select/src/lib/hlm-select.ts | 2 +-
.../lib/ui/separator/src/lib/hlm-separator.ts | 2 +-
.../lib/ui/sheet/src/lib/hlm-sheet-content.ts | 6 +-
.../ui/sheet/src/lib/hlm-sheet-description.ts | 2 +-
.../lib/ui/sheet/src/lib/hlm-sheet-footer.ts | 2 +-
.../lib/ui/sheet/src/lib/hlm-sheet-header.ts | 2 +-
.../lib/ui/sheet/src/lib/hlm-sheet-overlay.ts | 2 +-
.../lib/ui/sheet/src/lib/hlm-sheet-title.ts | 2 +-
.../ui/sidebar/src/lib/hlm-sidebar-content.ts | 2 +-
.../ui/sidebar/src/lib/hlm-sidebar-footer.ts | 2 +-
.../src/lib/hlm-sidebar-group-action.ts | 2 +-
.../src/lib/hlm-sidebar-group-content.ts | 2 +-
.../src/lib/hlm-sidebar-group-label.ts | 2 +-
.../ui/sidebar/src/lib/hlm-sidebar-group.ts | 2 +-
.../ui/sidebar/src/lib/hlm-sidebar-header.ts | 2 +-
.../ui/sidebar/src/lib/hlm-sidebar-input.ts | 4 +-
.../ui/sidebar/src/lib/hlm-sidebar-inset.ts | 2 +-
.../src/lib/hlm-sidebar-menu-action.ts | 2 +-
.../sidebar/src/lib/hlm-sidebar-menu-badge.ts | 2 +-
.../src/lib/hlm-sidebar-menu-button.ts | 4 +-
.../sidebar/src/lib/hlm-sidebar-menu-item.ts | 2 +-
.../src/lib/hlm-sidebar-menu-skeleton.ts | 4 +-
.../src/lib/hlm-sidebar-menu-sub-button.ts | 2 +-
.../src/lib/hlm-sidebar-menu-sub-item.ts | 2 +-
.../sidebar/src/lib/hlm-sidebar-menu-sub.ts | 2 +-
.../ui/sidebar/src/lib/hlm-sidebar-menu.ts | 2 +-
.../ui/sidebar/src/lib/hlm-sidebar-rail.ts | 2 +-
.../sidebar/src/lib/hlm-sidebar-separator.ts | 4 +-
.../ui/sidebar/src/lib/hlm-sidebar-trigger.ts | 2 +-
.../ui/sidebar/src/lib/hlm-sidebar-wrapper.ts | 2 +-
.../src/lib/ui/sidebar/src/lib/hlm-sidebar.ts | 4 +-
.../lib/ui/skeleton/src/lib/hlm-skeleton.ts | 2 +-
.../src/lib/ui/table/src/lib/hlm-table.ts | 2 +-
.../lib/ui/textarea/src/lib/hlm-textarea.ts | 2 +-
.../src/lib/ui/tooltip/src/lib/hlm-tooltip.ts | 2 +-
pnpm-lock.yaml | 3 +
107 files changed, 200 insertions(+), 124 deletions(-)
create mode 100644 packages/angular/scripts/rewrite-helm-imports.ts
diff --git a/.agents/skills/add-component/angular.md b/.agents/skills/add-component/angular.md
index e3d70db..1c2336d 100644
--- a/.agents/skills/add-component/angular.md
+++ b/.agents/skills/add-component/angular.md
@@ -32,6 +32,9 @@ don't hand-write helm code. Config lives in `packages/angular/components.json`
to `tsconfig.json`. The schematic is interactive unless `components.json` already
exists (it does) — pass `--defaults` for non-interactive runs.
+ Immediately after generating, run `pnpm --filter @surfnet/angular fix-helm-imports` —
+ see the note below on why the alias must not survive into the vendored files.
+
3. **Tie the component to the contract — for every axis it has.** Import the `*Name` unions
from `@surfnet/contracts` and wire them in. Two styles, by how the helm code models the
axis:
@@ -116,9 +119,17 @@ pnpm format
## Notes
-- Helm files import each other through the `@spartan-ng/helm/*` alias, which the tsconfig
- paths resolve to local source; `ng-packagr` inlines them. Don't rewrite these to
- relative imports — keeping the alias lets future `ng g` runs work unchanged.
+- The Spartan CLI always vendors cross-component imports through the `@spartan-ng/helm/*`
+ alias. `@spartan-ng/helm` isn't a real npm package — the alias only resolves when a
+ consuming *app*'s own bundler reads the tsconfig `paths` mapping at build time.
+ `@surfnet/angular` instead builds itself into a redistributable package via `ng-packagr`,
+ which does not consult tsconfig `paths`: it leaves the alias as an unresolved external
+ import in the published bundle (and can leave the real symbol duplicated wherever it's
+ also reached via a relative import elsewhere). Run
+ `pnpm --filter @surfnet/angular fix-helm-imports` (`packages/angular/scripts/rewrite-helm-imports.ts`)
+ after every `ng g` to rewrite the new alias imports to relative ones — verify with
+ `pnpm --filter @surfnet/angular build` and confirm `dist` has no `@spartan-ng/helm` left
+ (`grep -r "@spartan-ng/helm" packages/angular/dist`).
- The library has no global stylesheet in its build output; the theme tokens in
`src/styles.css` are loaded by Storybook (via the `styles` option) and are meant to be
imported by consuming apps.
diff --git a/packages/angular/package.json b/packages/angular/package.json
index df14110..46dd6bc 100644
--- a/packages/angular/package.json
+++ b/packages/angular/package.json
@@ -8,7 +8,8 @@
"dev": "ng build angular --watch --configuration development",
"lint": "ngc --noEmit -p tsconfig.json",
"storybook": "ng run angular:storybook",
- "build-storybook": "ng run angular:build-storybook"
+ "build-storybook": "ng run angular:build-storybook",
+ "fix-helm-imports": "jiti scripts/rewrite-helm-imports.ts"
},
"peerDependencies": {
"@angular/common": "^22.0.0",
@@ -51,6 +52,7 @@
"@surfnet/tokens": "workspace:*",
"@surfnet/typescript-config": "workspace:*",
"@tailwindcss/postcss": "4.3.1",
+ "jiti": "2.7.0",
"ng-packagr": "22.0.0",
"rxjs": "7.8.2",
"storybook": "10.4.5",
diff --git a/packages/angular/scripts/rewrite-helm-imports.ts b/packages/angular/scripts/rewrite-helm-imports.ts
new file mode 100644
index 0000000..07e579a
--- /dev/null
+++ b/packages/angular/scripts/rewrite-helm-imports.ts
@@ -0,0 +1,60 @@
+/**
+ * Rewrites `@spartan-ng/helm/` imports to relative paths.
+ *
+ * The Spartan CLI always vendors cross-component references through the
+ * `@spartan-ng/helm/` tsconfig path alias. That alias only resolves at
+ * build time in a consuming *app* (whose own bundler reads tsconfig `paths`).
+ * `@surfnet/angular` instead builds itself into a redistributable package via
+ * ng-packagr, which does not consult tsconfig `paths` and leaves the alias as
+ * an unresolved external import in the published bundle. Run this after every
+ * `ng g @spartan-ng/cli:ui ` to convert the new alias imports to
+ * relative ones so ng-packagr inlines them correctly.
+ *
+ * Usage: pnpm --filter @surfnet/angular fix-helm-imports
+ */
+
+import { readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
+import { dirname, join, relative } from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const packageRoot = join(dirname(fileURLToPath(import.meta.url)), '..');
+const srcRoot = join(packageRoot, 'src');
+const importPattern = /from (['"])@spartan-ng\/helm\/([a-z0-9-]+)\1/g;
+
+function walk(dir: string, files: string[] = []): string[] {
+ for (const entry of readdirSync(dir)) {
+ const full = join(dir, entry);
+ if (statSync(full).isDirectory()) {
+ walk(full, files);
+ } else if (entry.endsWith('.ts')) {
+ files.push(full);
+ }
+ }
+ return files;
+}
+
+function toRelativeSpecifier(fromFile: string, name: string): string {
+ const targetDir = join(srcRoot, 'lib', 'ui', name, 'src');
+ let rel = relative(dirname(fromFile), targetDir).replace(/\\/g, '/');
+ if (!rel.startsWith('.')) rel = `./${rel}`;
+ return rel;
+}
+
+let changedFiles = 0;
+let changedImports = 0;
+
+for (const file of walk(srcRoot)) {
+ const original = readFileSync(file, 'utf8');
+ let fileChanged = false;
+ const updated = original.replace(importPattern, (_match, quote: string, name: string) => {
+ fileChanged = true;
+ changedImports++;
+ return `from ${quote}${toRelativeSpecifier(file, name)}${quote}`;
+ });
+ if (fileChanged) {
+ writeFileSync(file, updated);
+ changedFiles++;
+ }
+}
+
+console.log(`Rewrote ${changedImports} import(s) across ${changedFiles} file(s).`);
diff --git a/packages/angular/src/lib/ui/avatar/src/lib/hlm-avatar-badge.ts b/packages/angular/src/lib/ui/avatar/src/lib/hlm-avatar-badge.ts
index db4151c..4d7dab9 100644
--- a/packages/angular/src/lib/ui/avatar/src/lib/hlm-avatar-badge.ts
+++ b/packages/angular/src/lib/ui/avatar/src/lib/hlm-avatar-badge.ts
@@ -1,5 +1,5 @@
import { Directive } from '@angular/core';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Directive({
selector: '[hlmAvatarBadge],hlm-avatar-badge',
diff --git a/packages/angular/src/lib/ui/avatar/src/lib/hlm-avatar-fallback.ts b/packages/angular/src/lib/ui/avatar/src/lib/hlm-avatar-fallback.ts
index 584e3f1..2cffd56 100644
--- a/packages/angular/src/lib/ui/avatar/src/lib/hlm-avatar-fallback.ts
+++ b/packages/angular/src/lib/ui/avatar/src/lib/hlm-avatar-fallback.ts
@@ -1,6 +1,6 @@
import { Directive } from '@angular/core';
import { BrnAvatarFallback } from '@spartan-ng/brain/avatar';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Directive({
selector: '[hlmAvatarFallback]',
diff --git a/packages/angular/src/lib/ui/avatar/src/lib/hlm-avatar-group-count.ts b/packages/angular/src/lib/ui/avatar/src/lib/hlm-avatar-group-count.ts
index 21d8646..f6cb428 100644
--- a/packages/angular/src/lib/ui/avatar/src/lib/hlm-avatar-group-count.ts
+++ b/packages/angular/src/lib/ui/avatar/src/lib/hlm-avatar-group-count.ts
@@ -1,5 +1,5 @@
import { Directive } from '@angular/core';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Directive({
selector: '[hlmAvatarGroupCount],hlm-avatar-group-count',
diff --git a/packages/angular/src/lib/ui/avatar/src/lib/hlm-avatar-group.ts b/packages/angular/src/lib/ui/avatar/src/lib/hlm-avatar-group.ts
index 1ec88b3..c441aa3 100644
--- a/packages/angular/src/lib/ui/avatar/src/lib/hlm-avatar-group.ts
+++ b/packages/angular/src/lib/ui/avatar/src/lib/hlm-avatar-group.ts
@@ -1,5 +1,5 @@
import { Directive } from '@angular/core';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Directive({
selector: '[hlmAvatarGroup],hlm-avatar-group',
diff --git a/packages/angular/src/lib/ui/avatar/src/lib/hlm-avatar-image.ts b/packages/angular/src/lib/ui/avatar/src/lib/hlm-avatar-image.ts
index 95ab3ed..87a2776 100644
--- a/packages/angular/src/lib/ui/avatar/src/lib/hlm-avatar-image.ts
+++ b/packages/angular/src/lib/ui/avatar/src/lib/hlm-avatar-image.ts
@@ -1,6 +1,6 @@
import { Directive, inject } from '@angular/core';
import { BrnAvatarImage } from '@spartan-ng/brain/avatar';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Directive({
selector: 'img[hlmAvatarImage]',
diff --git a/packages/angular/src/lib/ui/avatar/src/lib/hlm-avatar.ts b/packages/angular/src/lib/ui/avatar/src/lib/hlm-avatar.ts
index 6d91c12..b6149b9 100644
--- a/packages/angular/src/lib/ui/avatar/src/lib/hlm-avatar.ts
+++ b/packages/angular/src/lib/ui/avatar/src/lib/hlm-avatar.ts
@@ -1,6 +1,6 @@
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
import { BrnAvatar } from '@spartan-ng/brain/avatar';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
import type { AvatarSizeName } from '@surfnet/contracts';
@Component({
diff --git a/packages/angular/src/lib/ui/breadcrumb/src/lib/hlm-breadcrumb-ellipsis.ts b/packages/angular/src/lib/ui/breadcrumb/src/lib/hlm-breadcrumb-ellipsis.ts
index c58a1a8..3bddbd0 100644
--- a/packages/angular/src/lib/ui/breadcrumb/src/lib/hlm-breadcrumb-ellipsis.ts
+++ b/packages/angular/src/lib/ui/breadcrumb/src/lib/hlm-breadcrumb-ellipsis.ts
@@ -1,8 +1,8 @@
import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { phosphorDotsThree } from '@ng-icons/phosphor-icons/regular';
-import { HlmIcon } from '@spartan-ng/helm/icon';
-import { hlm } from '@spartan-ng/helm/utils';
+import { HlmIcon } from '../../../icon/src';
+import { hlm } from '../../../utils/src';
import type { ClassValue } from 'clsx';
@Component({
diff --git a/packages/angular/src/lib/ui/breadcrumb/src/lib/hlm-breadcrumb-item.ts b/packages/angular/src/lib/ui/breadcrumb/src/lib/hlm-breadcrumb-item.ts
index 649d45b..fab5819 100644
--- a/packages/angular/src/lib/ui/breadcrumb/src/lib/hlm-breadcrumb-item.ts
+++ b/packages/angular/src/lib/ui/breadcrumb/src/lib/hlm-breadcrumb-item.ts
@@ -1,5 +1,5 @@
import { Directive } from '@angular/core';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Directive({
selector: '[hlmBreadcrumbItem]',
diff --git a/packages/angular/src/lib/ui/breadcrumb/src/lib/hlm-breadcrumb-link.ts b/packages/angular/src/lib/ui/breadcrumb/src/lib/hlm-breadcrumb-link.ts
index b227180..3293120 100644
--- a/packages/angular/src/lib/ui/breadcrumb/src/lib/hlm-breadcrumb-link.ts
+++ b/packages/angular/src/lib/ui/breadcrumb/src/lib/hlm-breadcrumb-link.ts
@@ -1,6 +1,6 @@
import { Directive, input } from '@angular/core';
import { RouterLink } from '@angular/router';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Directive({
selector: '[hlmBreadcrumbLink]',
diff --git a/packages/angular/src/lib/ui/breadcrumb/src/lib/hlm-breadcrumb-list.ts b/packages/angular/src/lib/ui/breadcrumb/src/lib/hlm-breadcrumb-list.ts
index 80fb53c..cd07736 100644
--- a/packages/angular/src/lib/ui/breadcrumb/src/lib/hlm-breadcrumb-list.ts
+++ b/packages/angular/src/lib/ui/breadcrumb/src/lib/hlm-breadcrumb-list.ts
@@ -1,5 +1,5 @@
import { Directive } from '@angular/core';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Directive({
selector: '[hlmBreadcrumbList]',
diff --git a/packages/angular/src/lib/ui/breadcrumb/src/lib/hlm-breadcrumb-page.ts b/packages/angular/src/lib/ui/breadcrumb/src/lib/hlm-breadcrumb-page.ts
index e89ce87..7b3eb9e 100644
--- a/packages/angular/src/lib/ui/breadcrumb/src/lib/hlm-breadcrumb-page.ts
+++ b/packages/angular/src/lib/ui/breadcrumb/src/lib/hlm-breadcrumb-page.ts
@@ -1,5 +1,5 @@
import { Directive } from '@angular/core';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Directive({
selector: '[hlmBreadcrumbPage]',
diff --git a/packages/angular/src/lib/ui/breadcrumb/src/lib/hlm-breadcrumb-separator.ts b/packages/angular/src/lib/ui/breadcrumb/src/lib/hlm-breadcrumb-separator.ts
index e98c453..ea6acd6 100644
--- a/packages/angular/src/lib/ui/breadcrumb/src/lib/hlm-breadcrumb-separator.ts
+++ b/packages/angular/src/lib/ui/breadcrumb/src/lib/hlm-breadcrumb-separator.ts
@@ -1,7 +1,7 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { phosphorCaretRight } from '@ng-icons/phosphor-icons/regular';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Component({
// eslint-disable-next-line @angular-eslint/component-selector
diff --git a/packages/angular/src/lib/ui/button/src/lib/hlm-button.ts b/packages/angular/src/lib/ui/button/src/lib/hlm-button.ts
index 448a112..2819257 100644
--- a/packages/angular/src/lib/ui/button/src/lib/hlm-button.ts
+++ b/packages/angular/src/lib/ui/button/src/lib/hlm-button.ts
@@ -1,6 +1,6 @@
import { Directive, input, signal } from '@angular/core';
import { BrnButton } from '@spartan-ng/brain/button';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
import type { ButtonSizeName, ButtonVariantName } from '@surfnet/contracts';
import { cva, type VariantProps } from 'class-variance-authority';
import type { ClassValue } from 'clsx';
diff --git a/packages/angular/src/lib/ui/card/src/lib/hlm-card-action.ts b/packages/angular/src/lib/ui/card/src/lib/hlm-card-action.ts
index 0c8c4a5..e142d04 100644
--- a/packages/angular/src/lib/ui/card/src/lib/hlm-card-action.ts
+++ b/packages/angular/src/lib/ui/card/src/lib/hlm-card-action.ts
@@ -1,5 +1,5 @@
import { Directive } from '@angular/core';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Directive({
selector: '[hlmCardAction]',
diff --git a/packages/angular/src/lib/ui/card/src/lib/hlm-card-content.ts b/packages/angular/src/lib/ui/card/src/lib/hlm-card-content.ts
index d9cce06..0e1dd2c 100644
--- a/packages/angular/src/lib/ui/card/src/lib/hlm-card-content.ts
+++ b/packages/angular/src/lib/ui/card/src/lib/hlm-card-content.ts
@@ -1,5 +1,5 @@
import { Directive } from '@angular/core';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Directive({
selector: '[hlmCardContent]',
diff --git a/packages/angular/src/lib/ui/card/src/lib/hlm-card-description.ts b/packages/angular/src/lib/ui/card/src/lib/hlm-card-description.ts
index a8af485..79273c6 100644
--- a/packages/angular/src/lib/ui/card/src/lib/hlm-card-description.ts
+++ b/packages/angular/src/lib/ui/card/src/lib/hlm-card-description.ts
@@ -1,5 +1,5 @@
import { Directive } from '@angular/core';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Directive({
selector: '[hlmCardDescription]',
diff --git a/packages/angular/src/lib/ui/card/src/lib/hlm-card-footer.ts b/packages/angular/src/lib/ui/card/src/lib/hlm-card-footer.ts
index 472f3eb..3b9a493 100644
--- a/packages/angular/src/lib/ui/card/src/lib/hlm-card-footer.ts
+++ b/packages/angular/src/lib/ui/card/src/lib/hlm-card-footer.ts
@@ -1,5 +1,5 @@
import { Directive } from '@angular/core';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Directive({
selector: '[hlmCardFooter],hlm-card-footer',
diff --git a/packages/angular/src/lib/ui/card/src/lib/hlm-card-header.ts b/packages/angular/src/lib/ui/card/src/lib/hlm-card-header.ts
index 87a0c46..37c0801 100644
--- a/packages/angular/src/lib/ui/card/src/lib/hlm-card-header.ts
+++ b/packages/angular/src/lib/ui/card/src/lib/hlm-card-header.ts
@@ -1,5 +1,5 @@
import { Directive } from '@angular/core';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Directive({
selector: '[hlmCardHeader],hlm-card-header',
diff --git a/packages/angular/src/lib/ui/card/src/lib/hlm-card-title.ts b/packages/angular/src/lib/ui/card/src/lib/hlm-card-title.ts
index 08f3332..f72e8e2 100644
--- a/packages/angular/src/lib/ui/card/src/lib/hlm-card-title.ts
+++ b/packages/angular/src/lib/ui/card/src/lib/hlm-card-title.ts
@@ -1,5 +1,5 @@
import { Directive } from '@angular/core';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Directive({
selector: '[hlmCardTitle]',
diff --git a/packages/angular/src/lib/ui/card/src/lib/hlm-card.ts b/packages/angular/src/lib/ui/card/src/lib/hlm-card.ts
index 5221e35..0cac5f8 100644
--- a/packages/angular/src/lib/ui/card/src/lib/hlm-card.ts
+++ b/packages/angular/src/lib/ui/card/src/lib/hlm-card.ts
@@ -1,5 +1,5 @@
import { Directive, input } from '@angular/core';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
import { HlmCardConfig, injectHlmCardConfig } from './hlm-card.token';
@Directive({
diff --git a/packages/angular/src/lib/ui/checkbox/src/lib/hlm-checkbox.ts b/packages/angular/src/lib/ui/checkbox/src/lib/hlm-checkbox.ts
index 30755ac..27ce478 100644
--- a/packages/angular/src/lib/ui/checkbox/src/lib/hlm-checkbox.ts
+++ b/packages/angular/src/lib/ui/checkbox/src/lib/hlm-checkbox.ts
@@ -17,8 +17,8 @@ import { phosphorCheck } from '@ng-icons/phosphor-icons/regular';
import { BrnCheckbox } from '@spartan-ng/brain/checkbox';
import { BrnFieldControlDescribedBy } from '@spartan-ng/brain/field';
import type { ChangeFn, TouchFn } from '@spartan-ng/brain/forms';
-import { HlmIcon } from '@spartan-ng/helm/icon';
-import { hlm } from '@spartan-ng/helm/utils';
+import { HlmIcon } from '../../../icon/src';
+import { hlm } from '../../../utils/src';
import type { ClassValue } from 'clsx';
export const HLM_CHECKBOX_VALUE_ACCESSOR = {
diff --git a/packages/angular/src/lib/ui/data-table/src/lib/hlm-data-table-content.ts b/packages/angular/src/lib/ui/data-table/src/lib/hlm-data-table-content.ts
index b65c90b..7545c9b 100644
--- a/packages/angular/src/lib/ui/data-table/src/lib/hlm-data-table-content.ts
+++ b/packages/angular/src/lib/ui/data-table/src/lib/hlm-data-table-content.ts
@@ -1,5 +1,5 @@
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
-import { HlmTableImports } from '@spartan-ng/helm/table';
+import { HlmTableImports } from '../../../table/src';
import { FlexRenderDirective, type ColumnDef, type Table } from '@tanstack/angular-table';
/**
diff --git a/packages/angular/src/lib/ui/data-table/src/lib/hlm-data-table-pagination.ts b/packages/angular/src/lib/ui/data-table/src/lib/hlm-data-table-pagination.ts
index 3b1ca8b..9332366 100644
--- a/packages/angular/src/lib/ui/data-table/src/lib/hlm-data-table-pagination.ts
+++ b/packages/angular/src/lib/ui/data-table/src/lib/hlm-data-table-pagination.ts
@@ -1,5 +1,5 @@
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
-import { HlmButton } from '@spartan-ng/helm/button';
+import { HlmButton } from '../../../button/src';
import { type Table } from '@tanstack/angular-table';
/**
diff --git a/packages/angular/src/lib/ui/data-table/src/lib/hlm-data-table-toolbar.ts b/packages/angular/src/lib/ui/data-table/src/lib/hlm-data-table-toolbar.ts
index a0c6159..ce320f4 100644
--- a/packages/angular/src/lib/ui/data-table/src/lib/hlm-data-table-toolbar.ts
+++ b/packages/angular/src/lib/ui/data-table/src/lib/hlm-data-table-toolbar.ts
@@ -1,5 +1,5 @@
import { Directive } from '@angular/core';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
/**
* Layout wrapper for data-table controls (filter inputs, column toggles, actions). Mirrors
diff --git a/packages/angular/src/lib/ui/data-table/src/lib/hlm-data-table.stories.ts b/packages/angular/src/lib/ui/data-table/src/lib/hlm-data-table.stories.ts
index 4963ddd..8d37e0c 100644
--- a/packages/angular/src/lib/ui/data-table/src/lib/hlm-data-table.stories.ts
+++ b/packages/angular/src/lib/ui/data-table/src/lib/hlm-data-table.stories.ts
@@ -1,10 +1,10 @@
import { ChangeDetectionStrategy, Component, computed, input, signal } from '@angular/core';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { phosphorArrowsDownUp, phosphorDotsThreeVertical } from '@ng-icons/phosphor-icons/regular';
-import { HlmButton, HlmButtonImports } from '@spartan-ng/helm/button';
-import { HlmCheckbox } from '@spartan-ng/helm/checkbox';
-import { HlmDropdownMenuImports } from '@spartan-ng/helm/dropdown-menu';
-import { HlmInput } from '@spartan-ng/helm/input';
+import { HlmButton, HlmButtonImports } from '../../../button/src';
+import { HlmCheckbox } from '../../../checkbox/src';
+import { HlmDropdownMenuImports } from '../../../dropdown-menu/src';
+import { HlmInput } from '../../../input/src';
import { type Meta, type StoryObj } from '@storybook/angular';
import { dataTableContract } from '@surfnet/contracts';
import {
diff --git a/packages/angular/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-checkbox-indicator.ts b/packages/angular/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-checkbox-indicator.ts
index 4c62a2d..1993e43 100644
--- a/packages/angular/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-checkbox-indicator.ts
+++ b/packages/angular/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-checkbox-indicator.ts
@@ -1,7 +1,7 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { phosphorCheck } from '@ng-icons/phosphor-icons/regular';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Component({
selector: 'hlm-dropdown-menu-checkbox-indicator',
diff --git a/packages/angular/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-checkbox.ts b/packages/angular/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-checkbox.ts
index 16f1093..b830dba 100644
--- a/packages/angular/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-checkbox.ts
+++ b/packages/angular/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-checkbox.ts
@@ -1,7 +1,7 @@
import { type BooleanInput } from '@angular/cdk/coercion';
import { CdkMenuItemCheckbox } from '@angular/cdk/menu';
import { Directive, booleanAttribute, inject, input } from '@angular/core';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Directive({
selector: '[hlmDropdownMenuCheckbox]',
diff --git a/packages/angular/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-group.ts b/packages/angular/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-group.ts
index 1fd1cbe..d9b5680 100644
--- a/packages/angular/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-group.ts
+++ b/packages/angular/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-group.ts
@@ -1,6 +1,6 @@
import { CdkMenuGroup } from '@angular/cdk/menu';
import { Directive } from '@angular/core';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Directive({
selector: '[hlmDropdownMenuGroup],hlm-dropdown-menu-group',
diff --git a/packages/angular/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-item-sub-indicator.ts b/packages/angular/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-item-sub-indicator.ts
index d0291bb..470ad31 100644
--- a/packages/angular/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-item-sub-indicator.ts
+++ b/packages/angular/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-item-sub-indicator.ts
@@ -1,7 +1,7 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { phosphorCaretRight } from '@ng-icons/phosphor-icons/regular';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Component({
selector: 'hlm-dropdown-menu-item-sub-indicator',
diff --git a/packages/angular/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-item.ts b/packages/angular/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-item.ts
index 1b07e13..13d3ea9 100644
--- a/packages/angular/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-item.ts
+++ b/packages/angular/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-item.ts
@@ -1,7 +1,7 @@
import { type BooleanInput } from '@angular/cdk/coercion';
import { CdkMenuItem } from '@angular/cdk/menu';
import { booleanAttribute, Directive, HOST_TAG_NAME, inject, input } from '@angular/core';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
import type { DropdownMenuItemVariantName } from '@surfnet/contracts';
@Directive({
diff --git a/packages/angular/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-label.ts b/packages/angular/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-label.ts
index 195e87a..3156030 100644
--- a/packages/angular/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-label.ts
+++ b/packages/angular/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-label.ts
@@ -1,6 +1,6 @@
import { type BooleanInput } from '@angular/cdk/coercion';
import { booleanAttribute, Directive, input } from '@angular/core';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Directive({
selector: '[hlmDropdownMenuLabel],hlm-dropdown-menu-label',
diff --git a/packages/angular/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-radio-indicator.ts b/packages/angular/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-radio-indicator.ts
index 0838792..9f47b95 100644
--- a/packages/angular/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-radio-indicator.ts
+++ b/packages/angular/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-radio-indicator.ts
@@ -1,7 +1,7 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { phosphorCircle } from '@ng-icons/phosphor-icons/regular';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Component({
selector: 'hlm-dropdown-menu-radio-indicator',
diff --git a/packages/angular/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-radio.ts b/packages/angular/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-radio.ts
index 06984cf..47829da 100644
--- a/packages/angular/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-radio.ts
+++ b/packages/angular/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-radio.ts
@@ -1,7 +1,7 @@
import { type BooleanInput } from '@angular/cdk/coercion';
import { CdkMenuItemRadio } from '@angular/cdk/menu';
import { Directive, booleanAttribute, inject, input } from '@angular/core';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Directive({
selector: '[hlmDropdownMenuRadio]',
diff --git a/packages/angular/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-separator.ts b/packages/angular/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-separator.ts
index a5aca7d..4ef9585 100644
--- a/packages/angular/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-separator.ts
+++ b/packages/angular/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-separator.ts
@@ -1,5 +1,5 @@
import { Directive } from '@angular/core';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Directive({
selector: '[hlmDropdownMenuSeparator],hlm-dropdown-menu-separator',
diff --git a/packages/angular/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-shortcut.ts b/packages/angular/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-shortcut.ts
index 823dad1..1a936e1 100644
--- a/packages/angular/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-shortcut.ts
+++ b/packages/angular/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-shortcut.ts
@@ -1,5 +1,5 @@
import { Directive } from '@angular/core';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Directive({
selector: '[hlmDropdownMenuShortcut],hlm-dropdown-menu-shortcut',
diff --git a/packages/angular/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-sub.ts b/packages/angular/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-sub.ts
index 2c11471..e120743 100644
--- a/packages/angular/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-sub.ts
+++ b/packages/angular/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-sub.ts
@@ -1,7 +1,7 @@
import { CdkMenu } from '@angular/cdk/menu';
import { Directive, inject, signal } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Directive({
selector: '[hlmDropdownMenuSub],hlm-dropdown-menu-sub',
diff --git a/packages/angular/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu.ts b/packages/angular/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu.ts
index 32b1620..efd6853 100644
--- a/packages/angular/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu.ts
+++ b/packages/angular/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu.ts
@@ -2,7 +2,7 @@ import { type NumberInput } from '@angular/cdk/coercion';
import { CdkMenu } from '@angular/cdk/menu';
import { Directive, inject, input, numberAttribute, signal } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Directive({
selector: '[hlmDropdownMenu],hlm-dropdown-menu',
diff --git a/packages/angular/src/lib/ui/field/src/lib/hlm-field-content.ts b/packages/angular/src/lib/ui/field/src/lib/hlm-field-content.ts
index c59edad..8e2e868 100644
--- a/packages/angular/src/lib/ui/field/src/lib/hlm-field-content.ts
+++ b/packages/angular/src/lib/ui/field/src/lib/hlm-field-content.ts
@@ -1,5 +1,5 @@
import { Directive } from '@angular/core';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Directive({
selector: '[hlmFieldContent],hlm-field-content',
diff --git a/packages/angular/src/lib/ui/field/src/lib/hlm-field-description.ts b/packages/angular/src/lib/ui/field/src/lib/hlm-field-description.ts
index e25b4d4..16a92e0 100644
--- a/packages/angular/src/lib/ui/field/src/lib/hlm-field-description.ts
+++ b/packages/angular/src/lib/ui/field/src/lib/hlm-field-description.ts
@@ -1,6 +1,6 @@
import { Directive, effect, EffectRef, inject, input, OnDestroy } from '@angular/core';
import { BrnFieldA11yService } from '@spartan-ng/brain/field';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Directive({
selector: '[hlmFieldDescription],hlm-field-description',
diff --git a/packages/angular/src/lib/ui/field/src/lib/hlm-field-error.ts b/packages/angular/src/lib/ui/field/src/lib/hlm-field-error.ts
index 0402389..5ec9807 100644
--- a/packages/angular/src/lib/ui/field/src/lib/hlm-field-error.ts
+++ b/packages/angular/src/lib/ui/field/src/lib/hlm-field-error.ts
@@ -11,7 +11,7 @@ import {
OnDestroy,
} from '@angular/core';
import { BrnField, BrnFieldA11yService } from '@spartan-ng/brain/field';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Component({
selector: 'hlm-field-error',
diff --git a/packages/angular/src/lib/ui/field/src/lib/hlm-field-group.ts b/packages/angular/src/lib/ui/field/src/lib/hlm-field-group.ts
index f4062dc..7694a32 100644
--- a/packages/angular/src/lib/ui/field/src/lib/hlm-field-group.ts
+++ b/packages/angular/src/lib/ui/field/src/lib/hlm-field-group.ts
@@ -1,5 +1,5 @@
import { Directive } from '@angular/core';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Directive({
selector: '[hlmFieldGroup],hlm-field-group',
diff --git a/packages/angular/src/lib/ui/field/src/lib/hlm-field-label.ts b/packages/angular/src/lib/ui/field/src/lib/hlm-field-label.ts
index ac0140f..fa1510d 100644
--- a/packages/angular/src/lib/ui/field/src/lib/hlm-field-label.ts
+++ b/packages/angular/src/lib/ui/field/src/lib/hlm-field-label.ts
@@ -1,6 +1,6 @@
import { Directive } from '@angular/core';
-import { HlmLabel } from '@spartan-ng/helm/label';
-import { classes } from '@spartan-ng/helm/utils';
+import { HlmLabel } from '../../../label/src';
+import { classes } from '../../../utils/src';
@Directive({
selector: '[hlmFieldLabel],hlm-field-label',
diff --git a/packages/angular/src/lib/ui/field/src/lib/hlm-field-legend.ts b/packages/angular/src/lib/ui/field/src/lib/hlm-field-legend.ts
index 5108cbc..8ce107a 100644
--- a/packages/angular/src/lib/ui/field/src/lib/hlm-field-legend.ts
+++ b/packages/angular/src/lib/ui/field/src/lib/hlm-field-legend.ts
@@ -1,5 +1,5 @@
import { Directive, input } from '@angular/core';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Directive({
selector: 'legend[hlmFieldLegend]',
diff --git a/packages/angular/src/lib/ui/field/src/lib/hlm-field-separator.ts b/packages/angular/src/lib/ui/field/src/lib/hlm-field-separator.ts
index 0b2f807..3fb9145 100644
--- a/packages/angular/src/lib/ui/field/src/lib/hlm-field-separator.ts
+++ b/packages/angular/src/lib/ui/field/src/lib/hlm-field-separator.ts
@@ -1,6 +1,6 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
-import { HlmSeparator } from '@spartan-ng/helm/separator';
-import { classes } from '@spartan-ng/helm/utils';
+import { HlmSeparator } from '../../../separator/src';
+import { classes } from '../../../utils/src';
@Component({
selector: 'hlm-field-separator',
diff --git a/packages/angular/src/lib/ui/field/src/lib/hlm-field-set.ts b/packages/angular/src/lib/ui/field/src/lib/hlm-field-set.ts
index 6c51920..0237c6c 100644
--- a/packages/angular/src/lib/ui/field/src/lib/hlm-field-set.ts
+++ b/packages/angular/src/lib/ui/field/src/lib/hlm-field-set.ts
@@ -1,5 +1,5 @@
import { Directive } from '@angular/core';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Directive({
selector: 'fieldset[hlmFieldSet]',
diff --git a/packages/angular/src/lib/ui/field/src/lib/hlm-field-title.ts b/packages/angular/src/lib/ui/field/src/lib/hlm-field-title.ts
index d214eec..187ecca 100644
--- a/packages/angular/src/lib/ui/field/src/lib/hlm-field-title.ts
+++ b/packages/angular/src/lib/ui/field/src/lib/hlm-field-title.ts
@@ -1,5 +1,5 @@
import { Directive } from '@angular/core';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Directive({
selector: '[hlmFieldTitle],hlm-field-title',
diff --git a/packages/angular/src/lib/ui/field/src/lib/hlm-field.ts b/packages/angular/src/lib/ui/field/src/lib/hlm-field.ts
index 798f802..cd6a8a3 100644
--- a/packages/angular/src/lib/ui/field/src/lib/hlm-field.ts
+++ b/packages/angular/src/lib/ui/field/src/lib/hlm-field.ts
@@ -1,6 +1,6 @@
import { Directive, input } from '@angular/core';
import { BrnField } from '@spartan-ng/brain/field';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
import type { FieldOrientationName } from '@surfnet/contracts';
import { cva, VariantProps } from 'class-variance-authority';
import type { ClassValue } from 'clsx';
diff --git a/packages/angular/src/lib/ui/input-group/src/lib/hlm-input-group-addon.ts b/packages/angular/src/lib/ui/input-group/src/lib/hlm-input-group-addon.ts
index 4f4e44b..abecce3 100644
--- a/packages/angular/src/lib/ui/input-group/src/lib/hlm-input-group-addon.ts
+++ b/packages/angular/src/lib/ui/input-group/src/lib/hlm-input-group-addon.ts
@@ -1,5 +1,5 @@
import { Directive, input } from '@angular/core';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
import { cva, type VariantProps } from 'class-variance-authority';
const inputGroupAddonVariants = cva(
diff --git a/packages/angular/src/lib/ui/input-group/src/lib/hlm-input-group-button.ts b/packages/angular/src/lib/ui/input-group/src/lib/hlm-input-group-button.ts
index 8ecb8ba..0723978 100644
--- a/packages/angular/src/lib/ui/input-group/src/lib/hlm-input-group-button.ts
+++ b/packages/angular/src/lib/ui/input-group/src/lib/hlm-input-group-button.ts
@@ -1,6 +1,6 @@
import { Directive, input } from '@angular/core';
-import { HlmButton, provideBrnButtonConfig } from '@spartan-ng/helm/button';
-import { classes } from '@spartan-ng/helm/utils';
+import { HlmButton, provideBrnButtonConfig } from '../../../button/src';
+import { classes } from '../../../utils/src';
import { cva, type VariantProps } from 'class-variance-authority';
const inputGroupAddonVariants = cva('gap-2 text-sm flex items-center shadow-none', {
diff --git a/packages/angular/src/lib/ui/input-group/src/lib/hlm-input-group-input.ts b/packages/angular/src/lib/ui/input-group/src/lib/hlm-input-group-input.ts
index f1da174..84b8452 100644
--- a/packages/angular/src/lib/ui/input-group/src/lib/hlm-input-group-input.ts
+++ b/packages/angular/src/lib/ui/input-group/src/lib/hlm-input-group-input.ts
@@ -1,6 +1,6 @@
import { Directive } from '@angular/core';
-import { HlmInput } from '@spartan-ng/helm/input';
-import { classes } from '@spartan-ng/helm/utils';
+import { HlmInput } from '../../../input/src';
+import { classes } from '../../../utils/src';
@Directive({
selector: 'input[hlmInputGroupInput]',
diff --git a/packages/angular/src/lib/ui/input-group/src/lib/hlm-input-group-text.ts b/packages/angular/src/lib/ui/input-group/src/lib/hlm-input-group-text.ts
index f8a1f21..cba0414 100644
--- a/packages/angular/src/lib/ui/input-group/src/lib/hlm-input-group-text.ts
+++ b/packages/angular/src/lib/ui/input-group/src/lib/hlm-input-group-text.ts
@@ -1,5 +1,5 @@
import { Directive } from '@angular/core';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Directive({
selector: '[hlmInputGroupText],hlm-input-group-text',
diff --git a/packages/angular/src/lib/ui/input-group/src/lib/hlm-input-group-textarea.ts b/packages/angular/src/lib/ui/input-group/src/lib/hlm-input-group-textarea.ts
index ecd2ca6..0a1e788 100644
--- a/packages/angular/src/lib/ui/input-group/src/lib/hlm-input-group-textarea.ts
+++ b/packages/angular/src/lib/ui/input-group/src/lib/hlm-input-group-textarea.ts
@@ -1,6 +1,6 @@
import { Directive } from '@angular/core';
-import { HlmTextarea } from '@spartan-ng/helm/textarea';
-import { classes } from '@spartan-ng/helm/utils';
+import { HlmTextarea } from '../../../textarea/src';
+import { classes } from '../../../utils/src';
@Directive({
selector: 'textarea[hlmInputGroupTextarea]',
diff --git a/packages/angular/src/lib/ui/input-group/src/lib/hlm-input-group.stories.ts b/packages/angular/src/lib/ui/input-group/src/lib/hlm-input-group.stories.ts
index 66bf70d..48448ec 100644
--- a/packages/angular/src/lib/ui/input-group/src/lib/hlm-input-group.stories.ts
+++ b/packages/angular/src/lib/ui/input-group/src/lib/hlm-input-group.stories.ts
@@ -10,7 +10,7 @@ import type { Meta, StoryObj } from '@storybook/angular';
import { moduleMetadata } from '@storybook/angular';
import { inputGroupContract } from '@surfnet/contracts';
import { HlmInputGroup, HlmInputGroupImports } from '..';
-import { HlmDropdownMenuImports } from '@spartan-ng/helm/dropdown-menu';
+import { HlmDropdownMenuImports } from '../../../dropdown-menu/src';
const meta: Meta = {
title: 'Components/Input Group',
diff --git a/packages/angular/src/lib/ui/input-group/src/lib/hlm-input-group.ts b/packages/angular/src/lib/ui/input-group/src/lib/hlm-input-group.ts
index 222fc43..540d5de 100644
--- a/packages/angular/src/lib/ui/input-group/src/lib/hlm-input-group.ts
+++ b/packages/angular/src/lib/ui/input-group/src/lib/hlm-input-group.ts
@@ -1,5 +1,5 @@
import { Directive } from '@angular/core';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Directive({
selector: '[hlmInputGroup],hlm-input-group',
diff --git a/packages/angular/src/lib/ui/input/src/lib/hlm-input.ts b/packages/angular/src/lib/ui/input/src/lib/hlm-input.ts
index 09f6f50..37695a5 100644
--- a/packages/angular/src/lib/ui/input/src/lib/hlm-input.ts
+++ b/packages/angular/src/lib/ui/input/src/lib/hlm-input.ts
@@ -1,7 +1,7 @@
import { Directive } from '@angular/core';
import { BrnFieldControlDescribedBy } from '@spartan-ng/brain/field';
import { BrnInput } from '@spartan-ng/brain/input';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Directive({
selector: '[hlmInput]',
diff --git a/packages/angular/src/lib/ui/label/src/lib/hlm-label.ts b/packages/angular/src/lib/ui/label/src/lib/hlm-label.ts
index 29632cd..26a6778 100644
--- a/packages/angular/src/lib/ui/label/src/lib/hlm-label.ts
+++ b/packages/angular/src/lib/ui/label/src/lib/hlm-label.ts
@@ -1,6 +1,6 @@
import { Directive } from '@angular/core';
import { BrnLabel } from '@spartan-ng/brain/label';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Directive({
selector: '[hlmLabel]',
diff --git a/packages/angular/src/lib/ui/select/src/lib/hlm-select-content.ts b/packages/angular/src/lib/ui/select/src/lib/hlm-select-content.ts
index 33d93a2..fb26312 100644
--- a/packages/angular/src/lib/ui/select/src/lib/hlm-select-content.ts
+++ b/packages/angular/src/lib/ui/select/src/lib/hlm-select-content.ts
@@ -7,7 +7,7 @@ import {
input,
} from '@angular/core';
import { BrnSelectContent } from '@spartan-ng/brain/select';
-import { classes, hlm } from '@spartan-ng/helm/utils';
+import { classes, hlm } from '../../../utils/src';
import { HlmSelectScrollDown } from './hlm-select-scroll-down';
import { HlmSelectScrollUp } from './hlm-select-scroll-up';
diff --git a/packages/angular/src/lib/ui/select/src/lib/hlm-select-group.ts b/packages/angular/src/lib/ui/select/src/lib/hlm-select-group.ts
index 7b96d51..b07e4dc 100644
--- a/packages/angular/src/lib/ui/select/src/lib/hlm-select-group.ts
+++ b/packages/angular/src/lib/ui/select/src/lib/hlm-select-group.ts
@@ -1,6 +1,6 @@
import { Directive } from '@angular/core';
import { BrnSelectGroup } from '@spartan-ng/brain/select';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Directive({
selector: '[hlmSelectGroup],hlm-select-group',
diff --git a/packages/angular/src/lib/ui/select/src/lib/hlm-select-item.ts b/packages/angular/src/lib/ui/select/src/lib/hlm-select-item.ts
index 77cfcf7..b5cfdbe 100644
--- a/packages/angular/src/lib/ui/select/src/lib/hlm-select-item.ts
+++ b/packages/angular/src/lib/ui/select/src/lib/hlm-select-item.ts
@@ -2,7 +2,7 @@ import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { phosphorCheck } from '@ng-icons/phosphor-icons/regular';
import { BrnSelectItem } from '@spartan-ng/brain/select';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Component({
selector: 'hlm-select-item',
diff --git a/packages/angular/src/lib/ui/select/src/lib/hlm-select-label.ts b/packages/angular/src/lib/ui/select/src/lib/hlm-select-label.ts
index 596bc53..6e85ac7 100644
--- a/packages/angular/src/lib/ui/select/src/lib/hlm-select-label.ts
+++ b/packages/angular/src/lib/ui/select/src/lib/hlm-select-label.ts
@@ -1,6 +1,6 @@
import { Directive } from '@angular/core';
import { BrnSelectLabel } from '@spartan-ng/brain/select';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Directive({
selector: '[hlmSelectLabel],hlm-select-label',
diff --git a/packages/angular/src/lib/ui/select/src/lib/hlm-select-multiple.ts b/packages/angular/src/lib/ui/select/src/lib/hlm-select-multiple.ts
index bb8f446..a24f9fb 100644
--- a/packages/angular/src/lib/ui/select/src/lib/hlm-select-multiple.ts
+++ b/packages/angular/src/lib/ui/select/src/lib/hlm-select-multiple.ts
@@ -2,7 +2,7 @@ import { Directive } from '@angular/core';
import { provideBrnDialogDefaultOptions } from '@spartan-ng/brain/dialog';
import { BrnPopover, provideBrnPopoverConfig } from '@spartan-ng/brain/popover';
import { BrnSelectMultiple } from '@spartan-ng/brain/select';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Directive({
selector: '[hlmSelectMultiple],hlm-select-multiple',
diff --git a/packages/angular/src/lib/ui/select/src/lib/hlm-select-placeholder.ts b/packages/angular/src/lib/ui/select/src/lib/hlm-select-placeholder.ts
index 4f51103..ed75351 100644
--- a/packages/angular/src/lib/ui/select/src/lib/hlm-select-placeholder.ts
+++ b/packages/angular/src/lib/ui/select/src/lib/hlm-select-placeholder.ts
@@ -1,6 +1,6 @@
import { Directive } from '@angular/core';
import { BrnSelectPlaceholder } from '@spartan-ng/brain/select';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Directive({
selector: '[hlmSelectPlaceholder],hlm-select-placeholder',
diff --git a/packages/angular/src/lib/ui/select/src/lib/hlm-select-scroll-down.ts b/packages/angular/src/lib/ui/select/src/lib/hlm-select-scroll-down.ts
index fb389d6..4e7fb90 100644
--- a/packages/angular/src/lib/ui/select/src/lib/hlm-select-scroll-down.ts
+++ b/packages/angular/src/lib/ui/select/src/lib/hlm-select-scroll-down.ts
@@ -2,7 +2,7 @@ import { ChangeDetectionStrategy, Component } from '@angular/core';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { phosphorCaretDown } from '@ng-icons/phosphor-icons/regular';
import { BrnSelectScrollDown } from '@spartan-ng/brain/select';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Component({
selector: 'hlm-select-scroll-down',
diff --git a/packages/angular/src/lib/ui/select/src/lib/hlm-select-scroll-up.ts b/packages/angular/src/lib/ui/select/src/lib/hlm-select-scroll-up.ts
index 9d85b76..029388f 100644
--- a/packages/angular/src/lib/ui/select/src/lib/hlm-select-scroll-up.ts
+++ b/packages/angular/src/lib/ui/select/src/lib/hlm-select-scroll-up.ts
@@ -2,7 +2,7 @@ import { ChangeDetectionStrategy, Component } from '@angular/core';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { phosphorCaretUp } from '@ng-icons/phosphor-icons/regular';
import { BrnSelectScrollUp } from '@spartan-ng/brain/select';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Component({
selector: 'hlm-select-scroll-up',
diff --git a/packages/angular/src/lib/ui/select/src/lib/hlm-select-separator.ts b/packages/angular/src/lib/ui/select/src/lib/hlm-select-separator.ts
index 90fcdc0..1494419 100644
--- a/packages/angular/src/lib/ui/select/src/lib/hlm-select-separator.ts
+++ b/packages/angular/src/lib/ui/select/src/lib/hlm-select-separator.ts
@@ -1,6 +1,6 @@
import { Directive } from '@angular/core';
import { BrnSelectSeparator } from '@spartan-ng/brain/select';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Directive({
selector: '[hlmSelectSeparator],hlm-select-separator',
diff --git a/packages/angular/src/lib/ui/select/src/lib/hlm-select-trigger.ts b/packages/angular/src/lib/ui/select/src/lib/hlm-select-trigger.ts
index d548518..68aac59 100644
--- a/packages/angular/src/lib/ui/select/src/lib/hlm-select-trigger.ts
+++ b/packages/angular/src/lib/ui/select/src/lib/hlm-select-trigger.ts
@@ -10,7 +10,7 @@ import { NgIcon, provideIcons } from '@ng-icons/core';
import { phosphorCaretUpDown } from '@ng-icons/phosphor-icons/regular';
import { BrnFieldControlDescribedBy } from '@spartan-ng/brain/field';
import { BrnSelectTrigger } from '@spartan-ng/brain/select';
-import { hlm } from '@spartan-ng/helm/utils';
+import { hlm } from '../../../utils/src';
import type { SelectTriggerSizeName } from '@surfnet/contracts';
import type { ClassValue } from 'clsx';
diff --git a/packages/angular/src/lib/ui/select/src/lib/hlm-select-value.ts b/packages/angular/src/lib/ui/select/src/lib/hlm-select-value.ts
index fa97a12..d8ad345 100644
--- a/packages/angular/src/lib/ui/select/src/lib/hlm-select-value.ts
+++ b/packages/angular/src/lib/ui/select/src/lib/hlm-select-value.ts
@@ -1,6 +1,6 @@
import { Directive, inject } from '@angular/core';
import { BrnSelectValue } from '@spartan-ng/brain/select';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Directive({
selector: '[hlmSelectValue],hlm-select-value',
diff --git a/packages/angular/src/lib/ui/select/src/lib/hlm-select-values-content.ts b/packages/angular/src/lib/ui/select/src/lib/hlm-select-values-content.ts
index 0e46721..fef937f 100644
--- a/packages/angular/src/lib/ui/select/src/lib/hlm-select-values-content.ts
+++ b/packages/angular/src/lib/ui/select/src/lib/hlm-select-values-content.ts
@@ -1,5 +1,5 @@
import { Directive } from '@angular/core';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Directive({ selector: '[hlmSelectValuesContent],hlm-select-values-content' })
export class HlmSelectValuesContent {
diff --git a/packages/angular/src/lib/ui/select/src/lib/hlm-select.ts b/packages/angular/src/lib/ui/select/src/lib/hlm-select.ts
index e6aa7d2..52e9b45 100644
--- a/packages/angular/src/lib/ui/select/src/lib/hlm-select.ts
+++ b/packages/angular/src/lib/ui/select/src/lib/hlm-select.ts
@@ -2,7 +2,7 @@ import { Directive } from '@angular/core';
import { provideBrnDialogDefaultOptions } from '@spartan-ng/brain/dialog';
import { BrnPopover, provideBrnPopoverConfig } from '@spartan-ng/brain/popover';
import { BrnSelect } from '@spartan-ng/brain/select';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Directive({
selector: '[hlmSelect],hlm-select',
diff --git a/packages/angular/src/lib/ui/separator/src/lib/hlm-separator.ts b/packages/angular/src/lib/ui/separator/src/lib/hlm-separator.ts
index e709b37..f6d5ab4 100644
--- a/packages/angular/src/lib/ui/separator/src/lib/hlm-separator.ts
+++ b/packages/angular/src/lib/ui/separator/src/lib/hlm-separator.ts
@@ -1,6 +1,6 @@
import { Directive } from '@angular/core';
import { BrnSeparator } from '@spartan-ng/brain/separator';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
export const hlmSeparatorClass =
'inline-flex shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch';
diff --git a/packages/angular/src/lib/ui/sheet/src/lib/hlm-sheet-content.ts b/packages/angular/src/lib/ui/sheet/src/lib/hlm-sheet-content.ts
index 07705fa..8bb3ebe 100644
--- a/packages/angular/src/lib/ui/sheet/src/lib/hlm-sheet-content.ts
+++ b/packages/angular/src/lib/ui/sheet/src/lib/hlm-sheet-content.ts
@@ -13,9 +13,9 @@ import {
import { provideIcons } from '@ng-icons/core';
import { phosphorX } from '@ng-icons/phosphor-icons/regular';
import { injectExposedSideProvider, injectExposesStateProvider } from '@spartan-ng/brain/core';
-import { HlmButton } from '@spartan-ng/helm/button';
-import { HlmIconImports } from '@spartan-ng/helm/icon';
-import { classes } from '@spartan-ng/helm/utils';
+import { HlmButton } from '../../../button/src';
+import { HlmIconImports } from '../../../icon/src';
+import { classes } from '../../../utils/src';
import { HlmSheetClose } from './hlm-sheet-close';
@Component({
diff --git a/packages/angular/src/lib/ui/sheet/src/lib/hlm-sheet-description.ts b/packages/angular/src/lib/ui/sheet/src/lib/hlm-sheet-description.ts
index 9769212..09c6024 100644
--- a/packages/angular/src/lib/ui/sheet/src/lib/hlm-sheet-description.ts
+++ b/packages/angular/src/lib/ui/sheet/src/lib/hlm-sheet-description.ts
@@ -1,6 +1,6 @@
import { Directive } from '@angular/core';
import { BrnSheetDescription } from '@spartan-ng/brain/sheet';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Directive({
selector: '[hlmSheetDescription]',
diff --git a/packages/angular/src/lib/ui/sheet/src/lib/hlm-sheet-footer.ts b/packages/angular/src/lib/ui/sheet/src/lib/hlm-sheet-footer.ts
index 96632d7..0383ee3 100644
--- a/packages/angular/src/lib/ui/sheet/src/lib/hlm-sheet-footer.ts
+++ b/packages/angular/src/lib/ui/sheet/src/lib/hlm-sheet-footer.ts
@@ -1,5 +1,5 @@
import { Directive } from '@angular/core';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Directive({
selector: '[hlmSheetFooter],hlm-sheet-footer',
diff --git a/packages/angular/src/lib/ui/sheet/src/lib/hlm-sheet-header.ts b/packages/angular/src/lib/ui/sheet/src/lib/hlm-sheet-header.ts
index 9c21ffe..d847682 100644
--- a/packages/angular/src/lib/ui/sheet/src/lib/hlm-sheet-header.ts
+++ b/packages/angular/src/lib/ui/sheet/src/lib/hlm-sheet-header.ts
@@ -1,5 +1,5 @@
import { Directive } from '@angular/core';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Directive({
selector: '[hlmSheetHeader],hlm-sheet-header',
diff --git a/packages/angular/src/lib/ui/sheet/src/lib/hlm-sheet-overlay.ts b/packages/angular/src/lib/ui/sheet/src/lib/hlm-sheet-overlay.ts
index 75c8a7f..a29a5fd 100644
--- a/packages/angular/src/lib/ui/sheet/src/lib/hlm-sheet-overlay.ts
+++ b/packages/angular/src/lib/ui/sheet/src/lib/hlm-sheet-overlay.ts
@@ -1,7 +1,7 @@
import { Directive, computed, effect, input, untracked } from '@angular/core';
import { injectCustomClassSettable } from '@spartan-ng/brain/core';
import { BrnSheetOverlay } from '@spartan-ng/brain/sheet';
-import { hlm } from '@spartan-ng/helm/utils';
+import { hlm } from '../../../utils/src';
import type { ClassValue } from 'clsx';
@Directive({
diff --git a/packages/angular/src/lib/ui/sheet/src/lib/hlm-sheet-title.ts b/packages/angular/src/lib/ui/sheet/src/lib/hlm-sheet-title.ts
index a373a06..23b7b3d 100644
--- a/packages/angular/src/lib/ui/sheet/src/lib/hlm-sheet-title.ts
+++ b/packages/angular/src/lib/ui/sheet/src/lib/hlm-sheet-title.ts
@@ -1,6 +1,6 @@
import { Directive } from '@angular/core';
import { BrnSheetTitle } from '@spartan-ng/brain/sheet';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Directive({
selector: '[hlmSheetTitle]',
diff --git a/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-content.ts b/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-content.ts
index c12a7d7..dbcd72c 100644
--- a/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-content.ts
+++ b/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-content.ts
@@ -1,5 +1,5 @@
import { Directive } from '@angular/core';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Directive({
selector: '[hlmSidebarContent],hlm-sidebar-content',
diff --git a/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-footer.ts b/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-footer.ts
index 8abb9cd..e464298 100644
--- a/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-footer.ts
+++ b/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-footer.ts
@@ -1,5 +1,5 @@
import { Directive } from '@angular/core';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Directive({
selector: '[hlmSidebarFooter],hlm-sidebar-footer',
diff --git a/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-group-action.ts b/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-group-action.ts
index 23c2f0c..e41c4e4 100644
--- a/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-group-action.ts
+++ b/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-group-action.ts
@@ -1,5 +1,5 @@
import { Directive } from '@angular/core';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Directive({
selector: 'button[hlmSidebarGroupAction]',
diff --git a/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-group-content.ts b/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-group-content.ts
index 85ffe79..d263af4 100644
--- a/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-group-content.ts
+++ b/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-group-content.ts
@@ -1,5 +1,5 @@
import { Directive } from '@angular/core';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Directive({
selector: 'div[hlmSidebarGroupContent]',
diff --git a/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-group-label.ts b/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-group-label.ts
index 979360d..6a949fa 100644
--- a/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-group-label.ts
+++ b/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-group-label.ts
@@ -1,5 +1,5 @@
import { Directive } from '@angular/core';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Directive({
selector: 'div[hlmSidebarGroupLabel], button[hlmSidebarGroupLabel]',
diff --git a/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-group.ts b/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-group.ts
index ed7e43e..50bb4f9 100644
--- a/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-group.ts
+++ b/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-group.ts
@@ -1,5 +1,5 @@
import { Directive } from '@angular/core';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Directive({
selector: '[hlmSidebarGroup],hlm-sidebar-group',
diff --git a/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-header.ts b/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-header.ts
index 87fd84f..90e7cf6 100644
--- a/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-header.ts
+++ b/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-header.ts
@@ -1,5 +1,5 @@
import { Directive } from '@angular/core';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Directive({
selector: '[hlmSidebarHeader],hlm-sidebar-header',
diff --git a/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-input.ts b/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-input.ts
index 7541bc3..3510614 100644
--- a/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-input.ts
+++ b/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-input.ts
@@ -1,6 +1,6 @@
import { Directive } from '@angular/core';
-import { HlmInput } from '@spartan-ng/helm/input';
-import { classes } from '@spartan-ng/helm/utils';
+import { HlmInput } from '../../../input/src';
+import { classes } from '../../../utils/src';
@Directive({
selector: 'input[hlmSidebarInput]',
diff --git a/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-inset.ts b/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-inset.ts
index 69b26bc..610b471 100644
--- a/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-inset.ts
+++ b/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-inset.ts
@@ -1,5 +1,5 @@
import { Directive } from '@angular/core';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Directive({
selector: 'main[hlmSidebarInset]',
diff --git a/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-menu-action.ts b/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-menu-action.ts
index 6d4ac1a..63189ae 100644
--- a/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-menu-action.ts
+++ b/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-menu-action.ts
@@ -1,6 +1,6 @@
import { type BooleanInput } from '@angular/cdk/coercion';
import { booleanAttribute, Directive, input } from '@angular/core';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Directive({
selector: 'button[hlmSidebarMenuAction]',
diff --git a/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-menu-badge.ts b/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-menu-badge.ts
index 5009443..30926d3 100644
--- a/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-menu-badge.ts
+++ b/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-menu-badge.ts
@@ -1,5 +1,5 @@
import { Directive } from '@angular/core';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Directive({
selector: '[hlmSidebarMenuBadge],hlm-sidebar-menu-badge',
diff --git a/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-menu-button.ts b/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-menu-button.ts
index 872a7e3..ef86cb0 100644
--- a/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-menu-button.ts
+++ b/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-menu-button.ts
@@ -5,7 +5,7 @@ import {
BrnTooltipPosition,
provideBrnTooltipDefaultOptions,
} from '@spartan-ng/brain/tooltip';
-import { classes, hlm } from '@spartan-ng/helm/utils';
+import { classes, hlm } from '../../../utils/src';
import type { SidebarMenuButtonSizeName, SidebarMenuButtonVariantName } from '@surfnet/contracts';
import { cva } from 'class-variance-authority';
import { HlmSidebarService } from './hlm-sidebar.service';
@@ -14,7 +14,7 @@ import {
DEFAULT_TOOLTIP_CONTENT_CLASSES,
DEFAULT_TOOLTIP_SVG_CLASS,
tooltipPositionVariants,
-} from '@spartan-ng/helm/tooltip';
+} from '../../../tooltip/src';
const sidebarMenuButtonVariants = cva(
'ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground data-open:hover:bg-sidebar-accent data-open:hover:text-sidebar-accent-foreground gap-2 rounded-md p-2 text-start text-sm transition-[width,height,padding] group-has-data-[sidebar=menu-action]/menu-item:pe-8 group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! focus-visible:ring-2 data-active:font-medium peer/menu-button group/menu-button flex w-full items-center overflow-hidden outline-hidden disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_ng-icon]:shrink-0 [&_ng-icon]:text-[calc(var(--spacing)*4)] [&>span:last-child]:truncate',
diff --git a/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-menu-item.ts b/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-menu-item.ts
index 41d75dd..25b80a4 100644
--- a/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-menu-item.ts
+++ b/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-menu-item.ts
@@ -1,5 +1,5 @@
import { Directive } from '@angular/core';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Directive({
selector: 'li[hlmSidebarMenuItem]',
diff --git a/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-menu-skeleton.ts b/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-menu-skeleton.ts
index 43947ff..f7bfceb 100644
--- a/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-menu-skeleton.ts
+++ b/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-menu-skeleton.ts
@@ -1,7 +1,7 @@
import { type BooleanInput } from '@angular/cdk/coercion';
import { booleanAttribute, ChangeDetectionStrategy, Component, input } from '@angular/core';
-import { HlmSkeletonImports } from '@spartan-ng/helm/skeleton';
-import { classes } from '@spartan-ng/helm/utils';
+import { HlmSkeletonImports } from '../../../skeleton/src';
+import { classes } from '../../../utils/src';
@Component({
selector: 'hlm-sidebar-menu-skeleton,div[hlmSidebarMenuSkeleton]',
diff --git a/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-menu-sub-button.ts b/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-menu-sub-button.ts
index 57d3fc6..a7a7470 100644
--- a/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-menu-sub-button.ts
+++ b/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-menu-sub-button.ts
@@ -1,6 +1,6 @@
import { type BooleanInput } from '@angular/cdk/coercion';
import { booleanAttribute, Directive, inject, input } from '@angular/core';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
import { HlmSidebarService } from './hlm-sidebar.service';
import { injectHlmSidebarConfig } from './hlm-sidebar.token';
diff --git a/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-menu-sub-item.ts b/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-menu-sub-item.ts
index b8d3d3b..3ee3d9d 100644
--- a/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-menu-sub-item.ts
+++ b/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-menu-sub-item.ts
@@ -1,5 +1,5 @@
import { Directive } from '@angular/core';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Directive({
selector: 'li[hlmSidebarMenuSubItem]',
diff --git a/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-menu-sub.ts b/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-menu-sub.ts
index b57ecc3..edb988c 100644
--- a/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-menu-sub.ts
+++ b/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-menu-sub.ts
@@ -1,5 +1,5 @@
import { Directive } from '@angular/core';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Directive({
selector: 'ul[hlmSidebarMenuSub]',
diff --git a/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-menu.ts b/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-menu.ts
index 7e636d9..5a0d783 100644
--- a/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-menu.ts
+++ b/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-menu.ts
@@ -1,5 +1,5 @@
import { Directive } from '@angular/core';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Directive({
selector: 'ul[hlmSidebarMenu]',
diff --git a/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-rail.ts b/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-rail.ts
index b3d2364..e71caf9 100644
--- a/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-rail.ts
+++ b/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-rail.ts
@@ -1,5 +1,5 @@
import { Directive, inject, input } from '@angular/core';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
import { HlmSidebarService } from './hlm-sidebar.service';
@Directive({
diff --git a/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-separator.ts b/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-separator.ts
index d8b43cb..6d60e22 100644
--- a/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-separator.ts
+++ b/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-separator.ts
@@ -1,6 +1,6 @@
import { Directive } from '@angular/core';
-import { HlmSeparator } from '@spartan-ng/helm/separator';
-import { classes } from '@spartan-ng/helm/utils';
+import { HlmSeparator } from '../../../separator/src';
+import { classes } from '../../../utils/src';
@Directive({
selector: '[hlmSidebarSeparator],hlm-sidebar-separator',
diff --git a/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-trigger.ts b/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-trigger.ts
index 07d3371..f6e0d18 100644
--- a/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-trigger.ts
+++ b/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-trigger.ts
@@ -1,7 +1,7 @@
import { ChangeDetectionStrategy, Component, inject, input } from '@angular/core';
import { NgIcon, provideIcons } from '@ng-icons/core';
import { phosphorSidebarSimple } from '@ng-icons/phosphor-icons/regular';
-import { HlmButton, provideBrnButtonConfig } from '@spartan-ng/helm/button';
+import { HlmButton, provideBrnButtonConfig } from '../../../button/src';
import { HlmSidebarService } from './hlm-sidebar.service';
@Component({
diff --git a/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-wrapper.ts b/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-wrapper.ts
index 0fa585f..6495aaf 100644
--- a/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-wrapper.ts
+++ b/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar-wrapper.ts
@@ -1,5 +1,5 @@
import { Directive, input } from '@angular/core';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
import { injectHlmSidebarConfig } from './hlm-sidebar.token';
@Directive({
diff --git a/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar.ts b/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar.ts
index 3cae79c..df92904 100644
--- a/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar.ts
+++ b/packages/angular/src/lib/ui/sidebar/src/lib/hlm-sidebar.ts
@@ -1,10 +1,10 @@
import { NgTemplateOutlet } from '@angular/common';
import { ChangeDetectionStrategy, Component, computed, effect, inject, input } from '@angular/core';
-import { classes, hlm } from '@spartan-ng/helm/utils';
+import { classes, hlm } from '../../../utils/src';
import type { ClassValue } from 'clsx';
import { HlmSidebarService, type SidebarVariant } from './hlm-sidebar.service';
import { injectHlmSidebarConfig } from './hlm-sidebar.token';
-import { HlmSheetImports } from '@spartan-ng/helm/sheet';
+import { HlmSheetImports } from '../../../sheet/src';
@Component({
selector: 'hlm-sidebar',
diff --git a/packages/angular/src/lib/ui/skeleton/src/lib/hlm-skeleton.ts b/packages/angular/src/lib/ui/skeleton/src/lib/hlm-skeleton.ts
index 2491f58..5160216 100644
--- a/packages/angular/src/lib/ui/skeleton/src/lib/hlm-skeleton.ts
+++ b/packages/angular/src/lib/ui/skeleton/src/lib/hlm-skeleton.ts
@@ -1,5 +1,5 @@
import { Directive } from '@angular/core';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Directive({
selector: '[hlmSkeleton],hlm-skeleton',
diff --git a/packages/angular/src/lib/ui/table/src/lib/hlm-table.ts b/packages/angular/src/lib/ui/table/src/lib/hlm-table.ts
index f1a1eaf..da9f5f2 100644
--- a/packages/angular/src/lib/ui/table/src/lib/hlm-table.ts
+++ b/packages/angular/src/lib/ui/table/src/lib/hlm-table.ts
@@ -1,5 +1,5 @@
import { Directive } from '@angular/core';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Directive({
selector: 'div[hlmTableContainer]',
diff --git a/packages/angular/src/lib/ui/textarea/src/lib/hlm-textarea.ts b/packages/angular/src/lib/ui/textarea/src/lib/hlm-textarea.ts
index cea65e7..07b96fb 100644
--- a/packages/angular/src/lib/ui/textarea/src/lib/hlm-textarea.ts
+++ b/packages/angular/src/lib/ui/textarea/src/lib/hlm-textarea.ts
@@ -1,7 +1,7 @@
import { Directive } from '@angular/core';
import { BrnFieldControlDescribedBy } from '@spartan-ng/brain/field';
import { BrnTextarea } from '@spartan-ng/brain/textarea';
-import { classes } from '@spartan-ng/helm/utils';
+import { classes } from '../../../utils/src';
@Directive({
selector: '[hlmTextarea]',
diff --git a/packages/angular/src/lib/ui/tooltip/src/lib/hlm-tooltip.ts b/packages/angular/src/lib/ui/tooltip/src/lib/hlm-tooltip.ts
index 280c474..72032de 100644
--- a/packages/angular/src/lib/ui/tooltip/src/lib/hlm-tooltip.ts
+++ b/packages/angular/src/lib/ui/tooltip/src/lib/hlm-tooltip.ts
@@ -4,7 +4,7 @@ import {
BrnTooltipPosition,
provideBrnTooltipDefaultOptions,
} from '@spartan-ng/brain/tooltip';
-import { hlm } from '@spartan-ng/helm/utils';
+import { hlm } from '../../../utils/src';
import { cva } from 'class-variance-authority';
export const DEFAULT_TOOLTIP_SVG_CLASS =
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 8a56e75..fb31c3f 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -163,6 +163,9 @@ importers:
'@tailwindcss/postcss':
specifier: 4.3.1
version: 4.3.1
+ jiti:
+ specifier: 2.7.0
+ version: 2.7.0
ng-packagr:
specifier: 22.0.0
version: 22.0.0(@angular/compiler-cli@22.0.1(@angular/compiler@22.0.1)(typescript@6.0.3))(tailwindcss@4.3.1)(tslib@2.8.1)(typescript@6.0.3)
From 22975d97809accd3c15b9cff66ccbcd56908c8f1 Mon Sep 17 00:00:00 2001
From: sjoerdbeentjes <11621275+sjoerdbeentjes@users.noreply.github.com>
Date: Wed, 1 Jul 2026 16:03:21 +0200
Subject: [PATCH 6/6] docs: add ADR-018 for vendored helm relative-import fix
Record the @spartan-ng/helm alias/ng-packagr incompatibility and the
decision to rewrite cross-component imports to relative paths, per the
existing PoC decision log convention (brought over from main, which this
branch predates).
---
docs/decision-log.md | 76 +++++++++++++++++++++++++++++++++-----------
1 file changed, 57 insertions(+), 19 deletions(-)
diff --git a/docs/decision-log.md b/docs/decision-log.md
index 897eab8..19ac473 100644
--- a/docs/decision-log.md
+++ b/docs/decision-log.md
@@ -25,25 +25,26 @@ the replacement.
## Index
-| # | Decision | Status | Date |
-| --- | ----------------------------------------------------------------------------------------- | -------- | ---------- |
-| 01 | [Monorepo structure](#adr-001--monorepo-structure) | Accepted | 2026-06-30 |
-| 02 | [Base UI over Radix (React)](#adr-002--base-ui-over-radix-react) | Accepted | 2026-06-30 |
-| 03 | [Phosphor icons](#adr-003--phosphor-icons) | Accepted | 2026-06-30 |
-| 04 | [Tokens as single source of truth](#adr-004--tokens-as-single-source-of-truth) | Accepted | 2026-06-30 |
-| 05 | [Figma → code sync](#adr-005--figma--code-sync) | Accepted | 2026-06-30 |
-| 06 | [Theming via a class on ``](#adr-006--theming-via-a-class-on-html) | Accepted | 2026-06-30 |
-| 07 | [Token naming & roles](#adr-007--token-naming--roles) | Accepted | 2026-06-29 |
-| 08 | [Explicit colors over opacity](#adr-008--explicit-colors-over-opacity) | Accepted | 2026-06-29 |
-| 09 | [Modes vs themes](#adr-009--modes-vs-themes) | Accepted | 2026-06-29 |
-| 10 | [Deviating from shadcn is manageable](#adr-010--deviating-from-shadcn-is-manageable) | Accepted | 2026-06-29 |
-| 11 | [Cross-framework parity via contracts](#adr-011--cross-framework-parity-via-contracts) | Accepted | 2026-06-30 |
-| 12 | [Tailwind v4 for styling](#adr-012--tailwind-v4-for-styling) | Proposed | 2026-06-30 |
-| 13 | [Storybook + token docs](#adr-013--storybook--token-docs) | Accepted | 2026-06-30 |
-| 14 | [Versioning & publishing via Changesets](#adr-014--versioning--publishing-via-changesets) | Accepted | 2026-06-30 |
-| 15 | [Tree-shakeable React build](#adr-015--tree-shakeable-react-build) | Accepted | 2026-06-30 |
-| 16 | [Component scope built in parity](#adr-016--component-scope-built-in-parity) | Accepted | 2026-06-30 |
-| 17 | [Prove it in a real app](#adr-017--prove-it-in-a-real-app) | Proposed | 2026-06-30 |
+| # | Decision | Status | Date |
+| --- | -------------------------------------------------------------------------------------------------------------------- | -------- | ---------- |
+| 01 | [Monorepo structure](#adr-001--monorepo-structure) | Accepted | 2026-06-30 |
+| 02 | [Base UI over Radix (React)](#adr-002--base-ui-over-radix-react) | Accepted | 2026-06-30 |
+| 03 | [Phosphor icons](#adr-003--phosphor-icons) | Accepted | 2026-06-30 |
+| 04 | [Tokens as single source of truth](#adr-004--tokens-as-single-source-of-truth) | Accepted | 2026-06-30 |
+| 05 | [Figma → code sync](#adr-005--figma--code-sync) | Accepted | 2026-06-30 |
+| 06 | [Theming via a class on ``](#adr-006--theming-via-a-class-on-html) | Accepted | 2026-06-30 |
+| 07 | [Token naming & roles](#adr-007--token-naming--roles) | Accepted | 2026-06-29 |
+| 08 | [Explicit colors over opacity](#adr-008--explicit-colors-over-opacity) | Accepted | 2026-06-29 |
+| 09 | [Modes vs themes](#adr-009--modes-vs-themes) | Accepted | 2026-06-29 |
+| 10 | [Deviating from shadcn is manageable](#adr-010--deviating-from-shadcn-is-manageable) | Accepted | 2026-06-29 |
+| 11 | [Cross-framework parity via contracts](#adr-011--cross-framework-parity-via-contracts) | Accepted | 2026-06-30 |
+| 12 | [Tailwind v4 for styling](#adr-012--tailwind-v4-for-styling) | Proposed | 2026-06-30 |
+| 13 | [Storybook + token docs](#adr-013--storybook--token-docs) | Accepted | 2026-06-30 |
+| 14 | [Versioning & publishing via Changesets](#adr-014--versioning--publishing-via-changesets) | Accepted | 2026-06-30 |
+| 15 | [Tree-shakeable React build](#adr-015--tree-shakeable-react-build) | Accepted | 2026-06-30 |
+| 16 | [Component scope built in parity](#adr-016--component-scope-built-in-parity) | Accepted | 2026-06-30 |
+| 17 | [Prove it in a real app](#adr-017--prove-it-in-a-real-app) | Proposed | 2026-06-30 |
+| 18 | [Relative imports for vendored helm cross-references](#adr-018--relative-imports-for-vendored-helm-cross-references) | Accepted | 2026-07-01 |
### Open questions (not yet decided)
@@ -370,3 +371,40 @@ Storybook.
**Consequences.** Marked **Proposed** while the richer screens land. Apps stay consumers,
not published packages; keep the demo simple. See [AGENTS.md](../AGENTS.md).
+
+---
+
+## ADR-018 — Relative imports for vendored helm cross-references
+
+**Status:** Accepted · **Date:** 2026-07-01
+
+**Context.** The Spartan CLI vendors `helm` components into `src/lib/ui//` and wires
+cross-component references through a `@spartan-ng/helm/` tsconfig `paths` alias
+(`packages/angular/components.json` → `importAlias`). That alias isn't a real npm package
+— it only resolves when a consuming **app**'s own bundler reads the tsconfig mapping at
+build time. `@surfnet/angular` instead builds itself into a redistributable package via
+`ng-packagr`. `ng-packagr`'s rollup step hardcodes any bare (non-relative) import specifier
+as external unless it matches a registered secondary entry point
+(`node_modules/ng-packagr/.../flatten/rollup.js` → `isExternalDependency`) — it never
+consults tsconfig `paths`. With one entry point (`src/public-api.ts`), every alias-based
+cross-import leaked into the published bundle as an unresolved `@spartan-ng/helm/*` import,
+and in some cases duplicated the real symbol (once correctly inlined via a relative import
+path elsewhere, once left dangling as external).
+
+**Decision.** Rewrite all vendored cross-component imports from the
+`@spartan-ng/helm/` alias to relative paths. Added
+`packages/angular/scripts/rewrite-helm-imports.ts` (run via
+`pnpm --filter @surfnet/angular fix-helm-imports`, `jiti`) as a codemod to re-apply this
+after every `ng g @spartan-ng/cli:ui ` run, since the CLI always writes the
+alias form.
+
+**Rationale.** No supported config exists to make `ng-packagr` inline the alias for a
+single-entry-point library — not `components.json`'s `importAlias`, not `ng-package.json`.
+Moving to per-component secondary entry points (matching how Spartan's own `@spartan-ng/helm`
+package ships subpath exports) would fix it too, but is a much larger structural change;
+relative imports are the minimal fix within the current single-entry-point architecture.
+
+**Consequences.** The `angular.md` add-component playbook's "don't rewrite to relative
+imports" guidance was reversed — see the **Notes** section there. Run
+`fix-helm-imports` after every future `ng g` run and verify with
+`grep -r "@spartan-ng/helm" packages/angular/dist` (should be empty) before publishing.