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

@rishab3355/chefora-ui

v1.0.0

Published

Glassmorphic web components for the Chefora recipe finder and meal planner. Framework-agnostic, built with StencilJS.

Readme

@rishab3355/chefora-ui

Glassmorphic web components for Chefora, a recipe finder and meal planner. Built with StencilJS, so they are standard custom elements and work in any framework — or none.

npm install @rishab3355/chefora-ui

Quick start

The package exports a single defineCustomElements() that registers every component. Call it on the client only, before your app hydrates.

import { defineCustomElements } from '@rishab3355/chefora-ui';

defineCustomElements();
<chef-recipe-card id="card"></chef-recipe-card>

<script type="module">
	// `recipe` is an OBJECT prop, so it must be set as a property.
	// An HTML attribute would arrive as the string "[object Object]".
	document.getElementById('card').recipe = {
		id: 1,
		title: 'Burst Tomato & Basil Rigatoni',
		imageUrl: 'https://example.com/rigatoni.jpg',
		cuisineName: 'Italian',
		totalTimeMinutes: 25,
		servings: 4,
		difficulty: 'Easy',
		rating: 4.9
	};

	document.getElementById('card').addEventListener('chefora-favorite-toggle', (e) => {
		console.log(e.detail); // { id: 1, favorite: true }
	});
</script>

Components

<chef-glass-panel>

The layout primitive — a correctly-styled surface with named holes in it.

| Prop | Type | Default | Notes | | --------------- | --------------------------------------------- | ----------- | ------------------------------------ | | variant | 'flat' \| 'blurred' | 'flat' | See the blur budget below. | | padding | 'none' \| 'compact' \| 'regular' \| 'roomy' | 'regular' | | | sticky | boolean | false | Sticks to its scroll container. | | sticky-offset | string | '0px' | Top offset when sticky. | | as-article | boolean | false | Renders an <article> landmark. | | label | string | — | Accessible name; needs as-article. |

Slots: header, actions, default, footer. A row you do not fill collapses itself, so a content-only panel has no empty bordered strip.

Parts: panel, header, content, footer.

<chef-recipe-card>

| Prop | Type | Default | Notes | | --------------- | ---------------- | ------- | ------------------------------------------------- | | recipe | RecipeCardData | — | Object — set as a property, not an attribute. | | favorite | boolean | false | Kept separate from recipe.favorite. | | loading | boolean | false | Renders a skeleton. | | compact | boolean | false | Denser layout for rails. | | hide-favorite | boolean | false | For anonymous visitors. |

Events: chefora-favorite-toggle{ id, favorite } · chefora-card-open{ id, slug }

Slots: badges, actions, default (the title — provided in the light DOM so it is present in server-rendered HTML).

Parts: card, media, body, title, favorite, plan.

Add show-plan to surface a + Plan quick action, which emits chefora-plan-add{ id, slug }.

<chef-filter-chip>

| Prop | Type | Default | Notes | | ----------- | --------- | ------- | -------------------------------------------------- | | label | string | '' | Visible text, and the fallback accessible name. | | value | string | — | Identity in event payloads. Falls back to label. | | selected | boolean | false | Reflected, so light-DOM CSS can target it. | | removable | boolean | false | Adds a separate remove button. | | count | number | — | Trailing facet count. 0 renders, not hidden. |

Events: chefora-chip-toggle{ value, selected } · chefora-chip-remove{ value }

⚠ The remove affordance is a second focusable button with its own aria-label, not a glyph inside the toggle. Inside, it would be both unlabeled and unreachable by keyboard.

<chef-rating-stars>

| Prop | Type | Default | Notes | | ------------ | -------------------------- | ------- | ------------------------------------- | | value | number \| string \| null | 0 | Accepts a string — JSON BigDecimal. | | readonly | boolean | true | Display-only. | | size | 'sm' \| 'md' \| 'lg' | 'md' | | | show-value | boolean | false | Renders the number beside the stars. |

Events: chefora-rating-change{ value } (interactive mode only)

Readonly renders role="img" with one label — a screen reader says "4.6 out of 5" rather than reading ten star glyphs. Interactive renders a real radiogroup, so arrow-key navigation and a single tab stop come from the platform.

<chef-day-slot>

| Prop | Type | Default | Notes | | ----------- | ------------------------------------ | ---------- | ---------------------------------------------- | | day | number | 1 | ISO: 1 = Monday … 7 = Sunday. | | slot-name | 'BREAKFAST' \| 'LUNCH' \| 'DINNER' | 'DINNER' | ⚠ not slot — that is a global attribute. | | recipe | RecipeCardData | — | Object prop. Absent ⇒ empty slot. | | compact | boolean | false | For a dashboard strip. | | today | boolean | false | Marks the current day. |

Events: chefora-slot-add · chefora-slot-remove · chefora-slot-open, each → { day, slotName, recipeId }

A filled slot renders its open and remove actions as siblings: interactive content cannot nest, so a remove button inside a clickable card is invalid HTML and activates unpredictably.

<chef-quantity-stepper>

| Prop | Type | Default | Notes | | ------------ | --------- | ------------ | ------------------------------ | | value | number | 1 | Clamped into [min, max]. | | min max | number | 1 / 99 | | | label | string | 'Quantity' | Accessible name for the field. | | show-label | boolean | false | Renders the label visibly too. |

Events: chefora-quantity-change{ value, delta } — never emitted for a no-op at a bound.

Uses type="text" + inputmode="numeric", not type="number": number inputs silently discard non-numeric text, mutate on scroll, and have unstyleable spinners.

<chef-search-bar>

| Prop | Type | Default | Notes | | -------------- | --------- | ------- | --------------------------------------------- | | value | string | '' | | | placeholder | string | … | Not the accessible name — see label. | | label | string | … | The real accessible name. | | loading | boolean | false | Spinner + a polite live-region announcement. | | filter-count | number | — | Shows a Filters button with a badge when > 0. |

Events: chefora-search-submit · chefora-search-input{ value } · chefora-filters-open

Slots: trailing.

A real <form> around <input type="search">, so Enter-to-submit, the browser's clear affordance and search history all come from the platform.

<chef-category-tile>

| Prop | Type | Default | Notes | | ---------- | ------------------ | ------- | -------------------------------------------- | | category | CategoryTileData | — | Object prop. Absent ⇒ skeleton. | | href | string | — | Renders a real <a>; omit for a <button>. |

Events: chefora-category-open{ slug, name }

With href it is a link, so middle-click and open-in-new-tab keep working.

<chef-empty-state>

| Prop | Type | Default | Notes | | ------------ | -------------------- | --------- | ----------------------------------------- | | variant | 'empty' \| 'error' | 'empty' | error is role="alert" with a retry. | | heading | string | … | Variant-appropriate default. | | message | string | — | | | show-retry | boolean | — | Overrides the variant default either way. |

Events: chefora-retry

Slots: action, illustration, default.

Theming

Every colour, radius, font and spacing value is read as var(--ch-*, <fallback>), where the fallback is the real Chefora value. So the components look right out of the box, and custom properties pierce the shadow boundary — define the tokens anywhere above them and the whole library re-themes with no ::part plumbing:

:root {
	--ch-accent-solid: #cb3a00;
	--ch-ground: #faf2e9;
	--ch-r-card: 22px;
}

For structural changes, use the part hooks: chef-recipe-card::part(card).

Notes for framework consumers

Event names are kebab-case on purpose. Svelte 5 rejects camelCase handler attributes (onchefFavoriteToggle is invalid syntax), and DOM dispatch is case-sensitive, so a camelCase event would have no working declarative listener in either place. In Svelte, attach them with an action:

<chef-recipe-card use:attachEvents={{ 'chefora-favorite-toggle': onToggle }} {recipe}
></chef-recipe-card>

Server-side rendering. The package is import-safe on the server — importing it in Node does not touch HTMLElement, and defineCustomElements() no-ops when customElements is undefined. Registration still belongs on the client, before hydration rather than in a mount hook, so the first paint of a hard navigation does not take the un-upgraded path.

Reserve the box. Before an element upgrades, :host rules do not apply — the shadow root does not exist yet — so size it from the host app's own CSS or the grid will reflow when the elements upgrade:

chef-recipe-card {
	display: block;
	min-height: 320px;
	contain: layout paint;
}

Hiding un-upgraded elements — carefully. It is tempting to add chef-recipe-card:not(:defined) { opacity: 0 } to avoid a flash of unstyled content. Do not do that on its own: if the JavaScript never arrives — a CDN failure, an offline user, a blocked bundle — :defined never matches, and the content is hidden permanently. A blank page is much worse than a brief flash.

Gate it on something that is guaranteed to end. Set a class when registration finishes, and set it on a timer as well, so the hiding always stops:

html:not(.ce-ready) chef-recipe-card:not(:defined) {
	/* `visibility`, not `display` — the reserved box must keep its space. */
	visibility: hidden;
}
<!-- A CLASSIC script, not a module: it has to run even when module loading is
     what failed. -->
<script>
	setTimeout(() => document.documentElement.classList.add('ce-ready'), 1000);
</script>
Promise.all([customElements.whenDefined('chef-recipe-card')]).finally(() =>
	document.documentElement.classList.add('ce-ready')
);

Bundlers. sideEffects is an array, not false — the stylesheet is an import-time side effect, and a blanket false lets a bundler tree-shake it away.

Accessibility

Cards are keyboard-operable (Enter and Space), the favourite button carries aria-pressed and a descriptive label, and every colour pairing meets WCAG 2.2 AA against the design's cream ground. The skeleton shimmer respects prefers-reduced-motion.

License

MIT © rishab3355