@waveinno/ui 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,22 @@ prop, major = a renamed or removed export or prop.
6
6
 
7
7
  ## Unreleased
8
8
 
9
+ ## 0.3.0 — 2026-10-02
10
+
11
+ - **RadioGroup**, **RadioGroupItem** and **RadioCard** (whole-card choices with icon, description and a price or badge).
12
+ - **Slider**: single or range, value over the thumb while used (or always), labelled marks, gradient look.
13
+ - **NumberInput**: split (− value +) or stacked steppers, hold to repeat, ↑ ↓ with Shift for ten, clamps on blur,
14
+ prefix and suffix, grouped display out of focus.
15
+ - **PasswordInput**: show / hide, Caps Lock warning, optional four-step strength meter; `passwordScore()`.
16
+ - **PinInput**: one-time codes and PINs; paste and SMS autofill fill every box; boxed, underline and split looks;
17
+ `onComplete`, mask, error shake.
18
+ - **FileUpload**: dropzone or compact; rejected files listed with the reason; image thumbnails; upload progress and
19
+ retry.
20
+ - **Alert**: info, success, warning, danger, neutral in soft, outline, accent and solid looks; actions, dismiss.
21
+ - **Toaster** and **toast**: sonner with the WAVEINNO look, tone icons in tiles and a tone wash; AppShell uses it.
22
+ - **Spinner**: ring, dots and wave in five sizes; keeps turning slowly under reduced motion.
23
+ - **Kbd**: keys and key combinations.
24
+
9
25
  ## 0.2.0 — 2026-10-02
10
26
 
11
27
  - **Menu**: an always-open menu for settings sidebars and side panels. Looks: soft, line, raised, gradient, dark, plus
package/README.md CHANGED
@@ -45,7 +45,7 @@ through `labels` props, so each product passes its own language.
45
45
 
46
46
  | Group | Components |
47
47
  | --- | --- |
48
- | Primitives | Button (working + showcase variants), Badge (soft, solid, outline, dot, gradient, live), Input / Textarea (states, adornments), FormField, Select, Combobox (searchable), Checkbox, Switch, Segmented, Tabs (underline, pill, solid, enclosed), Calendar, DatePicker, DateTimePicker, DateRangePicker, TimePicker, Dialog (drawer that widens or goes full width), Sheet, ConfirmDialog, DropdownMenu (rich items, header, dark), Menu (soft, line, raised, gradient, dark, plus the shell's panel and rail; branches, sliding highlight), Popover (arrow, dark, brand), Tooltip, Progress / ProgressRing, Avatar, Skeleton |
48
+ | Primitives | Button (working + showcase variants), Badge (soft, solid, outline, dot, gradient, live), Input / Textarea (states, adornments), NumberInput, PasswordInput (strength, Caps Lock), PinInput (OTP), FileUpload, FormField, Select, Combobox (searchable), Checkbox, RadioGroup and RadioCard, Switch, Slider (range, marks), Segmented, Tabs (underline, pill, solid, enclosed), Calendar, DatePicker, DateTimePicker, DateRangePicker, TimePicker, Dialog (drawer that widens or goes full width), Sheet, ConfirmDialog, DropdownMenu (rich items, header, dark), Menu (soft, line, raised, gradient, dark, plus the shell's panel and rail; branches, sliding highlight), Popover (arrow, dark, brand), Tooltip, Alert (five tones, four looks), Toaster and toast, Spinner (ring, dots, wave), Kbd, Progress / ProgressRing, Avatar, Skeleton |
49
49
  | Patterns | AppLauncher (switch-app button: grid, list, spotlight, glass, dark), DataTable (advanced filters, saved views, five looks, inline and bulk edit, bulk delete, sticky actions), UserCell, RowActions, PageHero, PageHeader, StatCard, KpiCard, Money, ActivityFeed, ActivityTimeline, ChangeHistory, ChangeNotes, Note, Kanban, empty / error / loading states |
50
50
  | Charts | TrendChart, BarChart, LineChart, DonutChart, GaugeChart, Heatmap, BarList, ChartCard, validated palette |
51
51
  | Brand | WaveinnoLogo, WaveinnoSymbol, WaveMark (app icon: seven tones, round, 3D, animated), PoweredBy, Watermark; `@waveinno/ui/brand/waveinno-logo.svg` |
@@ -0,0 +1,28 @@
1
+ import * as React from "react";
2
+ /**
3
+ * Alert — a message that stays on the page until it is dealt with: a payout
4
+ * on hold, a gate offline, a plan about to lapse. (One that comes and goes is
5
+ * a toast.)
6
+ *
7
+ * Tones: info, success, warning, danger, neutral — each with its own icon, so
8
+ * the tone is never colour alone. Looks:
9
+ * soft — the default: a tinted box. In a page or a form.
10
+ * outline — a hairline in the tone on the surface: calmer, for lists of notices.
11
+ * solid — filled with the tone: the one message nobody may miss.
12
+ * accent — a bar on the left on a raised card: inside cards and drawers.
13
+ * Danger and warning are announced as they appear (role="alert"); the rest are polite.
14
+ */
15
+ export type AlertTone = "info" | "success" | "warning" | "danger" | "neutral";
16
+ export type AlertVariant = "soft" | "outline" | "solid" | "accent";
17
+ export interface AlertProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"> {
18
+ tone?: AlertTone;
19
+ variant?: AlertVariant;
20
+ title?: React.ReactNode;
21
+ /** Replace the tone's icon, or `false` for none. */
22
+ icon?: React.ReactNode | false;
23
+ /** Buttons under the text: "Retry", "Review". */
24
+ action?: React.ReactNode;
25
+ /** Shows a close button; called when it is pressed. */
26
+ onDismiss?: () => void;
27
+ }
28
+ export declare const Alert: React.ForwardRefExoticComponent<AlertProps & React.RefAttributes<HTMLDivElement>>;
@@ -0,0 +1 @@
1
+ "use client";import{jsx as e,jsxs as x}from"react/jsx-runtime";import{CircleAlert as m,CircleCheck as f,Info as b,TriangleAlert as _,X as w}from"lucide-react";import*as h from"react";import{cn as t}from"../../lib/utils.js";const y={info:{c:"var(--info)",s:"var(--info-subtle)",icon:e(b,{})},success:{c:"var(--success)",s:"var(--success-subtle)",icon:e(f,{})},warning:{c:"var(--warning)",s:"var(--warning-subtle)",icon:e(_,{})},danger:{c:"var(--danger)",s:"var(--danger-subtle)",icon:e(m,{})},neutral:{c:"var(--muted)",s:"var(--hover)",icon:e(b,{})}},R={soft:{box:"border border-[color-mix(in_srgb,var(--tone)_22%,transparent)] bg-[var(--tone-subtle)]",icon:"text-[var(--tone)]",title:"text-text",body:"text-body"},outline:{box:"border border-[color-mix(in_srgb,var(--tone)_45%,transparent)] bg-raised",icon:"text-[var(--tone)]",title:"text-text",body:"text-body"},solid:{box:"border border-transparent bg-[var(--tone)] text-white shadow-[inset_0_1px_0_rgba(255,255,255,.18),0_8px_20px_-10px_var(--tone)]",icon:"text-white",title:"text-white",body:"text-white/85"},accent:{box:"border border-border bg-raised pl-5 shadow-[var(--lit),0_1px_2px_rgba(5,8,22,.04)] before:absolute before:inset-y-2.5 before:left-2 before:w-[3px] before:rounded-full before:bg-[var(--tone)]",icon:"text-[var(--tone)]",title:"text-text",body:"text-body"}},A=h.forwardRef(({tone:o="info",variant:i="soft",title:n,icon:s,action:l,onDismiss:c,className:v,style:p,children:d,...g},u)=>{const a=y[o],r=R[i];return x("div",{ref:u,role:o==="danger"||o==="warning"?"alert":"status",style:{"--tone":a.c,"--tone-subtle":a.s,...p},className:t("relative flex gap-3 rounded-[12px] p-3.5 text-[13px] animate-[wave-in_220ms_var(--ease)]",r.box,v),...g,children:[s!==!1?e("span",{className:t("mt-px shrink-0 [&_svg]:size-[18px]",r.icon),children:s??a.icon}):null,x("div",{className:"min-w-0 flex-1",children:[n?e("p",{className:t("font-semibold leading-5",r.title),children:n}):null,d?e("div",{className:t("leading-5",n&&"mt-0.5",r.body),children:d}):null,l?e("div",{className:"mt-2.5 flex flex-wrap items-center gap-2",children:l}):null]}),c?e("button",{type:"button",onClick:c,"aria-label":"Dismiss",className:t("-m-1 grid size-7 shrink-0 cursor-pointer place-items-center rounded-[7px] transition-colors [&_svg]:size-3.5",i==="solid"?"text-white/80 hover:bg-white/15 hover:text-white":"text-muted hover:bg-[color-mix(in_srgb,var(--tone)_12%,transparent)] hover:text-text"),children:e(w,{})}):null]})});A.displayName="Alert";export{A as Alert};
@@ -0,0 +1,36 @@
1
+ import * as React from "react";
2
+ /**
3
+ * FileUpload — drop files or pick them, and see what was taken and what was not.
4
+ *
5
+ * Files that break `accept` or `maxSize` are not silently dropped: they are
6
+ * listed with the reason, so the person knows to shrink the photo rather than
7
+ * wonder why nothing happened. Images get a thumbnail; everything else a type
8
+ * icon. Pass `progress` (by file name, 0–100) to show uploads moving, and
9
+ * `failed` for the ones to retry.
10
+ *
11
+ * Looks: `dropzone` (default, a dashed area) and `compact` (one row with a
12
+ * button, for a form with other fields).
13
+ */
14
+ export type FileUploadVariant = "dropzone" | "compact";
15
+ export interface FileUploadProps {
16
+ value?: File[];
17
+ defaultValue?: File[];
18
+ onValueChange?: (files: File[]) => void;
19
+ /** As on <input type="file">: "image/*", ".csv,.xlsx". */
20
+ accept?: string;
21
+ multiple?: boolean;
22
+ /** Bytes. */
23
+ maxSize?: number;
24
+ maxFiles?: number;
25
+ variant?: FileUploadVariant;
26
+ title?: React.ReactNode;
27
+ hint?: React.ReactNode;
28
+ /** Upload progress by file name, 0–100. */
29
+ progress?: Record<string, number>;
30
+ /** Names of files whose upload failed; they get a retry button. */
31
+ failed?: string[];
32
+ onRetry?: (file: File) => void;
33
+ disabled?: boolean;
34
+ className?: string;
35
+ }
36
+ export declare function FileUpload({ value, defaultValue, onValueChange, accept, multiple, maxSize, maxFiles, variant, title, hint, progress, failed, onRetry, disabled, className, }: FileUploadProps): React.JSX.Element;
@@ -0,0 +1 @@
1
+ "use client";import{Fragment as Q,jsx as t,jsxs as n}from"react/jsx-runtime";import{CloudUpload as j,File as A,FileImage as H,FileSpreadsheet as K,FileText as X,RotateCcw as q,X as E}from"lucide-react";import*as c from"react";import{cn as x}from"../../lib/utils.js";const k=r=>r<1024*1024?`${Math.max(1,Math.round(r/1024))} KB`:`${(r/1024/1024).toFixed(1)} MB`;function G(r,i){return i?i.split(",").some(u=>{const a=u.trim().toLowerCase();return a.startsWith(".")?r.name.toLowerCase().endsWith(a):a.endsWith("/*")?r.type.startsWith(a.slice(0,-1)):r.type===a}):!0}function O({file:r}){const i=r.name.toLowerCase();return r.type.startsWith("image/")?t(H,{}):/\.(csv|xlsx?|ods)$/.test(i)?t(K,{}):/\.(pdf|docx?|txt|md)$/.test(i)?t(X,{}):t(A,{})}function J({file:r}){const[i,u]=c.useState(null);return c.useEffect(()=>{if(!r.type.startsWith("image/"))return;const a=URL.createObjectURL(r);return u(a),()=>URL.revokeObjectURL(a)},[r]),t("span",{className:"grid size-10 shrink-0 place-items-center overflow-hidden rounded-[9px] border border-border bg-surface text-muted [&_svg]:size-[18px]",children:i?t("img",{src:i,alt:"",className:"size-full object-cover"}):t(O,{file:r})})}function ee({value:r,defaultValue:i=[],onValueChange:u,accept:a,multiple:h=!0,maxSize:m,maxFiles:p,variant:M="dropzone",title:z,hint:W,progress:I,failed:T,onRetry:F,disabled:b,className:B}){const[V,P]=c.useState(i),g=r??V,[R,_]=c.useState([]),[f,y]=c.useState(!1),N=c.useRef(null),v=c.useRef(0),$=e=>{r===void 0&&P(e),u?.(e)},C=e=>{if(!e)return;const s=[],o=[];for(const d of Array.from(e))G(d,a)?m&&d.size>m?o.push({file:d,reason:`Over ${k(m)}`}):s.push(d):o.push({file:d,reason:"Not an accepted type"});let l=h?[...g,...s.filter(d=>!g.some(L=>L.name===d.name&&L.size===d.size))]:s.slice(0,1);p&&l.length>p&&(l.slice(p).forEach(d=>o.push({file:d,reason:`Only ${p} file${p>1?"s":""}`})),l=l.slice(0,p)),_(o),$(l)},S=e=>$(g.filter(s=>s!==e)),w=W??[a?.split(",").map(e=>e.trim().replace(/^\./,"").toUpperCase()).join(", "),m?`up to ${k(m)}`:null].filter(Boolean).join(" \xB7 "),D=t("input",{ref:N,type:"file",className:"sr-only",tabIndex:-1,accept:a,multiple:h,disabled:b,onChange:e=>{C(e.target.files),e.target.value=""}}),U={onDragEnter:e=>{e.preventDefault(),v.current+=1,y(!0)},onDragOver:e=>e.preventDefault(),onDragLeave:()=>{v.current-=1,v.current<=0&&y(!1)},onDrop:e=>{e.preventDefault(),v.current=0,y(!1),b||C(e.dataTransfer.files)}};return n("div",{className:x("w-full space-y-2.5",B),children:[M==="dropzone"?n("button",{type:"button",disabled:b,onClick:()=>N.current?.click(),...U,className:x("group relative flex w-full cursor-pointer flex-col items-center justify-center gap-2 overflow-hidden rounded-[14px] border-[1.5px] border-dashed border-border2 bg-surface px-6 py-8 text-center outline-none","transition-[border-color,background-color,box-shadow] duration-200 hover:border-primary/50 hover:bg-primary-subtle/30 focus-visible:ring-2 focus-visible:ring-primary/35",f&&"border-primary bg-primary-subtle/50 shadow-[0_0_0_4px_var(--primary-subtle)]",b&&"cursor-not-allowed opacity-50"),children:[t("span",{className:x("grid size-12 place-items-center rounded-[14px] border border-border bg-raised text-primary shadow-[var(--lit),0_4px_12px_-6px_rgba(79,70,229,.45)] transition-transform duration-300 ease-[cubic-bezier(.3,1.5,.5,1)] [&_svg]:size-[22px]",f?"-translate-y-1 scale-110":"group-hover:-translate-y-0.5"),children:t(j,{})}),t("span",{className:"text-[13.5px] font-semibold text-text",children:f?"Drop to add":z??n(Q,{children:[n("span",{className:"text-primary",children:["Choose ",h?"files":"a file"]})," or drag ",h?"them":"it"," here"]})}),w?t("span",{className:"text-[12px] text-muted",children:w}):null,D]}):n("div",{...U,className:x("flex items-center gap-3 rounded-ctl border border-dashed border-border2 bg-surface p-2 pl-3 transition-colors",f&&"border-primary bg-primary-subtle/50"),children:[t(j,{className:"size-4 shrink-0 text-muted"}),t("span",{className:"min-w-0 flex-1 truncate text-[12.5px] text-muted",children:f?"Drop to add":z??w??"Drag files here"}),t("button",{type:"button",disabled:b,onClick:()=>N.current?.click(),className:"h-7 shrink-0 cursor-pointer rounded-[7px] border border-border2 bg-raised px-2.5 text-[12px] font-medium text-text shadow-[0_1px_1px_rgba(5,8,22,.05)] transition-colors hover:bg-hover disabled:cursor-not-allowed disabled:opacity-50",children:"Browse"}),D]}),g.length||R.length?n("ul",{className:"space-y-1.5",children:[g.map(e=>{const s=I?.[e.name],o=T?.includes(e.name),l=s!==void 0&&s<100&&!o;return n("li",{className:"flex items-center gap-3 rounded-[11px] border border-border bg-raised p-2 pr-2.5 animate-[wave-in_220ms_var(--ease)]",children:[t(J,{file:e}),n("span",{className:"min-w-0 flex-1",children:[t("span",{className:"block truncate text-[13px] font-medium text-text",children:e.name}),n("span",{className:x("num flex items-center gap-1.5 text-[11.5px]",o?"text-danger":"text-muted"),children:[k(e.size),o?" \xB7 Upload failed":s===100?" \xB7 Uploaded":l?` \xB7 ${Math.round(s)}%`:null]}),l?t("span",{className:"mt-1.5 block h-1 overflow-hidden rounded-full bg-hover",children:t("span",{className:"block h-full rounded-full bg-[linear-gradient(90deg,#1f45ea,#7560fd)] transition-[width] duration-300",style:{width:`${s}%`}})}):null]}),o&&F?t("button",{type:"button",onClick:()=>F(e),"aria-label":`Retry ${e.name}`,className:"grid size-7 cursor-pointer place-items-center rounded-[7px] text-danger transition-colors hover:bg-danger-subtle [&_svg]:size-3.5",children:t(q,{})}):null,t("button",{type:"button",onClick:()=>S(e),"aria-label":`Remove ${e.name}`,className:"grid size-7 cursor-pointer place-items-center rounded-[7px] text-muted transition-colors hover:bg-hover hover:text-text [&_svg]:size-3.5",children:t(E,{})})]},`${e.name}-${e.size}-${e.lastModified}`)}),R.map(({file:e,reason:s})=>n("li",{className:"flex items-center gap-3 rounded-[11px] border border-danger/25 bg-danger-subtle/50 p-2 pr-2.5",role:"alert",children:[t("span",{className:"grid size-10 shrink-0 place-items-center rounded-[9px] border border-danger/20 bg-raised text-danger [&_svg]:size-[18px]",children:t(O,{file:e})}),n("span",{className:"min-w-0 flex-1",children:[t("span",{className:"block truncate text-[13px] font-medium text-text",children:e.name}),n("span",{className:"text-[11.5px] text-danger",children:[s," \xB7 not added"]})]}),t("button",{type:"button",onClick:()=>_(o=>o.filter(l=>l.file!==e)),"aria-label":`Dismiss ${e.name}`,className:"grid size-7 cursor-pointer place-items-center rounded-[7px] text-muted transition-colors hover:bg-raised hover:text-text [&_svg]:size-3.5",children:t(E,{})})]},`x-${e.name}-${e.size}`))]}):null]})}export{ee as FileUpload};
@@ -1,4 +1,5 @@
1
1
  /** Primitives: small, unopinionated building blocks (Radix underneath where it helps). */
2
+ export * from "./alert";
2
3
  export * from "./avatar";
3
4
  export * from "./badge";
4
5
  export * from "./button";
@@ -7,21 +8,30 @@ export * from "./card";
7
8
  export * from "./checkbox";
8
9
  export * from "./combobox";
9
10
  export * from "./confirm-dialog";
11
+ export * from "./date-picker";
10
12
  export * from "./dialog";
11
13
  export * from "./dropdown-menu";
14
+ export * from "./file-upload";
12
15
  export * from "./form-field";
13
16
  export * from "./input";
17
+ export * from "./kbd";
14
18
  export * from "./label";
15
19
  export * from "./menu";
20
+ export * from "./number-input";
21
+ export * from "./password-input";
22
+ export * from "./pin-input";
16
23
  export * from "./popover";
17
- export * from "./date-picker";
18
24
  export * from "./progress";
25
+ export * from "./radio-group";
19
26
  export * from "./segmented";
20
27
  export * from "./select";
21
28
  export * from "./separator";
22
29
  export * from "./sheet";
23
30
  export * from "./skeleton";
31
+ export * from "./slider";
32
+ export * from "./spinner";
24
33
  export * from "./switch";
25
34
  export * from "./tabs";
26
35
  export * from "./time-picker";
36
+ export * from "./toast";
27
37
  export * from "./tooltip";
@@ -1 +1 @@
1
- export*from"./avatar.js";export*from"./badge.js";export*from"./button.js";export*from"./calendar.js";export*from"./card.js";export*from"./checkbox.js";export*from"./combobox.js";export*from"./confirm-dialog.js";export*from"./dialog.js";export*from"./dropdown-menu.js";export*from"./form-field.js";export*from"./input.js";export*from"./label.js";export*from"./menu.js";export*from"./popover.js";export*from"./date-picker.js";export*from"./progress.js";export*from"./segmented.js";export*from"./select.js";export*from"./separator.js";export*from"./sheet.js";export*from"./skeleton.js";export*from"./switch.js";export*from"./tabs.js";export*from"./time-picker.js";export*from"./tooltip.js";
1
+ export*from"./alert.js";export*from"./avatar.js";export*from"./badge.js";export*from"./button.js";export*from"./calendar.js";export*from"./card.js";export*from"./checkbox.js";export*from"./combobox.js";export*from"./confirm-dialog.js";export*from"./date-picker.js";export*from"./dialog.js";export*from"./dropdown-menu.js";export*from"./file-upload.js";export*from"./form-field.js";export*from"./input.js";export*from"./kbd.js";export*from"./label.js";export*from"./menu.js";export*from"./number-input.js";export*from"./password-input.js";export*from"./pin-input.js";export*from"./popover.js";export*from"./progress.js";export*from"./radio-group.js";export*from"./segmented.js";export*from"./select.js";export*from"./separator.js";export*from"./sheet.js";export*from"./skeleton.js";export*from"./slider.js";export*from"./spinner.js";export*from"./switch.js";export*from"./tabs.js";export*from"./time-picker.js";export*from"./toast.js";export*from"./tooltip.js";
@@ -0,0 +1,13 @@
1
+ import * as React from "react";
2
+ /**
3
+ * Kbd — a key, drawn as a key: a lip along the bottom, so "⌘K" reads as
4
+ * something to press and not as text. Pass `keys` for a combination; they
5
+ * are set apart with a thin joiner, and read out with "+".
6
+ */
7
+ export declare function Kbd({ keys, size, className, children, }: {
8
+ /** A combination: ["⌘", "K"]. */
9
+ keys?: string[];
10
+ size?: "sm" | "md";
11
+ className?: string;
12
+ children?: React.ReactNode;
13
+ }): React.JSX.Element;
@@ -0,0 +1 @@
1
+ import{jsx as e,jsxs as p}from"react/jsx-runtime";import*as o from"react";import{cn as r}from"../../lib/utils.js";function c({keys:a,size:i="md",className:t,children:s}){const n=r("inline-grid place-items-center rounded-[6px] border border-border2 border-b-[2.5px] bg-raised font-sans font-medium text-body","shadow-[inset_0_-1px_0_var(--border),0_1px_1px_rgba(5,8,22,.06)]",i==="sm"?"h-5 min-w-5 px-1 text-[10.5px]":"h-6 min-w-6 px-1.5 text-[11.5px]");return a?e("kbd",{className:r("inline-flex items-center gap-1",t),"aria-label":a.join(" + "),children:a.map((m,d)=>p(o.Fragment,{children:[d>0?e("span",{"aria-hidden":!0,className:"text-[10px] text-placeholder",children:"+"}):null,e("kbd",{className:n,"aria-hidden":!0,children:m})]},d))}):e("kbd",{className:r(n,t),children:s})}export{c as Kbd};
@@ -0,0 +1,30 @@
1
+ import * as React from "react";
2
+ import { type InputState } from "./input";
3
+ /**
4
+ * NumberInput — a number you type or step: a rate, a capacity, a quantity.
5
+ *
6
+ * ↑ / ↓ step it (Shift for ten steps), the buttons repeat while held, and the
7
+ * value is clamped to min / max when the field is left, not while typing, so
8
+ * "1" on the way to "150" is never snapped to the minimum. Out of focus it
9
+ * shows grouped ("12,500"); in focus, the raw number, ready to edit.
10
+ *
11
+ * `stepper`: "split" puts − and + either side (counts, quantities), "stack"
12
+ * puts small chevrons on the right (forms full of numbers), "none" hides them.
13
+ */
14
+ export interface NumberInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "value" | "defaultValue" | "onChange" | "type" | "min" | "max" | "step" | "prefix"> {
15
+ value?: number | null;
16
+ defaultValue?: number | null;
17
+ onValueChange?: (value: number | null) => void;
18
+ min?: number;
19
+ max?: number;
20
+ step?: number;
21
+ /** Decimal places kept. Default: as many as `step` has. */
22
+ precision?: number;
23
+ /** Inside the box, before the number: "৳", "+880". */
24
+ prefix?: React.ReactNode;
25
+ /** After it: "min", "cars", "%". */
26
+ suffix?: React.ReactNode;
27
+ stepper?: "split" | "stack" | "none";
28
+ state?: InputState;
29
+ }
30
+ export declare const NumberInput: React.ForwardRefExoticComponent<NumberInputProps & React.RefAttributes<HTMLInputElement>>;
@@ -0,0 +1 @@
1
+ "use client";import{jsx as r,jsxs as k}from"react/jsx-runtime";import{ChevronDown as j,ChevronUp as q,Minus as G,Plus as J}from"lucide-react";import*as s from"react";import{cn as u}from"../../lib/utils.js";import{inputClass as Q}from"./input.js";const X=l=>(String(l).split(".")[1]??"").length,Y=s.forwardRef(({value:l,defaultValue:T=null,onValueChange:P,min:i=-1/0,max:c=1/0,step:v=1,precision:z,prefix:D,suffix:M,stepper:d="split",state:h="default",className:A,disabled:b,onBlur:H,onFocus:W,onKeyDown:_,...N},K)=>{const m=z??X(v),[O,V]=s.useState(T),t=l!==void 0?l:O,[B,S]=s.useState(!1),[$,y]=s.useState(""),R=e=>Number(e.toFixed(m)),F=e=>Math.min(c,Math.max(i,e)),w=e=>{l===void 0&&V(e),P?.(e)},L=B?$:t===null?"":t.toLocaleString(void 0,{minimumFractionDigits:m,maximumFractionDigits:m}),I=s.useRef(t);I.current=t;const p=s.useCallback((e,n=1)=>{const a=I.current??(i>-1/0?i:0),f=F(R(a+e*v*n));I.current=f,w(f),y(String(f))},[i,c,v,m,l,P]),U=s.useRef(void 0),o=()=>window.clearTimeout(U.current),x=e=>n=>{if(n.button!==0)return;n.preventDefault(),p(e);const a=f=>{U.current=window.setTimeout(()=>{p(e),a(Math.max(40,f*.85))},f)};a(400)};s.useEffect(()=>o,[]);const C=t!==null&&t<=i,E=t!==null&&t>=c,g="grid shrink-0 cursor-pointer place-items-center text-muted transition-colors hover:bg-hover hover:text-text active:bg-[var(--selected)] disabled:pointer-events-none disabled:opacity-35";return k("div",{className:u(Q,"flex items-stretch overflow-hidden p-0 focus-within:border-primary focus-within:ring-2 focus-within:ring-primary/20",h==="error"&&"border-danger bg-danger-subtle/30 focus-within:border-danger focus-within:ring-danger/20 wm-shake",h==="warning"&&"border-warning focus-within:border-warning focus-within:ring-warning/20",h==="success"&&"border-success focus-within:border-success focus-within:ring-success/20",b&&"cursor-not-allowed bg-hover",A),children:[d==="split"?r("button",{type:"button",tabIndex:-1,"aria-label":"Decrease",disabled:b||C,onPointerDown:x(-1),onPointerUp:o,onPointerLeave:o,className:u(g,"w-9 border-r border-border [&_svg]:size-3.5"),children:r(G,{strokeWidth:2.4})}):null,k("div",{className:u("flex min-w-0 flex-1 items-center gap-1.5 px-3",d==="split"&&"justify-center"),children:[D?r("span",{className:"shrink-0 text-[13px] text-muted",children:D}):null,r("input",{ref:K,type:"text",inputMode:m>0||i<0?"decimal":"numeric",role:"spinbutton","aria-valuenow":t??void 0,"aria-valuemin":Number.isFinite(i)?i:void 0,"aria-valuemax":Number.isFinite(c)?c:void 0,"aria-invalid":h==="error"?!0:N["aria-invalid"],disabled:b,value:L,style:d==="split"?{width:`${Math.max(2,(L||String(N.placeholder??"")).length+1)}ch`}:void 0,className:u("num h-full min-w-0 bg-transparent text-[13px] text-text outline-none placeholder:text-placeholder disabled:cursor-not-allowed",d==="split"?"max-w-full flex-none text-center":"w-full"),onFocus:e=>{y(t===null?"":String(t)),S(!0),W?.(e)},onChange:e=>{const n=e.target.value.replace(/[^\d.,-]/g,"");y(n);const a=Number(n.replace(/,/g,""));n===""||n==="-"?w(null):Number.isNaN(a)||w(a)},onBlur:e=>{if(S(!1),t!==null){const n=F(R(t));n!==t&&w(n)}H?.(e)},onKeyDown:e=>{_?.(e),!e.defaultPrevented&&(e.key==="ArrowUp"||e.key==="ArrowDown"?(e.preventDefault(),p(e.key==="ArrowUp"?1:-1,e.shiftKey?10:1)):e.key==="Home"&&Number.isFinite(i)?(e.preventDefault(),p(-1,1/0)):e.key==="End"&&Number.isFinite(c)&&(e.preventDefault(),p(1,1/0)))},...N}),M?r("span",{className:"shrink-0 text-[12.5px] text-muted",children:M}):null]}),d==="split"?r("button",{type:"button",tabIndex:-1,"aria-label":"Increase",disabled:b||E,onPointerDown:x(1),onPointerUp:o,onPointerLeave:o,className:u(g,"w-9 border-l border-border [&_svg]:size-3.5"),children:r(J,{strokeWidth:2.4})}):d==="stack"?k("div",{className:"flex w-7 shrink-0 flex-col border-l border-border [&_svg]:size-3",children:[r("button",{type:"button",tabIndex:-1,"aria-label":"Increase",disabled:b||E,onPointerDown:x(1),onPointerUp:o,onPointerLeave:o,className:u(g,"flex-1 border-b border-border"),children:r(q,{strokeWidth:2.4})}),r("button",{type:"button",tabIndex:-1,"aria-label":"Decrease",disabled:b||C,onPointerDown:x(-1),onPointerUp:o,onPointerLeave:o,className:u(g,"flex-1"),children:r(j,{strokeWidth:2.4})})]}):null]})});Y.displayName="NumberInput";export{Y as NumberInput};
@@ -0,0 +1,18 @@
1
+ import * as React from "react";
2
+ import { type InputState } from "./input";
3
+ /**
4
+ * PasswordInput — a password field that can be read back before it is sent.
5
+ *
6
+ * The eye shows and hides it. Caps Lock is called out while it is on, because
7
+ * a password that "will not work" is most often typed in capitals. `strength`
8
+ * adds a four-step meter under the field for new passwords (not for sign-in,
9
+ * where it only gets in the way).
10
+ */
11
+ export interface PasswordInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "type"> {
12
+ state?: InputState;
13
+ /** A meter under the field: for choosing a new password. */
14
+ strength?: boolean;
15
+ }
16
+ /** 0–4. Length counts most; each kind of character adds one. */
17
+ export declare function passwordScore(pw: string): number;
18
+ export declare const PasswordInput: React.ForwardRefExoticComponent<PasswordInputProps & React.RefAttributes<HTMLInputElement>>;
@@ -0,0 +1 @@
1
+ "use client";import{jsx as t,jsxs as p}from"react/jsx-runtime";import{ArrowBigUpDash as k,Eye as C,EyeOff as L}from"lucide-react";import*as i from"react";import{cn as u}from"../../lib/utils.js";import{Input as E}from"./input.js";const M=[{label:"Too short",tone:"bg-danger",text:"text-danger"},{label:"Weak",tone:"bg-danger",text:"text-danger"},{label:"Fair",tone:"bg-warning",text:"text-warning"},{label:"Good",tone:"bg-[#4f9d2a]",text:"text-[#4f9d2a]"},{label:"Strong",tone:"bg-success",text:"text-success"}];function P(s){if(s.length<6)return 0;const a=[/[a-z]/,/[A-Z]/,/\d/,/[^A-Za-z0-9]/].filter(d=>d.test(s)).length;let n=s.length>=12?2:s.length>=8?1:0;return n+=Math.max(0,a-1),Math.min(4,Math.max(1,n))}const z=i.forwardRef(({className:s,strength:a,onKeyUp:n,onKeyDown:d,onBlur:w,onChange:h,value:o,defaultValue:m,...v},y)=>{const[r,S]=i.useState(!1),[g,f]=i.useState(!1),[N,I]=i.useState(String(m??"")),l=o!==void 0?String(o):N,x=P(l),c=M[x],b=e=>f(e.getModifierState?.("CapsLock")??!1);return p("div",{className:u("w-full",s),children:[t(E,{ref:y,type:r?"text":"password",autoComplete:a?"new-password":"current-password",spellCheck:!1,value:o,defaultValue:m,onChange:e=>{o===void 0&&I(e.target.value),h?.(e)},onKeyUp:e=>{b(e),n?.(e)},onKeyDown:e=>{b(e),d?.(e)},onBlur:e=>{f(!1),w?.(e)},trailing:p("span",{className:"flex items-center gap-1",children:[g?p("span",{title:"Caps Lock is on",className:"grid size-6 place-items-center rounded-[6px] bg-warning-subtle text-warning animate-[wm-pop_180ms_var(--ease)] [&_svg]:!size-3.5",children:[t(k,{}),t("span",{className:"sr-only",children:"Caps Lock is on"})]}):null,t("button",{type:"button",onClick:()=>S(e=>!e),"aria-label":r?"Hide password":"Show password","aria-pressed":r,className:"grid size-7 cursor-pointer place-items-center rounded-[6px] text-muted transition-colors hover:bg-hover hover:text-text",children:r?t(L,{}):t(C,{})})]}),className:g?"pr-[72px]":"pr-11",...v}),a?p("div",{className:"mt-2 flex items-center gap-3","aria-live":"polite",children:[t("div",{className:"flex flex-1 gap-1",children:[1,2,3,4].map(e=>t("span",{className:"h-1 flex-1 overflow-hidden rounded-full bg-hover",children:t("span",{className:u("block h-full rounded-full transition-[width,background-color] duration-300",c.tone),style:{width:l&&x>=e?"100%":"0%"}})},e))}),t("span",{className:u("w-16 text-right text-[11.5px] font-medium",l?c.text:"text-muted"),children:l?c.label:"Strength"})]}):null]})});z.displayName="PasswordInput";export{z as PasswordInput,P as passwordScore};
@@ -0,0 +1,32 @@
1
+ import * as React from "react";
2
+ /**
3
+ * PinInput — a one-time code or a PIN, one box per character.
4
+ *
5
+ * Typing moves to the next box, Backspace to the previous, and a pasted code
6
+ * (or the SMS code the phone offers) fills every box at once. `onComplete`
7
+ * fires when the last box is filled, so a verify screen needs no button.
8
+ *
9
+ * Looks: `boxed` (default), `underline` (quiet, on a card), `split` (two
10
+ * halves with a dash between: 123–456, the way people read codes aloud).
11
+ */
12
+ export type PinInputVariant = "boxed" | "underline" | "split";
13
+ export interface PinInputProps {
14
+ length?: number;
15
+ value?: string;
16
+ defaultValue?: string;
17
+ onValueChange?: (value: string) => void;
18
+ onComplete?: (value: string) => void;
19
+ /** `numeric` (default) brings up the number pad; `alphanumeric` takes letters too. */
20
+ type?: "numeric" | "alphanumeric";
21
+ /** Dots instead of characters: PINs, not one-time codes. */
22
+ mask?: boolean;
23
+ variant?: PinInputVariant;
24
+ size?: "md" | "lg";
25
+ error?: boolean;
26
+ disabled?: boolean;
27
+ autoFocus?: boolean;
28
+ /** Read aloud as the group's name. */
29
+ "aria-label"?: string;
30
+ className?: string;
31
+ }
32
+ export declare function PinInput({ length, value, defaultValue, onValueChange, onComplete, type, mask, variant, size, error, disabled, autoFocus, "aria-label": ariaLabel, className, }: PinInputProps): React.JSX.Element;
@@ -0,0 +1 @@
1
+ "use client";import{jsx as u,jsxs as z}from"react/jsx-runtime";import*as c from"react";import{cn as y}from"../../lib/utils.js";const N={boxed:"rounded-[10px] border border-border2 bg-raised shadow-[inset_0_1px_1px_rgba(5,8,22,.05)] focus:border-primary focus:ring-[3px] focus:ring-primary/20 data-[filled]:border-border2",split:"rounded-[10px] border border-border2 bg-raised shadow-[inset_0_1px_1px_rgba(5,8,22,.05)] focus:border-primary focus:ring-[3px] focus:ring-primary/20",underline:"rounded-none border-0 border-b-2 border-border2 bg-transparent focus:border-primary"};function O({length:n=6,value:p,defaultValue:w="",onValueChange:h,onComplete:v,type:f="numeric",mask:_,variant:b="boxed",size:P="md",error:l,disabled:k,autoFocus:D,"aria-label":I="Verification code",className:C}){const[V,A]=c.useState(w.slice(0,n)),i=(p??V).slice(0,n),m=c.useRef([]),F=f==="numeric"?/\d/:/[a-z0-9]/i,g=t=>{p===void 0&&A(t),h?.(t),t.length===n&&!t.includes(" ")&&v?.(t)},d=t=>{const e=m.current[Math.max(0,Math.min(n-1,t))];e?.focus(),e?.select()},x=(t,e)=>{const o=[...e].filter(a=>F.test(a)).map(a=>a.toUpperCase());if(!o.length)return;const r=Array.from({length:n},(a,s)=>i[s]??" ");o.slice(0,n-t).forEach((a,s)=>r[t+s]=a),g(r.join("").trimEnd()),d(t+o.length)},M=Math.ceil(n/2),R=P==="lg";return u("div",{role:"group","aria-label":I,className:y("flex items-center gap-2",l&&"wm-shake",C),children:Array.from({length:n},(t,e)=>{const o=i[e]&&i[e]!==" "?i[e]:"";return z(c.Fragment,{children:[b==="split"&&e===M?u("span",{"aria-hidden":!0,className:"h-0.5 w-3 rounded-full bg-border2"}):null,u("input",{ref:r=>{m.current[e]=r},"aria-label":`Character ${e+1} of ${n}`,inputMode:f==="numeric"?"numeric":"text",autoComplete:e===0?"one-time-code":"off",autoFocus:D&&e===0,disabled:k,type:_?"password":"text",value:o,"data-filled":o?"":void 0,"aria-invalid":l||void 0,onFocus:r=>r.target.select(),onChange:r=>{const a=r.target.value.replace(o,"")||r.target.value;x(e,a.length>1?a:a.slice(-1))},onPaste:r=>{r.preventDefault(),x(e,r.clipboardData.getData("text"))},onKeyDown:r=>{if(r.key==="Backspace"){r.preventDefault();const a=Array.from({length:n},(B,E)=>i[E]??" "),s=o?e:e-1;if(s<0)return;a[s]=" ",g(a.join("").trimEnd()),d(s)}else r.key==="ArrowLeft"?(r.preventDefault(),d(e-1)):r.key==="ArrowRight"&&(r.preventDefault(),d(e+1))},className:y("num text-center font-semibold text-text caret-primary outline-none transition-[border-color,box-shadow,transform] duration-150","disabled:cursor-not-allowed disabled:opacity-50 data-[filled]:animate-[wm-pop_160ms_var(--ease)]",R?"h-14 w-12 text-[22px]":"h-11 w-10 text-[18px]",N[b],l&&"border-danger focus:border-danger focus:ring-danger/20 data-[filled]:border-danger")})]},e)})})}export{O as PinInput};
@@ -0,0 +1,27 @@
1
+ import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
2
+ import * as React from "react";
3
+ /**
4
+ * RadioGroup — one choice out of a few, all of them in view. Past five or six
5
+ * options it becomes a Select: a wall of radios is a list nobody reads.
6
+ *
7
+ * Two kinds of item, one group:
8
+ * RadioGroupItem — a dot and a label, with a line under it if it needs one. Forms.
9
+ * RadioCard — the whole card is the target: icon, title, description, and
10
+ * a price or badge on the right. Plans, payment methods, vehicle types.
11
+ * Arrow keys move the choice, as with native radios.
12
+ */
13
+ export declare const RadioGroup: React.ForwardRefExoticComponent<Omit<RadioGroupPrimitive.RadioGroupProps & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
14
+ export interface RadioGroupItemProps extends React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item> {
15
+ label?: React.ReactNode;
16
+ /** A muted line under the label. */
17
+ description?: React.ReactNode;
18
+ }
19
+ export declare const RadioGroupItem: React.ForwardRefExoticComponent<RadioGroupItemProps & React.RefAttributes<HTMLButtonElement>>;
20
+ export interface RadioCardProps extends Omit<React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>, "title"> {
21
+ title: React.ReactNode;
22
+ description?: React.ReactNode;
23
+ icon?: React.ReactNode;
24
+ /** On the right: a price, a time, a badge. */
25
+ aside?: React.ReactNode;
26
+ }
27
+ export declare const RadioCard: React.ForwardRefExoticComponent<RadioCardProps & React.RefAttributes<HTMLButtonElement>>;
@@ -0,0 +1 @@
1
+ "use client";import{jsx as e,jsxs as s}from"react/jsx-runtime";import*as n from"@radix-ui/react-radio-group";import{Check as m}from"lucide-react";import*as l from"react";import{cn as i}from"../../lib/utils.js";const u=l.forwardRef(({className:o,orientation:t,...a},r)=>e(n.Root,{ref:r,orientation:t,className:i(t==="horizontal"?"flex flex-wrap gap-x-5 gap-y-2":"grid gap-2.5",o),...a}));u.displayName="RadioGroup";const b="peer grid size-4 shrink-0 cursor-pointer place-items-center rounded-full border border-border2 bg-raised outline-none shadow-[inset_0_1px_1px_rgba(5,8,22,.06)] transition-[background-color,border-color,box-shadow] duration-150 hover:border-primary/60 focus-visible:ring-2 focus-visible:ring-primary/30 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:shadow-[0_1px_3px_rgba(79,70,229,.4)] disabled:cursor-not-allowed disabled:opacity-50",x=l.forwardRef(({className:o,label:t,description:a,disabled:r,...d},c)=>{const p=e(n.Item,{ref:c,disabled:r,className:i(b,t?"mt-0.5":null,!t&&o),...d,children:e(n.Indicator,{className:"size-1.5 rounded-full bg-white animate-[wm-pop_180ms_var(--ease)]"})});return t?s("label",{className:i("group flex cursor-pointer items-start gap-2.5",r&&"cursor-not-allowed opacity-60",o),children:[p,s("span",{className:"min-w-0",children:[e("span",{className:"block text-[13px] font-medium leading-5 text-text",children:t}),a?e("span",{className:"block text-[12px] leading-[17px] text-muted",children:a}):null]})]}):p});x.displayName="RadioGroupItem";const R=l.forwardRef(({className:o,title:t,description:a,icon:r,aside:d,...c},p)=>s(n.Item,{ref:p,className:i("group relative flex w-full cursor-pointer items-start gap-3 rounded-[12px] border border-border bg-raised p-3.5 pr-10 text-left outline-none","transition-[border-color,background-color,box-shadow] duration-200 hover:border-border2","focus-visible:ring-2 focus-visible:ring-primary/35","data-[state=checked]:border-primary data-[state=checked]:bg-[linear-gradient(180deg,var(--primary-subtle),transparent_120%)] data-[state=checked]:shadow-[0_0_0_3px_var(--primary-subtle),0_6px_16px_-8px_rgba(79,70,229,.35)]","disabled:cursor-not-allowed disabled:opacity-50",o),...c,children:[r?e("span",{className:i("grid size-9 shrink-0 place-items-center rounded-[10px] border border-border bg-surface text-muted transition-colors duration-200 [&_svg]:size-[18px]","group-data-[state=checked]:border-primary/25 group-data-[state=checked]:bg-raised group-data-[state=checked]:text-primary"),children:r}):null,s("span",{className:"min-w-0 flex-1",children:[s("span",{className:"flex items-center justify-between gap-3",children:[e("span",{className:"truncate text-[13.5px] font-semibold text-text",children:t}),d?e("span",{className:"num shrink-0 text-[13px] font-semibold text-text",children:d}):null]}),a?e("span",{className:"mt-0.5 block text-[12px] leading-[17px] text-muted",children:a}):null]}),e("span",{"aria-hidden":!0,className:i("absolute right-3.5 top-3.5 grid size-[18px] place-items-center rounded-full border border-border2 bg-raised text-white transition-[background-color,border-color] duration-200","group-data-[state=checked]:border-primary group-data-[state=checked]:bg-primary"),children:e(m,{className:"size-3 scale-0 transition-transform duration-200 ease-[cubic-bezier(.3,1.6,.5,1)] group-data-[state=checked]:scale-100",strokeWidth:3.2})})]}));R.displayName="RadioCard";export{R as RadioCard,u as RadioGroup,x as RadioGroupItem};
@@ -0,0 +1,24 @@
1
+ import * as SliderPrimitive from "@radix-ui/react-slider";
2
+ import * as React from "react";
3
+ /**
4
+ * Slider — a value along a range, or a range itself (pass two values).
5
+ *
6
+ * The value rides above the thumb while it is held, hovered or focused, so the
7
+ * number is read where the eye already is; `showValue="always"` keeps it up.
8
+ * `marks` put ticks under the track (with labels when given) for the stops
9
+ * that mean something: 0, the free hour, the cap. `variant="gradient"` fills
10
+ * the range in the brand gradient, for the one slider a screen is about.
11
+ */
12
+ export type SliderVariant = "default" | "gradient";
13
+ export interface SliderProps extends Omit<React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>, "children"> {
14
+ variant?: SliderVariant;
15
+ /** Ticks under the track. */
16
+ marks?: {
17
+ value: number;
18
+ label?: React.ReactNode;
19
+ }[];
20
+ /** The value over the thumb: while used (default), always, or never. */
21
+ showValue?: "active" | "always" | "never";
22
+ format?: (value: number) => string;
23
+ }
24
+ export declare const Slider: React.ForwardRefExoticComponent<SliderProps & React.RefAttributes<HTMLSpanElement>>;
@@ -0,0 +1 @@
1
+ "use client";import{jsx as t,jsxs as v}from"react/jsx-runtime";import*as n from"@radix-ui/react-slider";import*as f from"react";import{cn as r}from"../../lib/utils.js";const N=18,P=f.forwardRef(({className:g,variant:h="default",marks:i,showValue:s="active",format:x=p=>p.toLocaleString(),value:u,defaultValue:d,onValueChange:y,min:o=0,max:b=100,...c},_)=>{const[p,w]=f.useState(d??[o]),a=u??p,m=e=>(e-o)/(b-o||1)*100,S=e=>`calc(${m(e)}% + ${N/2*(1-m(e)/50)}px)`,R=i?.some(e=>e.label!==void 0);return v(n.Root,{ref:_,value:u,defaultValue:d,onValueChange:e=>{u===void 0&&w(e),y?.(e)},min:o,max:b,className:r("relative flex w-full touch-none select-none items-center py-2 data-[disabled]:opacity-50",s==="always"&&"mt-7",i&&(R?"mb-6":"mb-2"),g),...c,children:[t(n.Track,{className:"relative h-1.5 w-full grow overflow-hidden rounded-full bg-hover shadow-[inset_0_1px_2px_rgba(5,8,22,.08)]",children:t(n.Range,{className:r("absolute h-full rounded-full",h==="gradient"?"bg-[linear-gradient(90deg,#1f45ea,#7560fd)] shadow-[0_0_10px_rgba(91,74,245,.45)]":"bg-primary")})}),i?.map(e=>{const l=a.length>1?e.value>=a[0]&&e.value<=a[a.length-1]:e.value<=a[0];return v("span",{"aria-hidden":!0,className:"pointer-events-none absolute top-full -translate-x-1/2",style:{left:S(e.value)},children:[t("span",{className:r("mx-auto -mt-1.5 block h-1.5 w-px",l?"bg-primary/50":"bg-border2")}),e.label!==void 0?t("span",{className:"num mt-1 block whitespace-nowrap text-[11px] text-muted",children:e.label}):null]},e.value)}),a.map((e,l)=>t(n.Thumb,{"aria-label":a.length>1?l===0?"Minimum":"Maximum":c["aria-label"],className:r("group/thumb relative block size-[18px] cursor-grab rounded-full border-2 border-primary bg-white outline-none active:cursor-grabbing","shadow-[0_1px_3px_rgba(5,8,22,.25),0_0_0_0_var(--primary-subtle)] transition-[box-shadow,transform] duration-150","hover:shadow-[0_1px_3px_rgba(5,8,22,.25),0_0_0_6px_var(--primary-subtle)] focus-visible:shadow-[0_1px_3px_rgba(5,8,22,.25),0_0_0_6px_var(--primary-subtle)] active:scale-110"),children:s!=="never"?t("span",{className:r("num pointer-events-none absolute bottom-full left-1/2 mb-2.5 -translate-x-1/2 whitespace-nowrap rounded-[7px] bg-[#140d33] px-2 py-1 text-[11.5px] font-semibold leading-none text-white shadow-elev2","after:absolute after:left-1/2 after:top-full after:-translate-x-1/2 after:border-4 after:border-transparent after:border-t-[#140d33]","transition-[opacity,transform] duration-150",s==="always"?"opacity-100":"translate-y-1 opacity-0 group-hover/thumb:translate-y-0 group-hover/thumb:opacity-100 group-focus-visible/thumb:translate-y-0 group-focus-visible/thumb:opacity-100 group-active/thumb:translate-y-0 group-active/thumb:opacity-100"),children:x(e)}):null},l))]})});P.displayName="Slider";export{P as Slider};
@@ -0,0 +1,27 @@
1
+ import * as React from "react";
2
+ /**
3
+ * Spinner — work under way with no known end. (With a known end, Progress.)
4
+ *
5
+ * ring — the default: an arc that fades into its tail. Buttons, inline.
6
+ * dots — three dots in turn: "typing", "connecting", a chat waiting.
7
+ * wave — bars rising like the WAVEINNO wave: a whole panel or page loading.
8
+ * It colours with `currentColor` unless `tone="brand"`, so it takes the text
9
+ * colour of a button it sits in. The label is read out, not shown.
10
+ */
11
+ export type SpinnerVariant = "ring" | "dots" | "wave";
12
+ declare const SIZE: {
13
+ readonly xs: 12;
14
+ readonly sm: 16;
15
+ readonly md: 20;
16
+ readonly lg: 28;
17
+ readonly xl: 40;
18
+ };
19
+ export declare function Spinner({ variant, size, tone, label, className, }: {
20
+ variant?: SpinnerVariant;
21
+ size?: keyof typeof SIZE;
22
+ /** `current` follows the text colour; `brand` uses the primary (the gradient, for ring). */
23
+ tone?: "current" | "brand";
24
+ label?: string;
25
+ className?: string;
26
+ }): React.JSX.Element;
27
+ export {};
@@ -0,0 +1 @@
1
+ import{jsx as n,jsxs as g}from"react/jsx-runtime";import{cn as l}from"../../lib/utils.js";const m={xs:12,sm:16,md:20,lg:28,xl:40};function u({variant:i="ring",size:o="md",tone:s="current",label:c="Loading",className:d}){const e=m[o],p=s==="brand"?"text-primary":void 0;let r;if(i==="ring"){const a=Math.max(2,Math.round(e/9));r=n("span",{className:"block animate-spin rounded-full [animation-duration:.75s]",style:{width:e,height:e,background:s==="brand"?"conic-gradient(from 90deg, transparent 10%, #1f45ea 60%, #7560fd)":"conic-gradient(from 90deg, transparent 10%, currentColor)",mask:`radial-gradient(farthest-side, transparent calc(100% - ${a+.5}px), #000 calc(100% - ${a}px))`,WebkitMask:`radial-gradient(farthest-side, transparent calc(100% - ${a+.5}px), #000 calc(100% - ${a}px))`}})}else if(i==="dots"){const a=Math.max(3,Math.round(e/4.5));r=n("span",{className:"flex items-center",style:{gap:a*.7,height:e},children:[0,1,2].map(t=>n("span",{className:"wm-dot block rounded-full bg-current",style:{width:a,height:a,animationDelay:`${t*140}ms`}},t))})}else{const a=Math.max(2,Math.round(e/8));r=n("span",{className:"flex items-center",style:{gap:a*.8,height:e},children:[0,1,2,3,4].map(t=>n("span",{className:l("wm-bar block h-full rounded-full",s==="brand"?"bg-[linear-gradient(180deg,#7560fd,#1f45ea)]":"bg-current"),style:{width:a,animationDelay:`${t*90}ms`}},t))})}return g("span",{role:"status",className:l("inline-grid shrink-0 place-items-center",p,d),children:[r,n("span",{className:"sr-only",children:c})]})}export{u as Spinner};
@@ -0,0 +1,13 @@
1
+ import * as React from "react";
2
+ import { Toaster as Sonner, toast } from "sonner";
3
+ /**
4
+ * Toaster and toast — a message that comes and goes: saved, sent, exported.
5
+ * (One that must stay until it is dealt with is an Alert.)
6
+ *
7
+ * Put <Toaster /> once near the root (AppShell already has one), then call
8
+ * `toast("Saved")`, `toast.success(…)`, `toast.error(…)`, `toast.promise(…)`
9
+ * from anywhere. The toast is sonner's; the look is WAVEINNO's: a raised card,
10
+ * the tone's icon in a soft tile, and the theme's colours in light and dark.
11
+ */
12
+ export { toast };
13
+ export declare function Toaster({ position, ...props }: React.ComponentProps<typeof Sonner>): React.JSX.Element;
@@ -0,0 +1 @@
1
+ "use client";import{jsx as r}from"react/jsx-runtime";import{CircleAlert as a,CircleCheck as n,Info as i,LoaderCircle as s,TriangleAlert as d}from"lucide-react";import{Toaster as p,toast as c}from"sonner";const e=(t,o)=>r("span",{className:"grid size-7 place-items-center rounded-[8px] [&_svg]:size-4",style:{color:`var(--${t})`,background:`var(--${t}-subtle)`},children:o});function b({position:t="bottom-right",...o}){return r(p,{position:t,gap:10,icons:{success:e("success",r(n,{})),error:e("danger",r(a,{})),warning:e("warning",r(d,{})),info:e("info",r(i,{})),loading:e("primary",r(s,{className:"animate-spin"}))},toastOptions:{unstyled:!0,classNames:{toast:"group flex w-[356px] max-w-[calc(100vw-32px)] items-start gap-3 rounded-[14px] border border-border bg-raised p-3 pr-4 text-[13px] text-text shadow-[var(--lit),0_1px_2px_rgba(5,8,22,.06),0_16px_40px_-12px_rgba(5,8,22,.28)]",icon:"mt-px shrink-0 [&>svg]:hidden [&>span]:flex",content:"min-w-0 flex-1 pt-[5px]",title:"font-semibold leading-[18px] text-text",description:"mt-0.5 text-[12.5px] leading-[17px] text-muted",actionButton:"ml-auto shrink-0 cursor-pointer self-center rounded-[8px] bg-primary px-2.5 py-1.5 text-[12px] font-medium text-white transition-colors hover:bg-primary-hover",cancelButton:"shrink-0 cursor-pointer self-center rounded-[8px] border border-border px-2.5 py-1.5 text-[12px] font-medium text-body transition-colors hover:bg-hover",closeButton:"!border-border !bg-raised !text-muted hover:!text-text",success:"bg-[image:radial-gradient(130%_140%_at_0%_0%,var(--success-subtle),transparent_55%)] border-[color-mix(in_srgb,var(--success)_22%,var(--border))]!",error:"bg-[image:radial-gradient(130%_140%_at_0%_0%,var(--danger-subtle),transparent_55%)] border-[color-mix(in_srgb,var(--danger)_22%,var(--border))]!",warning:"bg-[image:radial-gradient(130%_140%_at_0%_0%,var(--warning-subtle),transparent_55%)] border-[color-mix(in_srgb,var(--warning)_22%,var(--border))]!",info:"bg-[image:radial-gradient(130%_140%_at_0%_0%,var(--info-subtle),transparent_55%)] border-[color-mix(in_srgb,var(--info)_22%,var(--border))]!"}},...o})}export{b as Toaster,c as toast};
@@ -1 +1 @@
1
- "use client";import{jsx as e,jsxs as u}from"react/jsx-runtime";import{useRouter as g}from"next/navigation";import*as i from"react";import{Toaster as v}from"sonner";import{TooltipProvider as R}from"../components/ui/tooltip.js";import{useHotkey as x,useNumberHotkeys as N}from"../hooks/use-hotkey.js";import{useApp as c}from"../store/app-store.js";import{useUi as f}from"../store/ui-store.js";import{CommandPalette as S}from"./command-palette.js";import{ShellProvider as w}from"./config.js";import{IconRail as P}from"./icon-rail.js";import{MobileNav as k}from"./mobile-nav.js";import{ModuleSidebar as A}from"./module-sidebar.js";import{RecordDrawer as C}from"./record-drawer.js";import{Topbar as D}from"./topbar.js";import{useActive as F}from"./use-active.js";function V({config:t,children:r}){return e(w,{config:t,children:e(T,{children:r})})}function T({children:t}){const r=f(o=>o.hydrate),a=c(o=>o.hydrate),m=c(o=>o.syncFromRoute),h=f(o=>o.togglePanel),l=g(),{cfg:p,pathname:y,mod:s,rail:b}=F();i.useEffect(()=>{r(),a(p.apps.map(o=>o.id))},[r,a,p.apps]),i.useEffect(()=>{m(s.app)},[s.app,m]),x({key:"b",mod:!0},h);const n=b.order;return N(i.useCallback(o=>{const d=n[o];d&&l.push(d.href)},[n,l])),u(R,{delayDuration:400,skipDelayDuration:200,children:[u("div",{className:"shell-grid overflow-hidden",children:[e(P,{}),e(A,{}),e(D,{}),e("main",{id:"main",className:"col-start-3 col-end-4 row-start-2 overflow-y-auto px-4 pb-16 pt-5 sm:px-6",children:e("div",{className:"page-in mx-auto w-full max-w-[1600px]",children:t},y)})]}),e(C,{}),e(S,{}),e(k,{}),e(v,{position:"bottom-right",toastOptions:{className:"!rounded-ctl !border !border-border !bg-raised !text-text !shadow-elev3 !text-[13px]"}})]})}export{V as AppShell};
1
+ "use client";import{jsx as e,jsxs as u}from"react/jsx-runtime";import{useRouter as g}from"next/navigation";import*as i from"react";import{Toaster as R}from"../components/ui/toast.js";import{TooltipProvider as b}from"../components/ui/tooltip.js";import{useHotkey as S,useNumberHotkeys as N}from"../hooks/use-hotkey.js";import{useApp as d}from"../store/app-store.js";import{useUi as f}from"../store/ui-store.js";import{CommandPalette as P}from"./command-palette.js";import{ShellProvider as k}from"./config.js";import{IconRail as w}from"./icon-rail.js";import{MobileNav as x}from"./mobile-nav.js";import{ModuleSidebar as A}from"./module-sidebar.js";import{RecordDrawer as C}from"./record-drawer.js";import{Topbar as D}from"./topbar.js";import{useActive as F}from"./use-active.js";function V({config:t,children:r}){return e(k,{config:t,children:e(T,{children:r})})}function T({children:t}){const r=f(o=>o.hydrate),a=d(o=>o.hydrate),m=d(o=>o.syncFromRoute),h=f(o=>o.togglePanel),l=g(),{cfg:p,pathname:y,mod:n,rail:v}=F();i.useEffect(()=>{r(),a(p.apps.map(o=>o.id))},[r,a,p.apps]),i.useEffect(()=>{m(n.app)},[n.app,m]),S({key:"b",mod:!0},h);const s=v.order;return N(i.useCallback(o=>{const c=s[o];c&&l.push(c.href)},[s,l])),u(b,{delayDuration:400,skipDelayDuration:200,children:[u("div",{className:"shell-grid overflow-hidden",children:[e(w,{}),e(A,{}),e(D,{}),e("main",{id:"main",className:"col-start-3 col-end-4 row-start-2 overflow-y-auto px-4 pb-16 pt-5 sm:px-6",children:e("div",{className:"page-in mx-auto w-full max-w-[1600px]",children:t},y)})]}),e(C,{}),e(P,{}),e(x,{}),e(R,{})]})}export{V as AppShell};
package/dist/styles.css CHANGED
@@ -656,3 +656,32 @@ select {
656
656
  100% { background-color: transparent }
657
657
  }
658
658
  .wm-saved { animation: wm-saved 900ms var(--ease) 1 }
659
+
660
+ /* ── input and loading motion ── */
661
+ /* A mark arriving: a radio's dot, a filled code box, the Caps Lock hint. */
662
+ @keyframes wm-pop {
663
+ 0% { transform: scale(.4); opacity: 0 }
664
+ 70% { transform: scale(1.12); opacity: 1 }
665
+ 100% { transform: scale(1) }
666
+ }
667
+ @keyframes wm-dot {
668
+ 0%, 80%, 100% { transform: translateY(0); opacity: .35 }
669
+ 40% { transform: translateY(-40%); opacity: 1 }
670
+ }
671
+ @keyframes wm-bar {
672
+ 0%, 100% { transform: scaleY(.35) }
673
+ 50% { transform: scaleY(1) }
674
+ }
675
+ .wm-dot { animation: wm-dot 1.1s var(--ease) infinite }
676
+ .wm-bar { animation: wm-bar 1s var(--ease) infinite; transform-origin: center }
677
+
678
+ /* A spinner that stops looks like a hang. Under reduced motion it keeps
679
+ turning, slowly, instead of being cut to one frame by the rule above. */
680
+ @media (prefers-reduced-motion: reduce) {
681
+ [role="status"] .animate-spin,
682
+ .wm-dot,
683
+ .wm-bar {
684
+ animation-duration: 1.8s !important;
685
+ animation-iteration-count: infinite !important;
686
+ }
687
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@waveinno/ui",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "WAVEINNO design system — tokens, shell, primitives and patterns shared by every WAVEINNO product",
5
5
  "keywords": [
6
6
  "waveinno",
@@ -64,8 +64,10 @@
64
64
  "@radix-ui/react-label": "^2.1.15",
65
65
  "@radix-ui/react-popover": "^1.1.23",
66
66
  "@radix-ui/react-progress": "^1.1.16",
67
+ "@radix-ui/react-radio-group": "^1.4.7",
67
68
  "@radix-ui/react-select": "^2.3.7",
68
69
  "@radix-ui/react-separator": "^1.1.15",
70
+ "@radix-ui/react-slider": "^1.4.7",
69
71
  "@radix-ui/react-slot": "^1.3.3",
70
72
  "@radix-ui/react-switch": "^1.3.7",
71
73
  "@radix-ui/react-tabs": "^1.1.21",