Kosmesis
Components

Shipping Estimator

A postal-code form for a shipping estimate. Purely presentational — no Morphos equivalent.

Installation

npx kosmesis add shipping-estimator
pnpm dlx kosmesis add shipping-estimator
yarn dlx kosmesis add shipping-estimator
bunx kosmesis add shipping-estimator

Copy and paste the following code into your project.

shipping-estimator.tsx
import { StatefulComponent, StatelessComponent } from "@praxisjs/core";import { Component, FunctionProp, Prop, State } from "@praxisjs/decorators";import type { Children } from "@praxisjs/shared";import { cn } from "@/lib/utils";export interface ShippingEstimatorProps {  onEstimate?: (postalCode: string) => void;  class?: string;  children?: Children;}/** The rate lookup is the consumer's responsibility: call `onEstimate` and render the resulting `ShippingOption`s as `children`. */@Component()export class ShippingEstimator extends StatefulComponent {  @Prop() class?: string;  @Prop() children?: Children;  @FunctionProp() onEstimate?: ShippingEstimatorProps["onEstimate"];  @State() _postalCode = "";  private readonly _handleSubmit = (event: SubmitEvent) => {    event.preventDefault();    const postalCode = this._postalCode.trim();    if (!postalCode) return;    this.onEstimate?.(postalCode);  };  render() {    return (      <div data-slot="shipping-estimator" class={cn("flex flex-col gap-3", this.class)}>        <form class="flex gap-2" onSubmit={this._handleSubmit}>          <input            aria-label="Postal code"            placeholder="Postal code"            value={() => this._postalCode}            class="flex-1 rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-xs outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50"            onInput={(event: Event) => { this._postalCode = (event.target as HTMLInputElement).value; }}          />          <button            type="submit"            class="rounded-md border bg-secondary px-3 py-2 text-sm font-medium text-secondary-foreground hover:bg-secondary/80"          >            Estimate          </button>        </form>        {this.children && <div class="flex flex-col gap-2">{this.children}</div>}      </div>    );  }}export interface ShippingOptionProps {  name: string;  price: string;  eta: string;  class?: string;}@Component()export class ShippingOption extends StatelessComponent<ShippingOptionProps> {  render() {    const { name, price, eta, class: cls } = this.props;    return (      <div class={cn("flex items-center justify-between rounded-md border px-3 py-2 text-sm", cls)}>        <div class="flex flex-col">          <span class="font-medium text-foreground">{name}</span>          <span class="text-xs text-muted-foreground">{eta}</span>        </div>        <span class="font-medium text-foreground">{price}</span>      </div>    );  }}

Install the following dependencies:

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

Copy and paste the following code into your project.

shipping-estimator.tsx
import { StatefulComponent, StatelessComponent } from "@praxisjs/core";import { cx, Stylesheet, Styled, tokenVars } from "@praxisjs/css";import { Component, FunctionProp, Prop, State } from "@praxisjs/decorators";import type { Children } from "@praxisjs/shared";import { KosmesisTokens } from "@/lib/kosmesis-theme";const t = tokenVars(KosmesisTokens);class ShippingEstimatorStyles extends Stylesheet {  $root = this.css({ display: "flex", flexDirection: "column", gap: "0.75rem" });  $form = this.css({ display: "flex", gap: "0.5rem" });  $input = this.css({    flex: "1 1 0%",    borderRadius: "0.375rem",    border: `1px solid ${t.input}`,    backgroundColor: "transparent",    padding: "0.5rem 0.75rem",    fontSize: "0.875rem",    outline: "none",    color: t.foreground,    boxShadow: "0 1px 2px 0 rgb(0 0 0 / 0.05)",  })    .placeholder({ color: t.mutedForeground })    .focusVisible({ borderColor: t.ring, boxShadow: `0 0 0 3px color-mix(in oklab, ${t.ring} 50%, transparent)` });  $button = this.css({    borderRadius: "0.375rem",    border: `1px solid ${t.border}`,    backgroundColor: t.secondary,    padding: "0.5rem 0.75rem",    fontSize: "0.875rem",    fontWeight: 500,    color: t.secondaryForeground,    cursor: "pointer",  }).on("&:hover", { backgroundColor: `color-mix(in oklab, ${t.secondary} 80%, transparent)` });  $results = this.css({ display: "flex", flexDirection: "column", gap: "0.5rem" });  $option = this.css({    display: "flex",    alignItems: "center",    justifyContent: "space-between",    borderRadius: "0.375rem",    border: `1px solid ${t.border}`,    padding: "0.5rem 0.75rem",    fontSize: "0.875rem",  });  $optionMeta = this.css({ display: "flex", flexDirection: "column" });  $optionName = this.css({ fontWeight: 500, color: t.foreground });  $optionEta = this.css({ fontSize: "0.75rem", color: t.mutedForeground });  $optionPrice = this.css({ fontWeight: 500, color: t.foreground });}export interface ShippingEstimatorProps {  onEstimate?: (postalCode: string) => void;  class?: string;  children?: Children;}/** The rate lookup is the consumer's responsibility: call `onEstimate` and render the resulting `ShippingOption`s as `children`. */@Component()export class ShippingEstimator extends StatefulComponent {  @Styled(ShippingEstimatorStyles) $s!: ShippingEstimatorStyles;  @Prop() class?: string;  @Prop() children?: Children;  @FunctionProp() onEstimate?: ShippingEstimatorProps["onEstimate"];  @State() _postalCode = "";  private readonly _handleSubmit = (event: SubmitEvent) => {    event.preventDefault();    const postalCode = this._postalCode.trim();    if (!postalCode) return;    this.onEstimate?.(postalCode);  };  render() {    return (      <div data-slot="shipping-estimator" class={cx(this.$s.$root, this.class)}>        <form class={this.$s.$form} onSubmit={this._handleSubmit}>          <input            aria-label="Postal code"            placeholder="Postal code"            value={() => this._postalCode}            class={this.$s.$input}            onInput={(event: Event) => { this._postalCode = (event.target as HTMLInputElement).value; }}          />          <button type="submit" class={this.$s.$button}>            Estimate          </button>        </form>        {this.children && <div class={this.$s.$results}>{this.children}</div>}      </div>    );  }}export interface ShippingOptionProps {  name: string;  price: string;  eta: string;  class?: string;}@Component()export class ShippingOption extends StatelessComponent<ShippingOptionProps> {  @Styled(ShippingEstimatorStyles) $s!: ShippingEstimatorStyles;  render() {    const { name, price, eta, class: cls } = this.props;    return (      <div class={cx(this.$s.$option, cls)}>        <div class={this.$s.$optionMeta}>          <span class={this.$s.$optionName}>{name}</span>          <span class={this.$s.$optionEta}>{eta}</span>        </div>        <span class={this.$s.$optionPrice}>{price}</span>      </div>    );  }}

Examples

About

Purely presentational — no Morphos equivalent. The actual rate lookup is business logic the consumer owns: call onEstimate and render the resulting ShippingOptions as children.

Usage

import { ShippingEstimator, ShippingOption } from "@/components/ui/shipping-estimator";

@Component()
class Estimator extends StatefulComponent {
  @State() rates: Array<{ name: string; price: string; eta: string }> = [];

  handleEstimate(postalCode: string) {
    this.rates = lookupRates(postalCode);
  }

  render() {
    return (
      <ShippingEstimator onEstimate={(code) => this.handleEstimate(code)}>
        {this.rates.map((rate) => <ShippingOption key={rate.name} {...rate} />)}
      </ShippingEstimator>
    );
  }
}

Props

ShippingEstimator

PropTypeDefault
onEstimate(postalCode: string) => void
classstring

ShippingOption

PropTypeDefault
namestring
pricestring
etastring
classstring

On this page