@pecb-ui/components 1.1.12 → 1.1.13

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/README.md CHANGED
@@ -416,6 +416,327 @@ pecb-course-content-panel {
416
416
  }
417
417
  ```
418
418
 
419
+ ### App Sidebar Component
420
+
421
+ The **application shell rail** from the PECB Central Hub: product switcher, navigation tree with
422
+ expandable branches, collapse-to-icons (with tooltips and flyouts), and the account block with its
423
+ dropdown — Profile, Billing, a Language submenu, a Switch Roles submenu, Customer Service and
424
+ Log Out.
425
+
426
+ It is entirely data-driven: describe the navigation, listen for the events. It is a **new**
427
+ component and does not touch the existing `pecb-sidebar`.
428
+
429
+ ```ts
430
+ import { AppSidebarComponent } from "@pecb-ui/components";
431
+ // or: import { AppSidebarComponent } from '@pecb-ui/components/navigation';
432
+ ```
433
+
434
+ The host is a plain flex child that owns its own width, so the app shell is a flex row with no
435
+ margins to keep in sync:
436
+
437
+ ```html
438
+ <div style="display:flex; min-height:100vh">
439
+ <pecb-app-sidebar [logoSrc]="logo" [collapsedLogoSrc]="mark" [products]="products" [(activeProductId)]="productId" [items]="nav" [(activeItemId)]="activeId" [user]="user" [menu]="accountMenu" [(collapsed)]="collapsed" [(mobileOpen)]="drawerOpen" storageKey="pecb-sidebar" (navigate)="go($event)" (productChange)="switchProduct($event)" (menuItemClick)="onAccountAction($event)" (menuOptionChange)="onLanguageOrRole($event)" />
440
+ <main style="flex:1; min-width:0"><!-- page --></main>
441
+ </div>
442
+ ```
443
+
444
+ **Navigation belongs to the product.** Give each switcher entry its own `items` (or `groups`) and
445
+ switching product swaps the entire tab set — the rail lands on that product's first tab and emits
446
+ `navigate` for it. A hub product that is really just a launcher declares one Dashboard entry and
447
+ shows nothing else; the real tabs appear only once the user switches into a product. Turn the
448
+ landing off with `[autoSelectFirstItem]="false"`.
449
+
450
+ ```ts
451
+ products: AppSidebarProduct[] = [
452
+ {
453
+ id: "hub",
454
+ label: "Central Hub",
455
+ description: "All PECB products",
456
+ featured: true,
457
+ items: [{ id: "hub-dashboard", label: "Dashboard", icon: "<svg …>" }],
458
+ },
459
+ {
460
+ id: "courses",
461
+ label: "Professional Courses",
462
+ section: "PECB Products",
463
+ items: [
464
+ { id: "pc-dashboard", label: "Dashboard" },
465
+ { id: "pc-browse", label: "Browse Courses" },
466
+ { id: "pc-exams", label: "Exams", badge: 2, children: [...] },
467
+ ],
468
+ },
469
+ { id: "executive", label: "Executive Education", section: "PECB Products", disabled: true, hint: "Coming soon" },
470
+ ];
471
+ ```
472
+
473
+ A single-product shell skips all that and feeds the rail's own `items` / `groups` inputs; the
474
+ switcher then only changes which application you are in. Navigation nests up to three levels either
475
+ way — branches expand in place while the rail is expanded, and inside the flyout while it is
476
+ collapsed:
477
+
478
+ ```ts
479
+ const nav: AppSidebarNavItem[] = [
480
+ { id: "hub", label: "Central Hub", icon: "<svg …>" },
481
+ { id: "certs", label: "Certifications", icon: "<svg …>", badge: 3 },
482
+ {
483
+ id: "courses",
484
+ label: "Professional Courses",
485
+ icon: "<svg …>",
486
+ children: [
487
+ { id: "courses-mine", label: "My courses" },
488
+ {
489
+ id: "courses-catalog",
490
+ label: "Course catalog",
491
+ children: [
492
+ { id: "catalog-iso", label: "ISO standards" },
493
+ { id: "catalog-cyber", label: "Cybersecurity" },
494
+ ],
495
+ },
496
+ ],
497
+ },
498
+ ];
499
+ ```
500
+
501
+ **The profile block is the role select.** It shows the user's name with their role beneath it, and
502
+ opens the account dropdown. Mark a submenu `reflectsRole` — `DEFAULT_APP_SIDEBAR_MENU` already does
503
+ on Switch Roles — and picking a role updates that line immediately, with no round-trip through the
504
+ host application. `user.role` is the fallback when nothing reflects it.
505
+
506
+ **The account dropdown ships ready-made.** `DEFAULT_APP_SIDEBAR_MENU` is the PECB menu in the order
507
+ the Central Hub renders it — Profile, Switch Roles, Language, Billing & Payments, Customer Service
508
+ and Log Out — icons included. Pass it straight in, or spread it to adjust a single entry:
509
+
510
+ ```ts
511
+ import { DEFAULT_APP_SIDEBAR_MENU } from "@pecb-ui/components";
512
+
513
+ accountMenu = DEFAULT_APP_SIDEBAR_MENU.map((item) => (item.id === "profile" ? { ...item, routerLink: "/me" } : item));
514
+ ```
515
+
516
+ `DEFAULT_APP_SIDEBAR_LANGUAGES`, `DEFAULT_APP_SIDEBAR_ROLES` and `APP_SIDEBAR_MENU_ICONS` are
517
+ exported alongside it for building your own variant.
518
+
519
+ **Angular Router.** Any entry — navigation item, product or menu row — can carry `routerLink`
520
+ (plus `queryParams`, `fragment`, `routerLinkActiveExact`) instead of `href`. Those entries render
521
+ as router anchors and highlight through `routerLinkActive`, so the rail follows the URL including
522
+ back/forward and deep links. `@angular/router` is an **optional** peer dependency: the router
523
+ directives are only instantiated for entries that actually use them, so router-free applications
524
+ are unaffected.
525
+
526
+ ```ts
527
+ const nav: AppSidebarNavItem[] = [
528
+ { id: "hub", label: "Central Hub", routerLink: "/", routerLinkActiveExact: true },
529
+ { id: "course", label: "Course", routerLink: ["/courses", id], fragment: "top" },
530
+ ];
531
+ ```
532
+
533
+ **Branding.** `logo` / `collapsedLogo` take inline SVG markup and `logoSrc` / `collapsedLogoSrc`
534
+ take image URLs; markup wins when both are supplied. The expanded wordmark is pinned to
535
+ `--pecb-app-sidebar-logo-height` (34px) and keeps its aspect ratio, so an `<img>` and an inline
536
+ `<svg>` land on exactly the same box; the collapsed mark keeps its natural size, capped to the rail.
537
+
538
+ **Localisation.** Nothing the shell renders is hard-coded. Every string it produces itself comes
539
+ from the `labels` input, and the ready-made account menu is built per locale — so a language switch
540
+ is a lookup, not a translation project. Six locales ship: **en · fr · es · de · ja · ko**.
541
+
542
+ ```ts
543
+ import {
544
+ APP_SIDEBAR_TRANSLATIONS,
545
+ APP_HEADER_TRANSLATIONS,
546
+ appSidebarMenu,
547
+ type AppShellLocale,
548
+ } from "@pecb-ui/components";
549
+
550
+ locale = signal<AppShellLocale>("en");
551
+ sidebarLabels = computed(() => APP_SIDEBAR_TRANSLATIONS[this.locale()]);
552
+ headerLabels = computed(() => APP_HEADER_TRANSLATIONS[this.locale()]);
553
+ menu = computed(() => appSidebarMenu(this.locale()));
554
+
555
+ onOption(change: AppSidebarMenuOptionChange) {
556
+ // The language option ids *are* the locale keys.
557
+ if (change.item.id === "language") this.locale.set(change.option.id as AppShellLocale);
558
+ }
559
+ ```
560
+
561
+ `appSidebarLabels('fr-CA')` / `appHeaderLabels('es-MX')` accept region tags and fall back to English
562
+ for anything unrecognised. For a language not shipped, pass your own object — `labels` takes a
563
+ **partial** bundle merged over the defaults, so overriding one word does not mean restating the rest:
564
+
565
+ ```html
566
+ <pecb-app-sidebar [labels]="{ collapse: 'Sluit zijbalk' }" />
567
+ ```
568
+
569
+ Language names in the picker stay endonyms ("Deutsch", "日本語") — a language is named in itself,
570
+ whatever the current locale. `DEFAULT_APP_SIDEBAR_MENU` and `DEFAULT_APP_SIDEBAR_ROLES` remain
571
+ available as the English builds of `appSidebarMenu()` / `appSidebarRoles()`.
572
+
573
+ **Stacking.** The rail defaults to `z-index: 30`, above the header's `20`, because the collapse
574
+ handle overhangs the rail's right border by 13px straight across the header — it has to stay
575
+ visible and clickable there. Retune both with `--pecb-app-sidebar-z` / `--pecb-app-header-z`, and
576
+ keep the rail the higher of the two.
577
+
578
+ **Inputs** — `logo`, `collapsedLogo`, `logoSrc`, `collapsedLogoSrc`, `logoHref`, `products`, `activeProductId` (two-way),
579
+ `items`, `groups`, `activeItemId` (two-way), `user`, `menu`, `collapsible`, `collapsed` (two-way),
580
+ `mobileOpen` (two-way), `mobileBreakpoint`, `width`, `collapsedWidth`, `autoCollapseSiblings`,
581
+ `autoSelectFirstItem`, `showTooltips`, `storageKey`, `labels`.
582
+
583
+ **Outputs** — `navigate`, `productChange`, `menuItemClick`, `menuOptionChange`, `userClick`.
584
+
585
+ **Slots** — `[appSidebarHeader]`, `[appSidebarPromo]`, `[appSidebarFooter]`; all three are hidden
586
+ while the rail is collapsed.
587
+
588
+ **Responsive.** At or below `mobileBreakpoint` (880px by default) the rail becomes an off-canvas
589
+ drawer with a scrim, a focus trap and body scroll-lock; the closed drawer is `inert`, so it is
590
+ skipped by tab order and screen readers. Bind `mobileOpen` to a hamburger, or read the rail's own
591
+ state to show one only when it is needed:
592
+
593
+ ```html
594
+ <pecb-app-sidebar #rail … /> <button *ngIf="rail.isMobile()" (click)="rail.toggleMobile()">☰</button>
595
+ ```
596
+
597
+ **Collapsed rail.** Labels give way to icons, hovering an icon shows a tooltip and a branch opens as
598
+ a flyout beside the rail. Pass `storageKey` to remember the state in `localStorage`.
599
+
600
+ **Performance.** `OnPush` with signals throughout, sanitised icon markup cached per string, and the
601
+ menus, flyouts and tooltips share a handful of CDK overlays that only exist while open — so they are
602
+ never clipped by the scroll container and always reposition inside the viewport.
603
+
604
+ **Theming:**
605
+
606
+ ```css
607
+ pecb-app-sidebar {
608
+ --pecb-app-sidebar-width: 270px; /* expanded rail width */
609
+ --pecb-app-sidebar-collapsed-width: 84px; /* collapsed rail width */
610
+ --pecb-app-sidebar-height: 100dvh; /* set 100% to embed it */
611
+ --pecb-app-sidebar-logo-height: 34px; /* wordmark height */
612
+ --pecb-app-sidebar-bg: #ffffff; /* surface */
613
+ --pecb-app-sidebar-border: #ececec; /* divider colour */
614
+ --pecb-app-sidebar-active-bg: #212427; /* active item background */
615
+ --pecb-app-sidebar-active-fg: #ffffff; /* active item foreground */
616
+ --pecb-app-sidebar-accent: #a11e29; /* brand accent */
617
+ --pecb-app-sidebar-z: 30; /* drawer stacking order */
618
+ }
619
+ ```
620
+
621
+ ### App Header Component
622
+
623
+ The **application shell header** from the Central Hub — the bar above the page, beside
624
+ `pecb-app-sidebar`. Page title, an optional status chip, a cluster of icon actions with corner
625
+ badges, and the notification bell with its dropdown: unread tint, coloured icon tiles, a
626
+ "view all" footer and an empty state.
627
+
628
+ ```ts
629
+ import { AppHeaderComponent } from "@pecb-ui/components";
630
+ ```
631
+
632
+ The two components pair without either knowing about the other — bind the hamburger to the rail's
633
+ own drawer state, so they can never disagree:
634
+
635
+ ```html
636
+ <div style="display:flex; min-height:100vh">
637
+ <pecb-app-sidebar #rail [items]="nav" [(mobileOpen)]="drawerOpen" />
638
+ <div style="flex:1; min-width:0">
639
+ <pecb-app-header heading="Central Hub" [chip]="{ label: 'Member' }" [actions]="actions" [notifications]="notifications" [showMenuButton]="rail.isMobile()" (menuClick)="rail.toggleMobile()" (actionClick)="onAction($event)" (notificationClick)="open($event)" (viewAllNotifications)="goToInbox()" />
640
+ <main><!-- page --></main>
641
+ </div>
642
+ </div>
643
+ ```
644
+
645
+ ```ts
646
+ const actions: AppHeaderAction[] = [
647
+ { id: "cart", icon: "<svg …>", label: "Cart", badge: cartCount }, // a 0 renders no badge
648
+ { id: "help", icon: "<svg …>", label: "Help centre", href: "https://help.pecb.com/", target: "_blank" },
649
+ ];
650
+
651
+ const notifications: AppHeaderNotification[] = [
652
+ {
653
+ id: "renew",
654
+ title: "Your certificate expires in 25 days.",
655
+ meta: "Today · High priority",
656
+ read: false,
657
+ icon: "<svg …>",
658
+ iconBackground: "#fbeaea",
659
+ iconColor: "#a11e29",
660
+ },
661
+ ];
662
+ ```
663
+
664
+ **Inputs** — `heading`, `chip`, `actions`, `showNotifications`, `notifications`,
665
+ `notificationsLimit`, `notificationsOpen` (two-way), `showMenuButton`, `sticky`, `labels`.
666
+
667
+ **Outputs** — `menuClick`, `actionClick`, `notificationClick`, `viewAllNotifications`.
668
+
669
+ **Slots** — `[appHeaderTitle]` replaces the heading; `[appHeaderStart]` and `[appHeaderEnd]` add
670
+ your own controls at either end of the bar.
671
+
672
+ **Localisation** — same contract as the rail: `APP_HEADER_TRANSLATIONS[locale]` or
673
+ `appHeaderLabels('fr-CA')`, or your own partial bundle. The unread badge's screen-reader text uses
674
+ a `{n}` placeholder so each locale decides where the number sits — `'{n} ungelesen'` in German,
675
+ `'未読 {n} 件'` in Japanese.
676
+
677
+ The unread count is derived from the data, so the bell badge never drifts. The dropdown renders
678
+ through the CDK overlay — never clipped by the header's stacking context, repositioned inside the
679
+ viewport, closed by Escape or an outside click, with focus moved into it and returned on close.
680
+
681
+ **Theming:**
682
+
683
+ ```css
684
+ pecb-app-header {
685
+ --pecb-app-header-bg: #ffffff; /* surface */
686
+ --pecb-app-header-border: #ececec; /* bottom rule */
687
+ --pecb-app-header-accent: #a11e29; /* badges and links */
688
+ --pecb-app-header-height: auto; /* minimum bar height */
689
+ --pecb-app-header-z: 20; /* sticky stacking */
690
+ }
691
+ ```
692
+
693
+ ### Content Container (`.pecb-container`)
694
+
695
+ The one page-measure rule, applied everywhere content is shown. It reproduces the content wrapper
696
+ from the PECB designs — `max-width: 1180px; margin: 0 auto; padding: 14px`, the gutter widening to
697
+ 16px below 880px — so every page in a project gets identical side spacing from a single class:
698
+
699
+ ```scss
700
+ // styles.scss — ships with the package
701
+ @use "@pecb-ui/components/styles/main";
702
+ ```
703
+
704
+ ```html
705
+ <div class="pecb-container">…page content…</div>
706
+ ```
707
+
708
+ One knob, settable on the element or any ancestor (any CSS length, or `none` for full bleed):
709
+
710
+ ```css
711
+ :root {
712
+ --pecb-container-max-width: 1180px;
713
+ }
714
+ ```
715
+
716
+ SCSS consumers can include the same rule directly in a component:
717
+
718
+ ```scss
719
+ @use "@pecb-ui/components/styles/abstracts/mixins" as *;
720
+
721
+ .my-page {
722
+ @include content-container;
723
+ }
724
+ ```
725
+
726
+ ### Project Layout Component
727
+
728
+ The sidebar + header + content shell built on `pecb-sidebar`. Project your page into the
729
+ `[layoutContent]` slot and it fills the content column — unchanged, full-bleed, exactly as existing
730
+ consumers use it today. To give a page the centred measure, opt in with the container class:
731
+
732
+ ```html
733
+ <pecb-project-layout pageTitle="Dashboard">
734
+ <div layoutContent>
735
+ <div class="pecb-container">…page content…</div>
736
+ </div>
737
+ </pecb-project-layout>
738
+ ```
739
+
419
740
  ## Services
420
741
 
421
742
  ### NotificationService
@@ -1,4 +1,4 @@
1
- export { AdminHeaderComponent, AdminHeaderFieldTemplateDirective, AffixComponent, AnchorComponent, BackToTopComponent, BreadcrumbsComponent, DEFAULT_LANGUAGES, HeaderActionsComponent, HeaderComponent, HeaderDividerComponent, HeaderLanguageComponent, HeaderSearchComponent, HeaderUserComponent, LanguageDropdownComponent, StepperComponent, TabComponent } from '@pecb-ui/components';
1
+ export { APP_SIDEBAR_MENU_ICONS, AdminHeaderComponent, AdminHeaderFieldTemplateDirective, AffixComponent, AnchorComponent, AppHeaderComponent, AppSidebarComponent, BackToTopComponent, BreadcrumbsComponent, DEFAULT_APP_HEADER_LABELS, DEFAULT_APP_SIDEBAR_LABELS, DEFAULT_APP_SIDEBAR_LANGUAGES, DEFAULT_APP_SIDEBAR_MENU, DEFAULT_APP_SIDEBAR_ROLES, DEFAULT_LANGUAGES, HeaderActionsComponent, HeaderComponent, HeaderDividerComponent, HeaderLanguageComponent, HeaderSearchComponent, HeaderUserComponent, LanguageDropdownComponent, StepperComponent, TabComponent } from '@pecb-ui/components';
2
2
 
3
3
  /*
4
4
  * @pecb-ui/components/navigation — secondary entry point (audit F18).
@@ -1 +1 @@
1
- {"version":3,"file":"pecb-ui-components-navigation.mjs","sources":["../../../projects/ui-components/navigation/src/public-api.ts","../../../projects/ui-components/navigation/src/pecb-ui-components-navigation.ts"],"sourcesContent":["/*\n * @pecb-ui/components/navigation — secondary entry point (audit F18).\n *\n * Re-exports navigation components (header, breadcrumbs, tabs, stepper, anchor,\n * affix, back-to-top, language-dropdown) from the primary entry point:\n *\n * import { BreadcrumbsComponent } from '@pecb-ui/components/navigation';\n *\n * Existing imports from '@pecb-ui/components' continue to work unchanged.\n */\nexport {\n // Header + its subcomponents\n HeaderComponent,\n HeaderActionsComponent,\n HeaderDividerComponent,\n HeaderLanguageComponent,\n HeaderSearchComponent,\n HeaderUserComponent,\n // Admin header\n AdminHeaderComponent,\n AdminHeaderFieldTemplateDirective,\n // Breadcrumbs / anchors / affix\n BreadcrumbsComponent,\n AnchorComponent,\n AffixComponent,\n BackToTopComponent,\n // Tabs / stepper\n TabComponent,\n StepperComponent,\n // Language dropdown\n LanguageDropdownComponent,\n DEFAULT_LANGUAGES\n} from '@pecb-ui/components';\n\nexport type {\n // Breadcrumbs\n BreadcrumbSeparator,\n // Anchor\n AnchorDirection,\n AnchorItem,\n AnchorLevel,\n // Affix\n AffixPosition,\n // Tab\n TabSize,\n TabStyle,\n // Stepper\n StepItem,\n StepOrder,\n StepState,\n StepperDirection,\n StepperSize,\n StepperTailStyle,\n StepperType,\n // Header\n HeaderActionButton,\n HeaderLanguageOption,\n HeaderSearchCategory,\n // Admin header\n AdminHeaderAction,\n AdminHeaderActionType,\n AdminHeaderBadgeVariant,\n AdminHeaderField,\n AdminHeaderFieldType,\n AdminHeaderMetadata,\n AdminHeaderMetadataType,\n AdminHeaderProfile,\n // Language dropdown\n LanguageDisplayMode,\n LanguageOption\n} from '@pecb-ui/components';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":";;AAAA;;;;;;;;;AASG;;ACTH;;AAEG"}
1
+ {"version":3,"file":"pecb-ui-components-navigation.mjs","sources":["../../../projects/ui-components/navigation/src/public-api.ts","../../../projects/ui-components/navigation/src/pecb-ui-components-navigation.ts"],"sourcesContent":["/*\n * @pecb-ui/components/navigation — secondary entry point (audit F18).\n *\n * Re-exports navigation components (header, breadcrumbs, tabs, stepper, anchor,\n * affix, back-to-top, language-dropdown) from the primary entry point:\n *\n * import { BreadcrumbsComponent } from '@pecb-ui/components/navigation';\n *\n * Existing imports from '@pecb-ui/components' continue to work unchanged.\n */\nexport {\n // Header + its subcomponents\n HeaderComponent,\n HeaderActionsComponent,\n HeaderDividerComponent,\n HeaderLanguageComponent,\n HeaderSearchComponent,\n HeaderUserComponent,\n // Admin header\n AdminHeaderComponent,\n AdminHeaderFieldTemplateDirective,\n // Breadcrumbs / anchors / affix\n BreadcrumbsComponent,\n AnchorComponent,\n AffixComponent,\n BackToTopComponent,\n // Tabs / stepper\n TabComponent,\n StepperComponent,\n // Language dropdown\n LanguageDropdownComponent,\n DEFAULT_LANGUAGES,\n // Application shell sidebar + header\n AppSidebarComponent,\n AppHeaderComponent,\n DEFAULT_APP_SIDEBAR_LABELS,\n DEFAULT_APP_SIDEBAR_MENU,\n DEFAULT_APP_SIDEBAR_LANGUAGES,\n DEFAULT_APP_SIDEBAR_ROLES,\n APP_SIDEBAR_MENU_ICONS,\n DEFAULT_APP_HEADER_LABELS\n} from '@pecb-ui/components';\n\nexport type {\n // Breadcrumbs\n BreadcrumbSeparator,\n // Anchor\n AnchorDirection,\n AnchorItem,\n AnchorLevel,\n // Affix\n AffixPosition,\n // Tab\n TabSize,\n TabStyle,\n // Stepper\n StepItem,\n StepOrder,\n StepState,\n StepperDirection,\n StepperSize,\n StepperTailStyle,\n StepperType,\n // Header\n HeaderActionButton,\n HeaderLanguageOption,\n HeaderSearchCategory,\n // Admin header\n AdminHeaderAction,\n AdminHeaderActionType,\n AdminHeaderBadgeVariant,\n AdminHeaderField,\n AdminHeaderFieldType,\n AdminHeaderMetadata,\n AdminHeaderMetadataType,\n AdminHeaderProfile,\n // Language dropdown\n LanguageDisplayMode,\n LanguageOption,\n // Application shell sidebar\n AppSidebarLabels,\n AppSidebarMenuItem,\n AppSidebarMenuOption,\n AppSidebarMenuOptionChange,\n AppSidebarNavGroup,\n AppSidebarNavItem,\n AppSidebarProduct,\n AppSidebarRouterLink,\n AppSidebarUser,\n // Application shell header\n AppHeaderAction,\n AppHeaderChip,\n AppHeaderChipTone,\n AppHeaderLabels,\n AppHeaderNotification\n} from '@pecb-ui/components';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":";;AAAA;;;;;;;;;AASG;;ACTH;;AAEG"}