npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@odx/product-fruits

v6.1.0

Published

## Table of contents

Readme

@odx/product-fruits

Table of contents

Install 🚀

npm install @odx/product-fruits

⚠️ Note: @odx/product-fruits includes everything you need. Avoid installing npm install product-fruits separately, as it may cause issues.

Usage

Setup product fruits by initializing it's enviroment providers with provideProductFruits inside the applications providers array:

import { provideProductFruits } from '@odx/product-fruits';

bootstrapApplication(AppComponent, {
  providers: [
    // ...
    provideProductFruits(/* ...options */),
  ],
});

Configuration Options

The provideProductFruits function accepts two parameters:

  1. Options Factory - A function that returns the Product Fruits configuration
  2. Setup Options (optional) - Additional setup configuration

Synchronous Configuration

The simplest way to configure Product Fruits is with a synchronous object:

provideProductFruits(() => ({
  language: 'en',
  workspaceCode: 'your_workspace_code',
  user: {
    username: '[email protected]',
    props: {
      // Optional custom user properties
      role: 'admin',
      company: 'Draeger',
    },
  },
  // Optional: error handler
  onError: (error) => console.error('Product Fruits error:', error),
}));

Asynchronous Configuration

You can use Observables or Promises to load configuration asynchronously, for example from an authentication service:

provideProductFruits(() =>
  inject(AuthService).identityClaims$.pipe(
    filter((user) => !!user), // Filter out unauthenticated users
    map((user) => ({
      language: user?.preferredLanguage ?? 'en',
      user: {
        username: user?.email,
        props: {
          /* user props */
        },
      },
      workspaceCode: /* workspace code */,
    })),
  ),
);

Important: When using Observables with operators like filter(), the Observable might never emit (e.g., for unauthenticated users). To prevent blocking the app bootstrap, a timeout is automatically applied (see Setup Options below).

Setup Options

The second parameter allows you to configure initialization behavior:

provideProductFruits(
  () => ({
    /* ...config */
  }),
  {
    // Disable Product Fruits initialization entirely
    disabled: false,

    // Timeout in milliseconds for Observable-based configuration
    // If the Observable doesn't emit within this time, initialization is skipped
    // Default: 5000ms (5 seconds)
    timeout: 5000,
  },
);

Timeout behavior:

  • Only applies to Observable-based configurations
  • Prevents app from hanging if Observable never emits (e.g., filtered authentication streams)
  • Default is 5000ms - enough for most auth checks, but not too long for unauthenticated users
  • Adjust value basing on your authentication service needs
  • The app will continue bootstrapping after the timeout, Product Fruits simply won't initialize

Example with custom timeout:

provideProductFruits(
  () =>
    inject(AuthService).identityClaims$.pipe(
      filter((user) => !!user),
      map((user) => ({
        /* ...config */
      })),
    ),
  { timeout: 3000 }, // Wait up to 3 seconds for authentication
);

Returning null:

If your factory returns null, Product Fruits won't be initialized:

provideProductFruits(() => {
  const env = inject(Environment);
  // Don't initialize in development
  if (env.isDevelopment) return null;
  return {
    /* ...config */
  };
});

Newsfeed

In order to setup the newsfeed UI add the ProductFruitsNewsFeedDirective to your imports and use it in the HeaderComponent:

<odx-header>
  <odx-action-group>
    <button odxButton odxProductFruitsNewsfeed></button>
  </odx-action-group>
</odx-header>

The newsfeed button will be disabled if product fruits failed to initialize.

Legacy LifeRing

Deprecated - ProductFruitsLifeRingDirective no longer supports legacy LifeRing. If you had collections of help articles created for the earlier version, you can easily migrate them to v2 in the PF admin app.

LifeRing v2

If you have the basic setup of help articles in your PF admin, the default lifering floating button will appear itself on your webpage - thats all.

However, in order to setup the alternative lifering trigger button, add the ProductFruitsLifeRingDirective to your imports and use it in the HeaderComponent. This will automatically hide the floating button, but it is also recommended to turn it off in your PF configuration.

Heads‑up:

The odxProductFruitsLifeRing trigger may not work reliably when placed inside nested, dynamically positioned UI (for example, odx dropdowns, overlays, or popovers). Prefer mounting it in a stable, top‑level container such as your header. Ensure the trigger is reachable and accessible by design: it must be visible and enabled, part of the natural tab order, not clipped or covered by other elements, and operable via keyboard and assistive technologies.

<odx-header>
  <odx-action-group>
    <button odxButton odxProductFruitsLifeRing></button>
  </odx-action-group>
</odx-header>
provideProductFruits(() => ({
  language: 'en',
  user: { username: 'TEST_USER' },
  workspaceCode: 'your_workspace_code'
  hideInAppCenterLauncher: true, // <-- add this
})),

The life-ring button will be disabled if product fruits failed to initialize.

Change language

In order to change the language during runtime use the updateLanguage method of ProductFruitsService.

Host Theming

By default, this library applies ODX theming to certain Product Fruits components to maintain a consistent look. You can control this behavior using the hostTheming configuration object.

hostTheming.bannerV3Background

When false (default), the library forces the banner background color to match ODX theming, overriding any inline styles Product Fruits may set. When true, Product Fruits (or your own host CSS) controls the banner background.

provideProductFruits(() => ({
  language: 'en',
  user: { username: 'TEST_USER' },
  workspaceCode: 'your_workspace_code',
  hostTheming: {
    bannerV3Background: true, // Allow PF/host to control banner background
  },
}));

Knowledgebase custom styles

Inside the Customer Portal Settings under the Help category, find the Body HTML or script tags field and paste the snippet below to apply ODX typography, colors, and article navigation styles across the knowledgebase:

<style>
  /* Draeger Pangea Text - Regular (400) */
  @font-face {
    font-display: swap;
    font-family: 'Draeger Pangea Text';
    font-style: normal;
    font-weight: 400;
    src: url('https://42.draeger.com/globalAssets/Fonts/DraegerPangeaText-Regular-Latin.woff2') format('woff2');
    unicode-range: U+0000-00FF, U+0100-024F, U+1E00-1EFF; /* Latin + extended */
  }

  @font-face {
    font-display: swap;
    font-family: 'Draeger Pangea Text';
    font-style: normal;
    font-weight: 400;
    src: url('https://42.draeger.com/globalAssets/Fonts/DraegerPangeaText-Regular-Greek.woff2') format('woff2');
    unicode-range: U+0370-03FF, U+1F00-1FFF; /* Greek */
  }

  @font-face {
    font-display: swap;
    font-family: 'Draeger Pangea Text';
    font-style: normal;
    font-weight: 400;
    src: url('https://42.draeger.com/globalAssets/Fonts/DraegerPangeaText-Regular-Cyrillic.woff2') format('woff2');
    unicode-range: U+0400-04FF, U+0500-052F, U+2DE0-2DFF, U+A640-A69F; /* Cyrillic */
  }

  /* Draeger Pangea Text - SemiBold (600) */
  @font-face {
    font-display: swap;
    font-family: 'Draeger Pangea Text';
    font-style: normal;
    font-weight: 600;
    src: url('https://42.draeger.com/globalAssets/Fonts/DraegerPangeaText-SemiBold-Latin.woff2') format('woff2');
    unicode-range: U+0000-00FF, U+0100-024F, U+1E00-1EFF; /* Latin + extended */
  }

  @font-face {
    font-display: swap;
    font-family: 'Draeger Pangea Text';
    font-style: normal;
    font-weight: 600;
    src: url('https://42.draeger.com/globalAssets/Fonts/DraegerPangeaText-SemiBold-Greek.woff2') format('woff2');
    unicode-range: U+0370-03FF, U+1F00-1FFF; /* Greek */
  }

  @font-face {
    font-display: swap;
    font-family: 'Draeger Pangea Text';
    font-style: normal;
    font-weight: 600;
    src: url('https://42.draeger.com/globalAssets/Fonts/DraegerPangeaText-SemiBold-Cyrillic.woff2') format('woff2');
    unicode-range: U+0400-04FF, U+0500-052F, U+2DE0-2DFF, U+A640-A69F; /* Cyrillic */
  }

  /* Draeger Pangea Display - Medium (500) */
  @font-face {
    font-display: swap;
    font-family: 'Draeger Pangea Display';
    font-style: normal;
    font-weight: 500;
    src: url('https://42.draeger.com/globalAssets/Fonts/DraegerPangeaDisplay-Medium-Latin.woff2') format('woff2');
    unicode-range: U+0000-00FF, U+0100-024F, U+1E00-1EFF; /* Latin + extended */
  }

  @font-face {
    font-display: swap;
    font-family: 'Draeger Pangea Display';
    font-style: normal;
    font-weight: 500;
    src: url('https://42.draeger.com/globalAssets/Fonts/DraegerPangeaDisplay-Medium-Greek.woff2') format('woff2');
    unicode-range: U+0370-03FF, U+1F00-1FFF; /* Greek */
  }

  @font-face {
    font-display: swap;
    font-family: 'Draeger Pangea Display';
    font-style: normal;
    font-weight: 500;
    src: url('https://42.draeger.com/globalAssets/Fonts/DraegerPangeaDisplay-Medium-Cyrillic.woff2') format('woff2');
    unicode-range: U+0400-04FF, U+0500-052F, U+2DE0-2DFF, U+A640-A69F; /* Cyrillic */
  }

  /* Standalone subset of ODX tokens used by this theme (light mode values) */
  :root {
    /* Palette subset */
    --odx-palette-blue-00: #eef3fc;
    --odx-palette-blue-80: #002766;
    --odx-palette-coolgray-00: #f9fafb;
    --odx-palette-white: #ffffff;

    /* Derived colors (resolved for light theme) */
    --odx-color-background-base: var(--odx-palette-coolgray-00);
    --odx-color-background-primary-rest: var(--odx-palette-blue-80);
    --odx-color-foreground-rest: var(--odx-palette-blue-80);

    /* Theme convenience aliases */
    --primary-bg-color: var(--odx-color-background-primary-rest, #002766);
    --primary-fg-color: var(--odx-color-foreground-rest, #0f172a);

    /* Typography families */
    --odx-typography-font-family-base: 'Draeger Pangea Text';
    --odx-typography-font-family-brand: 'Draeger Pangea Display';

    /* Heading sizes */
    --odx-typography-font-size-heading-xxl: 2.5rem;
    --odx-typography-font-size-heading-xl: 1.875rem;
    --odx-typography-font-size-heading-lg: 1.5rem;
    --odx-typography-font-size-heading-md: 1.25rem;
    --odx-typography-font-size-heading-sm: 1rem;
    --odx-typography-font-size-heading-xs: 0.875rem;
  }

  html {
    color: var(--odx-color-foreground-rest);
  }

  html,
  body {
    font-family:
      'Draeger Pangea Text',
      Inter,
      system-ui,
      -apple-system,
      'Segoe UI',
      Roboto,
      Helvetica,
      Arial,
      'Apple Color Emoji',
      'Segoe UI Emoji',
      'Segoe UI Symbol';
  }

  body {
    background-color: var(--odx-color-background-base);
  }

  .footer {
    background-color: var(--odx-color-background-base);
  }

  .article-v2 h1,
  .article-v2 h2,
  .article-v2 h3,
  .article-v2 h4,
  .article-v2 h5,
  .article-v2 h6 {
    color: var(--odx-color-foreground-rest);
    font-weight: 600;
  }

  .article-v2 h1,
  .article-v2 h2 {
    font-family:
      var(--odx-typography-font-family-brand),
      'Draeger Pangea Text',
      Inter,
      system-ui,
      -apple-system,
      'Segoe UI',
      Roboto,
      Helvetica,
      Arial,
      'Apple Color Emoji',
      'Segoe UI Emoji',
      'Segoe UI Symbol';
    font-weight: 500;
  }

  .article h1,
  .article h2,
  .article h3,
  .article h4,
  .article h5,
  .article h6 {
    color: var(--odx-color-foreground-rest);
    font-weight: 600;
  }

  .article h1 {
    font-size: var(--odx-typography-font-size-heading-xxl, 2.5rem);
  }

  .article h2 {
    font-size: var(--odx-typography-font-size-heading-xl, 1.875rem);
  }

  .article h3 {
    font-size: var(--odx-typography-font-size-heading-lg, 1.5rem);
  }

  .article h4 {
    font-size: var(--odx-typography-font-size-heading-md, 1.25rem);
  }

  .article h5 {
    font-size: var(--odx-typography-font-size-heading-sm, 1rem);
  }

  .article h6 {
    font-size: var(--odx-typography-font-size-heading-xs, 0.875rem);
  }

  .article-navigation .article-nav-link {
    padding-left: 3px;
  }

  .article-navigation .active {
    color: var(--primary-bg-color);
    font-weight: 600;
  }

  .articles-menu .active {
    box-shadow: -2px 0 0 0 var(--primary-bg-color);
  }

  .articles-menu .article-menu-item:hover {
    box-shadow: -2px 0 0 0 var(--primary-bg-color);
  }
</style>

Live demo ⭐

Please refer to our Storybook, to see the components in action and to get further information.