Kosmesis
Components

Review Summary

An average-rating + star-count breakdown. Purely presentational — no Morphos equivalent.

Installation

npx kosmesis add review-summary
pnpm dlx kosmesis add review-summary
yarn dlx kosmesis add review-summary
bunx kosmesis add review-summary

Install the following dependencies:

npm install @morphos/icons
pnpm add @morphos/icons
yarn add @morphos/icons
bun add @morphos/icons

Copy and paste the following code into your project.

review-summary.tsx
import { StatelessComponent } from "@praxisjs/core";import { Component } from "@praxisjs/decorators";import { Icon } from "@morphos/icons";import { cn } from "@/lib/utils";export interface ReviewSummaryProps {  average: number;  count: number;  breakdown?: number[];  class?: string;}/** `breakdown` is 5 counts ordered 5★..1★ — index 0 is the 5-star count. */@Component()export class ReviewSummary extends StatelessComponent<ReviewSummaryProps> {  render() {    const { average, count, breakdown, class: cls } = this.props;    const max = breakdown ? Math.max(1, ...breakdown) : 1;    return (      <div data-slot="review-summary" class={cn("flex flex-col gap-3", cls)}>        <div class="flex items-baseline gap-2">          <span class="text-3xl font-bold text-foreground">{average.toFixed(1)}</span>          <span class="text-sm text-muted-foreground">{`out of 5 · ${String(count)} reviews`}</span>        </div>        {breakdown && (          <div class="flex flex-col gap-1">            {breakdown.map((value, i) => {              const stars = 5 - i;              const pct = (value / max) * 100;              return (                <div key={stars} class="flex items-center gap-2 text-xs text-muted-foreground">                  <span class="flex w-8 shrink-0 items-center gap-0.5">                    {stars}                    <Icon name="Star" size={10} class="fill-current" />                  </span>                  <div class="h-1.5 flex-1 overflow-hidden rounded-full bg-muted">                    <div class="h-full rounded-full bg-yellow-400" style={{ width: `${String(pct)}%` }} />                  </div>                  <span class="w-8 shrink-0 text-right">{value}</span>                </div>              );            })}          </div>        )}      </div>    );  }}

Install the following dependencies:

npm install @praxisjs/css @morphos/icons
pnpm add @praxisjs/css @morphos/icons
yarn add @praxisjs/css @morphos/icons
bun add @praxisjs/css @morphos/icons

Copy and paste the following code into your project.

review-summary.tsx
import { StatelessComponent } from "@praxisjs/core";import { cx, Stylesheet, Styled, tokenVars } from "@praxisjs/css";import { Component } from "@praxisjs/decorators";import { Icon } from "@morphos/icons";import { KosmesisTokens } from "@/lib/kosmesis-theme";const t = tokenVars(KosmesisTokens);class ReviewSummaryStyles extends Stylesheet {  $root = this.css({ display: "flex", flexDirection: "column", gap: "0.75rem" });  $header = this.css({ display: "flex", alignItems: "baseline", gap: "0.5rem" });  $average = this.css({ fontSize: "1.875rem", fontWeight: 700, color: t.foreground });  $count = this.css({ fontSize: "0.875rem", color: t.mutedForeground });  $rows = this.css({ display: "flex", flexDirection: "column", gap: "0.25rem" });  $row = this.css({ display: "flex", alignItems: "center", gap: "0.5rem", fontSize: "0.75rem", color: t.mutedForeground });  $label = this.css({ display: "flex", width: "2rem", flexShrink: 0, alignItems: "center", gap: "0.125rem" });  $track = this.css({ height: "0.375rem", flex: "1 1 0%", overflow: "hidden", borderRadius: "9999px", backgroundColor: t.muted });  $fill = this.css({ height: "100%", borderRadius: "9999px", backgroundColor: "#facc15" });  $value = this.css({ width: "2rem", flexShrink: 0, textAlign: "right" });}export interface ReviewSummaryProps {  average: number;  count: number;  breakdown?: number[];  class?: string;}/** `breakdown` is 5 counts ordered 5★..1★ — index 0 is the 5-star count. */@Component()export class ReviewSummary extends StatelessComponent<ReviewSummaryProps> {  @Styled(ReviewSummaryStyles) $s!: ReviewSummaryStyles;  render() {    const { average, count, breakdown, class: cls } = this.props;    const max = breakdown ? Math.max(1, ...breakdown) : 1;    return (      <div data-slot="review-summary" class={cx(this.$s.$root, cls)}>        <div class={this.$s.$header}>          <span class={this.$s.$average}>{average.toFixed(1)}</span>          <span class={this.$s.$count}>{`out of 5 · ${String(count)} reviews`}</span>        </div>        {breakdown && (          <div class={this.$s.$rows}>            {breakdown.map((value, i) => {              const stars = 5 - i;              const pct = (value / max) * 100;              return (                <div key={stars} class={this.$s.$row}>                  <span class={this.$s.$label}>                    {stars}                    <Icon name="Star" size={10} style={{ fill: "currentColor" }} />                  </span>                  <div class={this.$s.$track}>                    <div class={this.$s.$fill} style={{ width: `${String(pct)}%` }} />                  </div>                  <span class={this.$s.$value}>{value}</span>                </div>              );            })}          </div>        )}      </div>    );  }}

Examples

About

Purely presentational — no Morphos equivalent. breakdown is 5 counts ordered 5★..1★ (index 0 = 5-star count); each bar is scaled relative to the largest count in the array. Omit breakdown to show just the average/count.

Usage

import { ReviewSummary } from "@/components/ui/review-summary";

<ReviewSummary average={4.6} count={238} breakdown={[180, 40, 10, 5, 3]} />

Props

PropTypeDefault
averagenumber
countnumber
breakdownnumber[] (5 counts, 5★..1★)
classstring

On this page