@surfnet/curve-angular 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. package/.postcssrc.json +7 -0
  2. package/.storybook/main.ts +9 -0
  3. package/.storybook/manager.ts +3 -0
  4. package/.storybook/preview.ts +28 -0
  5. package/.storybook/tsconfig.json +8 -0
  6. package/.turbo/turbo-build.log +32 -0
  7. package/CHANGELOG.md +29 -0
  8. package/angular.json +52 -0
  9. package/components.json +5 -0
  10. package/dist/fesm2022/surfnet-curve-angular.mjs +4237 -0
  11. package/dist/fesm2022/surfnet-curve-angular.mjs.map +1 -0
  12. package/dist/files/geist-cyrillic-ext-wght-italic.woff2 +0 -0
  13. package/dist/files/geist-cyrillic-ext-wght-normal.woff2 +0 -0
  14. package/dist/files/geist-cyrillic-wght-italic.woff2 +0 -0
  15. package/dist/files/geist-cyrillic-wght-normal.woff2 +0 -0
  16. package/dist/files/geist-latin-ext-wght-italic.woff2 +0 -0
  17. package/dist/files/geist-latin-ext-wght-normal.woff2 +0 -0
  18. package/dist/files/geist-latin-wght-italic.woff2 +0 -0
  19. package/dist/files/geist-latin-wght-normal.woff2 +0 -0
  20. package/dist/files/geist-vietnamese-wght-italic.woff2 +0 -0
  21. package/dist/files/geist-vietnamese-wght-normal.woff2 +0 -0
  22. package/dist/styles.css +2 -0
  23. package/dist/types/surfnet-curve-angular.d.ts +1017 -0
  24. package/ng-package.json +20 -0
  25. package/package.json +73 -0
  26. package/scripts/copy-font-files.ts +15 -0
  27. package/scripts/rewrite-helm-imports.ts +60 -0
  28. package/src/foundations/design-tokens.stories.ts +198 -0
  29. package/src/lib/ui/avatar/src/index.ts +22 -0
  30. package/src/lib/ui/avatar/src/lib/hlm-avatar-badge.ts +19 -0
  31. package/src/lib/ui/avatar/src/lib/hlm-avatar-fallback.ts +20 -0
  32. package/src/lib/ui/avatar/src/lib/hlm-avatar-group-count.ts +17 -0
  33. package/src/lib/ui/avatar/src/lib/hlm-avatar-group.ts +17 -0
  34. package/src/lib/ui/avatar/src/lib/hlm-avatar-image.ts +19 -0
  35. package/src/lib/ui/avatar/src/lib/hlm-avatar.stories.ts +107 -0
  36. package/src/lib/ui/avatar/src/lib/hlm-avatar.ts +32 -0
  37. package/src/lib/ui/breadcrumb/src/index.ts +30 -0
  38. package/src/lib/ui/breadcrumb/src/lib/hlm-breadcrumb-ellipsis.ts +36 -0
  39. package/src/lib/ui/breadcrumb/src/lib/hlm-breadcrumb-item.ts +14 -0
  40. package/src/lib/ui/breadcrumb/src/lib/hlm-breadcrumb-link.ts +36 -0
  41. package/src/lib/ui/breadcrumb/src/lib/hlm-breadcrumb-list.ts +17 -0
  42. package/src/lib/ui/breadcrumb/src/lib/hlm-breadcrumb-page.ts +17 -0
  43. package/src/lib/ui/breadcrumb/src/lib/hlm-breadcrumb-separator.ts +27 -0
  44. package/src/lib/ui/breadcrumb/src/lib/hlm-breadcrumb.stories.ts +81 -0
  45. package/src/lib/ui/breadcrumb/src/lib/hlm-breadcrumb.ts +13 -0
  46. package/src/lib/ui/button/src/index.ts +6 -0
  47. package/src/lib/ui/button/src/lib/hlm-button.stories.ts +170 -0
  48. package/src/lib/ui/button/src/lib/hlm-button.token.ts +22 -0
  49. package/src/lib/ui/button/src/lib/hlm-button.ts +76 -0
  50. package/src/lib/ui/card/src/index.ts +25 -0
  51. package/src/lib/ui/card/src/lib/hlm-card-action.ts +12 -0
  52. package/src/lib/ui/card/src/lib/hlm-card-content.ts +12 -0
  53. package/src/lib/ui/card/src/lib/hlm-card-description.ts +12 -0
  54. package/src/lib/ui/card/src/lib/hlm-card-footer.ts +14 -0
  55. package/src/lib/ui/card/src/lib/hlm-card-header.ts +15 -0
  56. package/src/lib/ui/card/src/lib/hlm-card-title.ts +12 -0
  57. package/src/lib/ui/card/src/lib/hlm-card.stories.ts +105 -0
  58. package/src/lib/ui/card/src/lib/hlm-card.token.ts +19 -0
  59. package/src/lib/ui/card/src/lib/hlm-card.ts +22 -0
  60. package/src/lib/ui/checkbox/src/index.ts +5 -0
  61. package/src/lib/ui/checkbox/src/lib/hlm-checkbox.stories.ts +78 -0
  62. package/src/lib/ui/checkbox/src/lib/hlm-checkbox.ts +160 -0
  63. package/src/lib/ui/data-table/src/index.ts +14 -0
  64. package/src/lib/ui/data-table/src/lib/hlm-data-table-content.ts +87 -0
  65. package/src/lib/ui/data-table/src/lib/hlm-data-table-pagination.ts +62 -0
  66. package/src/lib/ui/data-table/src/lib/hlm-data-table-toolbar.ts +16 -0
  67. package/src/lib/ui/data-table/src/lib/hlm-data-table.stories.ts +479 -0
  68. package/src/lib/ui/data-table/src/lib/inject-data-table.ts +86 -0
  69. package/src/lib/ui/dropdown-menu/src/index.ts +45 -0
  70. package/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-checkbox-indicator.ts +20 -0
  71. package/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-checkbox.ts +36 -0
  72. package/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-group.ts +16 -0
  73. package/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-item-sub-indicator.ts +17 -0
  74. package/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-item.ts +41 -0
  75. package/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-label.ts +20 -0
  76. package/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-radio-indicator.ts +20 -0
  77. package/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-radio.ts +36 -0
  78. package/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-separator.ts +14 -0
  79. package/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-shortcut.ts +14 -0
  80. package/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-sub.ts +58 -0
  81. package/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-token.ts +26 -0
  82. package/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-trigger.ts +49 -0
  83. package/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu.stories.ts +125 -0
  84. package/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu.ts +62 -0
  85. package/src/lib/ui/field/src/index.ts +34 -0
  86. package/src/lib/ui/field/src/lib/hlm-field-content.ts +12 -0
  87. package/src/lib/ui/field/src/lib/hlm-field-description.ts +53 -0
  88. package/src/lib/ui/field/src/lib/hlm-field-error.ts +100 -0
  89. package/src/lib/ui/field/src/lib/hlm-field-group.ts +15 -0
  90. package/src/lib/ui/field/src/lib/hlm-field-label.ts +17 -0
  91. package/src/lib/ui/field/src/lib/hlm-field-legend.ts +17 -0
  92. package/src/lib/ui/field/src/lib/hlm-field-separator.ts +24 -0
  93. package/src/lib/ui/field/src/lib/hlm-field-set.ts +15 -0
  94. package/src/lib/ui/field/src/lib/hlm-field-title.ts +15 -0
  95. package/src/lib/ui/field/src/lib/hlm-field.stories.ts +117 -0
  96. package/src/lib/ui/field/src/lib/hlm-field.ts +49 -0
  97. package/src/lib/ui/icon/src/index.ts +7 -0
  98. package/src/lib/ui/icon/src/lib/hlm-icon.token.ts +20 -0
  99. package/src/lib/ui/icon/src/lib/hlm-icon.ts +42 -0
  100. package/src/lib/ui/input/src/index.ts +5 -0
  101. package/src/lib/ui/input/src/lib/hlm-input.stories.ts +94 -0
  102. package/src/lib/ui/input/src/lib/hlm-input.ts +20 -0
  103. package/src/lib/ui/input-group/src/index.ts +22 -0
  104. package/src/lib/ui/input-group/src/lib/hlm-input-group-addon.ts +40 -0
  105. package/src/lib/ui/input-group/src/lib/hlm-input-group-button.ts +47 -0
  106. package/src/lib/ui/input-group/src/lib/hlm-input-group-input.ts +17 -0
  107. package/src/lib/ui/input-group/src/lib/hlm-input-group-text.ts +14 -0
  108. package/src/lib/ui/input-group/src/lib/hlm-input-group-textarea.ts +17 -0
  109. package/src/lib/ui/input-group/src/lib/hlm-input-group.stories.ts +133 -0
  110. package/src/lib/ui/input-group/src/lib/hlm-input-group.ts +18 -0
  111. package/src/lib/ui/label/src/index.ts +5 -0
  112. package/src/lib/ui/label/src/lib/hlm-label.stories.ts +55 -0
  113. package/src/lib/ui/label/src/lib/hlm-label.ts +17 -0
  114. package/src/lib/ui/select/src/index.ts +52 -0
  115. package/src/lib/ui/select/src/lib/hlm-select-content.ts +44 -0
  116. package/src/lib/ui/select/src/lib/hlm-select-group.ts +14 -0
  117. package/src/lib/ui/select/src/lib/hlm-select-item.ts +36 -0
  118. package/src/lib/ui/select/src/lib/hlm-select-label.ts +14 -0
  119. package/src/lib/ui/select/src/lib/hlm-select-multiple.ts +45 -0
  120. package/src/lib/ui/select/src/lib/hlm-select-placeholder.ts +17 -0
  121. package/src/lib/ui/select/src/lib/hlm-select-portal.ts +8 -0
  122. package/src/lib/ui/select/src/lib/hlm-select-scroll-down.ts +22 -0
  123. package/src/lib/ui/select/src/lib/hlm-select-scroll-up.ts +22 -0
  124. package/src/lib/ui/select/src/lib/hlm-select-separator.ts +14 -0
  125. package/src/lib/ui/select/src/lib/hlm-select-trigger.ts +59 -0
  126. package/src/lib/ui/select/src/lib/hlm-select-value-template.ts +5 -0
  127. package/src/lib/ui/select/src/lib/hlm-select-value.ts +18 -0
  128. package/src/lib/ui/select/src/lib/hlm-select-values-content.ts +9 -0
  129. package/src/lib/ui/select/src/lib/hlm-select-values.ts +5 -0
  130. package/src/lib/ui/select/src/lib/hlm-select.stories.ts +105 -0
  131. package/src/lib/ui/select/src/lib/hlm-select.ts +45 -0
  132. package/src/lib/ui/separator/src/index.ts +5 -0
  133. package/src/lib/ui/separator/src/lib/hlm-separator.stories.ts +88 -0
  134. package/src/lib/ui/separator/src/lib/hlm-separator.ts +19 -0
  135. package/src/lib/ui/sheet/src/index.ts +34 -0
  136. package/src/lib/ui/sheet/src/lib/hlm-sheet-close.ts +9 -0
  137. package/src/lib/ui/sheet/src/lib/hlm-sheet-content.ts +66 -0
  138. package/src/lib/ui/sheet/src/lib/hlm-sheet-description.ts +14 -0
  139. package/src/lib/ui/sheet/src/lib/hlm-sheet-footer.ts +12 -0
  140. package/src/lib/ui/sheet/src/lib/hlm-sheet-header.ts +12 -0
  141. package/src/lib/ui/sheet/src/lib/hlm-sheet-overlay.ts +27 -0
  142. package/src/lib/ui/sheet/src/lib/hlm-sheet-portal.ts +8 -0
  143. package/src/lib/ui/sheet/src/lib/hlm-sheet-title.ts +14 -0
  144. package/src/lib/ui/sheet/src/lib/hlm-sheet-trigger.ts +9 -0
  145. package/src/lib/ui/sheet/src/lib/hlm-sheet.ts +29 -0
  146. package/src/lib/ui/sidebar/src/index.ts +75 -0
  147. package/src/lib/ui/sidebar/src/lib/hlm-sidebar-content.ts +18 -0
  148. package/src/lib/ui/sidebar/src/lib/hlm-sidebar-footer.ts +15 -0
  149. package/src/lib/ui/sidebar/src/lib/hlm-sidebar-group-action.ts +18 -0
  150. package/src/lib/ui/sidebar/src/lib/hlm-sidebar-group-content.ts +15 -0
  151. package/src/lib/ui/sidebar/src/lib/hlm-sidebar-group-label.ts +18 -0
  152. package/src/lib/ui/sidebar/src/lib/hlm-sidebar-group.ts +15 -0
  153. package/src/lib/ui/sidebar/src/lib/hlm-sidebar-header.ts +15 -0
  154. package/src/lib/ui/sidebar/src/lib/hlm-sidebar-input.ts +17 -0
  155. package/src/lib/ui/sidebar/src/lib/hlm-sidebar-inset.ts +15 -0
  156. package/src/lib/ui/sidebar/src/lib/hlm-sidebar-menu-action.ts +24 -0
  157. package/src/lib/ui/sidebar/src/lib/hlm-sidebar-menu-badge.ts +18 -0
  158. package/src/lib/ui/sidebar/src/lib/hlm-sidebar-menu-button.ts +97 -0
  159. package/src/lib/ui/sidebar/src/lib/hlm-sidebar-menu-item.ts +15 -0
  160. package/src/lib/ui/sidebar/src/lib/hlm-sidebar-menu-skeleton.ts +33 -0
  161. package/src/lib/ui/sidebar/src/lib/hlm-sidebar-menu-sub-button.ts +41 -0
  162. package/src/lib/ui/sidebar/src/lib/hlm-sidebar-menu-sub-item.ts +15 -0
  163. package/src/lib/ui/sidebar/src/lib/hlm-sidebar-menu-sub.ts +18 -0
  164. package/src/lib/ui/sidebar/src/lib/hlm-sidebar-menu.ts +15 -0
  165. package/src/lib/ui/sidebar/src/lib/hlm-sidebar-rail.ts +34 -0
  166. package/src/lib/ui/sidebar/src/lib/hlm-sidebar-separator.ts +17 -0
  167. package/src/lib/ui/sidebar/src/lib/hlm-sidebar-trigger.ts +35 -0
  168. package/src/lib/ui/sidebar/src/lib/hlm-sidebar-wrapper.ts +24 -0
  169. package/src/lib/ui/sidebar/src/lib/hlm-sidebar.service.ts +136 -0
  170. package/src/lib/ui/sidebar/src/lib/hlm-sidebar.stories.ts +177 -0
  171. package/src/lib/ui/sidebar/src/lib/hlm-sidebar.token.ts +33 -0
  172. package/src/lib/ui/sidebar/src/lib/hlm-sidebar.ts +139 -0
  173. package/src/lib/ui/skeleton/src/index.ts +5 -0
  174. package/src/lib/ui/skeleton/src/lib/hlm-skeleton.ts +14 -0
  175. package/src/lib/ui/table/src/index.ts +25 -0
  176. package/src/lib/ui/table/src/lib/hlm-table.stories.ts +70 -0
  177. package/src/lib/ui/table/src/lib/hlm-table.ts +129 -0
  178. package/src/lib/ui/textarea/src/index.ts +5 -0
  179. package/src/lib/ui/textarea/src/lib/hlm-textarea.stories.ts +72 -0
  180. package/src/lib/ui/textarea/src/lib/hlm-textarea.ts +21 -0
  181. package/src/lib/ui/tooltip/src/index.ts +5 -0
  182. package/src/lib/ui/tooltip/src/lib/hlm-tooltip.ts +44 -0
  183. package/src/lib/ui/utils/src/index.ts +1 -0
  184. package/src/lib/ui/utils/src/lib/hlm.ts +313 -0
  185. package/src/public-api.ts +24 -0
  186. package/src/styles.css +43 -0
  187. package/tsconfig.json +45 -0
  188. package/tsconfig.lib.json +11 -0
  189. package/tsconfig.lib.prod.json +9 -0
@@ -0,0 +1,105 @@
1
+ import type { Meta, StoryObj } from '@storybook/angular';
2
+ import { argsToTemplate, moduleMetadata } from '@storybook/angular';
3
+ import { HlmCard, HlmCardImports } from '..';
4
+ import { HlmButton } from '../../../button/src/lib/hlm-button';
5
+ import { cardContract } from '@surfnet/curve-contracts';
6
+
7
+ const meta: Meta<HlmCard> = {
8
+ title: 'Components/Card',
9
+ component: HlmCard,
10
+ decorators: [
11
+ moduleMetadata({
12
+ imports: [HlmButton, HlmCardImports],
13
+ }),
14
+ ],
15
+ parameters: {
16
+ docs: {
17
+ description: {
18
+ component: cardContract.docs.description,
19
+ },
20
+ },
21
+ },
22
+
23
+ argTypes: {
24
+ size: {
25
+ control: 'radio',
26
+ options: ['default', 'sm'],
27
+ description: 'Spacing density of the card.',
28
+ table: { defaultValue: { summary: 'default' } },
29
+ },
30
+ },
31
+ args: {
32
+ size: 'default',
33
+ },
34
+ };
35
+
36
+ export default meta;
37
+ type Story = StoryObj<HlmCard>;
38
+
39
+ /** Default card with header, content, and footer. */
40
+ export const Default: Story = {
41
+ render: (args) => ({
42
+ props: args,
43
+ template: `
44
+ <hlm-card class="w-80" ${argsToTemplate(args)}>
45
+ <div hlmCardHeader>
46
+ <h3 hlmCardTitle>Card title</h3>
47
+ <p hlmCardDescription>A short description of what this card is about.</p>
48
+ </div>
49
+ <p hlmCardContent class="text-muted-foreground">Card body content goes here.</p>
50
+ <p hlmCardFooter class="justify-end gap-2">
51
+ <button hlmBtn variant="outline">Cancel</button>
52
+ <button hlmBtn>Confirm</button>
53
+ </p>
54
+ </hlm-card>
55
+ `,
56
+ }),
57
+ };
58
+
59
+ /** Compact (`sm`) card with tighter spacing. */
60
+ export const Small: Story = {
61
+ render: () => ({
62
+ template: `
63
+ <section hlmCard size="sm" class="w-80">
64
+ <div hlmCardHeader>
65
+ <h3 hlmCardTitle>Small card</h3>
66
+ <p hlmCardDescription>Compact spacing variant.</p>
67
+ </div>
68
+ <p hlmCardContent class="text-muted-foreground">Less padding all round.</p>
69
+ </section>
70
+ `,
71
+ }),
72
+ };
73
+
74
+ /** Card with an action slot in the header (e.g. a menu trigger). */
75
+ export const WithHeaderAction: Story = {
76
+ render: () => ({
77
+ template: `
78
+ <section hlmCard class='w-80'>
79
+ <div hlmCardHeader>
80
+ <h3 hlmCardTitle>With action</h3>
81
+ <p hlmCardDescription>
82
+ An action button lives in the header.
83
+ </p>
84
+ <div hlmCardAction>
85
+ <button hlmBtn variant="ghost" size="sm">Edit</button>
86
+ </div>
87
+ </div>
88
+ <p hlmCardContent class="text-muted-foreground">Content area.</p>
89
+ </section>
90
+ `,
91
+ }),
92
+ };
93
+
94
+ /** Card with only a content area — no header or footer. */
95
+ export const ContentOnly: Story = {
96
+ render: () => ({
97
+ template: `
98
+ <section hlmCard class='w-80'>
99
+ <p hlmCardContent class="text-muted-foreground">
100
+ Just a card with content and no header or footer.
101
+ </p>
102
+ </section>
103
+ `,
104
+ }),
105
+ };
@@ -0,0 +1,19 @@
1
+ import { inject, InjectionToken, type ValueProvider } from '@angular/core';
2
+
3
+ export type HlmCardConfig = {
4
+ size: 'sm' | 'default';
5
+ };
6
+
7
+ const defaultConfig: HlmCardConfig = {
8
+ size: 'default',
9
+ };
10
+
11
+ const HlmCardConfigToken = new InjectionToken<HlmCardConfig>('HlmCardConfig');
12
+
13
+ export function provideHlmCardConfig(config: Partial<HlmCardConfig>): ValueProvider {
14
+ return { provide: HlmCardConfigToken, useValue: { ...defaultConfig, ...config } };
15
+ }
16
+
17
+ export function injectHlmCardConfig(): HlmCardConfig {
18
+ return inject(HlmCardConfigToken, { optional: true }) ?? defaultConfig;
19
+ }
@@ -0,0 +1,22 @@
1
+ import { Directive, input } from '@angular/core';
2
+ import { classes } from '../../../utils/src';
3
+ import { HlmCardConfig, injectHlmCardConfig } from './hlm-card.token';
4
+
5
+ @Directive({
6
+ selector: '[hlmCard],hlm-card',
7
+ host: {
8
+ 'data-slot': 'card',
9
+ '[attr.data-size]': 'size()',
10
+ },
11
+ })
12
+ export class HlmCard {
13
+ private readonly _defaultConfig = injectHlmCardConfig();
14
+ public readonly size = input<HlmCardConfig['size']>(this._defaultConfig.size);
15
+
16
+ constructor() {
17
+ classes(
18
+ () =>
19
+ 'ring-foreground/10 bg-card text-card-foreground gap-(--card-spacing) overflow-hidden rounded-xl py-(--card-spacing) text-sm shadow-xs ring-1 [--card-spacing:--spacing(6)] has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(4)] *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl group/card flex flex-col',
20
+ );
21
+ }
22
+ }
@@ -0,0 +1,5 @@
1
+ import { HlmCheckbox } from './lib/hlm-checkbox';
2
+
3
+ export * from './lib/hlm-checkbox';
4
+
5
+ export const HlmCheckboxImports = [HlmCheckbox] as const;
@@ -0,0 +1,78 @@
1
+ import type { Meta, StoryObj } from '@storybook/angular';
2
+ import { moduleMetadata } from '@storybook/angular';
3
+ import { HlmCheckbox, HlmCheckboxImports } from '..';
4
+ import { HlmLabel } from '../../../label/src/lib/hlm-label';
5
+ import { checkboxContract } from '@surfnet/curve-contracts';
6
+
7
+ const meta: Meta<HlmCheckbox> = {
8
+ title: 'Components/Checkbox',
9
+ component: HlmCheckbox,
10
+ decorators: [
11
+ moduleMetadata({
12
+ imports: [HlmCheckboxImports, HlmLabel],
13
+ }),
14
+ ],
15
+ parameters: {
16
+ docs: {
17
+ description: {
18
+ component: checkboxContract.docs.description,
19
+ },
20
+ },
21
+ },
22
+ };
23
+
24
+ export default meta;
25
+ type Story = StoryObj<HlmCheckbox>;
26
+
27
+ /** A controlled checkbox driven by React state — the typical usage pattern. */
28
+ export const Default: Story = {
29
+ args: { disabled: false },
30
+ render: (args) => ({
31
+ props: args,
32
+ template: `
33
+ <hlm-checkbox inputId="testCheckbox" checked [disabled]="disabled" />
34
+ `,
35
+ }),
36
+ };
37
+
38
+ /** A checkbox paired with a clickable label. */
39
+ export const WithLabel: Story = {
40
+ render: () => ({
41
+ template: `
42
+ <label class="gap-2" hlmLabel>
43
+ <hlm-checkbox checked/>
44
+ Accept terms and conditions
45
+ </label>
46
+ `,
47
+ }),
48
+ };
49
+
50
+ /** Every state side by side. */
51
+ export const States: Story = {
52
+ render: () => ({
53
+ template: `
54
+ <div class="flex flex-col gap-3">
55
+ <label class="gap-2" hlmLabel>
56
+ <hlm-checkbox/>
57
+ Unchecked
58
+ </label>
59
+ <label class="gap-2" hlmLabel>
60
+ <hlm-checkbox checked/>
61
+ Checked
62
+ </label>
63
+ <label class="gap-2" hlmLabel>
64
+ <hlm-checkbox indeterminate="true"/>
65
+ Indeterminate
66
+ </label>
67
+ <label class="gap-2" hlmLabel>
68
+ <hlm-checkbox disabled/>
69
+ Disabled
70
+ </label>
71
+ <label class="gap-2" hlmLabel>
72
+ <hlm-checkbox checked disabled/>
73
+ Disabled checked
74
+ </label>
75
+ </div>
76
+ `,
77
+ }),
78
+ };
@@ -0,0 +1,160 @@
1
+ import type { BooleanInput } from '@angular/cdk/coercion';
2
+ import {
3
+ booleanAttribute,
4
+ ChangeDetectionStrategy,
5
+ Component,
6
+ computed,
7
+ forwardRef,
8
+ input,
9
+ linkedSignal,
10
+ model,
11
+ output,
12
+ viewChild,
13
+ } from '@angular/core';
14
+ import { type ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
15
+ import { NgIcon, provideIcons } from '@ng-icons/core';
16
+ import { phosphorCheck } from '@ng-icons/phosphor-icons/regular';
17
+ import { BrnCheckbox } from '@spartan-ng/brain/checkbox';
18
+ import { BrnFieldControlDescribedBy } from '@spartan-ng/brain/field';
19
+ import type { ChangeFn, TouchFn } from '@spartan-ng/brain/forms';
20
+ import { HlmIcon } from '../../../icon/src';
21
+ import { hlm } from '../../../utils/src';
22
+ import type { ClassValue } from 'clsx';
23
+
24
+ export const HLM_CHECKBOX_VALUE_ACCESSOR = {
25
+ provide: NG_VALUE_ACCESSOR,
26
+ useExisting: forwardRef(() => HlmCheckbox),
27
+ multi: true,
28
+ };
29
+
30
+ @Component({
31
+ selector: 'hlm-checkbox',
32
+ imports: [BrnCheckbox, NgIcon, HlmIcon],
33
+ providers: [HLM_CHECKBOX_VALUE_ACCESSOR],
34
+ viewProviders: [provideIcons({ phosphorCheck })],
35
+ changeDetection: ChangeDetectionStrategy.OnPush,
36
+ hostDirectives: [BrnFieldControlDescribedBy],
37
+ host: {
38
+ class: 'contents peer',
39
+ 'data-slot': 'checkbox',
40
+ '[attr.aria-label]': 'null',
41
+ '[attr.aria-labelledby]': 'null',
42
+ '[attr.data-disabled]': '_disabled() ? "" : null',
43
+ },
44
+ template: `
45
+ <brn-checkbox
46
+ [id]="inputId()"
47
+ [name]="name()"
48
+ [class]="_computedClass()"
49
+ [checked]="checked()"
50
+ [(indeterminate)]="indeterminate"
51
+ [disabled]="_disabled()"
52
+ [required]="required()"
53
+ [aria-label]="ariaLabel()"
54
+ [aria-labelledby]="ariaLabelledby()"
55
+ [aria-describedby]="ariaDescribedby()"
56
+ [forceInvalid]="forceInvalid()"
57
+ (checkedChange)="_handleChange($event)"
58
+ (touched)="_onTouched?.()"
59
+ >
60
+ @if (checked() || indeterminate()) {
61
+ <span class="flex items-center justify-center text-current transition-none">
62
+ <ng-icon hlm size="14px" name="phosphorCheck" />
63
+ </span>
64
+ }
65
+ </brn-checkbox>
66
+ `,
67
+ })
68
+ export class HlmCheckbox implements ControlValueAccessor {
69
+ public readonly userClass = input<ClassValue>('', { alias: 'class' });
70
+
71
+ protected readonly _computedClass = computed(() =>
72
+ hlm(
73
+ 'border-input dark:bg-input/30 data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary data-checked:border-primary data-[matches-spartan-invalid=true]:aria-checked:border-primary data-[matches-spartan-invalid=true]:border-destructive dark:data-[matches-spartan-invalid=true]:border-destructive/50 focus-visible:border-ring focus-visible:ring-ring/50 data-[matches-spartan-invalid=true]:ring-destructive/20 dark:data-[matches-spartan-invalid=true]:ring-destructive/40 flex size-4 items-center justify-center rounded-[4px] border shadow-xs transition-shadow group-has-disabled/field:opacity-50 focus-visible:ring-3 data-[matches-spartan-invalid=true]:ring-3 peer shrink-0 cursor-default outline-none disabled:cursor-not-allowed disabled:opacity-50',
74
+ this.userClass(),
75
+ this._errorStateClass(),
76
+ ),
77
+ );
78
+
79
+ /** Used to set the id on the underlying brn element. */
80
+ public readonly inputId = input<string | null>(null);
81
+
82
+ /** Used to set the aria-label attribute on the underlying brn element. */
83
+ public readonly ariaLabel = input<string | null>(null, { alias: 'aria-label' });
84
+
85
+ /** Used to set the aria-labelledby attribute on the underlying brn element. */
86
+ public readonly ariaLabelledby = input<string | null>(null, { alias: 'aria-labelledby' });
87
+
88
+ /** Used to set the aria-describedby attribute on the underlying brn element. */
89
+ public readonly ariaDescribedby = input<string | null>(null, { alias: 'aria-describedby' });
90
+
91
+ /** The checked state of the checkbox. */
92
+ public readonly checkedInput = input<boolean, BooleanInput>(false, {
93
+ alias: 'checked',
94
+ transform: booleanAttribute,
95
+ });
96
+ public readonly checked = linkedSignal(this.checkedInput);
97
+
98
+ /** Emits when checked state changes. */
99
+ public readonly checkedChange = output<boolean>();
100
+
101
+ /**
102
+ * The indeterminate state of the checkbox.
103
+ * For example, a "select all/deselect all" checkbox may be in the indeterminate state when some but not all of its sub-controls are checked.
104
+ */
105
+ public readonly indeterminate = model<boolean>(false);
106
+
107
+ /** The name attribute of the checkbox. */
108
+ public readonly name = input<string | null>(null);
109
+
110
+ /** Whether the checkbox is required. */
111
+ public readonly required = input<boolean, BooleanInput>(false, { transform: booleanAttribute });
112
+
113
+ /** Whether the checkbox is disabled. */
114
+ public readonly disabled = input<boolean, BooleanInput>(false, { transform: booleanAttribute });
115
+
116
+ /** Whether to force the checkbox into an invalid state. */
117
+ public readonly forceInvalid = input<boolean, BooleanInput>(false, {
118
+ transform: booleanAttribute,
119
+ });
120
+
121
+ protected readonly _disabled = linkedSignal(this.disabled);
122
+
123
+ private readonly _brnCheckbox = viewChild.required(BrnCheckbox);
124
+
125
+ private readonly _spartanInvalid = computed(
126
+ () => this.forceInvalid() || this._brnCheckbox().spartanInvalid?.(),
127
+ );
128
+ protected readonly _errorStateClass = computed(() =>
129
+ this._spartanInvalid()
130
+ ? 'border-destructive focus-visible:border-destructive focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40'
131
+ : '',
132
+ );
133
+
134
+ protected _onChange?: ChangeFn<boolean>;
135
+ protected _onTouched?: TouchFn;
136
+
137
+ protected _handleChange(value: boolean): void {
138
+ if (this._disabled()) return;
139
+ this.checked.set(value);
140
+ this.checkedChange.emit(value);
141
+ this._onChange?.(value);
142
+ }
143
+
144
+ /** CONTROL VALUE ACCESSOR */
145
+ writeValue(value: boolean): void {
146
+ this.checked.set(value);
147
+ }
148
+
149
+ registerOnChange(fn: ChangeFn<boolean>): void {
150
+ this._onChange = fn;
151
+ }
152
+
153
+ registerOnTouched(fn: TouchFn): void {
154
+ this._onTouched = fn;
155
+ }
156
+
157
+ setDisabledState(isDisabled: boolean): void {
158
+ this._disabled.set(isDisabled);
159
+ }
160
+ }
@@ -0,0 +1,14 @@
1
+ import { HlmDataTableContent } from './lib/hlm-data-table-content';
2
+ import { HlmDataTablePagination } from './lib/hlm-data-table-pagination';
3
+ import { HlmDataTableToolbar } from './lib/hlm-data-table-toolbar';
4
+
5
+ export * from './lib/inject-data-table';
6
+ export * from './lib/hlm-data-table-content';
7
+ export * from './lib/hlm-data-table-pagination';
8
+ export * from './lib/hlm-data-table-toolbar';
9
+
10
+ export const HlmDataTableImports = [
11
+ HlmDataTableContent,
12
+ HlmDataTablePagination,
13
+ HlmDataTableToolbar,
14
+ ] as const;
@@ -0,0 +1,87 @@
1
+ import { ChangeDetectionStrategy, Component, input } from '@angular/core';
2
+ import { HlmTableImports } from '../../../table/src';
3
+ import { FlexRenderDirective, type ColumnDef, type Table } from '@tanstack/angular-table';
4
+
5
+ /**
6
+ * Renders a TanStack table instance into the styled helm `table` directives. Mirrors React's
7
+ * `DataTableContent`: header groups, body rows (with `data-state="selected"` per selected
8
+ * row), and an empty-state row when there are no rows.
9
+ *
10
+ * String / primitive header and cell defs are rendered as inert escaped text by `*flexRender`
11
+ * (matching React's `flexRender` of a string node); component cell defs created with
12
+ * `flexRenderComponent()` are mounted in place. To render markup, return a
13
+ * `flexRenderComponent()` rather than an HTML string — raw strings are never parsed as HTML.
14
+ */
15
+ @Component({
16
+ selector: 'hlm-data-table-content',
17
+ imports: [HlmTableImports, FlexRenderDirective],
18
+ changeDetection: ChangeDetectionStrategy.OnPush,
19
+ host: {
20
+ 'data-slot': 'data-table-content',
21
+ class: 'block overflow-hidden rounded-md border',
22
+ },
23
+ template: `
24
+ <div hlmTableContainer>
25
+ <table hlmTable>
26
+ <thead hlmTableHeader>
27
+ @for (headerGroup of table().getHeaderGroups(); track headerGroup.id) {
28
+ <tr hlmTableRow>
29
+ @for (header of headerGroup.headers; track header.id) {
30
+ <th hlmTableHead>
31
+ @if (!header.isPlaceholder) {
32
+ <ng-container
33
+ *flexRender="
34
+ header.column.columnDef.header;
35
+ props: header.getContext();
36
+ let headerContent
37
+ "
38
+ >
39
+ {{ headerContent }}
40
+ </ng-container>
41
+ }
42
+ </th>
43
+ }
44
+ </tr>
45
+ }
46
+ </thead>
47
+ <tbody hlmTableBody>
48
+ @if (table().getRowModel().rows.length) {
49
+ @for (row of table().getRowModel().rows; track row.id) {
50
+ <tr hlmTableRow [attr.data-state]="row.getIsSelected() ? 'selected' : null">
51
+ @for (cell of row.getVisibleCells(); track cell.id) {
52
+ <td hlmTableCell>
53
+ <ng-container
54
+ *flexRender="
55
+ cell.column.columnDef.cell;
56
+ props: cell.getContext();
57
+ let cellContent
58
+ "
59
+ >
60
+ {{ cellContent }}
61
+ </ng-container>
62
+ </td>
63
+ }
64
+ </tr>
65
+ }
66
+ } @else {
67
+ <tr hlmTableRow>
68
+ <td hlmTableCell [attr.colspan]="columns().length" class="h-24 text-center">
69
+ {{ noResultsLabel() }}
70
+ </td>
71
+ </tr>
72
+ }
73
+ </tbody>
74
+ </table>
75
+ </div>
76
+ `,
77
+ })
78
+ export class HlmDataTableContent<TData> {
79
+ /** The TanStack table instance, e.g. from `injectDataTable`. */
80
+ public readonly table = input.required<Table<TData>>();
81
+
82
+ /** The column defs — used to span the empty-state row across all columns. */
83
+ public readonly columns = input.required<ColumnDef<TData, unknown>[]>();
84
+
85
+ /** Content shown in the body when there are no rows. */
86
+ public readonly noResultsLabel = input<string>('No results.');
87
+ }
@@ -0,0 +1,62 @@
1
+ import { ChangeDetectionStrategy, Component, input } from '@angular/core';
2
+ import { HlmButton } from '../../../button/src';
3
+ import { type Table } from '@tanstack/angular-table';
4
+
5
+ /**
6
+ * Selection summary plus Previous / Next paging controls. Mirrors React's
7
+ * `DataTablePagination`.
8
+ */
9
+ @Component({
10
+ selector: 'hlm-data-table-pagination',
11
+ imports: [HlmButton],
12
+ changeDetection: ChangeDetectionStrategy.OnPush,
13
+ host: {
14
+ 'data-slot': 'data-table-pagination',
15
+ class: 'flex items-center justify-end gap-2 py-4',
16
+ },
17
+ template: `
18
+ <div class="text-muted-foreground flex-1 text-sm">
19
+ {{
20
+ selectionLabel()(
21
+ table().getFilteredSelectedRowModel().rows.length,
22
+ table().getFilteredRowModel().rows.length
23
+ )
24
+ }}
25
+ </div>
26
+ <div class="flex gap-2">
27
+ <button
28
+ hlmBtn
29
+ variant="outline"
30
+ size="sm"
31
+ [disabled]="!table().getCanPreviousPage()"
32
+ (click)="table().previousPage()"
33
+ >
34
+ {{ previousLabel() }}
35
+ </button>
36
+ <button
37
+ hlmBtn
38
+ variant="outline"
39
+ size="sm"
40
+ [disabled]="!table().getCanNextPage()"
41
+ (click)="table().nextPage()"
42
+ >
43
+ {{ nextLabel() }}
44
+ </button>
45
+ </div>
46
+ `,
47
+ })
48
+ export class HlmDataTablePagination<TData> {
49
+ /** The TanStack table instance, e.g. from `injectDataTable`. */
50
+ public readonly table = input.required<Table<TData>>();
51
+
52
+ /** Label for the "previous page" button. */
53
+ public readonly previousLabel = input<string>('Previous');
54
+
55
+ /** Label for the "next page" button. */
56
+ public readonly nextLabel = input<string>('Next');
57
+
58
+ /** Builds the selection-summary text from the selected and total row counts. */
59
+ public readonly selectionLabel = input<(selectedCount: number, totalCount: number) => string>(
60
+ (selectedCount, totalCount) => `${selectedCount} of ${totalCount} row(s) selected.`,
61
+ );
62
+ }
@@ -0,0 +1,16 @@
1
+ import { Directive } from '@angular/core';
2
+ import { classes } from '../../../utils/src';
3
+
4
+ /**
5
+ * Layout wrapper for data-table controls (filter inputs, column toggles, actions). Mirrors
6
+ * React's `DataTableToolbar` — a styled flex row above the table.
7
+ */
8
+ @Directive({
9
+ selector: '[hlmDataTableToolbar]',
10
+ host: { 'data-slot': 'data-table-toolbar' },
11
+ })
12
+ export class HlmDataTableToolbar {
13
+ constructor() {
14
+ classes(() => 'flex items-center gap-2 py-4');
15
+ }
16
+ }