@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.
- package/.postcssrc.json +7 -0
- package/.storybook/main.ts +9 -0
- package/.storybook/manager.ts +3 -0
- package/.storybook/preview.ts +28 -0
- package/.storybook/tsconfig.json +8 -0
- package/.turbo/turbo-build.log +32 -0
- package/CHANGELOG.md +29 -0
- package/angular.json +52 -0
- package/components.json +5 -0
- package/dist/fesm2022/surfnet-curve-angular.mjs +4237 -0
- package/dist/fesm2022/surfnet-curve-angular.mjs.map +1 -0
- package/dist/files/geist-cyrillic-ext-wght-italic.woff2 +0 -0
- package/dist/files/geist-cyrillic-ext-wght-normal.woff2 +0 -0
- package/dist/files/geist-cyrillic-wght-italic.woff2 +0 -0
- package/dist/files/geist-cyrillic-wght-normal.woff2 +0 -0
- package/dist/files/geist-latin-ext-wght-italic.woff2 +0 -0
- package/dist/files/geist-latin-ext-wght-normal.woff2 +0 -0
- package/dist/files/geist-latin-wght-italic.woff2 +0 -0
- package/dist/files/geist-latin-wght-normal.woff2 +0 -0
- package/dist/files/geist-vietnamese-wght-italic.woff2 +0 -0
- package/dist/files/geist-vietnamese-wght-normal.woff2 +0 -0
- package/dist/styles.css +2 -0
- package/dist/types/surfnet-curve-angular.d.ts +1017 -0
- package/ng-package.json +20 -0
- package/package.json +73 -0
- package/scripts/copy-font-files.ts +15 -0
- package/scripts/rewrite-helm-imports.ts +60 -0
- package/src/foundations/design-tokens.stories.ts +198 -0
- package/src/lib/ui/avatar/src/index.ts +22 -0
- package/src/lib/ui/avatar/src/lib/hlm-avatar-badge.ts +19 -0
- package/src/lib/ui/avatar/src/lib/hlm-avatar-fallback.ts +20 -0
- package/src/lib/ui/avatar/src/lib/hlm-avatar-group-count.ts +17 -0
- package/src/lib/ui/avatar/src/lib/hlm-avatar-group.ts +17 -0
- package/src/lib/ui/avatar/src/lib/hlm-avatar-image.ts +19 -0
- package/src/lib/ui/avatar/src/lib/hlm-avatar.stories.ts +107 -0
- package/src/lib/ui/avatar/src/lib/hlm-avatar.ts +32 -0
- package/src/lib/ui/breadcrumb/src/index.ts +30 -0
- package/src/lib/ui/breadcrumb/src/lib/hlm-breadcrumb-ellipsis.ts +36 -0
- package/src/lib/ui/breadcrumb/src/lib/hlm-breadcrumb-item.ts +14 -0
- package/src/lib/ui/breadcrumb/src/lib/hlm-breadcrumb-link.ts +36 -0
- package/src/lib/ui/breadcrumb/src/lib/hlm-breadcrumb-list.ts +17 -0
- package/src/lib/ui/breadcrumb/src/lib/hlm-breadcrumb-page.ts +17 -0
- package/src/lib/ui/breadcrumb/src/lib/hlm-breadcrumb-separator.ts +27 -0
- package/src/lib/ui/breadcrumb/src/lib/hlm-breadcrumb.stories.ts +81 -0
- package/src/lib/ui/breadcrumb/src/lib/hlm-breadcrumb.ts +13 -0
- package/src/lib/ui/button/src/index.ts +6 -0
- package/src/lib/ui/button/src/lib/hlm-button.stories.ts +170 -0
- package/src/lib/ui/button/src/lib/hlm-button.token.ts +22 -0
- package/src/lib/ui/button/src/lib/hlm-button.ts +76 -0
- package/src/lib/ui/card/src/index.ts +25 -0
- package/src/lib/ui/card/src/lib/hlm-card-action.ts +12 -0
- package/src/lib/ui/card/src/lib/hlm-card-content.ts +12 -0
- package/src/lib/ui/card/src/lib/hlm-card-description.ts +12 -0
- package/src/lib/ui/card/src/lib/hlm-card-footer.ts +14 -0
- package/src/lib/ui/card/src/lib/hlm-card-header.ts +15 -0
- package/src/lib/ui/card/src/lib/hlm-card-title.ts +12 -0
- package/src/lib/ui/card/src/lib/hlm-card.stories.ts +105 -0
- package/src/lib/ui/card/src/lib/hlm-card.token.ts +19 -0
- package/src/lib/ui/card/src/lib/hlm-card.ts +22 -0
- package/src/lib/ui/checkbox/src/index.ts +5 -0
- package/src/lib/ui/checkbox/src/lib/hlm-checkbox.stories.ts +78 -0
- package/src/lib/ui/checkbox/src/lib/hlm-checkbox.ts +160 -0
- package/src/lib/ui/data-table/src/index.ts +14 -0
- package/src/lib/ui/data-table/src/lib/hlm-data-table-content.ts +87 -0
- package/src/lib/ui/data-table/src/lib/hlm-data-table-pagination.ts +62 -0
- package/src/lib/ui/data-table/src/lib/hlm-data-table-toolbar.ts +16 -0
- package/src/lib/ui/data-table/src/lib/hlm-data-table.stories.ts +479 -0
- package/src/lib/ui/data-table/src/lib/inject-data-table.ts +86 -0
- package/src/lib/ui/dropdown-menu/src/index.ts +45 -0
- package/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-checkbox-indicator.ts +20 -0
- package/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-checkbox.ts +36 -0
- package/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-group.ts +16 -0
- package/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-item-sub-indicator.ts +17 -0
- package/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-item.ts +41 -0
- package/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-label.ts +20 -0
- package/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-radio-indicator.ts +20 -0
- package/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-radio.ts +36 -0
- package/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-separator.ts +14 -0
- package/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-shortcut.ts +14 -0
- package/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-sub.ts +58 -0
- package/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-token.ts +26 -0
- package/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu-trigger.ts +49 -0
- package/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu.stories.ts +125 -0
- package/src/lib/ui/dropdown-menu/src/lib/hlm-dropdown-menu.ts +62 -0
- package/src/lib/ui/field/src/index.ts +34 -0
- package/src/lib/ui/field/src/lib/hlm-field-content.ts +12 -0
- package/src/lib/ui/field/src/lib/hlm-field-description.ts +53 -0
- package/src/lib/ui/field/src/lib/hlm-field-error.ts +100 -0
- package/src/lib/ui/field/src/lib/hlm-field-group.ts +15 -0
- package/src/lib/ui/field/src/lib/hlm-field-label.ts +17 -0
- package/src/lib/ui/field/src/lib/hlm-field-legend.ts +17 -0
- package/src/lib/ui/field/src/lib/hlm-field-separator.ts +24 -0
- package/src/lib/ui/field/src/lib/hlm-field-set.ts +15 -0
- package/src/lib/ui/field/src/lib/hlm-field-title.ts +15 -0
- package/src/lib/ui/field/src/lib/hlm-field.stories.ts +117 -0
- package/src/lib/ui/field/src/lib/hlm-field.ts +49 -0
- package/src/lib/ui/icon/src/index.ts +7 -0
- package/src/lib/ui/icon/src/lib/hlm-icon.token.ts +20 -0
- package/src/lib/ui/icon/src/lib/hlm-icon.ts +42 -0
- package/src/lib/ui/input/src/index.ts +5 -0
- package/src/lib/ui/input/src/lib/hlm-input.stories.ts +94 -0
- package/src/lib/ui/input/src/lib/hlm-input.ts +20 -0
- package/src/lib/ui/input-group/src/index.ts +22 -0
- package/src/lib/ui/input-group/src/lib/hlm-input-group-addon.ts +40 -0
- package/src/lib/ui/input-group/src/lib/hlm-input-group-button.ts +47 -0
- package/src/lib/ui/input-group/src/lib/hlm-input-group-input.ts +17 -0
- package/src/lib/ui/input-group/src/lib/hlm-input-group-text.ts +14 -0
- package/src/lib/ui/input-group/src/lib/hlm-input-group-textarea.ts +17 -0
- package/src/lib/ui/input-group/src/lib/hlm-input-group.stories.ts +133 -0
- package/src/lib/ui/input-group/src/lib/hlm-input-group.ts +18 -0
- package/src/lib/ui/label/src/index.ts +5 -0
- package/src/lib/ui/label/src/lib/hlm-label.stories.ts +55 -0
- package/src/lib/ui/label/src/lib/hlm-label.ts +17 -0
- package/src/lib/ui/select/src/index.ts +52 -0
- package/src/lib/ui/select/src/lib/hlm-select-content.ts +44 -0
- package/src/lib/ui/select/src/lib/hlm-select-group.ts +14 -0
- package/src/lib/ui/select/src/lib/hlm-select-item.ts +36 -0
- package/src/lib/ui/select/src/lib/hlm-select-label.ts +14 -0
- package/src/lib/ui/select/src/lib/hlm-select-multiple.ts +45 -0
- package/src/lib/ui/select/src/lib/hlm-select-placeholder.ts +17 -0
- package/src/lib/ui/select/src/lib/hlm-select-portal.ts +8 -0
- package/src/lib/ui/select/src/lib/hlm-select-scroll-down.ts +22 -0
- package/src/lib/ui/select/src/lib/hlm-select-scroll-up.ts +22 -0
- package/src/lib/ui/select/src/lib/hlm-select-separator.ts +14 -0
- package/src/lib/ui/select/src/lib/hlm-select-trigger.ts +59 -0
- package/src/lib/ui/select/src/lib/hlm-select-value-template.ts +5 -0
- package/src/lib/ui/select/src/lib/hlm-select-value.ts +18 -0
- package/src/lib/ui/select/src/lib/hlm-select-values-content.ts +9 -0
- package/src/lib/ui/select/src/lib/hlm-select-values.ts +5 -0
- package/src/lib/ui/select/src/lib/hlm-select.stories.ts +105 -0
- package/src/lib/ui/select/src/lib/hlm-select.ts +45 -0
- package/src/lib/ui/separator/src/index.ts +5 -0
- package/src/lib/ui/separator/src/lib/hlm-separator.stories.ts +88 -0
- package/src/lib/ui/separator/src/lib/hlm-separator.ts +19 -0
- package/src/lib/ui/sheet/src/index.ts +34 -0
- package/src/lib/ui/sheet/src/lib/hlm-sheet-close.ts +9 -0
- package/src/lib/ui/sheet/src/lib/hlm-sheet-content.ts +66 -0
- package/src/lib/ui/sheet/src/lib/hlm-sheet-description.ts +14 -0
- package/src/lib/ui/sheet/src/lib/hlm-sheet-footer.ts +12 -0
- package/src/lib/ui/sheet/src/lib/hlm-sheet-header.ts +12 -0
- package/src/lib/ui/sheet/src/lib/hlm-sheet-overlay.ts +27 -0
- package/src/lib/ui/sheet/src/lib/hlm-sheet-portal.ts +8 -0
- package/src/lib/ui/sheet/src/lib/hlm-sheet-title.ts +14 -0
- package/src/lib/ui/sheet/src/lib/hlm-sheet-trigger.ts +9 -0
- package/src/lib/ui/sheet/src/lib/hlm-sheet.ts +29 -0
- package/src/lib/ui/sidebar/src/index.ts +75 -0
- package/src/lib/ui/sidebar/src/lib/hlm-sidebar-content.ts +18 -0
- package/src/lib/ui/sidebar/src/lib/hlm-sidebar-footer.ts +15 -0
- package/src/lib/ui/sidebar/src/lib/hlm-sidebar-group-action.ts +18 -0
- package/src/lib/ui/sidebar/src/lib/hlm-sidebar-group-content.ts +15 -0
- package/src/lib/ui/sidebar/src/lib/hlm-sidebar-group-label.ts +18 -0
- package/src/lib/ui/sidebar/src/lib/hlm-sidebar-group.ts +15 -0
- package/src/lib/ui/sidebar/src/lib/hlm-sidebar-header.ts +15 -0
- package/src/lib/ui/sidebar/src/lib/hlm-sidebar-input.ts +17 -0
- package/src/lib/ui/sidebar/src/lib/hlm-sidebar-inset.ts +15 -0
- package/src/lib/ui/sidebar/src/lib/hlm-sidebar-menu-action.ts +24 -0
- package/src/lib/ui/sidebar/src/lib/hlm-sidebar-menu-badge.ts +18 -0
- package/src/lib/ui/sidebar/src/lib/hlm-sidebar-menu-button.ts +97 -0
- package/src/lib/ui/sidebar/src/lib/hlm-sidebar-menu-item.ts +15 -0
- package/src/lib/ui/sidebar/src/lib/hlm-sidebar-menu-skeleton.ts +33 -0
- package/src/lib/ui/sidebar/src/lib/hlm-sidebar-menu-sub-button.ts +41 -0
- package/src/lib/ui/sidebar/src/lib/hlm-sidebar-menu-sub-item.ts +15 -0
- package/src/lib/ui/sidebar/src/lib/hlm-sidebar-menu-sub.ts +18 -0
- package/src/lib/ui/sidebar/src/lib/hlm-sidebar-menu.ts +15 -0
- package/src/lib/ui/sidebar/src/lib/hlm-sidebar-rail.ts +34 -0
- package/src/lib/ui/sidebar/src/lib/hlm-sidebar-separator.ts +17 -0
- package/src/lib/ui/sidebar/src/lib/hlm-sidebar-trigger.ts +35 -0
- package/src/lib/ui/sidebar/src/lib/hlm-sidebar-wrapper.ts +24 -0
- package/src/lib/ui/sidebar/src/lib/hlm-sidebar.service.ts +136 -0
- package/src/lib/ui/sidebar/src/lib/hlm-sidebar.stories.ts +177 -0
- package/src/lib/ui/sidebar/src/lib/hlm-sidebar.token.ts +33 -0
- package/src/lib/ui/sidebar/src/lib/hlm-sidebar.ts +139 -0
- package/src/lib/ui/skeleton/src/index.ts +5 -0
- package/src/lib/ui/skeleton/src/lib/hlm-skeleton.ts +14 -0
- package/src/lib/ui/table/src/index.ts +25 -0
- package/src/lib/ui/table/src/lib/hlm-table.stories.ts +70 -0
- package/src/lib/ui/table/src/lib/hlm-table.ts +129 -0
- package/src/lib/ui/textarea/src/index.ts +5 -0
- package/src/lib/ui/textarea/src/lib/hlm-textarea.stories.ts +72 -0
- package/src/lib/ui/textarea/src/lib/hlm-textarea.ts +21 -0
- package/src/lib/ui/tooltip/src/index.ts +5 -0
- package/src/lib/ui/tooltip/src/lib/hlm-tooltip.ts +44 -0
- package/src/lib/ui/utils/src/index.ts +1 -0
- package/src/lib/ui/utils/src/lib/hlm.ts +313 -0
- package/src/public-api.ts +24 -0
- package/src/styles.css +43 -0
- package/tsconfig.json +45 -0
- package/tsconfig.lib.json +11 -0
- 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,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
|
+
};
|