Skip to content
Marketing block

Filterable FAQ with Fallback

A search that narrows the list across answers and keywords, announces the count, and turns an empty result into the contact route it should always have been.

231 lineslucide-reactAdded 24 Aug 2026Updated 13 Sept 2026
  • faq
  • search
  • filter
  • help centre
  • support

What's included

  • components/faq-search.tsx
  • Needs lucide-react

Works with

  • React
  • Next.js
  • Tailwind CSS
  • TypeScript

npx hoverlab add faq-search

Or over MCP, from your editor's agent — no account needed.

License

Free to read, copy and install, for personal and non-commercial projects. Shipping it in client work or a paid product needs Pro ($79 once). The source lands in your repo and stops being ours — no attribution, nothing to upgrade.

Was this useful?

Start a page with this section — add more, order them, and leave with the page source.

Preview

Help

Search the questions

Can I change plans mid-cycle?

Yes, immediately. Upgrades are prorated to the day; downgrades credit the unused remainder against your next invoice rather than refunding it.

Do you support single sign-on?

SAML 2.0 and OIDC on Team and above, with SCIM provisioning on Enterprise. Okta, Entra ID, Google Workspace and JumpCloud are tested; anything standards-compliant should work.

How do you handle compliance?

SOC 2 Type II, audited annually, with the current report available under NDA. GDPR and UK GDPR as a processor, with a DPA you can sign from the billing page. HIPAA BAAs on Enterprise.

What is the API rate limit?

600 requests a minute per workspace on paid plans, 60 on trials. Limits are per workspace rather than per key, so adding keys does not add capacity.

Can we get a refund?

Within 30 days of a first purchase, no questions asked, on any plan. After that we credit rather than refund, except where local law says otherwise.

Where is our data stored?

In the region you pick at workspace creation: Virginia, Frankfurt or Sydney. It does not leave that region, including for backups.

Do you help with migration?

Yes, at no charge, on any plan. Most migrations take an afternoon and our exports import without a schema rewrite.

What happens when a trial ends?

The workspace becomes read-only rather than being deleted. Everything is still there for 30 days, and adding a card restores it instantly.

Rendered live in your current theme — this is the same component whose source is below, not a screenshot of it.

Source

components/faq-search.tsx
'use client'

/**
 * <FaqSearch> — a filter over the questions, and a way out when it finds nothing.
 *
 * <FaqCategorized> helps a reader who knows which topic their question is
 * under. Plenty of readers do not — they know a word. They know "SSO", or
 * "refund", or "rate limit", and they will type it into whatever box is
 * nearest, which on most help pages is the browser's own find bar.
 *
 * Filtering rather than jumping is the difference from every other FAQ block
 * here. Cmd-F highlights the first match and leaves the other thirty-nine
 * questions on screen; this removes them, which is what turns forty
 * questions into the two that matter.
 *
 * It searches answers as well as questions. Someone typing "SOC 2" is
 * looking for a word that appears in an answer whose question is phrased
 * "how do you handle compliance", and a title-only filter tells them the
 * page has nothing — the single most common way this pattern fails.
 *
 * The empty state is the reason the block exists. A search that finds
 * nothing is the clearest signal you will ever get that a visitor has an
 * unanswered question and is about to leave, so it is the one moment worth
 * spending a contact route on. An empty state that only says "no results" is
 * a dead end at exactly the wrong time.
 *
 * Two accessibility details:
 *
 *   - The result count is a `role="status"` live region, so a screen reader
 *     hears "3 of 12 questions" as the list narrows. Without it, typing
 *     silently changes the page for anyone not watching it.
 *   - The input is a real `<label>` and a `type="search"`, not a placeholder
 *     doing double duty. Placeholder-as-label disappears on first keystroke,
 *     which is when it is most needed.
 *
 * Uncontrolled `<details>` here rather than the exclusive `name` grouping
 * <FaqAccordion> uses: with a filter, having one answer close another is
 * disorienting, because the reader is usually comparing the two results the
 * search left them with.
 */

import * as React from 'react'
import { Plus, Search } from 'lucide-react'

export interface SearchableQuestion {
  question: string
  answer: string
  /** Extra words that should match but do not belong in the copy. */
  keywords?: string[]
}

export interface FaqSearchProps {
  eyebrow?: string
  heading?: string
  inputLabel?: string
  placeholder?: string
  questions?: SearchableQuestion[]
  emptyHeading?: string
  emptyBody?: string
  contactLabel?: string
  contactHref?: string
  className?: string
}

const DEFAULT_QUESTIONS: SearchableQuestion[] = [
  {
    question: 'Can I change plans mid-cycle?',
    answer:
      'Yes, immediately. Upgrades are prorated to the day; downgrades credit the unused remainder against your next invoice rather than refunding it.',
    keywords: ['upgrade', 'downgrade', 'proration'],
  },
  {
    question: 'Do you support single sign-on?',
    answer:
      'SAML 2.0 and OIDC on Team and above, with SCIM provisioning on Enterprise. Okta, Entra ID, Google Workspace and JumpCloud are tested; anything standards-compliant should work.',
    keywords: ['SSO', 'SAML', 'Okta', 'SCIM'],
  },
  {
    question: 'How do you handle compliance?',
    answer:
      'SOC 2 Type II, audited annually, with the current report available under NDA. GDPR and UK GDPR as a processor, with a DPA you can sign from the billing page. HIPAA BAAs on Enterprise.',
    keywords: ['SOC 2', 'GDPR', 'HIPAA', 'DPA'],
  },
  {
    question: 'What is the API rate limit?',
    answer:
      '600 requests a minute per workspace on paid plans, 60 on trials. Limits are per workspace rather than per key, so adding keys does not add capacity.',
    keywords: ['throttle', '429'],
  },
  {
    question: 'Can we get a refund?',
    answer:
      'Within 30 days of a first purchase, no questions asked, on any plan. After that we credit rather than refund, except where local law says otherwise.',
    keywords: ['money back', 'cancel'],
  },
  {
    question: 'Where is our data stored?',
    answer:
      'In the region you pick at workspace creation: Virginia, Frankfurt or Sydney. It does not leave that region, including for backups.',
    keywords: ['residency', 'EU', 'region'],
  },
  {
    question: 'Do you help with migration?',
    answer:
      'Yes, at no charge, on any plan. Most migrations take an afternoon and our exports import without a schema rewrite.',
    keywords: ['import', 'switch', 'onboarding'],
  },
  {
    question: 'What happens when a trial ends?',
    answer:
      'The workspace becomes read-only rather than being deleted. Everything is still there for 30 days, and adding a card restores it instantly.',
    keywords: ['expire', 'trial'],
  },
]

export function FaqSearch({
  eyebrow = 'Help',
  heading = 'Search the questions',
  inputLabel = 'Search frequently asked questions',
  placeholder = 'Try “SSO”, “refund”, “rate limit”…',
  questions = DEFAULT_QUESTIONS,
  emptyHeading = 'Nothing here matches that',
  emptyBody = 'Which probably means it is a question we should be answering. Send it over and you will get a real reply, usually the same day.',
  contactLabel = 'Ask us directly',
  contactHref = '#',
  className = '',
}: FaqSearchProps) {
  /*
    Ids come from `useId`, not from a literal.

    A literal is correct until the section appears twice on one page — and
    then `aria-labelledby` resolves to whichever copy is first in the
    document, so the second one is announced with the first one's heading.
    It renders identically and reads wrongly, which is why nothing catches
    it but an id check.
  */
  const headingId = React.useId()
  const inputId = React.useId()

  const [query, setQuery] = React.useState('')

  const results = React.useMemo(() => {
    const needle = query.trim().toLowerCase()
    if (!needle) return questions
    return questions.filter((item) =>
      [item.question, item.answer, ...(item.keywords ?? [])]
        .join(' ')
        .toLowerCase()
        .includes(needle),
    )
  }, [query, questions])

  const filtering = query.trim().length > 0

  return (
    <section
      aria-labelledby={headingId}
      className={`mx-auto w-full max-w-3xl px-4 py-16 sm:px-6 lg:px-8 ${className}`}
    >
      <div className="text-center">
        <p className="text-sm font-medium uppercase tracking-wider text-primary">{eyebrow}</p>
        <h2
          id={headingId}
          className="mt-3 text-balance text-3xl font-bold tracking-tight text-foreground sm:text-4xl"
        >
          {heading}
        </h2>
      </div>

      <div className="mt-8">
        <label htmlFor={inputId} className="sr-only">
          {inputLabel}
        </label>
        <div className="relative">
          <Search
            aria-hidden
            className="pointer-events-none absolute start-4 top-1/2 h-5 w-5 -translate-y-1/2 text-muted-foreground"
          />
          <input
            id={inputId}
            type="search"
            value={query}
            onChange={(event) => setQuery(event.target.value)}
            placeholder={placeholder}
            className="h-12 w-full rounded-xl border border-field bg-background ps-12 pe-4 text-base text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
          />
        </div>

        {/* Announced as the list narrows, not just drawn. */}
        <p role="status" className="mt-3 h-5 text-sm text-muted-foreground">
          {filtering
            ? `${results.length} of ${questions.length} question${questions.length === 1 ? '' : 's'}`
            : ''}
        </p>
      </div>

      {results.length > 0 ? (
        <div className="mt-4 divide-y divide-border/60 border-y border-border/60">
          {results.map((item) => (
            <details key={item.question} className="group py-1">
              <summary className="flex cursor-pointer list-none items-start justify-between gap-6 py-4 text-start font-medium text-foreground marker:content-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background">
                {item.question}
                <Plus
                  aria-hidden
                  className="mt-0.5 h-5 w-5 shrink-0 text-muted-foreground transition-transform duration-200 group-open:rotate-45"
                />
              </summary>
              <p className="max-w-prose text-pretty pb-5 pe-10 leading-relaxed text-muted-foreground">
                {item.answer}
              </p>
            </details>
          ))}
        </div>
      ) : (
        <div className="mt-4 rounded-2xl border border-dashed border-border p-10 text-center">
          <p className="text-lg font-semibold text-foreground">{emptyHeading}</p>
          <p className="mx-auto mt-2 max-w-md text-pretty leading-relaxed text-muted-foreground">
            {emptyBody}
          </p>
          <a
            href={contactHref}
            className="mt-6 inline-flex h-10 items-center rounded-lg bg-primary px-5 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
          >
            {contactLabel}
          </a>
        </div>
      )}
    </section>
  )
}

Before you paste

  • Styling is Tailwind utility classes on semantic tokens (bg-card, text-muted-foreground) — it inherits your theme instead of overriding it.
  • Install: npm i lucide-react
  • Every prop has a default, so it renders standalone before you wire it up.

Where it goes

Drop it at components/faq-search.tsx and import it where you need the section:

import { FaqSearch } from '@/components/faq-search'

Customize

7 of this block’s props are simple enough to drive from here. Change them and the block below re-renders — it is the same component whose source is above, not a mock of it. Everything else it accepts is in the table underneath.

Props

Read out of the component’s own type and signature, so this cannot drift from the source below. Every prop has a default — the component renders standalone before you pass it anything.

PropTypeDefault
eyebrowstring'Help'
headingstring'Search the questions'
inputLabelstring'Search frequently asked questions'
placeholderstring'Try “SSO”, “refund”, “rate limit”…'
questionsSearchableQuestion[]DEFAULT_QUESTIONS
emptyHeadingstring'Nothing here matches that'
emptyBodystring—
contactLabelstring'Ask us directly'
contactHrefstring'#'
classNamestring''

Not using React?

The same block rendered once to markup, wrapped as a file your framework compiles. Tailwind classes are framework-agnostic, so the design transfers intact — the behaviour does not.

This block is interactive. The markup below is its initial state with the event handlers stripped — you will need to re-wire the behaviour in your framework.

faq-search.html
<!--
  Filterable FAQ with Fallback — markup from the Hoverlab catalog.

  This is the block rendered once to HTML and wrapped as a component
  file. It is not a port of the React source: the Tailwind classes carry
  the design, which is the part that took the work, and they are the same
  in every framework.

  This block is interactive in React and the handlers are NOT here.
  Buttons, toggles and menus render in their initial state and do
  nothing until you wire them up.
-->
<section aria-labelledby="_R_0_" class="mx-auto w-full max-w-3xl px-4 py-16 sm:px-6 lg:px-8 ">
  <div class="text-center">
    <p class="text-sm font-medium uppercase tracking-wider text-primary">Help</p>
    <h2 id="_R_0_" class="mt-3 text-balance text-3xl font-bold tracking-tight text-foreground sm:text-4xl">Search the questions</h2>
  </div>
  <div class="mt-8">
    <label for="_R_0H1_" class="sr-only">Search frequently asked questions</label>
    <div class="relative">
      <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-search pointer-events-none absolute start-4 top-1/2 h-5 w-5 -translate-y-1/2 text-muted-foreground" aria-hidden="true">
        <path d="m21 21-4.34-4.34"></path>
        <circle cx="11" cy="11" r="8"></circle>
      </svg>
      <input id="_R_0H1_" type="search" placeholder="Try “SSO”, “refund”, “rate limit”…" class="h-12 w-full rounded-xl border border-field bg-background ps-12 pe-4 text-base text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background" value="" />
    </div>
    <p role="status" class="mt-3 h-5 text-sm text-muted-foreground"></p>
  </div>
  <div class="mt-4 divide-y divide-border/60 border-y border-border/60">
    <details class="group py-1">
      <summary class="flex cursor-pointer list-none items-start justify-between gap-6 py-4 text-start font-medium text-foreground marker:content-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background">Can I change plans mid-cycle?<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-plus mt-0.5 h-5 w-5 shrink-0 text-muted-foreground transition-transform duration-200 group-open:rotate-45" aria-hidden="true"><path d="M5 12h14"></path><path d="M12 5v14"></path></svg></summary>
      <p class="max-w-prose text-pretty pb-5 pe-10 leading-relaxed text-muted-foreground">Yes, immediately. Upgrades are prorated to the day; downgrades credit the unused remainder against your next invoice rather than refunding it.</p>
    </details>
    <details class="group py-1">
      <summary class="flex cursor-pointer list-none items-start justify-between gap-6 py-4 text-start font-medium text-foreground marker:content-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background">Do you support single sign-on?<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-plus mt-0.5 h-5 w-5 shrink-0 text-muted-foreground transition-transform duration-200 group-open:rotate-45" aria-hidden="true"><path d="M5 12h14"></path><path d="M12 5v14"></path></svg></summary>
      <p class="max-w-prose text-pretty pb-5 pe-10 leading-relaxed text-muted-foreground">SAML 2.0 and OIDC on Team and above, with SCIM provisioning on Enterprise. Okta, Entra ID, Google Workspace and JumpCloud are tested; anything standards-compliant should work.</p>
    </details>
    <details class="group py-1">
      <summary class="flex cursor-pointer list-none items-start justify-between gap-6 py-4 text-start font-medium text-foreground marker:content-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background">How do you handle compliance?<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-plus mt-0.5 h-5 w-5 shrink-0 text-muted-foreground transition-transform duration-200 group-open:rotate-45" aria-hidden="true"><path d="M5 12h14"></path><path d="M12 5v14"></path></svg></summary>
      <p class="max-w-prose text-pretty pb-5 pe-10 leading-relaxed text-muted-foreground">SOC 2 Type II, audited annually, with the current report available under NDA. GDPR and UK GDPR as a processor, with a DPA you can sign from the billing page. HIPAA BAAs on Enterprise.</p>
    </details>
    <details class="group py-1">
      <summary class="flex cursor-pointer list-none items-start justify-between gap-6 py-4 text-start font-medium text-foreground marker:content-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background">What is the API rate limit?<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-plus mt-0.5 h-5 w-5 shrink-0 text-muted-foreground transition-transform duration-200 group-open:rotate-45" aria-hidden="true"><path d="M5 12h14"></path><path d="M12 5v14"></path></svg></summary>
      <p class="max-w-prose text-pretty pb-5 pe-10 leading-relaxed text-muted-foreground">600 requests a minute per workspace on paid plans, 60 on trials. Limits are per workspace rather than per key, so adding keys does not add capacity.</p>
    </details>
    <details class="group py-1">
      <summary class="flex cursor-pointer list-none items-start justify-between gap-6 py-4 text-start font-medium text-foreground marker:content-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background">Can we get a refund?<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-plus mt-0.5 h-5 w-5 shrink-0 text-muted-foreground transition-transform duration-200 group-open:rotate-45" aria-hidden="true"><path d="M5 12h14"></path><path d="M12 5v14"></path></svg></summary>
      <p class="max-w-prose text-pretty pb-5 pe-10 leading-relaxed text-muted-foreground">Within 30 days of a first purchase, no questions asked, on any plan. After that we credit rather than refund, except where local law says otherwise.</p>
    </details>
    <details class="group py-1">
      <summary class="flex cursor-pointer list-none items-start justify-between gap-6 py-4 text-start font-medium text-foreground marker:content-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background">Where is our data stored?<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-plus mt-0.5 h-5 w-5 shrink-0 text-muted-foreground transition-transform duration-200 group-open:rotate-45" aria-hidden="true"><path d="M5 12h14"></path><path d="M12 5v14"></path></svg></summary>
      <p class="max-w-prose text-pretty pb-5 pe-10 leading-relaxed text-muted-foreground">In the region you pick at workspace creation: Virginia, Frankfurt or Sydney. It does not leave that region, including for backups.</p>
    </details>
    <details class="group py-1">
      <summary class="flex cursor-pointer list-none items-start justify-between gap-6 py-4 text-start font-medium text-foreground marker:content-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background">Do you help with migration?<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-plus mt-0.5 h-5 w-5 shrink-0 text-muted-foreground transition-transform duration-200 group-open:rotate-45" aria-hidden="true"><path d="M5 12h14"></path><path d="M12 5v14"></path></svg></summary>
      <p class="max-w-prose text-pretty pb-5 pe-10 leading-relaxed text-muted-foreground">Yes, at no charge, on any plan. Most migrations take an afternoon and our exports import without a schema rewrite.</p>
    </details>
    <details class="group py-1">
      <summary class="flex cursor-pointer list-none items-start justify-between gap-6 py-4 text-start font-medium text-foreground marker:content-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background">What happens when a trial ends?<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-plus mt-0.5 h-5 w-5 shrink-0 text-muted-foreground transition-transform duration-200 group-open:rotate-45" aria-hidden="true"><path d="M5 12h14"></path><path d="M12 5v14"></path></svg></summary>
      <p class="max-w-prose text-pretty pb-5 pe-10 leading-relaxed text-muted-foreground">The workspace becomes read-only rather than being deleted. Everything is still there for 30 days, and adding a card restores it instantly.</p>
    </details>
  </div>
</section>
  • This is rendered HTML, not a translation of the React source. The Tailwind classes carry the design and work in any framework.
  • It is one frame: the component in its initial state, with no props applied beyond the defaults.
  • This block is interactive in React — toggles, menus or form state. None of that survives here; the markup is the closed/default state and the handlers are gone. Re-wire them in your own framework.
  • Requires Tailwind, and the design tokens the classes reference (bg-card, text-muted-foreground, and so on). The template ZIPs ship a globals.css that defines them.

What each framework gets across the whole catalog — effects convert properly; this rung is markup.

For AI

The component, its props, the design tokens it expects and the command that installs it — as one prompt. Paste it into Claude, Cursor, v0 or ChatGPT and what they build around it will match the rest of the catalog instead of inventing its own system.

See the prompt

Used in these pages

Want the whole screen instead of this one section? Open a page and copy it entire.

6 more blocks in FAQ

All of them free to read, copy and install — no account, no locked tiles, no watermarked preview. The whole catalog is open, and so are the API and the CLI.

Browse FAQ

Shipping one commercially

Copying the code is free. Putting it in client work or a paid product is what Pro is for — the licence, not the access.

  • A commercial licence for everything in the catalog
  • Unlimited bundle exports, in Vue, Svelte and Tailwind
  • One payment — no subscription, nothing to renew
Pro — $79 once

More FAQ blocks

View category
Open the full page for this block

No-JS FAQ Accordion

Exclusive accordion behaviour from named <details> elements — no state, no hooks, no JavaScript, full keyboard support for free.

FAQ121 lines1 dep
Open the full page for this block

Two-Column FAQ with Help Rail

Questions on the right against a sticky rail that keeps a route to a human in view — for the visitor whose objection is not on the list.

FAQ170 lines1 dep
Open the full page for this block

Open FAQ Grid

Every answer rendered and visible in a two-column description list — the FAQ shape for a marketing page, where collapsed text is text a search engine will not quote.

FAQ109 linesNo deps
Open the full page for this block

FAQ Grouped by Topic

Anchor-linked topic pills over per-topic details groups with the first answer open — for the FAQ that outgrew a single list, working with JavaScript disabled.

FAQ237 lines1 dep
Open the full page for this block

Objection FAQ

An FAQ written from the objections that stop a sale rather than from the questions that are comfortable to answer.

FAQ147 linesNo deps
Open the full page for this block

Security FAQ

The security questions a buyer's IT reviewer asks, answered with scope and dates rather than with the word "enterprise-grade".

FAQ143 linesNo deps