Components
Shipping Estimator
A postal-code form for a shipping estimate. Purely presentational — no Morphos equivalent.
Installation
npx kosmesis add shipping-estimatorpnpm dlx kosmesis add shipping-estimatoryarn dlx kosmesis add shipping-estimatorbunx kosmesis add shipping-estimatorCopy and paste the following code into your project.
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/csspnpm add @praxisjs/cssyarn add @praxisjs/cssbun add @praxisjs/cssCopy and paste the following code into your project.
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
| Prop | Type | Default |
|---|---|---|
onEstimate | (postalCode: string) => void | — |
class | string | — |
ShippingOption
| Prop | Type | Default |
|---|---|---|
name | string | — |
price | string | — |
eta | string | — |
class | string | — |