@skiddph/prui 0.11.0 → 0.13.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.
@@ -6,6 +6,7 @@ export { Portal, useReducedMotion, useFocusTrap, useEscapeKey, useScrollLock, us
6
6
  export { moveIndex, homeIndex, endIndex, typeaheadIndex } from './list-nav';
7
7
  export { Button, buttonPropsMeta, type ButtonProps, type ButtonVariant, type ButtonSize, } from './button';
8
8
  export { Input, inputPropsMeta, Textarea, textareaPropsMeta, type InputProps, type TextareaProps, } from './input';
9
+ export { TagInput, tagInputPropsMeta, type TagInputProps, type TagInputSize, } from './tag-input';
9
10
  export { Label, labelPropsMeta, Separator, separatorPropsMeta, type LabelProps, type SeparatorProps } from './label';
10
11
  export { Select, SelectTrigger, SelectValue, SelectContent, SelectItem, selectPropsMeta, type SelectProps, type SelectOption, type SelectTriggerProps, type SelectValueProps, type SelectContentProps, type SelectItemProps, } from './select';
11
12
  export { Switch, switchPropsMeta, type SwitchProps } from './switch';
@@ -14,8 +14,12 @@ export interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElem
14
14
  variant?: "default" | "filled";
15
15
  /** Icon rendered inside the field's leading edge. */
16
16
  icon?: React.ReactNode;
17
- /** Icon rendered inside the field's trailing edge. */
17
+ /** Icon rendered inside the field's trailing edge. A custom trailingIcon
18
+ * takes over the slot and suppresses the password eye toggle. */
18
19
  trailingIcon?: React.ReactNode;
20
+ /** With type="password", render an eye show/hide toggle in the trailing
21
+ * slot (the default). Set false to keep a plain masked field. */
22
+ passwordToggle?: boolean;
19
23
  }
20
24
  export declare const Input: React.ForwardRefExoticComponent<InputProps & React.RefAttributes<HTMLInputElement>>;
21
25
  export declare const inputPropsMeta: PropsMeta;
@@ -1,59 +1,83 @@
1
- import { jsx as a, jsxs as N } from "react/jsx-runtime";
2
- import * as d from "react";
3
- import { resolveSurface as R, withSurface as j } from "./surface.js";
4
- import { cn as u } from "./cn.js";
5
- const p = "prui-f-control prui-input w-full bg-[var(--prui-s-bg,var(--prui-background))] text-[var(--prui-s-fg,var(--prui-fg))] placeholder:text-[var(--prui-dim)] border border-[var(--prui-line)] rounded-[var(--prui-s-radius,var(--prui-radius))] text-sm outline-none transition-colors focus:border-[var(--prui-brand)] focus:ring-2 focus:ring-[var(--prui-brand)]/30 disabled:opacity-50 disabled:cursor-not-allowed", z = {
1
+ import { jsx as e, jsxs as S } from "react/jsx-runtime";
2
+ import * as u from "react";
3
+ import { EyeOff as T, Eye as k } from "lucide-react";
4
+ import { resolveSurface as z, withSurface as E } from "./surface.js";
5
+ import { cn as h } from "./cn.js";
6
+ import { usePruiI18n as M } from "../i18n.js";
7
+ const v = "prui-f-control prui-input w-full bg-[var(--prui-s-bg,var(--prui-background))] text-[var(--prui-s-fg,var(--prui-fg))] placeholder:text-[var(--prui-dim)] border border-[var(--prui-line)] rounded-[var(--prui-s-radius,var(--prui-radius))] text-sm outline-none transition-colors focus:border-[var(--prui-brand)] focus:ring-2 focus:ring-[var(--prui-brand)]/30 disabled:opacity-50 disabled:cursor-not-allowed", D = {
6
8
  sm: "h-8 px-2.5",
7
9
  md: "h-9 px-3",
8
10
  lg: "h-11 px-4 text-base"
9
- }, C = d.forwardRef(
10
- ({ className: r, style: n, type: l = "text", size: o = "md", variant: c = "default", icon: e, trailingIcon: t, bg: m, fg: f, radius: g, texture: x, textureColor: v, elevation: y, ...b }, h) => {
11
- const w = R({ bg: m, fg: f, radius: g, texture: x, textureColor: v, elevation: y }), s = j(
12
- u(
13
- p,
14
- z[o],
15
- c === "filled" && "bg-[var(--prui-s-bg,var(--prui-raise))] border-transparent",
16
- e && "pl-9",
17
- t && "pr-9",
18
- r
11
+ }, B = u.forwardRef(
12
+ ({ className: s, style: l, type: a = "text", size: i = "md", variant: g = "default", icon: r, trailingIcon: t, passwordToggle: y = !0, bg: b, fg: x, radius: w, texture: N, textureColor: R, elevation: P, ...p }, j) => {
13
+ const { t: c } = M(), [n, C] = u.useState(!1), o = a === "password" && y !== !1 && !t, I = z({ bg: b, fg: x, radius: w, texture: N, textureColor: R, elevation: P }), f = E(
14
+ h(
15
+ v,
16
+ D[i],
17
+ g === "filled" && "bg-[var(--prui-s-bg,var(--prui-raise))] border-transparent",
18
+ r && "pl-9",
19
+ (t || o) && "pr-9",
20
+ s
19
21
  ),
20
- n,
21
- w
22
- ), i = /* @__PURE__ */ a("input", { ref: h, type: l, className: s.className, style: s.style, ...b });
23
- return !e && !t ? i : /* @__PURE__ */ N("span", { className: "relative inline-flex w-full", children: [
24
- e ? /* @__PURE__ */ a("span", { "aria-hidden": !0, className: "pointer-events-none absolute left-3 top-1/2 flex -translate-y-1/2 items-center text-[var(--prui-dim)] [&>svg]:h-4 [&>svg]:w-4", children: e }) : null,
25
- i,
26
- t ? /* @__PURE__ */ a("span", { "aria-hidden": !0, className: "pointer-events-none absolute right-3 top-1/2 flex -translate-y-1/2 items-center text-[var(--prui-dim)] [&>svg]:h-4 [&>svg]:w-4", children: t }) : null
22
+ l,
23
+ I
24
+ ), m = /* @__PURE__ */ e(
25
+ "input",
26
+ {
27
+ ref: j,
28
+ type: o && n ? "text" : a,
29
+ className: f.className,
30
+ style: f.style,
31
+ ...p
32
+ }
33
+ );
34
+ return !r && !t && !o ? m : /* @__PURE__ */ S("span", { className: "relative inline-flex w-full", children: [
35
+ r ? /* @__PURE__ */ e("span", { "aria-hidden": !0, className: "pointer-events-none absolute left-3 top-1/2 flex -translate-y-1/2 items-center text-[var(--prui-dim)] [&>svg]:h-4 [&>svg]:w-4", children: r }) : null,
36
+ m,
37
+ t ? /* @__PURE__ */ e("span", { "aria-hidden": !0, className: "pointer-events-none absolute right-3 top-1/2 flex -translate-y-1/2 items-center text-[var(--prui-dim)] [&>svg]:h-4 [&>svg]:w-4", children: t }) : o ? /* @__PURE__ */ e(
38
+ "button",
39
+ {
40
+ type: "button",
41
+ "aria-label": n ? c.hidePassword : c.showPassword,
42
+ "aria-pressed": n,
43
+ disabled: p.disabled,
44
+ onMouseDown: (d) => d.preventDefault(),
45
+ onClick: () => C((d) => !d),
46
+ className: "absolute right-1.5 top-1/2 flex h-6 w-6 -translate-y-1/2 cursor-pointer items-center justify-center rounded-[var(--prui-radius-1)] text-[var(--prui-dim)] hover:bg-[var(--prui-raise)] hover:text-[var(--prui-fg)] disabled:cursor-not-allowed disabled:opacity-50",
47
+ children: n ? /* @__PURE__ */ e(T, { className: "h-4 w-4", "aria-hidden": !0 }) : /* @__PURE__ */ e(k, { className: "h-4 w-4", "aria-hidden": !0 })
48
+ }
49
+ ) : null
27
50
  ] });
28
51
  }
29
52
  );
30
- C.displayName = "Input";
31
- const P = {
53
+ B.displayName = "Input";
54
+ const J = {
32
55
  name: "Input",
33
56
  props: [
34
57
  { name: "type", type: "string", default: "'text'", control: "select", options: ["text", "password", "email", "number", "search"] },
35
58
  { name: "size", type: "'sm' | 'md' | 'lg'", default: "'md'", control: "select", options: ["sm", "md", "lg"] },
36
59
  { name: "variant", type: "'default' | 'filled'", default: "'default'", control: "select", options: ["default", "filled"] },
37
60
  { name: "icon", type: "ReactNode", default: "undefined", control: "none", description: "Icon inside the leading edge (padding adjusts)." },
38
- { name: "trailingIcon", type: "ReactNode", default: "undefined", control: "none", description: "Icon inside the trailing edge." },
61
+ { name: "trailingIcon", type: "ReactNode", default: "undefined", control: "none", description: "Icon inside the trailing edge; takes over the slot from the password eye." },
62
+ { name: "passwordToggle", type: "boolean", default: "true", control: "boolean", description: "type=password renders an eye show/hide toggle unless a trailingIcon owns the slot." },
39
63
  { name: "value", type: "string", default: null, control: "text" },
40
64
  { name: "placeholder", type: "string", default: "undefined", control: "text" },
41
65
  { name: "disabled", type: "boolean", default: "false", control: "boolean" },
42
66
  { name: "onChange", type: "(e) => void", default: null, control: "none" }
43
67
  ]
44
- }, I = d.forwardRef(
45
- ({ className: r, style: n, ...l }, o) => /* @__PURE__ */ a(
68
+ }, O = u.forwardRef(
69
+ ({ className: s, style: l, ...a }, i) => /* @__PURE__ */ e(
46
70
  "textarea",
47
71
  {
48
- ref: o,
49
- style: n,
50
- className: u(p, "h-auto min-h-20 resize-y px-3 py-2", r),
51
- ...l
72
+ ref: i,
73
+ style: l,
74
+ className: h(v, "h-auto min-h-20 resize-y px-3 py-2", s),
75
+ ...a
52
76
  }
53
77
  )
54
78
  );
55
- I.displayName = "Textarea";
56
- const k = {
79
+ O.displayName = "Textarea";
80
+ const K = {
57
81
  name: "Textarea",
58
82
  props: [
59
83
  { name: "value", type: "string", default: null, control: "textarea" },
@@ -63,8 +87,8 @@ const k = {
63
87
  ]
64
88
  };
65
89
  export {
66
- C as Input,
67
- I as Textarea,
68
- P as inputPropsMeta,
69
- k as textareaPropsMeta
90
+ B as Input,
91
+ O as Textarea,
92
+ J as inputPropsMeta,
93
+ K as textareaPropsMeta
70
94
  };
@@ -0,0 +1,47 @@
1
+ import { SurfaceProps } from './surface';
2
+ import { PropsMeta } from './props-meta';
3
+ import * as React from "react";
4
+ /**
5
+ * TagInput: comma-separated entry as removable chips (tags, email
6
+ * recipients, keywords). Text commits on a separator, Enter, paste of
7
+ * multi-value text, or blur; Backspace on an empty field removes the last
8
+ * tag. The chip and the value are separate concerns: labelFor decides what
9
+ * the chip displays while onChange keeps emitting the original strings, and
10
+ * renderTag takes over the chip entirely.
11
+ */
12
+ export type TagInputSize = "sm" | "md" | "lg";
13
+ export interface TagInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "size" | "value" | "defaultValue" | "onChange" | "children">, SurfaceProps {
14
+ /** Control height step (the field grows vertically with wrapped chips). */
15
+ size?: TagInputSize;
16
+ /** Visual variant: outlined (default) or filled. */
17
+ variant?: "default" | "filled";
18
+ /** Controlled tag values. */
19
+ value?: string[];
20
+ /** Initial tags for the uncontrolled field. */
21
+ defaultValue?: string[];
22
+ /** Emits the full tag array on every add/remove. */
23
+ onChange?: (tags: string[]) => void;
24
+ /** Characters that commit the typed text as a tag (default: comma). */
25
+ separators?: string[];
26
+ /** Commit the pending text when the field blurs (default true). */
27
+ commitOnBlur?: boolean;
28
+ /** Programmable commit: split raw text into tag values. Defaults to
29
+ * splitting on `separators`, trimming, and dropping empties. Use it to
30
+ * accept pair syntaxes (e.g. "Name, email" -> "Name <email>"). */
31
+ parseInput?: (raw: string) => string[];
32
+ /** Reject a candidate; rejected text stays editable in the field. */
33
+ validate?: (tag: string) => boolean;
34
+ /** Stop accepting new tags at this count. */
35
+ maxTags?: number;
36
+ /** Allow the same value more than once (default false). */
37
+ allowDuplicates?: boolean;
38
+ /** What the chip displays — the emitted value keeps the original string
39
+ * (e.g. show only the name for "Name <email>"). */
40
+ labelFor?: (tag: string) => React.ReactNode;
41
+ /** Fully custom chip: (tag, { remove }) => node. */
42
+ renderTag?: (tag: string, api: {
43
+ remove: () => void;
44
+ }) => React.ReactNode;
45
+ }
46
+ export declare const TagInput: React.ForwardRefExoticComponent<TagInputProps & React.RefAttributes<HTMLInputElement>>;
47
+ export declare const tagInputPropsMeta: PropsMeta;
@@ -0,0 +1,140 @@
1
+ import { jsxs as I, jsx as u } from "react/jsx-runtime";
2
+ import * as c from "react";
3
+ import { X as L } from "lucide-react";
4
+ import { resolveSurface as Q, withSurface as W } from "./surface.js";
5
+ import { cn as Y } from "./cn.js";
6
+ import { usePruiI18n as Z } from "../i18n.js";
7
+ const ee = (v) => v.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), te = {
8
+ sm: "min-h-8 text-sm",
9
+ md: "min-h-9 text-sm",
10
+ lg: "min-h-11 text-base"
11
+ }, ne = c.forwardRef(function({
12
+ className: j,
13
+ style: k,
14
+ size: R = "md",
15
+ variant: T = "default",
16
+ value: b,
17
+ defaultValue: B,
18
+ onChange: d,
19
+ separators: l = [","],
20
+ commitOnBlur: P = !0,
21
+ parseInput: S,
22
+ validate: h,
23
+ maxTags: y,
24
+ allowDuplicates: C = !1,
25
+ labelFor: x,
26
+ renderTag: w,
27
+ disabled: m,
28
+ bg: z,
29
+ fg: E,
30
+ radius: F,
31
+ texture: K,
32
+ textureColor: A,
33
+ elevation: M,
34
+ ...n
35
+ }, O) {
36
+ const { t: U } = Z(), N = b !== void 0, [V, X] = c.useState(B ?? []), r = N ? b : V, [o, f] = c.useState(""), _ = S ?? ((e) => e.split(new RegExp(l.map(ee).join("|"))).map((t) => t.trim()).filter(Boolean)), $ = (e) => {
37
+ N || X(e), d == null || d(e);
38
+ }, p = (e) => {
39
+ const t = _(e);
40
+ if (!t.length) {
41
+ f("");
42
+ return;
43
+ }
44
+ const a = [...r], i = [];
45
+ for (const s of t)
46
+ y !== void 0 && a.length >= y ? i.push(s) : !C && a.includes(s) || (h && !h(s) ? i.push(s) : a.push(s));
47
+ f(i.join(l[0] ?? ", ")), a.length !== r.length && $(a);
48
+ }, g = (e) => $(r.filter((t, a) => a !== e)), q = (e) => {
49
+ var t;
50
+ e.key === "Enter" || l.includes(e.key) ? (e.preventDefault(), p(o)) : e.key === "Backspace" && !o && r.length && (e.preventDefault(), g(r.length - 1)), (t = n.onKeyDown) == null || t.call(n, e);
51
+ }, G = (e) => {
52
+ var a;
53
+ const t = e.clipboardData.getData("text");
54
+ if (t && l.some((i) => t.includes(i))) {
55
+ e.preventDefault(), p(o ? `${o}${l[0] ?? ","}${t}` : t);
56
+ return;
57
+ }
58
+ (a = n.onPaste) == null || a.call(n, e);
59
+ }, H = (e) => {
60
+ var t;
61
+ P && p(o), (t = n.onBlur) == null || t.call(n, e);
62
+ }, J = Q({ bg: z, fg: E, radius: F, texture: K, textureColor: A, elevation: M }), D = W(
63
+ Y(
64
+ "prui-f-control prui-taginput flex w-full flex-wrap items-center gap-1 px-1.5 py-1",
65
+ "border border-[var(--prui-line)] rounded-[var(--prui-s-radius,var(--prui-radius))]",
66
+ "bg-[var(--prui-s-bg,var(--prui-background))] text-[var(--prui-s-fg,var(--prui-fg))] placeholder:text-[var(--prui-dim)]",
67
+ "outline-none transition-colors focus-within:border-[var(--prui-brand)] focus-within:ring-2 focus-within:ring-[var(--prui-brand)]/30",
68
+ te[R],
69
+ T === "filled" && "bg-[var(--prui-s-bg,var(--prui-raise))] border-transparent",
70
+ m && "opacity-50 cursor-not-allowed",
71
+ j
72
+ ),
73
+ k,
74
+ J
75
+ );
76
+ return /* @__PURE__ */ I("div", { className: D.className, style: D.style, children: [
77
+ r.map(
78
+ (e, t) => w ? /* @__PURE__ */ u(c.Fragment, { children: w(e, { remove: () => g(t) }) }, `${e}-${t}`) : /* @__PURE__ */ I(
79
+ "span",
80
+ {
81
+ className: "inline-flex max-w-full items-center gap-1 rounded-[var(--prui-radius-full)] border border-[var(--prui-line)] bg-[var(--prui-raise)] py-0.5 pl-2 pr-1 text-xs text-[var(--prui-fg)]",
82
+ children: [
83
+ /* @__PURE__ */ u("span", { className: "max-w-64 truncate", children: x ? x(e) : e }),
84
+ /* @__PURE__ */ u(
85
+ "button",
86
+ {
87
+ type: "button",
88
+ "aria-label": U.removeTag.replace("{label}", e),
89
+ disabled: m,
90
+ onClick: () => g(t),
91
+ className: "flex h-4 w-4 cursor-pointer items-center justify-center rounded-full text-[var(--prui-dim)] hover:bg-[var(--prui-line)] hover:text-[var(--prui-fg)] disabled:cursor-not-allowed",
92
+ children: /* @__PURE__ */ u(L, { className: "h-3 w-3", "aria-hidden": !0 })
93
+ }
94
+ )
95
+ ]
96
+ },
97
+ `${e}-${t}`
98
+ )
99
+ ),
100
+ /* @__PURE__ */ u(
101
+ "input",
102
+ {
103
+ ref: O,
104
+ value: o,
105
+ onChange: (e) => f(e.target.value),
106
+ onKeyDown: q,
107
+ onPaste: G,
108
+ onBlur: H,
109
+ disabled: m,
110
+ ...n,
111
+ className: "h-7 min-w-16 flex-1 bg-transparent text-sm outline-none placeholder:text-[var(--prui-dim)] disabled:cursor-not-allowed"
112
+ }
113
+ )
114
+ ] });
115
+ });
116
+ ne.displayName = "TagInput";
117
+ const se = {
118
+ name: "TagInput",
119
+ props: [
120
+ { name: "value", type: "string[]", default: "uncontrolled", control: "none" },
121
+ { name: "defaultValue", type: "string[]", default: "[]", control: "none" },
122
+ { name: "onChange", type: "(tags: string[]) => void", default: null, control: "none" },
123
+ { name: "separators", type: "string[]", default: "[',']", control: "none", description: "Characters that commit the typed text (plus Enter, paste, blur)." },
124
+ { name: "commitOnBlur", type: "boolean", default: "true", control: "boolean" },
125
+ { name: "parseInput", type: "(raw: string) => string[]", default: "split on separators", control: "none", description: "Programmable commit: build tag values from raw text (pair syntaxes, normalization)." },
126
+ { name: "validate", type: "(tag: string) => boolean", default: "undefined", control: "none", description: "Reject a candidate; it stays editable in the field." },
127
+ { name: "labelFor", type: "(tag: string) => ReactNode", default: "undefined", control: "none", description: "Chip display; onChange keeps emitting the original strings." },
128
+ { name: "renderTag", type: "(tag, { remove }) => ReactNode", default: "default chip", control: "none", description: "Fully custom chip rendering." },
129
+ { name: "maxTags", type: "number", default: "undefined", control: "number" },
130
+ { name: "allowDuplicates", type: "boolean", default: "false", control: "boolean" },
131
+ { name: "size", type: "'sm' | 'md' | 'lg'", default: "'md'", control: "select", options: ["sm", "md", "lg"] },
132
+ { name: "variant", type: "'default' | 'filled'", default: "'default'", control: "select", options: ["default", "filled"] },
133
+ { name: "placeholder", type: "string", default: "undefined", control: "text" },
134
+ { name: "disabled", type: "boolean", default: "false", control: "boolean" }
135
+ ]
136
+ };
137
+ export {
138
+ ne as TagInput,
139
+ se as tagInputPropsMeta
140
+ };