@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,17 @@
1
+ import { ChangeDetectionStrategy, Component } from '@angular/core';
2
+ import { NgIcon, provideIcons } from '@ng-icons/core';
3
+ import { phosphorCaretRight } from '@ng-icons/phosphor-icons/regular';
4
+ import { classes } from '../../../utils/src';
5
+
6
+ @Component({
7
+ selector: 'hlm-dropdown-menu-item-sub-indicator',
8
+ imports: [NgIcon],
9
+ providers: [provideIcons({ phosphorCaretRight })],
10
+ changeDetection: ChangeDetectionStrategy.OnPush,
11
+ template: ` <ng-icon name="phosphorCaretRight" class="text-base rtl:rotate-180" /> `,
12
+ })
13
+ export class HlmDropdownMenuItemSubIndicator {
14
+ constructor() {
15
+ classes(() => 'ms-auto size-4');
16
+ }
17
+ }
@@ -0,0 +1,41 @@
1
+ import { type BooleanInput } from '@angular/cdk/coercion';
2
+ import { CdkMenuItem } from '@angular/cdk/menu';
3
+ import { booleanAttribute, Directive, HOST_TAG_NAME, inject, input } from '@angular/core';
4
+ import { classes } from '../../../utils/src';
5
+ import type { DropdownMenuItemVariantName } from '@surfnet/curve-contracts';
6
+
7
+ @Directive({
8
+ selector: '[hlmDropdownMenuItem],hlm-dropdown-menu-item',
9
+ hostDirectives: [
10
+ {
11
+ directive: CdkMenuItem,
12
+ inputs: ['cdkMenuItemDisabled: disabled'],
13
+ outputs: ['cdkMenuItemTriggered: triggered'],
14
+ },
15
+ ],
16
+ host: {
17
+ 'data-slot': 'dropdown-menu-item',
18
+ '[attr.disabled]': '_isButton && disabled() ? "" : null',
19
+ '[attr.data-disabled]': 'disabled() ? "" : null',
20
+ '[attr.data-variant]': 'variant()',
21
+ '[attr.data-inset]': 'inset() ? "" : null',
22
+ },
23
+ })
24
+ export class HlmDropdownMenuItem {
25
+ protected readonly _isButton = inject(HOST_TAG_NAME) === 'button';
26
+
27
+ public readonly disabled = input<boolean, BooleanInput>(false, { transform: booleanAttribute });
28
+
29
+ public readonly variant = input<DropdownMenuItemVariantName>('default');
30
+
31
+ public readonly inset = input<boolean, BooleanInput>(false, {
32
+ transform: booleanAttribute,
33
+ });
34
+
35
+ constructor() {
36
+ classes(
37
+ () =>
38
+ "hover:bg-accent focus-visible:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[ng-icon]:!text-destructive [&_ng-icon:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_ng-icon]:pointer-events-none [&_ng-icon]:shrink-0 [&_svg:not([class*='text-'])]:text-base",
39
+ );
40
+ }
41
+ }
@@ -0,0 +1,20 @@
1
+ import { type BooleanInput } from '@angular/cdk/coercion';
2
+ import { booleanAttribute, Directive, input } from '@angular/core';
3
+ import { classes } from '../../../utils/src';
4
+
5
+ @Directive({
6
+ selector: '[hlmDropdownMenuLabel],hlm-dropdown-menu-label',
7
+ host: {
8
+ 'data-slot': 'dropdown-menu-label',
9
+ '[attr.data-inset]': 'inset() ? "" : null',
10
+ },
11
+ })
12
+ export class HlmDropdownMenuLabel {
13
+ constructor() {
14
+ classes(() => 'block px-2 py-1.5 text-sm font-medium data-[inset]:pl-8');
15
+ }
16
+
17
+ public readonly inset = input<boolean, BooleanInput>(false, {
18
+ transform: booleanAttribute,
19
+ });
20
+ }
@@ -0,0 +1,20 @@
1
+ import { ChangeDetectionStrategy, Component } from '@angular/core';
2
+ import { NgIcon, provideIcons } from '@ng-icons/core';
3
+ import { phosphorCircle } from '@ng-icons/phosphor-icons/regular';
4
+ import { classes } from '../../../utils/src';
5
+
6
+ @Component({
7
+ selector: 'hlm-dropdown-menu-radio-indicator',
8
+ imports: [NgIcon],
9
+ providers: [provideIcons({ phosphorCircle })],
10
+ changeDetection: ChangeDetectionStrategy.OnPush,
11
+ template: ` <ng-icon name="phosphorCircle" class="text-[0.5rem] *:[svg]:fill-current" /> `,
12
+ })
13
+ export class HlmDropdownMenuRadioIndicator {
14
+ constructor() {
15
+ classes(
16
+ () =>
17
+ 'pointer-events-none absolute start-2 flex size-3.5 items-center justify-center opacity-0 group-data-[checked]:opacity-100',
18
+ );
19
+ }
20
+ }
@@ -0,0 +1,36 @@
1
+ import { type BooleanInput } from '@angular/cdk/coercion';
2
+ import { CdkMenuItemRadio } from '@angular/cdk/menu';
3
+ import { Directive, booleanAttribute, inject, input } from '@angular/core';
4
+ import { classes } from '../../../utils/src';
5
+
6
+ @Directive({
7
+ selector: '[hlmDropdownMenuRadio]',
8
+ hostDirectives: [
9
+ {
10
+ directive: CdkMenuItemRadio,
11
+ inputs: ['cdkMenuItemDisabled: disabled', 'cdkMenuItemChecked: checked'],
12
+ outputs: ['cdkMenuItemTriggered: triggered'],
13
+ },
14
+ ],
15
+ host: {
16
+ 'data-slot': 'dropdown-menu-radio-item',
17
+ '[attr.data-disabled]': 'disabled() ? "" : null',
18
+ '[attr.data-checked]': 'checked() ? "" : null',
19
+ },
20
+ })
21
+ export class HlmDropdownMenuRadio {
22
+ private readonly _cdkMenuItem = inject(CdkMenuItemRadio);
23
+ public readonly checked = input<boolean, BooleanInput>(this._cdkMenuItem.checked, {
24
+ transform: booleanAttribute,
25
+ });
26
+ public readonly disabled = input<boolean, BooleanInput>(this._cdkMenuItem.disabled, {
27
+ transform: booleanAttribute,
28
+ });
29
+
30
+ constructor() {
31
+ classes(
32
+ () =>
33
+ 'hover:bg-accent hover:text-accent-foreground focus-visible:bg-accent focus-visible:text-accent-foreground group relative flex w-full cursor-default items-center rounded-sm py-1.5 ps-8 pe-2 text-sm transition-colors outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
34
+ );
35
+ }
36
+ }
@@ -0,0 +1,14 @@
1
+ import { Directive } from '@angular/core';
2
+ import { classes } from '../../../utils/src';
3
+
4
+ @Directive({
5
+ selector: '[hlmDropdownMenuSeparator],hlm-dropdown-menu-separator',
6
+ host: {
7
+ 'data-slot': 'dropdown-menu-separator',
8
+ },
9
+ })
10
+ export class HlmDropdownMenuSeparator {
11
+ constructor() {
12
+ classes(() => 'bg-border -mx-1 my-1 block h-px');
13
+ }
14
+ }
@@ -0,0 +1,14 @@
1
+ import { Directive } from '@angular/core';
2
+ import { classes } from '../../../utils/src';
3
+
4
+ @Directive({
5
+ selector: '[hlmDropdownMenuShortcut],hlm-dropdown-menu-shortcut',
6
+ host: {
7
+ 'data-slot': 'dropdown-menu-shortcut',
8
+ },
9
+ })
10
+ export class HlmDropdownMenuShortcut {
11
+ constructor() {
12
+ classes(() => 'text-muted-foreground ml-auto text-xs tracking-widest');
13
+ }
14
+ }
@@ -0,0 +1,58 @@
1
+ import { CdkMenu } from '@angular/cdk/menu';
2
+ import { Directive, inject, signal } from '@angular/core';
3
+ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
4
+ import { classes } from '../../../utils/src';
5
+
6
+ @Directive({
7
+ selector: '[hlmDropdownMenuSub],hlm-dropdown-menu-sub',
8
+ hostDirectives: [CdkMenu],
9
+ host: {
10
+ 'data-slot': 'dropdown-menu-sub',
11
+ '[attr.data-state]': '_state()',
12
+ '[attr.data-side]': '_side()',
13
+ },
14
+ })
15
+ export class HlmDropdownMenuSub {
16
+ private readonly _host = inject(CdkMenu);
17
+
18
+ protected readonly _state = signal('open');
19
+ protected readonly _side = signal('top');
20
+
21
+ constructor() {
22
+ this.setSideWithDarkMagic();
23
+ // this is a best effort, but does not seem to work currently
24
+ // TODO: figure out a way for us to know the host is about to be closed. might not be possible with CDK
25
+ this._host.closed.pipe(takeUntilDestroyed()).subscribe(() => this._state.set('closed'));
26
+
27
+ classes(
28
+ () =>
29
+ 'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-top overflow-hidden rounded-md border p-1 shadow-lg',
30
+ );
31
+ }
32
+
33
+ private setSideWithDarkMagic() {
34
+ /**
35
+ * This is an ugly workaround to at least figure out the correct side of where a submenu
36
+ * will appear and set the attribute to the host accordingly
37
+ *
38
+ * First of all we take advantage of the menu stack not being aware of the root
39
+ * object immediately after it is added. This code executes before the root element is added,
40
+ * which means the stack is still empty and the peek method returns undefined.
41
+ */
42
+ const isRoot = this._host.menuStack.peek() === undefined;
43
+ setTimeout(() => {
44
+ // our menu trigger directive leaves the last position used for use immediately after opening
45
+ // we can access it here and determine the correct side.
46
+ // eslint-disable-next-line
47
+ const ps = (this._host as any)._parentTrigger._spartanLastPosition;
48
+ if (!ps) {
49
+ // if we have no last position we default to the most likely option
50
+ // I hate that we have to do this and hope we can revisit soon and improve
51
+ this._side.set(isRoot ? 'top' : 'left');
52
+ return;
53
+ }
54
+ const side = isRoot ? ps.originY : ps.originX === 'end' ? 'right' : 'left';
55
+ this._side.set(side);
56
+ });
57
+ }
58
+ }
@@ -0,0 +1,26 @@
1
+ import { InjectionToken, type ValueProvider, inject } from '@angular/core';
2
+ import { type MenuAlign, type MenuSide } from '@spartan-ng/brain/core';
3
+
4
+ export interface HlmDropdownMenuConfig {
5
+ align: MenuAlign;
6
+ side: MenuSide;
7
+ }
8
+
9
+ const defaultConfig: HlmDropdownMenuConfig = {
10
+ align: 'start',
11
+ side: 'bottom',
12
+ };
13
+
14
+ const HlmDropdownMenuConfigToken = new InjectionToken<HlmDropdownMenuConfig>(
15
+ 'HlmDropdownMenuConfig',
16
+ );
17
+
18
+ export function provideHlmDropdownMenuConfig(
19
+ config: Partial<HlmDropdownMenuConfig>,
20
+ ): ValueProvider {
21
+ return { provide: HlmDropdownMenuConfigToken, useValue: { ...defaultConfig, ...config } };
22
+ }
23
+
24
+ export function injectHlmDropdownMenuConfig(): HlmDropdownMenuConfig {
25
+ return inject(HlmDropdownMenuConfigToken, { optional: true }) ?? defaultConfig;
26
+ }
@@ -0,0 +1,49 @@
1
+ import { CdkMenuTrigger } from '@angular/cdk/menu';
2
+ import { computed, Directive, effect, inject, input } from '@angular/core';
3
+ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
4
+ import { createMenuPosition, type MenuAlign, type MenuSide } from '@spartan-ng/brain/core';
5
+ import { injectHlmDropdownMenuConfig } from './hlm-dropdown-menu-token';
6
+
7
+ @Directive({
8
+ selector: '[hlmDropdownMenuTrigger]',
9
+ hostDirectives: [
10
+ {
11
+ directive: CdkMenuTrigger,
12
+ inputs: [
13
+ 'cdkMenuTriggerFor: hlmDropdownMenuTrigger',
14
+ 'cdkMenuTriggerData: hlmDropdownMenuTriggerData',
15
+ ],
16
+ outputs: ['cdkMenuOpened: hlmDropdownMenuOpened', 'cdkMenuClosed: hlmDropdownMenuClosed'],
17
+ },
18
+ ],
19
+ host: {
20
+ 'data-slot': 'dropdown-menu-trigger',
21
+ },
22
+ })
23
+ export class HlmDropdownMenuTrigger {
24
+ private readonly _cdkTrigger = inject(CdkMenuTrigger, { host: true });
25
+ private readonly _config = injectHlmDropdownMenuConfig();
26
+
27
+ public readonly align = input<MenuAlign>(this._config.align);
28
+ public readonly side = input<MenuSide>(this._config.side);
29
+
30
+ private readonly _menuPosition = computed(() => createMenuPosition(this.align(), this.side()));
31
+
32
+ constructor() {
33
+ // once the trigger opens we wait until the next tick and then grab the last position
34
+ // used to position the menu. we store this in our trigger which the brnMenu directive has
35
+ // access to through DI
36
+ this._cdkTrigger.opened.pipe(takeUntilDestroyed()).subscribe(() =>
37
+ setTimeout(
38
+ () =>
39
+ // eslint-disable-next-line
40
+ ((this._cdkTrigger as any)._spartanLastPosition = // eslint-disable-next-line
41
+ (this._cdkTrigger as any).overlayRef._positionStrategy._lastPosition),
42
+ ),
43
+ );
44
+
45
+ effect(() => {
46
+ this._cdkTrigger.menuPosition = this._menuPosition();
47
+ });
48
+ }
49
+ }
@@ -0,0 +1,125 @@
1
+ import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
2
+ import { NgIcon, provideIcons } from '@ng-icons/core';
3
+ import { phosphorDotsThreeVertical } from '@ng-icons/phosphor-icons/regular';
4
+ import { HlmDropdownMenu, HlmDropdownMenuImports } from '..';
5
+ import { type Meta, moduleMetadata, type StoryObj } from '@storybook/angular';
6
+ import { HlmButton, HlmButtonImports } from '../../../../../public-api';
7
+ import { dropdownMenuContract } from '@surfnet/curve-contracts';
8
+
9
+ const meta: Meta<HlmDropdownMenu> = {
10
+ title: 'Components/Dropdown Menu',
11
+ component: HlmDropdownMenu,
12
+ decorators: [
13
+ moduleMetadata({
14
+ imports: [HlmDropdownMenuImports, HlmButton, NgIcon],
15
+ providers: [provideIcons({ phosphorDotsThreeVertical })],
16
+ }),
17
+ ],
18
+ parameters: {
19
+ docs: {
20
+ description: {
21
+ component: dropdownMenuContract.docs.description,
22
+ },
23
+ },
24
+ },
25
+ };
26
+
27
+ export default meta;
28
+ type Story = StoryObj<HlmDropdownMenu>;
29
+
30
+ /** A menu with a label, grouped items, and a destructive action. */
31
+ export const Default: Story = {
32
+ render: () => ({
33
+ template: `
34
+ <div class="w-full flex justify-center items-center">
35
+ <button hlmBtn variant="outline" [hlmDropdownMenuTrigger]="menu">Open menu</button>
36
+ </div>
37
+ <ng-template #menu>
38
+ <hlm-dropdown-menu align="start" class="w-48">
39
+ <hlm-dropdown-menu-group>
40
+ <hlm-dropdown-menu-label>My Account</hlm-dropdown-menu-label>
41
+ <button hlmDropdownMenuItem>
42
+ <span>Profile</span>
43
+ <hlm-dropdown-menu-shortcut>⇧⌘P</hlm-dropdown-menu-shortcut>
44
+ </button>
45
+
46
+ <button hlmDropdownMenuItem>Settings</button>
47
+ </hlm-dropdown-menu-group>
48
+
49
+ <hlm-dropdown-menu-separator />
50
+
51
+ <button hlmDropdownMenuItem variant="destructive">Delete</button>
52
+ </hlm-dropdown-menu>
53
+ </ng-template>`,
54
+ }),
55
+ };
56
+
57
+ /** The "⋯" row-actions pattern from a data table. */
58
+ export const RowActions: Story = {
59
+ render: () => ({
60
+ template: `
61
+ <div class="w-full flex justify-center items-center">
62
+ <button hlmBtn variant="ghost" size="icon-sm" align="end" aria-label="Row actions" [hlmDropdownMenuTrigger]="menu">
63
+ <ng-icon name="phosphorDotsThreeVertical" />
64
+ </button>
65
+ </div>
66
+ <ng-template #menu>
67
+ <hlm-dropdown-menu class="w-40">
68
+ <button hlmDropdownMenuItem>View</button>
69
+ <button hlmDropdownMenuItem>Edit</button>
70
+ <hlm-dropdown-menu-separator />
71
+ <button hlmDropdownMenuItem variant="destructive">Remove</button>
72
+ </hlm-dropdown-menu>
73
+ </ng-template>`,
74
+ }),
75
+ };
76
+
77
+ /** Checkbox items for toggling options. */
78
+ export const WithCheckboxItems: Story = {
79
+ render: () => ({
80
+ moduleMetadata: {
81
+ imports: [DropdownCheckboxes],
82
+ },
83
+ template: '<stateful-dropdown-story/>',
84
+ }),
85
+ };
86
+
87
+ @Component({
88
+ selector: 'stateful-dropdown-story',
89
+ imports: [HlmDropdownMenuImports, HlmButtonImports],
90
+ changeDetection: ChangeDetectionStrategy.OnPush,
91
+ template: ` <div class="w-full flex justify-center items-center">
92
+ <button
93
+ hlmBtn
94
+ variant="outline"
95
+ aria-label="With Checkbox Items"
96
+ [hlmDropdownMenuTrigger]="menu"
97
+ >
98
+ Columns
99
+ </button>
100
+ </div>
101
+ <ng-template #menu>
102
+ <hlm-dropdown-menu class="w-44">
103
+ <hlm-dropdown-menu-label>Toggle columns</hlm-dropdown-menu-label>
104
+ <button hlmDropdownMenuCheckbox [checked]="true">
105
+ Name
106
+ <hlm-dropdown-menu-checkbox-indicator />
107
+ </button>
108
+ <button hlmDropdownMenuCheckbox [checked]="true">
109
+ Category
110
+ <hlm-dropdown-menu-checkbox-indicator />
111
+ </button>
112
+ <button
113
+ hlmDropdownMenuCheckbox
114
+ [checked]="isStatusChecked()"
115
+ (triggered)="isStatusChecked.set(!isStatusChecked())"
116
+ >
117
+ Status
118
+ <hlm-dropdown-menu-checkbox-indicator />
119
+ </button>
120
+ </hlm-dropdown-menu>
121
+ </ng-template>`,
122
+ })
123
+ class DropdownCheckboxes {
124
+ public readonly isStatusChecked = signal(false);
125
+ }
@@ -0,0 +1,62 @@
1
+ import { type NumberInput } from '@angular/cdk/coercion';
2
+ import { CdkMenu } from '@angular/cdk/menu';
3
+ import { Directive, inject, input, numberAttribute, signal } from '@angular/core';
4
+ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
5
+ import { classes } from '../../../utils/src';
6
+
7
+ @Directive({
8
+ selector: '[hlmDropdownMenu],hlm-dropdown-menu',
9
+ hostDirectives: [CdkMenu],
10
+ host: {
11
+ 'data-slot': 'dropdown-menu',
12
+ '[attr.data-state]': '_state()',
13
+ '[attr.data-side]': '_side()',
14
+ '[style.--side-offset]': 'sideOffset()',
15
+ },
16
+ })
17
+ export class HlmDropdownMenu {
18
+ private readonly _host = inject(CdkMenu);
19
+
20
+ protected readonly _state = signal('open');
21
+ protected readonly _side = signal('top');
22
+
23
+ public readonly sideOffset = input<number, NumberInput>(1, { transform: numberAttribute });
24
+
25
+ constructor() {
26
+ classes(
27
+ () =>
28
+ 'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 my-[--spacing(var(--side-offset))] min-w-[8rem] origin-top overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md',
29
+ );
30
+
31
+ this.setSideWithDarkMagic();
32
+ // this is a best effort, but does not seem to work currently
33
+ // TODO: figure out a way for us to know the host is about to be closed. might not be possible with CDK
34
+ this._host.closed.pipe(takeUntilDestroyed()).subscribe(() => this._state.set('closed'));
35
+ }
36
+
37
+ private setSideWithDarkMagic() {
38
+ /**
39
+ * This is an ugly workaround to at least figure out the correct side of where a submenu
40
+ * will appear and set the attribute to the host accordingly
41
+ *
42
+ * First of all we take advantage of the menu stack not being aware of the root
43
+ * object immediately after it is added. This code executes before the root element is added,
44
+ * which means the stack is still empty and the peek method returns undefined.
45
+ */
46
+ const isRoot = this._host.menuStack.peek() === undefined;
47
+ setTimeout(() => {
48
+ // our menu trigger directive leaves the last position used for use immediately after opening
49
+ // we can access it here and determine the correct side.
50
+ // eslint-disable-next-line
51
+ const ps = (this._host as any)._parentTrigger._spartanLastPosition;
52
+ if (!ps) {
53
+ // if we have no last position we default to the most likely option
54
+ // I hate that we have to do this and hope we can revisit soon and improve
55
+ this._side.set(isRoot ? 'top' : 'left');
56
+ return;
57
+ }
58
+ const side = isRoot ? ps.originY : ps.originX === 'end' ? 'right' : 'left';
59
+ this._side.set(side);
60
+ });
61
+ }
62
+ }
@@ -0,0 +1,34 @@
1
+ import { HlmField } from './lib/hlm-field';
2
+ import { HlmFieldContent } from './lib/hlm-field-content';
3
+ import { HlmFieldDescription } from './lib/hlm-field-description';
4
+ import { HlmFieldError } from './lib/hlm-field-error';
5
+ import { HlmFieldGroup } from './lib/hlm-field-group';
6
+ import { HlmFieldLabel } from './lib/hlm-field-label';
7
+ import { HlmFieldLegend } from './lib/hlm-field-legend';
8
+ import { HlmFieldSeparator } from './lib/hlm-field-separator';
9
+ import { HlmFieldSet } from './lib/hlm-field-set';
10
+ import { HlmFieldTitle } from './lib/hlm-field-title';
11
+
12
+ export * from './lib/hlm-field';
13
+ export * from './lib/hlm-field-content';
14
+ export * from './lib/hlm-field-description';
15
+ export * from './lib/hlm-field-error';
16
+ export * from './lib/hlm-field-group';
17
+ export * from './lib/hlm-field-label';
18
+ export * from './lib/hlm-field-legend';
19
+ export * from './lib/hlm-field-separator';
20
+ export * from './lib/hlm-field-set';
21
+ export * from './lib/hlm-field-title';
22
+
23
+ export const HlmFieldImports = [
24
+ HlmField,
25
+ HlmFieldContent,
26
+ HlmFieldDescription,
27
+ HlmFieldError,
28
+ HlmFieldGroup,
29
+ HlmFieldLabel,
30
+ HlmFieldLegend,
31
+ HlmFieldSeparator,
32
+ HlmFieldSet,
33
+ HlmFieldTitle,
34
+ ] as const;
@@ -0,0 +1,12 @@
1
+ import { Directive } from '@angular/core';
2
+ import { classes } from '../../../utils/src';
3
+
4
+ @Directive({
5
+ selector: '[hlmFieldContent],hlm-field-content',
6
+ host: { 'data-slot': 'field-content' },
7
+ })
8
+ export class HlmFieldContent {
9
+ constructor() {
10
+ classes(() => 'gap-1 group/field-content flex flex-1 flex-col leading-snug');
11
+ }
12
+ }
@@ -0,0 +1,53 @@
1
+ import { Directive, effect, EffectRef, inject, input, OnDestroy } from '@angular/core';
2
+ import { BrnFieldA11yService } from '@spartan-ng/brain/field';
3
+ import { classes } from '../../../utils/src';
4
+
5
+ @Directive({
6
+ selector: '[hlmFieldDescription],hlm-field-description',
7
+ host: {
8
+ 'data-slot': 'field-description',
9
+ '[attr.id]': 'id()',
10
+ },
11
+ })
12
+ export class HlmFieldDescription implements OnDestroy {
13
+ private static _id = 0;
14
+
15
+ private readonly _a11y = inject(BrnFieldA11yService, { optional: true, host: true });
16
+
17
+ public readonly id = input<string>(`hlm-field-description-${HlmFieldDescription._id++}`);
18
+
19
+ private _registeredId?: string;
20
+
21
+ private readonly _cleanup: EffectRef | null = this._a11y
22
+ ? effect(() => {
23
+ const a11y = this._a11y;
24
+ if (!a11y) return;
25
+
26
+ const id = this.id();
27
+ if (this._registeredId && this._registeredId !== id) {
28
+ a11y.unregisterDescription(this._registeredId);
29
+ }
30
+
31
+ if (this._registeredId !== id) {
32
+ a11y.registerDescription(id);
33
+ this._registeredId = id;
34
+ }
35
+ })
36
+ : null;
37
+
38
+ constructor() {
39
+ classes(() => [
40
+ 'text-muted-foreground text-start text-sm [[data-variant=legend]+&]:-mt-1.5 leading-normal font-normal group-has-data-horizontal/field:text-balance',
41
+ 'last:mt-0 nth-last-2:-mt-1',
42
+ '[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4',
43
+ ]);
44
+ }
45
+
46
+ ngOnDestroy() {
47
+ this._cleanup?.destroy();
48
+
49
+ if (this._registeredId) {
50
+ this._a11y?.unregisterDescription(this._registeredId);
51
+ }
52
+ }
53
+ }