@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,136 @@
1
+ import { isPlatformServer } from '@angular/common';
2
+ import {
3
+ afterNextRender,
4
+ computed,
5
+ DestroyRef,
6
+ DOCUMENT,
7
+ inject,
8
+ Injectable,
9
+ PLATFORM_ID,
10
+ REQUEST,
11
+ type Signal,
12
+ signal,
13
+ } from '@angular/core';
14
+ import { injectHlmSidebarConfig } from './hlm-sidebar.token';
15
+
16
+ export type SidebarVariant = 'sidebar' | 'floating' | 'inset';
17
+
18
+ @Injectable({ providedIn: 'root' })
19
+ export class HlmSidebarService {
20
+ private readonly _platformId = inject(PLATFORM_ID);
21
+ private readonly _request = inject(REQUEST, { optional: true });
22
+ private readonly _config = injectHlmSidebarConfig();
23
+ private readonly _document = inject(DOCUMENT);
24
+ private readonly _window = this._document.defaultView;
25
+ private readonly _open = signal<boolean>(true);
26
+ private readonly _openMobile = signal<boolean>(false);
27
+ private readonly _isMobile = signal<boolean>(false);
28
+ private readonly _variant = signal<SidebarVariant>('sidebar');
29
+ private _mediaQuery: MediaQueryList | null = null;
30
+
31
+ public readonly open: Signal<boolean> = this._open.asReadonly();
32
+ public readonly openMobile: Signal<boolean> = this._openMobile.asReadonly();
33
+ public readonly isMobile: Signal<boolean> = this._isMobile.asReadonly();
34
+ public readonly variant: Signal<SidebarVariant> = this._variant.asReadonly();
35
+
36
+ public readonly state = computed<'expanded' | 'collapsed'>(() =>
37
+ this._open() ? 'expanded' : 'collapsed',
38
+ );
39
+
40
+ constructor() {
41
+ const destroyRef = inject(DestroyRef);
42
+ this.restoreStateFromCookie();
43
+
44
+ afterNextRender(() => {
45
+ if (!this._window) return;
46
+
47
+ // Initialize MediaQueryList
48
+ this._mediaQuery = this._window.matchMedia(`(max-width: ${this._config.mobileBreakpoint})`);
49
+ this._isMobile.set(this._mediaQuery.matches);
50
+
51
+ // Add media query listener
52
+ const mediaQueryHandler = (e: MediaQueryListEvent) => {
53
+ this._isMobile.set(e.matches);
54
+ // If switching from mobile to desktop, close mobile sidebar
55
+ if (!e.matches) this._openMobile.set(false);
56
+ };
57
+ this._mediaQuery.addEventListener('change', mediaQueryHandler);
58
+
59
+ // Add keyboard shortcut listener
60
+ const keydownHandler = (event: KeyboardEvent) => {
61
+ if (
62
+ event.key === this._config.sidebarKeyboardShortcut &&
63
+ (event.ctrlKey || event.metaKey)
64
+ ) {
65
+ event.preventDefault();
66
+ this.toggleSidebar();
67
+ }
68
+ };
69
+ this._window.addEventListener('keydown', keydownHandler);
70
+
71
+ // Add resize listener with debounce
72
+ let resizeTimeout: number;
73
+ const resizeHandler = () => {
74
+ if (!this._window) return;
75
+
76
+ if (resizeTimeout) this._window.clearTimeout(resizeTimeout);
77
+ resizeTimeout = this._window.setTimeout(() => {
78
+ if (this._mediaQuery) this._isMobile.set(this._mediaQuery.matches);
79
+ }, 100);
80
+ };
81
+ this._window.addEventListener('resize', resizeHandler);
82
+
83
+ // Cleanup listeners on destroy
84
+ destroyRef.onDestroy(() => {
85
+ if (!this._window) return;
86
+
87
+ if (this._mediaQuery) this._mediaQuery.removeEventListener('change', mediaQueryHandler);
88
+ this._window.removeEventListener('keydown', keydownHandler);
89
+ this._window.removeEventListener('resize', resizeHandler);
90
+ if (resizeTimeout) this._window.clearTimeout(resizeTimeout);
91
+ });
92
+ });
93
+ }
94
+
95
+ public setOpen(open: boolean): void {
96
+ this._open.set(open);
97
+ this._document.cookie = `${this._config.sidebarCookieName}=${open}; path=/; max-age=${this._config.sidebarCookieMaxAge}`;
98
+ }
99
+
100
+ public setOpenMobile(open: boolean): void {
101
+ if (this._isMobile()) {
102
+ this._openMobile.set(open);
103
+ }
104
+ }
105
+
106
+ public setVariant(variant: SidebarVariant): void {
107
+ this._variant.set(variant);
108
+ }
109
+
110
+ public toggleSidebar(): void {
111
+ if (this._isMobile()) {
112
+ this._openMobile.update((value) => !value);
113
+ } else {
114
+ this.setOpen(!this._open());
115
+ }
116
+ }
117
+
118
+ private restoreStateFromCookie(): void {
119
+ const cookieString = isPlatformServer(this._platformId)
120
+ ? this._request?.headers.get('cookie')
121
+ : this._document.cookie;
122
+
123
+ if (!cookieString) return;
124
+
125
+ const prefix = `${this._config.sidebarCookieName}=`;
126
+ const cookieValue = cookieString
127
+ .split(';')
128
+ .map((c) => c.trim())
129
+ .find((c) => c.startsWith(prefix))
130
+ ?.slice(prefix.length);
131
+
132
+ if (cookieValue !== undefined) {
133
+ this._open.set(cookieValue === 'true');
134
+ }
135
+ }
136
+ }
@@ -0,0 +1,177 @@
1
+ import { Component, Input } from '@angular/core';
2
+ import { NgIcon, provideIcons } from '@ng-icons/core';
3
+ import {
4
+ phosphorGauge,
5
+ phosphorGear,
6
+ phosphorSquaresFour,
7
+ phosphorUser,
8
+ } from '@ng-icons/phosphor-icons/regular';
9
+ import { type Meta, moduleMetadata, type StoryObj } from '@storybook/angular';
10
+ import { sidebarContract, sidebarMenuButtonContract } from '@surfnet/curve-contracts';
11
+ import { HlmSidebar, HlmSidebarImports } from '..';
12
+
13
+ const meta: Meta<HlmSidebar> = {
14
+ title: 'Components/Sidebar',
15
+ component: HlmSidebar,
16
+ decorators: [
17
+ moduleMetadata({
18
+ imports: [HlmSidebarImports],
19
+ }),
20
+ ],
21
+ parameters: {
22
+ layout: 'fullscreen',
23
+ docs: {
24
+ description: {
25
+ component: sidebarContract.docs.description,
26
+ },
27
+ },
28
+ },
29
+ };
30
+
31
+ export default meta;
32
+ type Story = StoryObj<HlmSidebar>;
33
+
34
+ @Component({
35
+ selector: 'sidebar',
36
+ imports: [HlmSidebarImports, NgIcon],
37
+ providers: [provideIcons({ phosphorGear, phosphorGauge, phosphorSquaresFour, phosphorUser })],
38
+ template: `
39
+ <div hlmSidebarWrapper>
40
+ <hlm-sidebar>
41
+ <div hlmSidebarHeader>
42
+ <div hlmSidebarMenu>
43
+ <div hlmSidebarMenuItem>
44
+ <button hlmSidebarMenuButton size="lg">
45
+ <ng-icon name="phosphorSquaresFour" />
46
+ <span>App catalog</span>
47
+ </button>
48
+ </div>
49
+ </div>
50
+ </div>
51
+ <div hlmSidebarContent>
52
+ <div hlmSidebarGroup>
53
+ <div hlmSidebarGroupLabel>Platform</div>
54
+ <div hlmSidebarGroupContent>
55
+ <ul hlmSidebarMenu>
56
+ @for (item of nav; track item.title) {
57
+ <li hlmSidebarMenuItem>
58
+ <button hlmSidebarMenuButton [isActive]="item.active" [tooltip]="item.title">
59
+ <ng-icon [name]="item.icon" />
60
+ <span>{{ item.title }}</span>
61
+ </button>
62
+ @if (item.title === 'Apps') {
63
+ <ul hlmSidebarMenuSub>
64
+ <li hlmSidebarMenuSubItem>
65
+ <button hlmSidebarMenuSubButton>Catalog</button>
66
+ </li>
67
+ <li hlmSidebarMenuSubItem>
68
+ <button hlmSidebarMenuSubButton>Installed</button>
69
+ </li>
70
+ </ul>
71
+ }
72
+ </li>
73
+ }
74
+ </ul>
75
+ </div>
76
+ </div>
77
+ </div>
78
+ <div hlmSidebarFooter>
79
+ <div hlmSidebarMenu>
80
+ <div hlmSidebarMenuItem>
81
+ <button hlmSidebarMenuButton tooltip="Settings">
82
+ <ng-icon name="phosphorGear" />
83
+ <span>Settings</span>
84
+ </button>
85
+ </div>
86
+ </div>
87
+ </div>
88
+ </hlm-sidebar>
89
+ <main hlmSidebarInset>
90
+ <header class="flex h-12 items-center gap-2 border-b px-4">
91
+ <button hlmSidebarTrigger></button>
92
+ <span class="text-sm font-medium">App catalog</span>
93
+ </header>
94
+ <div class="p-4 text-sm text-muted-foreground">Page content goes here.</div>
95
+ </main>
96
+ </div>
97
+ `,
98
+ })
99
+ class Sidebar {
100
+ public readonly nav = [
101
+ { title: 'Dashboard', icon: 'phosphorGauge', active: true },
102
+ { title: 'Apps', icon: 'phosphorSquaresFour' },
103
+ { title: 'Users', icon: 'phosphorUser' },
104
+ ];
105
+ }
106
+
107
+ /** A full sidebar with a collapsible icon rail, grouped menu, submenu, and trigger. */
108
+ export const Default: Story = {
109
+ render: () => ({
110
+ moduleMetadata: {
111
+ imports: [Sidebar],
112
+ },
113
+ template: '<sidebar/>',
114
+ }),
115
+ };
116
+
117
+ @Component({
118
+ selector: 'sidebar-variant',
119
+ imports: [HlmSidebarImports, NgIcon],
120
+ providers: [provideIcons({ phosphorSquaresFour })],
121
+ template: `
122
+ <div hlmSidebarWrapper>
123
+ <hlm-sidebar collapsible="none" class="w-64 border-r">
124
+ <div hlmSidebarContent>
125
+ <div hlmSidebarGroup>
126
+ <div hlmSidebarGroupLabel>Variants</div>
127
+ <div hlmSidebarGroupContent>
128
+ <ul hlmSidebarMenu>
129
+ @for (variant of contract.props.variants; track variant) {
130
+ <li hlmSidebarMenuItem>
131
+ <button
132
+ hlmSidebarMenuButton
133
+ [variant]="variant"
134
+ [title]="contract.docs.variants[variant]"
135
+ >
136
+ <ng-icon name="phosphorSquaresFour" />
137
+ <span>{{ variant }}</span>
138
+ </button>
139
+ </li>
140
+ }
141
+ </ul>
142
+ </div>
143
+ </div>
144
+ <div hlmSidebarGroup>
145
+ <div hlmSidebarGroupLabel>Sizes</div>
146
+ <div hlmSidebarGroupContent>
147
+ <ul hlmSidebarMenu>
148
+ @for (size of contract.props.sizes; track size) {
149
+ <li hlmSidebarMenuItem>
150
+ <button hlmSidebarMenuButton [size]="size" [title]="contract.docs.sizes[size]">
151
+ <ng-icon name="phosphorSquaresFour" />
152
+ <span>{{ size }}</span>
153
+ </button>
154
+ </li>
155
+ }
156
+ </ul>
157
+ </div>
158
+ </div>
159
+ </div>
160
+ </hlm-sidebar>
161
+ </div>
162
+ `,
163
+ })
164
+ class SidebarVariant {
165
+ @Input() contract!: typeof sidebarMenuButtonContract;
166
+ }
167
+
168
+ /** The `SidebarMenuButton` variants and sizes declared in the contract. */
169
+ export const MenuButtonVariants: Story = {
170
+ render: () => ({
171
+ moduleMetadata: {
172
+ imports: [SidebarVariant],
173
+ },
174
+ props: { contract: sidebarMenuButtonContract },
175
+ template: '<sidebar-variant [contract]="contract"></sidebar-variant>',
176
+ }),
177
+ };
@@ -0,0 +1,33 @@
1
+ import { inject, InjectionToken, type ValueProvider } from '@angular/core';
2
+
3
+ export interface HlmSidebarConfig {
4
+ sidebarWidth: string;
5
+ sidebarWidthMobile: string;
6
+ sidebarWidthIcon: string;
7
+ sidebarCookieName: string;
8
+ sidebarCookieMaxAge: number;
9
+ sidebarKeyboardShortcut: string;
10
+ mobileBreakpoint: string;
11
+ closeMobileSidebarOnMenuButtonClick: boolean;
12
+ }
13
+
14
+ const defaultConfig: HlmSidebarConfig = {
15
+ sidebarWidth: '16rem',
16
+ sidebarWidthMobile: '18rem',
17
+ sidebarWidthIcon: '3rem',
18
+ sidebarCookieName: 'sidebar_state',
19
+ sidebarCookieMaxAge: 60 * 60 * 24 * 7, // 7 days in seconds
20
+ sidebarKeyboardShortcut: 'b',
21
+ mobileBreakpoint: '768px',
22
+ closeMobileSidebarOnMenuButtonClick: false,
23
+ };
24
+
25
+ const HlmSidebarConfigToken = new InjectionToken<HlmSidebarConfig>('HlmSidebarConfig');
26
+
27
+ export function provideHlmSidebarConfig(config: Partial<HlmSidebarConfig>): ValueProvider {
28
+ return { provide: HlmSidebarConfigToken, useValue: { ...defaultConfig, ...config } };
29
+ }
30
+
31
+ export function injectHlmSidebarConfig(): HlmSidebarConfig {
32
+ return inject(HlmSidebarConfigToken, { optional: true }) ?? defaultConfig;
33
+ }
@@ -0,0 +1,139 @@
1
+ import { NgTemplateOutlet } from '@angular/common';
2
+ import { ChangeDetectionStrategy, Component, computed, effect, inject, input } from '@angular/core';
3
+ import { classes, hlm } from '../../../utils/src';
4
+ import type { ClassValue } from 'clsx';
5
+ import { HlmSidebarService, type SidebarVariant } from './hlm-sidebar.service';
6
+ import { injectHlmSidebarConfig } from './hlm-sidebar.token';
7
+ import { HlmSheetImports } from '../../../sheet/src';
8
+
9
+ @Component({
10
+ selector: 'hlm-sidebar',
11
+ imports: [NgTemplateOutlet, HlmSheetImports],
12
+ changeDetection: ChangeDetectionStrategy.OnPush,
13
+ host: {
14
+ '[attr.data-slot]': '_dataSlot()',
15
+ '[attr.data-state]': '_dataState()',
16
+ '[attr.data-collapsible]': '_dataCollapsible()',
17
+ '[attr.data-variant]': '_dataVariant()',
18
+ '[attr.data-side]': '_dataSide()',
19
+ },
20
+ template: `
21
+ <ng-template #contentContainer>
22
+ <ng-content />
23
+ </ng-template>
24
+
25
+ @if (collapsible() === 'none') {
26
+ <ng-container *ngTemplateOutlet="contentContainer"></ng-container>
27
+ } @else if (_sidebarService.isMobile()) {
28
+ <hlm-sheet
29
+ [side]="side()"
30
+ [state]="_sidebarService.openMobile() ? 'open' : 'closed'"
31
+ (stateChanged)="_sidebarService.setOpenMobile($event === 'open')"
32
+ >
33
+ <hlm-sheet-content
34
+ *hlmSheetPortal="let ctx"
35
+ data-slot="sidebar"
36
+ data-sidebar="sidebar"
37
+ data-mobile="true"
38
+ class="bg-sidebar text-sidebar-foreground h-svh w-(--sidebar-width) p-0 [&>button]:hidden"
39
+ [style.--sidebar-width]="sidebarWidthMobile()"
40
+ >
41
+ <div class="flex h-full w-full flex-col">
42
+ <ng-container *ngTemplateOutlet="contentContainer" />
43
+ </div>
44
+ </hlm-sheet-content>
45
+ </hlm-sheet>
46
+ } @else {
47
+ <!-- Sidebar gap on desktop -->
48
+ <div data-slot="sidebar-gap" [class]="_sidebarGapComputedClass()"></div>
49
+ <div
50
+ data-slot="sidebar-container"
51
+ [attr.data-side]="_dataSide()"
52
+ [class]="_sidebarContainerComputedClass()"
53
+ >
54
+ <div
55
+ data-sidebar="sidebar"
56
+ data-slot="sidebar-inner"
57
+ class="bg-sidebar group-data-[variant=floating]:ring-sidebar-border group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:shadow-sm group-data-[variant=floating]:ring-1 flex size-full flex-col"
58
+ >
59
+ <ng-container *ngTemplateOutlet="contentContainer" />
60
+ </div>
61
+ </div>
62
+ }
63
+ `,
64
+ })
65
+ export class HlmSidebar {
66
+ protected readonly _sidebarService = inject(HlmSidebarService);
67
+ private readonly _config = injectHlmSidebarConfig();
68
+ public readonly sidebarWidthMobile = input<string>(this._config.sidebarWidthMobile);
69
+
70
+ public readonly side = input<'left' | 'right'>('left');
71
+ public readonly variant = input<SidebarVariant>(this._sidebarService.variant());
72
+ public readonly collapsible = input<'offcanvas' | 'icon' | 'none'>('offcanvas');
73
+
74
+ protected readonly _sidebarGapComputedClass = computed(() =>
75
+ hlm(
76
+ 'transition-[width] duration-200 ease-linear relative w-(--sidebar-width) bg-transparent',
77
+ 'group-data-[collapsible=offcanvas]:w-0',
78
+ 'group-data-[side=right]:rotate-180',
79
+ this.variant() === 'floating' || this.variant() === 'inset'
80
+ ? 'group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4)))]'
81
+ : 'group-data-[collapsible=icon]:w-(--sidebar-width-icon)',
82
+ ),
83
+ );
84
+
85
+ public readonly sidebarContainerClass = input<ClassValue>('');
86
+ protected readonly _sidebarContainerComputedClass = computed(() =>
87
+ hlm(
88
+ 'fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear data-[side=left]:left-0 data-[side=left]:group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)] data-[side=right]:right-0 data-[side=right]:group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)] md:flex',
89
+ this.variant() === 'floating' || this.variant() === 'inset'
90
+ ? 'p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]'
91
+ : 'group-data-[collapsible=icon]:w-(--sidebar-width-icon) group-data-[side=left]:border-r group-data-[side=right]:border-l',
92
+ this.sidebarContainerClass(),
93
+ ),
94
+ );
95
+
96
+ protected readonly _dataSlot = computed(() => {
97
+ return !this._sidebarService.isMobile() ? 'sidebar' : undefined;
98
+ });
99
+
100
+ private readonly _collapsibleAndNonMobile = computed(() => {
101
+ return this.collapsible() !== 'none' && !this._sidebarService.isMobile();
102
+ });
103
+
104
+ protected readonly _dataState = computed(() => {
105
+ return this._collapsibleAndNonMobile() ? this._sidebarService.state() : undefined;
106
+ });
107
+
108
+ protected readonly _dataCollapsible = computed(() => {
109
+ if (this._collapsibleAndNonMobile()) {
110
+ return this._sidebarService.state() === 'collapsed' ? this.collapsible() : '';
111
+ }
112
+ return undefined;
113
+ });
114
+
115
+ protected readonly _dataVariant = computed(() => {
116
+ return this._collapsibleAndNonMobile() ? this.variant() : undefined;
117
+ });
118
+
119
+ protected readonly _dataSide = computed(() => {
120
+ return this._collapsibleAndNonMobile() ? this.side() : undefined;
121
+ });
122
+
123
+ constructor() {
124
+ // Sync variant input with service
125
+ effect(() => {
126
+ this._sidebarService.setVariant(this.variant());
127
+ });
128
+
129
+ classes(() => {
130
+ if (this.collapsible() === 'none') {
131
+ return hlm('bg-sidebar text-sidebar-foreground flex h-svh w-(--sidebar-width) flex-col');
132
+ } else if (this._sidebarService.isMobile()) {
133
+ return '';
134
+ } else {
135
+ return hlm('group peer text-sidebar-foreground hidden md:block');
136
+ }
137
+ });
138
+ }
139
+ }
@@ -0,0 +1,5 @@
1
+ import { HlmSkeleton } from './lib/hlm-skeleton';
2
+
3
+ export * from './lib/hlm-skeleton';
4
+
5
+ export const HlmSkeletonImports = [HlmSkeleton] as const;
@@ -0,0 +1,14 @@
1
+ import { Directive } from '@angular/core';
2
+ import { classes } from '../../../utils/src';
3
+
4
+ @Directive({
5
+ selector: '[hlmSkeleton],hlm-skeleton',
6
+ host: {
7
+ 'data-slot': 'skeleton',
8
+ },
9
+ })
10
+ export class HlmSkeleton {
11
+ constructor() {
12
+ classes(() => 'bg-muted rounded-md block motion-safe:animate-pulse');
13
+ }
14
+ }
@@ -0,0 +1,25 @@
1
+ import {
2
+ HlmCaption,
3
+ HlmTable,
4
+ HlmTableContainer,
5
+ HlmTBody,
6
+ HlmTd,
7
+ HlmTFoot,
8
+ HlmTh,
9
+ HlmTHead,
10
+ HlmTr,
11
+ } from './lib/hlm-table';
12
+
13
+ export * from './lib/hlm-table';
14
+
15
+ export const HlmTableImports = [
16
+ HlmCaption,
17
+ HlmTableContainer,
18
+ HlmTable,
19
+ HlmTBody,
20
+ HlmTd,
21
+ HlmTFoot,
22
+ HlmTh,
23
+ HlmTHead,
24
+ HlmTr,
25
+ ] as const;
@@ -0,0 +1,70 @@
1
+ import { type Meta, type StoryObj } from '@storybook/angular';
2
+ import { tableContract } from '@surfnet/curve-contracts';
3
+ import { HlmTable } from './hlm-table';
4
+ import { Component, Input } from '@angular/core';
5
+ import { HlmTableImports } from '..';
6
+
7
+ const meta: Meta<HlmTable> = {
8
+ title: 'Components/Table',
9
+ component: HlmTable,
10
+ parameters: {
11
+ docs: {
12
+ description: {
13
+ component: tableContract.docs.description,
14
+ },
15
+ },
16
+ },
17
+ };
18
+
19
+ export default meta;
20
+ type Story = StoryObj<HlmTable>;
21
+
22
+ const apps = [
23
+ { name: 'SURFconext', category: 'Identity', status: 'Active' },
24
+ { name: 'SURFdrive', category: 'Storage', status: 'Active' },
25
+ { name: 'SURFspot', category: 'Marketplace', status: 'Inactive' },
26
+ ];
27
+
28
+ @Component({
29
+ selector: 'simple-table',
30
+ imports: [HlmTableImports],
31
+ template: `
32
+ <div hlmTableContainer>
33
+ <table hlmTable class="w-[28rem]">
34
+ <caption hlmTableCaption>
35
+ A list of available apps.
36
+ </caption>
37
+ <thead hlmTableHeader>
38
+ <tr hlmTableRow>
39
+ <th hlmTableHead>Name</th>
40
+ <th hlmTableHead>Category</th>
41
+ <th hlmTableHead class="text-right">Status</th>
42
+ </tr>
43
+ </thead>
44
+ <tbody hlmTableBody>
45
+ @for (app of apps; track app) {
46
+ <tr hlmTableRow>
47
+ <td hlmTableCell class="font-medium">{{ app.name }}</td>
48
+ <td hlmTableCell>{{ app.category }}</td>
49
+ <td hlmTableCell class="text-right">{{ app.status }}</td>
50
+ </tr>
51
+ }
52
+ </tbody>
53
+ </table>
54
+ </div>
55
+ `,
56
+ })
57
+ class SimpleTable {
58
+ @Input() apps!: { name: string; category: string; status: string }[];
59
+ }
60
+
61
+ /** A basic table with a header, body, and caption. */
62
+ export const MenuButtonVariants: Story = {
63
+ render: () => ({
64
+ moduleMetadata: {
65
+ imports: [SimpleTable],
66
+ },
67
+ props: { apps: apps },
68
+ template: '<simple-table [apps]="apps"></simple-table>',
69
+ }),
70
+ };