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

@mena-ai/booking-widget

v1.2.0

Published

Embeddable <mena-booking> web component to book therapy appointments on any website. Zero runtime dependencies, Shadow DOM isolation, CSS-custom-property theming, en + pt.

Readme

@mena-ai/booking-widget

<mena-booking> — an embeddable booking widget for clinics and therapists on Mena. One custom element, zero runtime dependencies, Shadow DOM isolation, fully themeable through CSS custom properties.

It runs the full booking flow end to end: service picker → therapist picker → time-slot picker → visitor details + consents → email verification (one-time code) → confirmed booking, with localized error recovery throughout. It is keyboard-accessible, WCAG AA across every preset, and ships English + Portuguese.

Install

npm

npm install @mena-ai/booking-widget
import '@mena-ai/booking-widget';

Importing the package registers the <mena-booking> element (idempotent — safe to import from multiple bundles). Named exports: defineMenaBooking() to register explicitly, and the MenaBookingElement class.

CDN script injection

Pin an exact version (recommended for production — the bytes are immutable, so the Subresource-Integrity hash stays valid forever):

<script
  type="module"
  src="https://cdn.mena-ai.pt/booking-widget/1.1.0/mena-booking.js"
  integrity="sha384-HJ7oFkCdSLln0eHrHGoa4tposvInwKf0G560+crcOLVCrYEoFOxCZF9KaXCpLs9M"
  crossorigin="anonymous"
></script>
<mena-booking publishable-key="mena_pk_live_..." locale="pt"></mena-booking>

For pages that cannot use module scripts, load the IIFE build:

<script
  src="https://cdn.mena-ai.pt/booking-widget/1.1.0/mena-booking.iife.js"
  integrity="sha384-vFzFc0iAL63/BCu9vvffwykQcjqDBXCVJTJl0ATV9lx5D/8JShuboNzhZaoXC4oV"
  crossorigin="anonymous"
></script>

To auto-receive non-breaking updates, use the rolling major channel without an integrity hash (the file is replaced on each patch, so a pinned hash would break):

<script type="module" src="https://cdn.mena-ai.pt/booking-widget/v1/mena-booking.js"></script>

Each published version's hashes are also available at https://cdn.mena-ai.pt/booking-widget/<version>/sri.json and in the GitHub Release notes.

Usage

<mena-booking publishable-key="mena_pk_..." preset="calm" locale="pt"></mena-booking>

Attributes

| Attribute | Required | Values / default | Meaning | | ----------------- | -------- | -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | publishable-key | yes | — | The company's public booking key. Missing/invalid keys render a polite "booking unavailable" state (and emit booking:error) — never a broken widget. | | preset | no | calm (default) · clinical · minimal · warm · bold · dark | Visual preset. Unknown values fall back to calm. | | locale | no | en · pt | UI language. Default resolution: this attribute → browser language → the company's configured locales → en. Unsupported values fall back, never throw. | | therapist-id | no | therapist UUID | Locks the flow to one therapist (the therapist step is skipped). Unknown ids degrade to the config-error state. | | service-id | no | service UUID | Preselects a service; the service step is skipped when the id matches a bookable service. | | api-base | no | https://api.mena-ai.pt | Origin of the production booking API (the default). Override only to point at a non-production backend — e.g. the demo targets a local mock, staging targets the staging origin. |

Events

| Event | When | detail | | ------------------- | ------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------- | | booking:error | Config cannot load (bad key, origin not allow-listed, company not found, network) or catalog/slot data fails | { scope: 'config' \| 'data', code?: string, status: number } | | booking:completed | A booking is created (status pending = awaiting clinic confirmation, or scheduled = auto-confirmed) | { bookingReference: string, status: 'pending' \| 'scheduled' } |

Both events are dispatched with composed: true, bubbles: true, so you can listen on the element, an ancestor, or document. Details carry no PII and no tokens — only the booking reference and status — and never include the publishable key or response bodies.

Theming

The widget is styled entirely through --mb-* custom properties. The preset attribute selects one of six built-in token sheets; any token can be overridden from the host page with plain CSS, because author CSS on the host element always beats the shadow :host defaults:

mena-booking {
  --mb-primary: #0a6e5c;
  --mb-primary-soft: #e1f1ec;
  --mb-focus: #0a6e5c;
}

Tokens (26) and preset values

| Token | calm | clinical | minimal | warm | bold | dark | | -------------------- | -------------------------------------------------------------- | ---------------------------------------- | ------------------------------------------------ | -------------------------------------------------------------- | ---------------------------------------- | -------------------------------------- | | --mb-primary | #3E6557 | #1D5C9C | #161616 | #99471F | #5526D9 | #8FD7BE | | --mb-on-primary | #FFFFFF | #FFFFFF | #FFFFFF | #FFFFFF | #FFFFFF | #0C1A14 | | --mb-primary-soft | #E7EFEA | #E7EFF8 | #F1F1F0 | #F5E7DB | #ECE6FC | #1F2D28 | | --mb-surface | #FAF9F7 | #FFFFFF | #FFFFFF | #FBF5ED | #FFFFFF | #16191F | | --mb-surface-2 | #FFFFFF | #F6F8FA | #FFFFFF | #FFFBF5 | #FAFAFC | #1D2128 | | --mb-field-bg | #FFFFFF | #FFFFFF | #FFFFFF | #FFFBF5 | #FFFFFF | #14171D | | --mb-text | #262E2A | #1B2733 | #161616 | #382E26 | #17131F | #E8EAEF | | --mb-muted | #5C6963 | #4E6071 | #666666 | #6E5F52 | #5C5768 | #9BA4B5 | | --mb-border | #E5E2DA | #D9E0E7 | #E4E4E2 | #EADCCB | #E5E2EC | #2B313C | | --mb-widget-border | none | 1px solid #D9E0E7 | 1px solid #161616 | none | 1.5px solid #17131F | 1px solid #2B313C | | --mb-card-border | 1px solid #ECE9E1 | 1px solid #D9E0E7 | 1px solid #E4E4E2 | 1px solid #EFE3D3 | 1px solid #E5E2EC | 1px solid #2B313C | | --mb-shadow | 0 2px 6px rgba(38,46,42,.04), 0 18px 44px rgba(38,46,42,.09) | 0 1px 2px rgba(27,39,51,.05) | none | 0 2px 8px rgba(96,72,48,.06), 0 18px 44px rgba(96,72,48,.10) | 10px 10px 0 #ECE6FC | 0 20px 50px rgba(0,0,0,.45) | | --mb-shadow-card | 0 1px 3px rgba(38,46,42,.05) | none | none | 0 1px 3px rgba(96,72,48,.06) | 0 1px 2px rgba(23,19,31,.05) | none | | --mb-radius | 18px | 10px | 0px | 22px | 14px | 14px | | --mb-radius-btn | 999px | 7px | 0px | 999px | 10px | 10px | | --mb-radius-field | 12px | 7px | 0px | 14px | 10px | 10px | | --mb-radius-check | 7px | 4px | 0px | 8px | 6px | 6px | | --mb-font-heading | 'Lora', Georgia, serif | 'IBM Plex Sans', system-ui, sans-serif | 'EB Garamond', Georgia, serif | 'Nunito', system-ui, sans-serif | 'Space Grotesk', system-ui, sans-serif | 'Sora', system-ui, sans-serif | | --mb-font-body | 'Karla', system-ui, sans-serif | 'IBM Plex Sans', system-ui, sans-serif | 'Helvetica Neue', Helvetica, Arial, sans-serif | 'Nunito Sans', system-ui, sans-serif | system-ui, -apple-system, sans-serif | system-ui, -apple-system, sans-serif | | --mb-h-size | 23px | 21px | 27px | 22px | 24px | 22px | | --mb-h-weight | 600 | 600 | 500 | 800 | 700 | 600 | | --mb-h-track | 0em | -0.01em | 0em | 0em | -0.02em | -0.01em | | --mb-density | 1.1 | 0.9 | 1 | 1.15 | 1 | 1 | | --mb-focus | #3E6557 | #1D5C9C | #161616 | #99471F | #5526D9 | #8FD7BE | | --mb-error | #A8453A | #B3362B | #9E2F23 | #A8453A | #C72C2C | #E89890 | | --mb-error-bg | #F7ECEA | #F9EBEA | #F6EDEB | #F8ECE6 | #FBEAEA | #3A2422 |

Every preset passes an automated WCAG AA contrast gate enforced by unit tests — text, muted, on-primary and error colours at 4.5:1, focus ring at 3:1, on every surface the widget uses (surface, surface-2, field background, the brand-soft notice tint, and error background). The three "bold" alternate-primary brand colours (violet/crimson/teal) are checked the same way, so a custom --mb-primary keeps AA.

::part() styling

For styling beyond the tokens, these shadow parts are exported:

| Part | Surface | | -------------- | -------------------------------------------------------------- | | container | The widget box itself | | header | Clinic logo + name row | | logo | The 30px logo/initials disc | | progress | Progress row (segments + "Step a of 5" label) | | step | The step heading (h2) | | back | The "← Back" button | | card | Service cards, therapist cards, and the selection summary card | | cta | The primary Continue button | | slot | Individual time-slot buttons | | day | Day cells in the date strip | | day-strip | The scrollable date strip | | jump | The "Next availability" jump button on empty days | | field | Text inputs on the details form (name, email, phone, note) | | consent | A consent card on the details step | | consent-box | The checkbox box inside a consent card | | otp-input | The one-time-code input | | notice | The slot-taken / past / advance-limit notice banner | | chip | The "awaiting confirmation" chip on the pending screen | | confirm-icon | The clock / check icon disc on the confirmation screens | | lock | The "too many attempts" panel on the verification step | | rate-limit | The 429 cooldown box | | retry | The retry button in the data-error state | | error | The config-error / data-error containers | | footer | The "Bookings by Mena.ai" attribution row |

mena-booking::part(cta) {
  text-transform: uppercase;
}

Fonts

Presets name webfonts but the widget never loads font files (privacy + CSP: it makes no third-party requests). Every stack carries a bundled fallback, so the widget always renders correctly. If you want the exact designed look, load the preset's family on your page:

<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />

| Preset | Add this stylesheet link | | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | calm | <link href="https://fonts.googleapis.com/css2?family=Lora:wght@500;600&family=Karla:wght@400;500;600;700&display=swap" rel="stylesheet"> | | clinical | <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600&display=swap" rel="stylesheet"> | | minimal | <link href="https://fonts.googleapis.com/css2?family=EB+Garamond:wght@500;600&display=swap" rel="stylesheet"> | | warm | <link href="https://fonts.googleapis.com/css2?family=Nunito:wght@700;800&family=Nunito+Sans:wght@400;600;700&display=swap" rel="stylesheet"> | | bold | <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&display=swap" rel="stylesheet"> | | dark | <link href="https://fonts.googleapis.com/css2?family=Sora:wght@400;500;600&display=swap" rel="stylesheet"> |

Accessibility

  • Keyboard: every control is reachable and operable by keyboard. The date strip and the time-slot grid use a roving tabindex — one Tab stop each, then arrow keys (and Home/End) move between days/times; disabled days are skipped. The consent checkboxes toggle with Space/Enter.
  • Focus: focus moves to the step heading on each step change and is restored after re-renders. Every interactive element shows a visible --mb-focus ring (:focus-visible).
  • Screen readers: the verification code is a single labelled field; form errors are associated with their inputs and announced; the progress indicator exposes role="progressbar"; the confirmation screen announces via a live region.
  • Motion: all animation honours prefers-reduced-motion: reduce.
  • An automated axe-core gate (run in CI, in en + pt, across every step) asserts zero serious/critical violations.

Design deviations

A few intentional differences from the visual prototype, each for a functional reason:

  • The verification code is one segmented input (autocomplete="one-time-code"), not six boxes, so screen readers hear a single field (the design's own production note).
  • "Change" on the time step restarts the selection at the service picker; the "← Back" arrow still moves one step.
  • The header omits the prototype's clinic city label — the public config carries no city field.
  • Webfonts are named but never loaded by the widget (see Fonts above).
  • A network failure after the server has already created a booking (response lost) can't be told apart from a pre-commit failure; the backend de-duplicates the visitor, so a v1 re-attempt is safe rather than producing two distinct patients.

Privacy

  • No cookies, no localStorage/sessionStorage, no analytics, no tracking of any kind.

  • Visitor data and tokens live in memory only and disappear when the page unloads.

  • The publishable key is never logged and never included in error events or messages.

  • The widget runs no inline scripts and loads no third-party scripts or fonts; it makes network requests only to the Mena booking API. A host-page Content-Security-Policy needs the CDN origin in script-src and the API origin in connect-src:

    Content-Security-Policy:
      script-src 'self' https://cdn.mena-ai.pt;
      connect-src 'self' https://api.mena-ai.pt;

    Pin the integrity hash on an exact-version CDN <script> (see CDN script injection). If you opt into the designed webfonts, also add https://fonts.googleapis.com to style-src and https://fonts.gstatic.com to font-src.

Browser support

Modern evergreen browsers. The bundle targets ES2022 and uses container queries and constructable stylesheets (with a <style> fallback where adoptedStyleSheets is unavailable).

Versioning & channels

The widget follows semver. The CDN exposes two kinds of path:

  • …/booking-widget/<major>.<minor>.<patch>/ — an exact version. Immutable, cached one year, safe to pin with Subresource Integrity. Use this in production.
  • …/booking-widget/v<major>/ — a rolling channel that always serves the latest non-breaking (v1 = newest 1.x) release. Short cache; do not pin SRI here.

Non-breaking changes (new features, fixes) ship within the same /vN/ channel. Breaking changes ship a new major and a new /v<major+1>/ channel; the previous channel keeps working. The npm package version and the CDN path always match.