Kosmesis
Components

Promo Code Input

A coupon-code input + apply button. Purely presentational — no Morphos equivalent.

Installation

npx kosmesis add promo-code-input
pnpm dlx kosmesis add promo-code-input
yarn dlx kosmesis add promo-code-input
bunx kosmesis add promo-code-input

Copy and paste the following code into your project.

promo-code-input.tsx
import { StatefulComponent } from "@praxisjs/core";import { Component, FunctionProp, Prop, State } from "@praxisjs/decorators";import { cn } from "@/lib/utils";export type PromoCodeStatus = "idle" | "applied" | "invalid";export interface PromoCodeInputProps {  onApply?: (code: string) => void;  status?: PromoCodeStatus;  message?: string;  class?: string;}// `status` and `message` are driven entirely by the consumer's own validation result after `onApply` fires.@Component()export class PromoCodeInput extends StatefulComponent {  @Prop() status: PromoCodeStatus = "idle";  @Prop() message?: string;  @Prop() class?: string;  @FunctionProp() onApply?: PromoCodeInputProps["onApply"];  @State() _code = "";  private readonly _handleSubmit = (event: SubmitEvent) => {    event.preventDefault();    const code = this._code.trim();    if (!code) return;    this.onApply?.(code);  };  render() {    return (      <form data-slot="promo-code-input" class={cn("flex flex-col gap-1.5", this.class)} onSubmit={this._handleSubmit}>        <div class="flex gap-2">          <input            aria-label="Promo code"            placeholder="Promo code"            value={() => this._code}            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._code = (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"          >            Apply          </button>        </div>        {() =>          this.message ? (            <p              data-status={this.status}              class={cn(                "text-xs",                "data-[status=applied]:text-primary",                "data-[status=invalid]:text-destructive",                "data-[status=idle]:text-muted-foreground",              )}            >              {this.message}            </p>          ) : null        }      </form>    );  }}

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.

promo-code-input.tsx
import { StatefulComponent } from "@praxisjs/core";import { cx, Stylesheet, Styled, tokenVars } from "@praxisjs/css";import { Component, FunctionProp, Prop, State } from "@praxisjs/decorators";import { KosmesisTokens } from "@/lib/kosmesis-theme";const t = tokenVars(KosmesisTokens);class PromoCodeInputStyles extends Stylesheet {  $root = this.css({ display: "flex", flexDirection: "column", gap: "0.375rem" });  $row = 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)` });  $message = this.css({ fontSize: "0.75rem" })    .on('&[data-status="applied"]', { color: t.primary })    .on('&[data-status="invalid"]', { color: t.destructive })    .on('&[data-status="idle"]', { color: t.mutedForeground });}export type PromoCodeStatus = "idle" | "applied" | "invalid";export interface PromoCodeInputProps {  onApply?: (code: string) => void;  status?: PromoCodeStatus;  message?: string;  class?: string;}// `status` and `message` are driven entirely by the consumer's own validation result after `onApply` fires.@Component()export class PromoCodeInput extends StatefulComponent {  @Styled(PromoCodeInputStyles) $s!: PromoCodeInputStyles;  @Prop() status: PromoCodeStatus = "idle";  @Prop() message?: string;  @Prop() class?: string;  @FunctionProp() onApply?: PromoCodeInputProps["onApply"];  @State() _code = "";  private readonly _handleSubmit = (event: SubmitEvent) => {    event.preventDefault();    const code = this._code.trim();    if (!code) return;    this.onApply?.(code);  };  render() {    return (      <form data-slot="promo-code-input" class={cx(this.$s.$root, this.class)} onSubmit={this._handleSubmit}>        <div class={this.$s.$row}>          <input            aria-label="Promo code"            placeholder="Promo code"            value={() => this._code}            class={this.$s.$input}            onInput={(event: Event) => { this._code = (event.target as HTMLInputElement).value; }}          />          <button type="submit" class={this.$s.$button}>            Apply          </button>        </div>        {() => (this.message ? <p data-status={this.status} class={this.$s.$message}>{this.message}</p> : null)}      </form>    );  }}

Examples

About

Purely presentational — no Morphos equivalent. status/message are driven by the consumer's own validation result after onApply fires — this component has no knowledge of whether a code is actually valid.

Usage

import { PromoCodeInput } from "@/components/ui/promo-code-input";

@Component()
class Coupon extends StatefulComponent {
  @State() status: "idle" | "applied" | "invalid" = "idle";
  @State() message = "";

  handleApply(code: string) {
    const valid = checkCode(code);
    this.status = valid ? "applied" : "invalid";
    this.message = valid ? "10% discount applied!" : "That code isn't valid.";
  }

  render() {
    return <PromoCodeInput status={this.status} message={this.message} onApply={(code) => this.handleApply(code)} />;
  }
}

Props

PropTypeDefault
status"idle" | "applied" | "invalid""idle"
messagestring
onApply(code: string) => void
classstring

On this page