Components
Review Summary
An average-rating + star-count breakdown. Purely presentational — no Morphos equivalent.
Installation
npx kosmesis add review-summarypnpm dlx kosmesis add review-summaryyarn dlx kosmesis add review-summarybunx kosmesis add review-summaryInstall the following dependencies:
npm install @morphos/iconspnpm add @morphos/iconsyarn add @morphos/iconsbun add @morphos/iconsCopy and paste the following code into your project.
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/iconspnpm add @praxisjs/css @morphos/iconsyarn add @praxisjs/css @morphos/iconsbun add @praxisjs/css @morphos/iconsCopy and paste the following code into your project.
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
| Prop | Type | Default |
|---|---|---|
average | number | — |
count | number | — |
breakdown | number[] (5 counts, 5★..1★) | — |
class | string | — |