@compsych-ui-components/angular 6.1.2 → 7.0.1
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.
|
@@ -48,8 +48,8 @@ export class SidebarComponent {
|
|
|
48
48
|
/** Currently active item id — supports two-way binding via `[(activeId)]`. Updated on click for route items; unchanged for `nonRoute` items. */
|
|
49
49
|
activeId = model('home', ...(ngDevMode ? [{ debugName: "activeId" }] : /* istanbul ignore next */ []));
|
|
50
50
|
/**
|
|
51
|
-
* Emitted with the item's `id`
|
|
52
|
-
*
|
|
51
|
+
* Emitted with the item's `id` when a non-disabled action (`nonRoute`) item is clicked.
|
|
52
|
+
* Navigation (route) items do not emit — they update `activeId` instead (bind via `[(activeId)]`).
|
|
53
53
|
* Group items (those with `children`) do not emit — they only toggle their expanded state.
|
|
54
54
|
*/
|
|
55
55
|
itemClick = output();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sidebar.js","sourceRoot":"","sources":["../../../../../../packages/angular/src/lib/sidebar/sidebar.ts","../../../../../../packages/angular/src/lib/sidebar/sidebar.html"],"names":[],"mappings":"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","sourcesContent":["import {\r\n ChangeDetectionStrategy,\r\n Component,\r\n model,\r\n input,\r\n output,\r\n signal,\r\n} from '@angular/core';\r\nimport { DomSanitizer, SafeHtml } from '@angular/platform-browser';\r\nimport { LUCIDE_ICONS } from '@compsych-ui-components/shared/assets/lucide-icons.generated';\r\n\r\nexport interface SidebarItem {\r\n /** Stable identifier used by `activeId` and emitted by `itemClick`. */\r\n id: string;\r\n /** Text label displayed next to the icon. */\r\n label: string;\r\n /** Lucide icon name (kebab-case), e.g. `'house'` or `'folder'`. */\r\n icon: string;\r\n /** Child items — renders this item as an expandable group with a chevron toggle. Child items cannot have their own children. */\r\n children?: SidebarItem[];\r\n /** When true, renders a horizontal divider above this item in the list. */\r\n dividerBefore?: boolean;\r\n /** When true, the item is non-interactive — it cannot be clicked and receives no hover or active styling. */\r\n disabled?: boolean;\r\n /**\r\n * When true, clicking the item emits `itemClick` but does not highlight it\r\n * as active or update `activeId`. Use for action items (e.g. logout)\r\n * that are not navigation destinations.\r\n */\r\n nonRoute?: boolean;\r\n}\r\n\r\n/** Default content from the Figma (node 260:4700). */\r\nconst DEFAULT_ITEMS: SidebarItem[] = [\r\n { id: 'home', label: 'Home', icon: 'house' },\r\n { id: 'connect', label: 'Connect Me', icon: 'message-circle', dividerBefore: true },\r\n { id: 'guide', label: 'Guide Me', icon: 'compass' },\r\n { id: 'assess', label: 'Assess Me', icon: 'binoculars' },\r\n {\r\n id: 'resources',\r\n label: 'Resource Library',\r\n icon: 'folder',\r\n dividerBefore: true,\r\n children: [\r\n { id: 'wellness', label: 'Wellness', icon: 'leaf' },\r\n { id: 'relationships', label: 'Relationships', icon: 'users' },\r\n { id: 'work', label: 'Work & Education', icon: 'briefcase' },\r\n { id: 'financial', label: 'Financial', icon: 'landmark' },\r\n { id: 'legal', label: 'Legal', icon: 'scale' },\r\n { id: 'lifestyle', label: 'Lifestyle', icon: 'smile' },\r\n { id: 'home-auto', label: 'Home & Auto', icon: 'car' },\r\n { id: 'abroad', label: 'Working Abroad', icon: 'plane' },\r\n ],\r\n },\r\n];\r\n\r\nconst DEFAULT_FOOTER: SidebarItem[] = [\r\n { id: 'location', label: 'Location & Language', icon: 'globe', nonRoute: true },\r\n { id: 'logout', label: 'Logout', icon: 'log-out', nonRoute: true },\r\n];\r\n\r\n/**\r\n * Sidebar — vertical app navigation from the ComPsych Design System.\r\n *\r\n * Figma: Core Components — base 260:4700, parts 213:1585.\r\n *\r\n * Icon + label items with default / hover / selected / focus / disabled states,\r\n * section dividers, expandable groups (chevron), and a pinned footer\r\n * (location & language, logout). Data-driven via `items` / `footerItems`,\r\n * defaulting to the design's content. All values are sys tokens.\r\n */\r\n@Component({\r\n selector: 'compsych-sidebar',\r\n standalone: true,\r\n changeDetection: ChangeDetectionStrategy.OnPush,\r\n imports: [],\r\n templateUrl: './sidebar.html',\r\n styleUrl: './sidebar.css',\r\n host: { role: 'navigation', 'aria-label': 'Sidebar' },\r\n})\r\nexport class SidebarComponent {\r\n /** Navigation items rendered in the scrollable main area. Defaults to the standard ComPsych nav set. */\r\n readonly items = input<SidebarItem[]>(DEFAULT_ITEMS);\r\n /** Items pinned to the bottom of the sidebar, outside the scroll region. Intended for utility actions such as language selection and logout. */\r\n readonly footerItems = input<SidebarItem[]>(DEFAULT_FOOTER);\r\n /** Currently active item id — supports two-way binding via `[(activeId)]`. Updated on click for route items; unchanged for `nonRoute` items. */\r\n readonly activeId = model<string>('home');\r\n /**\r\n * Emitted with the item's `id` whenever a non-disabled, non-group item is clicked.\r\n * Fires for both route items and `nonRoute` items.\r\n * Group items (those with `children`) do not emit — they only toggle their expanded state.\r\n */\r\n readonly itemClick = output<string>();\r\n\r\n private readonly expanded = signal<Set<string>>(new Set());\r\n\r\n constructor(private readonly sanitizer: DomSanitizer) {}\r\n\r\n isExpanded(id: string): boolean {\r\n return this.expanded().has(id);\r\n }\r\n\r\n isActive(id: string): boolean {\r\n return this.activeId() === id;\r\n }\r\n\r\n onItemClick(item: SidebarItem): void {\r\n if (item.disabled) return;\r\n if (item.children?.length) {\r\n this.expanded.update((s) => {\r\n const next = new Set(s);\r\n next.has(item.id) ? next.delete(item.id) : next.add(item.id);\r\n return next;\r\n });\r\n return;\r\n }\r\n if (item.nonRoute) {\r\n this.itemClick.emit(item.id);\r\n return;\r\n }\r\n this.activeId.set(item.id);\r\n }\r\n\r\n /**\r\n * Looks up a Lucide icon by name and injects explicit width/height attributes\r\n * so the SVG renders at the correct size when injected via [innerHTML].\r\n * Angular's CSS encapsulation does not add _ngcontent attributes to dynamically\r\n * injected content, so explicit SVG attributes are the reliable solution.\r\n */\r\n iconSvg(name: string, size = 20, strokeWidth = 1.5): SafeHtml {\r\n let svg = LUCIDE_ICONS[name] ?? '';\r\n svg = svg.replace('<svg ', `<svg width=\"${size}\" height=\"${size}\" `);\r\n if (strokeWidth !== 1.5) {\r\n svg = svg.replace('stroke-width=\"1.5\"', `stroke-width=\"${strokeWidth}\"`);\r\n }\r\n return this.sanitizer.bypassSecurityTrustHtml(svg);\r\n }\r\n}\r\n","<div class=\"cs-sb__scroll\">\r\n <ul class=\"cs-sb__list\">\r\n @for (item of items(); track item.id) {\r\n @if (item.dividerBefore) {\r\n <li class=\"cs-sb__divider\" aria-hidden=\"true\"></li>\r\n }\r\n <li class=\"cs-sb__item\">\r\n <button\r\n type=\"button\"\r\n class=\"cs-sb__row\"\r\n [class.cs-sb__row--active]=\"!item.nonRoute && isActive(item.id)\"\r\n [disabled]=\"item.disabled || null\"\r\n [attr.aria-current]=\"!item.nonRoute && isActive(item.id) ? 'page' : null\"\r\n [attr.aria-expanded]=\"item.children?.length ? isExpanded(item.id) : null\"\r\n (click)=\"onItemClick(item)\"\r\n >\r\n <span\r\n class=\"cs-sb__icon\"\r\n [innerHTML]=\"iconSvg(item.icon, 20, !item.nonRoute && isActive(item.id) ? 2 : 1.5)\"\r\n aria-hidden=\"true\"\r\n ></span>\r\n <span class=\"cs-sb__label\">{{ item.label }}</span>\r\n @if (item.children?.length) {\r\n <span\r\n class=\"cs-sb__chevron\"\r\n [class.cs-sb__chevron--open]=\"isExpanded(item.id)\"\r\n [innerHTML]=\"iconSvg('chevron-down', 18)\"\r\n aria-hidden=\"true\"\r\n ></span>\r\n }\r\n </button>\r\n\r\n @if (item.children?.length && isExpanded(item.id)) {\r\n <ul class=\"cs-sb__sublist\">\r\n @for (child of item.children; track child.id) {\r\n <li>\r\n <button\r\n type=\"button\"\r\n class=\"cs-sb__row cs-sb__row--sub\"\r\n [class.cs-sb__row--active]=\"isActive(child.id)\"\r\n [disabled]=\"child.disabled || null\"\r\n [attr.aria-current]=\"isActive(child.id) ? 'page' : null\"\r\n (click)=\"onItemClick(child)\"\r\n >\r\n <span\r\n class=\"cs-sb__icon\"\r\n [innerHTML]=\"iconSvg(child.icon, 20, isActive(child.id) ? 2 : 1.5)\"\r\n aria-hidden=\"true\"\r\n ></span>\r\n <span class=\"cs-sb__label\">{{ child.label }}</span>\r\n </button>\r\n </li>\r\n }\r\n </ul>\r\n }\r\n </li>\r\n }\r\n </ul>\r\n</div>\r\n\r\n<div class=\"cs-sb__footer\">\r\n <ul class=\"cs-sb__list\">\r\n @for (item of footerItems(); track item.id) {\r\n <li class=\"cs-sb__item\">\r\n <button\r\n type=\"button\"\r\n class=\"cs-sb__row\"\r\n [class.cs-sb__row--active]=\"!item.nonRoute && isActive(item.id)\"\r\n [attr.aria-current]=\"!item.nonRoute && isActive(item.id) ? 'page' : null\"\r\n [disabled]=\"item.disabled || null\"\r\n (click)=\"onItemClick(item)\"\r\n >\r\n <span\r\n class=\"cs-sb__icon\"\r\n [innerHTML]=\"iconSvg(item.icon, 20, !item.nonRoute && isActive(item.id) ? 2 : 1.5)\"\r\n aria-hidden=\"true\"\r\n ></span>\r\n <span class=\"cs-sb__label\">{{ item.label }}</span>\r\n </button>\r\n </li>\r\n }\r\n </ul>\r\n</div>\r\n"]}
|
|
1
|
+
{"version":3,"file":"sidebar.js","sourceRoot":"","sources":["../../../../../../packages/angular/src/lib/sidebar/sidebar.ts","../../../../../../packages/angular/src/lib/sidebar/sidebar.html"],"names":[],"mappings":"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","sourcesContent":["import {\r\n ChangeDetectionStrategy,\r\n Component,\r\n model,\r\n input,\r\n output,\r\n signal,\r\n} from '@angular/core';\r\nimport { DomSanitizer, SafeHtml } from '@angular/platform-browser';\r\nimport { LUCIDE_ICONS } from '@compsych-ui-components/shared/assets/lucide-icons.generated';\r\n\r\nexport interface SidebarItem {\r\n /** Stable identifier used by `activeId` and emitted by `itemClick`. */\r\n id: string;\r\n /** Text label displayed next to the icon. */\r\n label: string;\r\n /** Lucide icon name (kebab-case), e.g. `'house'` or `'folder'`. */\r\n icon: string;\r\n /** Child items — renders this item as an expandable group with a chevron toggle. Child items cannot have their own children. */\r\n children?: SidebarItem[];\r\n /** When true, renders a horizontal divider above this item in the list. */\r\n dividerBefore?: boolean;\r\n /** When true, the item is non-interactive — it cannot be clicked and receives no hover or active styling. */\r\n disabled?: boolean;\r\n /**\r\n * When true, clicking the item emits `itemClick` but does not highlight it\r\n * as active or update `activeId`. Use for action items (e.g. logout)\r\n * that are not navigation destinations.\r\n */\r\n nonRoute?: boolean;\r\n}\r\n\r\n/** Default content from the Figma (node 260:4700). */\r\nconst DEFAULT_ITEMS: SidebarItem[] = [\r\n { id: 'home', label: 'Home', icon: 'house' },\r\n { id: 'connect', label: 'Connect Me', icon: 'message-circle', dividerBefore: true },\r\n { id: 'guide', label: 'Guide Me', icon: 'compass' },\r\n { id: 'assess', label: 'Assess Me', icon: 'binoculars' },\r\n {\r\n id: 'resources',\r\n label: 'Resource Library',\r\n icon: 'folder',\r\n dividerBefore: true,\r\n children: [\r\n { id: 'wellness', label: 'Wellness', icon: 'leaf' },\r\n { id: 'relationships', label: 'Relationships', icon: 'users' },\r\n { id: 'work', label: 'Work & Education', icon: 'briefcase' },\r\n { id: 'financial', label: 'Financial', icon: 'landmark' },\r\n { id: 'legal', label: 'Legal', icon: 'scale' },\r\n { id: 'lifestyle', label: 'Lifestyle', icon: 'smile' },\r\n { id: 'home-auto', label: 'Home & Auto', icon: 'car' },\r\n { id: 'abroad', label: 'Working Abroad', icon: 'plane' },\r\n ],\r\n },\r\n];\r\n\r\nconst DEFAULT_FOOTER: SidebarItem[] = [\r\n { id: 'location', label: 'Location & Language', icon: 'globe', nonRoute: true },\r\n { id: 'logout', label: 'Logout', icon: 'log-out', nonRoute: true },\r\n];\r\n\r\n/**\r\n * Sidebar — vertical app navigation from the ComPsych Design System.\r\n *\r\n * Figma: Core Components — base 260:4700, parts 213:1585.\r\n *\r\n * Icon + label items with default / hover / selected / focus / disabled states,\r\n * section dividers, expandable groups (chevron), and a pinned footer\r\n * (location & language, logout). Data-driven via `items` / `footerItems`,\r\n * defaulting to the design's content. All values are sys tokens.\r\n */\r\n@Component({\r\n selector: 'compsych-sidebar',\r\n standalone: true,\r\n changeDetection: ChangeDetectionStrategy.OnPush,\r\n imports: [],\r\n templateUrl: './sidebar.html',\r\n styleUrl: './sidebar.css',\r\n host: { role: 'navigation', 'aria-label': 'Sidebar' },\r\n})\r\nexport class SidebarComponent {\r\n /** Navigation items rendered in the scrollable main area. Defaults to the standard ComPsych nav set. */\r\n readonly items = input<SidebarItem[]>(DEFAULT_ITEMS);\r\n /** Items pinned to the bottom of the sidebar, outside the scroll region. Intended for utility actions such as language selection and logout. */\r\n readonly footerItems = input<SidebarItem[]>(DEFAULT_FOOTER);\r\n /** Currently active item id — supports two-way binding via `[(activeId)]`. Updated on click for route items; unchanged for `nonRoute` items. */\r\n readonly activeId = model<string>('home');\r\n /**\r\n * Emitted with the item's `id` when a non-disabled action (`nonRoute`) item is clicked.\r\n * Navigation (route) items do not emit — they update `activeId` instead (bind via `[(activeId)]`).\r\n * Group items (those with `children`) do not emit — they only toggle their expanded state.\r\n */\r\n readonly itemClick = output<string>();\r\n\r\n private readonly expanded = signal<Set<string>>(new Set());\r\n\r\n constructor(private readonly sanitizer: DomSanitizer) {}\r\n\r\n isExpanded(id: string): boolean {\r\n return this.expanded().has(id);\r\n }\r\n\r\n isActive(id: string): boolean {\r\n return this.activeId() === id;\r\n }\r\n\r\n onItemClick(item: SidebarItem): void {\r\n if (item.disabled) return;\r\n if (item.children?.length) {\r\n this.expanded.update((s) => {\r\n const next = new Set(s);\r\n next.has(item.id) ? next.delete(item.id) : next.add(item.id);\r\n return next;\r\n });\r\n return;\r\n }\r\n if (item.nonRoute) {\r\n this.itemClick.emit(item.id);\r\n return;\r\n }\r\n this.activeId.set(item.id);\r\n }\r\n\r\n /**\r\n * Looks up a Lucide icon by name and injects explicit width/height attributes\r\n * so the SVG renders at the correct size when injected via [innerHTML].\r\n * Angular's CSS encapsulation does not add _ngcontent attributes to dynamically\r\n * injected content, so explicit SVG attributes are the reliable solution.\r\n */\r\n iconSvg(name: string, size = 20, strokeWidth = 1.5): SafeHtml {\r\n let svg = LUCIDE_ICONS[name] ?? '';\r\n svg = svg.replace('<svg ', `<svg width=\"${size}\" height=\"${size}\" `);\r\n if (strokeWidth !== 1.5) {\r\n svg = svg.replace('stroke-width=\"1.5\"', `stroke-width=\"${strokeWidth}\"`);\r\n }\r\n return this.sanitizer.bypassSecurityTrustHtml(svg);\r\n }\r\n}\r\n","<div class=\"cs-sb__scroll\">\r\n <ul class=\"cs-sb__list\">\r\n @for (item of items(); track item.id) {\r\n @if (item.dividerBefore) {\r\n <li class=\"cs-sb__divider\" aria-hidden=\"true\"></li>\r\n }\r\n <li class=\"cs-sb__item\">\r\n <button\r\n type=\"button\"\r\n class=\"cs-sb__row\"\r\n [class.cs-sb__row--active]=\"!item.nonRoute && isActive(item.id)\"\r\n [disabled]=\"item.disabled || null\"\r\n [attr.aria-current]=\"!item.nonRoute && isActive(item.id) ? 'page' : null\"\r\n [attr.aria-expanded]=\"item.children?.length ? isExpanded(item.id) : null\"\r\n (click)=\"onItemClick(item)\"\r\n >\r\n <span\r\n class=\"cs-sb__icon\"\r\n [innerHTML]=\"iconSvg(item.icon, 20, !item.nonRoute && isActive(item.id) ? 2 : 1.5)\"\r\n aria-hidden=\"true\"\r\n ></span>\r\n <span class=\"cs-sb__label\">{{ item.label }}</span>\r\n @if (item.children?.length) {\r\n <span\r\n class=\"cs-sb__chevron\"\r\n [class.cs-sb__chevron--open]=\"isExpanded(item.id)\"\r\n [innerHTML]=\"iconSvg('chevron-down', 18)\"\r\n aria-hidden=\"true\"\r\n ></span>\r\n }\r\n </button>\r\n\r\n @if (item.children?.length && isExpanded(item.id)) {\r\n <ul class=\"cs-sb__sublist\">\r\n @for (child of item.children; track child.id) {\r\n <li>\r\n <button\r\n type=\"button\"\r\n class=\"cs-sb__row cs-sb__row--sub\"\r\n [class.cs-sb__row--active]=\"isActive(child.id)\"\r\n [disabled]=\"child.disabled || null\"\r\n [attr.aria-current]=\"isActive(child.id) ? 'page' : null\"\r\n (click)=\"onItemClick(child)\"\r\n >\r\n <span\r\n class=\"cs-sb__icon\"\r\n [innerHTML]=\"iconSvg(child.icon, 20, isActive(child.id) ? 2 : 1.5)\"\r\n aria-hidden=\"true\"\r\n ></span>\r\n <span class=\"cs-sb__label\">{{ child.label }}</span>\r\n </button>\r\n </li>\r\n }\r\n </ul>\r\n }\r\n </li>\r\n }\r\n </ul>\r\n</div>\r\n\r\n<div class=\"cs-sb__footer\">\r\n <ul class=\"cs-sb__list\">\r\n @for (item of footerItems(); track item.id) {\r\n <li class=\"cs-sb__item\">\r\n <button\r\n type=\"button\"\r\n class=\"cs-sb__row\"\r\n [class.cs-sb__row--active]=\"!item.nonRoute && isActive(item.id)\"\r\n [attr.aria-current]=\"!item.nonRoute && isActive(item.id) ? 'page' : null\"\r\n [disabled]=\"item.disabled || null\"\r\n (click)=\"onItemClick(item)\"\r\n >\r\n <span\r\n class=\"cs-sb__icon\"\r\n [innerHTML]=\"iconSvg(item.icon, 20, !item.nonRoute && isActive(item.id) ? 2 : 1.5)\"\r\n aria-hidden=\"true\"\r\n ></span>\r\n <span class=\"cs-sb__label\">{{ item.label }}</span>\r\n </button>\r\n </li>\r\n }\r\n </ul>\r\n</div>\r\n"]}
|
package/lib/sidebar/sidebar.d.ts
CHANGED
|
@@ -39,8 +39,8 @@ export declare class SidebarComponent {
|
|
|
39
39
|
/** Currently active item id — supports two-way binding via `[(activeId)]`. Updated on click for route items; unchanged for `nonRoute` items. */
|
|
40
40
|
readonly activeId: import("@angular/core").ModelSignal<string>;
|
|
41
41
|
/**
|
|
42
|
-
* Emitted with the item's `id`
|
|
43
|
-
*
|
|
42
|
+
* Emitted with the item's `id` when a non-disabled action (`nonRoute`) item is clicked.
|
|
43
|
+
* Navigation (route) items do not emit — they update `activeId` instead (bind via `[(activeId)]`).
|
|
44
44
|
* Group items (those with `children`) do not emit — they only toggle their expanded state.
|
|
45
45
|
*/
|
|
46
46
|
readonly itemClick: import("@angular/core").OutputEmitterRef<string>;
|