@cystackapp/ui 1.4.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/dist/assets/background-pattern-circles-md.svg.js +5 -0
- package/dist/components/badge/Badge.d.ts +29 -0
- package/dist/components/badge/Badge.js +67 -0
- package/dist/components/badge/BadgeTestStory.d.ts +5 -0
- package/dist/components/badge/badge-utils.d.ts +27 -0
- package/dist/components/badge/badge-utils.js +74 -0
- package/dist/components/badge/variants/BadgeMore.d.ts +6 -0
- package/dist/components/badge/variants/BadgeMore.js +10 -0
- package/dist/components/badge/variants/BadgeMoreTestStory.d.ts +5 -0
- package/dist/components/badge/variants/BadgeTag.d.ts +10 -0
- package/dist/components/badge/variants/BadgeTag.js +50 -0
- package/dist/components/combobox/Combobox.d.ts +96 -0
- package/dist/components/combobox/Combobox.js +206 -0
- package/dist/components/combobox/use-validated-combobox.d.ts +17 -0
- package/dist/components/combobox/use-validated-combobox.js +23 -0
- package/dist/components/error-state/ErrorState.d.ts +9 -0
- package/dist/components/error-state/ErrorState.js +33 -0
- package/dist/components/page-title/PageTitle.d.ts +7 -0
- package/dist/components/page-title/PageTitle.js +6 -0
- package/dist/components/popover/Popover.d.ts +8 -0
- package/dist/components/popover/Popover.js +42 -0
- package/dist/components/popover/use-popover-coord.d.ts +24 -0
- package/dist/components/popover/use-popover-coord.js +130 -0
- package/dist/components/switch/Switch.d.ts +7 -0
- package/dist/components/switch/Switch.js +39 -0
- package/dist/components/tooltip/Tooltip.d.ts +11 -0
- package/dist/components/tooltip/Tooltip.js +58 -0
- package/dist/components/tooltip/tooltip-utils.d.ts +4 -0
- package/dist/components/tooltip/tooltip-utils.js +120 -0
- package/dist/hooks/element-shift/use-animation-frame.d.ts +4 -0
- package/dist/hooks/element-shift/use-animation-frame.js +14 -0
- package/dist/hooks/element-shift/use-element-shift.d.ts +17 -0
- package/dist/hooks/element-shift/use-element-shift.js +22 -0
- package/dist/hooks/element-shift/use-mutation-observer.d.ts +4 -0
- package/dist/hooks/element-shift/use-mutation-observer.js +15 -0
- package/dist/hooks/element-shift/use-resize-observer.d.ts +5 -0
- package/dist/hooks/element-shift/use-resize-observer.js +13 -0
- package/dist/hooks/element-shift/use-scroll-listener.d.ts +5 -0
- package/dist/hooks/element-shift/use-scroll-listener.js +26 -0
- package/dist/hooks/element-shift/use-transition-end-listener.d.ts +5 -0
- package/dist/hooks/element-shift/use-transition-end-listener.js +28 -0
- package/dist/hooks/element-shift/use-window-resize-listener.d.ts +4 -0
- package/dist/hooks/element-shift/use-window-resize-listener.js +10 -0
- package/dist/i18n/resources.d.ts +2 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +42 -0
- package/dist/node_modules/clsx/dist/clsx.js +16 -0
- package/dist/node_modules/tailwind-merge/dist/bundle-mjs.js +2924 -0
- package/dist/utils/cn.d.ts +2 -0
- package/dist/utils/cn.js +8 -0
- package/package.json +66 -0
- package/theme.css +358 -0
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import * as e from "react";
|
|
2
|
+
const r = (t) => /* @__PURE__ */ e.createElement("svg", { height: 480, width: 480, viewBox: "0 0 480 480", xmlns: "http://www.w3.org/2000/svg", ...t }, /* @__PURE__ */ e.createElement("defs", null, /* @__PURE__ */ e.createElement("radialGradient", { id: "radialGradient-mask", cx: "50%", cy: "50%", r: "50%", fx: "50%", fy: "50%" }, /* @__PURE__ */ e.createElement("stop", { offset: "0%", stopColor: "white" }), /* @__PURE__ */ e.createElement("stop", { offset: "100%", stopColor: "black" })), /* @__PURE__ */ e.createElement("mask", { id: "background-pattern-decorative-mask" }, /* @__PURE__ */ e.createElement("rect", { x: 0, y: 0, height: "100%", width: "100%", fill: "url(#radialGradient-mask)" }))), /* @__PURE__ */ e.createElement("g", { mask: "url(#background-pattern-decorative-mask)" }, /* @__PURE__ */ e.createElement("circle", { cx: 240, cy: 240, r: 240, stroke: "#E9EAEB", fill: "transparent" }), /* @__PURE__ */ e.createElement("circle", { cx: 240, cy: 240, r: 208, stroke: "#E9EAEB", fill: "transparent" }), /* @__PURE__ */ e.createElement("circle", { cx: 240, cy: 240, r: 176, stroke: "#E9EAEB", fill: "transparent" }), /* @__PURE__ */ e.createElement("circle", { cx: 240, cy: 240, r: 144, stroke: "#E9EAEB", fill: "transparent" }), /* @__PURE__ */ e.createElement("circle", { cx: 240, cy: 240, r: 112, stroke: "#E9EAEB", fill: "transparent" }), /* @__PURE__ */ e.createElement("circle", { cx: 240, cy: 240, r: 80, stroke: "#E9EAEB", fill: "transparent" }), /* @__PURE__ */ e.createElement("circle", { cx: 240, cy: 240, r: 48, stroke: "#E9EAEB", fill: "transparent" })));
|
|
3
|
+
export {
|
|
4
|
+
r as default
|
|
5
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { BadgeColor, BadgeSize, BadgeType } from './badge-utils';
|
|
3
|
+
export interface BadgeProps {
|
|
4
|
+
children: ReactNode;
|
|
5
|
+
color?: BadgeColor;
|
|
6
|
+
size?: BadgeSize;
|
|
7
|
+
type?: BadgeType;
|
|
8
|
+
dot?: boolean;
|
|
9
|
+
iconLeading?: ReactNode;
|
|
10
|
+
iconTrailing?: ReactNode;
|
|
11
|
+
/** When provided, renders a close/dismiss button. */
|
|
12
|
+
onClose?: () => void;
|
|
13
|
+
className?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Visual indicator for categorizing items. Supports three style types:
|
|
17
|
+
* pill (rounded-full), badge (rounded-md), and modern (shadow + white bg).
|
|
18
|
+
*
|
|
19
|
+
* `dot` and `iconLeading` share the same leading slot — if both are provided,
|
|
20
|
+
* `iconLeading` takes precedence and the dot is not rendered.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```tsx
|
|
24
|
+
* <Badge color="success" size="sm">Active</Badge>
|
|
25
|
+
* <Badge type="modern" dot color="error">Failed</Badge>
|
|
26
|
+
* <Badge iconLeading={<AlertCircle />} onClose={handleDismiss}>Warning</Badge>
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export declare const Badge: ({ children, color, size, type, dot, iconLeading, iconTrailing, onClose, className, }: BadgeProps) => import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { jsxs as b, jsx as e } from "react/jsx-runtime";
|
|
2
|
+
import { useRef as D, useState as i, useCallback as y, useEffect as N } from "react";
|
|
3
|
+
import { XClose as k } from "@untitled-ui/icons-react";
|
|
4
|
+
import { cn as u } from "../../utils/cn.js";
|
|
5
|
+
import { Tooltip as B } from "../tooltip/Tooltip.js";
|
|
6
|
+
import { useResizeObserver as R } from "../../hooks/element-shift/use-resize-observer.js";
|
|
7
|
+
import { BADGE_ICON_SIZE_CLASSES as T, BADGE_DOT_CLASSES as h, BADGE_MODERN_TEXT_CLASSES as w, BADGE_COLOR_CLASSES as G, BADGE_SIZE_CLASSES as O, BADGE_TYPE_CLASSES as v } from "./badge-utils.js";
|
|
8
|
+
const X = ({
|
|
9
|
+
children: t,
|
|
10
|
+
color: o = "gray",
|
|
11
|
+
size: a = "md",
|
|
12
|
+
type: l = "pill",
|
|
13
|
+
dot: f = !1,
|
|
14
|
+
iconLeading: n,
|
|
15
|
+
iconTrailing: c,
|
|
16
|
+
onClose: m,
|
|
17
|
+
className: p
|
|
18
|
+
}) => {
|
|
19
|
+
const s = D(null), [S, E] = i(!1), [_, A] = i(!1), d = y(() => {
|
|
20
|
+
const r = s.current;
|
|
21
|
+
r && E(r.scrollWidth > r.clientWidth);
|
|
22
|
+
}, []);
|
|
23
|
+
R(s, d), N(() => {
|
|
24
|
+
A(!0);
|
|
25
|
+
}, []);
|
|
26
|
+
const C = f && !n;
|
|
27
|
+
return /* @__PURE__ */ b(
|
|
28
|
+
"span",
|
|
29
|
+
{
|
|
30
|
+
className: u(
|
|
31
|
+
"inline-flex items-center w-fit max-w-32 font-medium transition-all",
|
|
32
|
+
_ ? "opacity-100 scale-100" : "opacity-0 scale-90",
|
|
33
|
+
v[l],
|
|
34
|
+
O[a],
|
|
35
|
+
l === "modern" ? `bg-white border-gray-v2-300 ${w[o]}` : G[o],
|
|
36
|
+
p
|
|
37
|
+
),
|
|
38
|
+
children: [
|
|
39
|
+
C ? /* @__PURE__ */ e(
|
|
40
|
+
"span",
|
|
41
|
+
{
|
|
42
|
+
className: u(
|
|
43
|
+
"size-1.5 rounded-full shrink-0",
|
|
44
|
+
h[o]
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
) : null,
|
|
48
|
+
n || null,
|
|
49
|
+
S && typeof t == "string" ? /* @__PURE__ */ e(B, { content: t, children: /* @__PURE__ */ e("span", { ref: s, className: "block truncate min-w-0", children: t }) }) : /* @__PURE__ */ e("span", { ref: s, className: "block truncate min-w-0", children: t }),
|
|
50
|
+
c || null,
|
|
51
|
+
m ? /* @__PURE__ */ e(
|
|
52
|
+
"button",
|
|
53
|
+
{
|
|
54
|
+
type: "button",
|
|
55
|
+
"aria-label": typeof t == "string" ? `Remove ${t}` : "Remove",
|
|
56
|
+
onClick: m,
|
|
57
|
+
className: "shrink-0 cursor-pointer rounded-full p-0 hover:opacity-70 transition-opacity",
|
|
58
|
+
children: /* @__PURE__ */ e(k, { className: T[a] })
|
|
59
|
+
}
|
|
60
|
+
) : null
|
|
61
|
+
]
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
};
|
|
65
|
+
export {
|
|
66
|
+
X as Badge
|
|
67
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Available badge colors — matches Untitled UI's standard palette.
|
|
3
|
+
*
|
|
4
|
+
* These must correspond to color tokens defined in `packages/ui/theme.css`
|
|
5
|
+
* `@theme` block (e.g. `--color-gray-v2-50`, `--color-brand-v2-200`). If a color is
|
|
6
|
+
* added or removed from the theme, update this array and all class maps below.
|
|
7
|
+
*/
|
|
8
|
+
export declare const BADGE_COLORS: readonly ["gray", "brand", "error", "warning", "success", "gray-blue", "blue-light", "blue", "indigo", "purple", "pink", "orange"];
|
|
9
|
+
/** Available badge sizes. */
|
|
10
|
+
export declare const BADGE_SIZES: readonly ["sm", "md", "lg"];
|
|
11
|
+
/** Available badge shape/style types. */
|
|
12
|
+
export declare const BADGE_TYPES: readonly ["pill", "badge", "modern"];
|
|
13
|
+
export type BadgeColor = (typeof BADGE_COLORS)[number];
|
|
14
|
+
export type BadgeSize = (typeof BADGE_SIZES)[number];
|
|
15
|
+
export type BadgeType = (typeof BADGE_TYPES)[number];
|
|
16
|
+
/** Padding, font-size, and gap per size. */
|
|
17
|
+
export declare const BADGE_SIZE_CLASSES: Record<BadgeSize, string>;
|
|
18
|
+
/** Icon dimensions per size. */
|
|
19
|
+
export declare const BADGE_ICON_SIZE_CLASSES: Record<BadgeSize, string>;
|
|
20
|
+
/** Border-radius and structural classes per type. */
|
|
21
|
+
export declare const BADGE_TYPE_CLASSES: Record<BadgeType, string>;
|
|
22
|
+
/** Background, border, and text classes for `pill` and `badge` types. */
|
|
23
|
+
export declare const BADGE_COLOR_CLASSES: Record<BadgeColor, string>;
|
|
24
|
+
/** Text-only classes for `modern` type (white bg, gray border). */
|
|
25
|
+
export declare const BADGE_MODERN_TEXT_CLASSES: Record<BadgeColor, string>;
|
|
26
|
+
/** Dot indicator background per color. */
|
|
27
|
+
export declare const BADGE_DOT_CLASSES: Record<BadgeColor, string>;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const r = [
|
|
2
|
+
"gray",
|
|
3
|
+
"brand",
|
|
4
|
+
"error",
|
|
5
|
+
"warning",
|
|
6
|
+
"success",
|
|
7
|
+
"gray-blue",
|
|
8
|
+
"blue-light",
|
|
9
|
+
"blue",
|
|
10
|
+
"indigo",
|
|
11
|
+
"purple",
|
|
12
|
+
"pink",
|
|
13
|
+
"orange"
|
|
14
|
+
], e = {
|
|
15
|
+
sm: "px-2 py-0.5 text-xs gap-1",
|
|
16
|
+
md: "px-2.5 py-0.5 text-sm gap-1.5",
|
|
17
|
+
lg: "px-3 py-1 text-sm gap-1.5"
|
|
18
|
+
}, g = {
|
|
19
|
+
sm: "size-3",
|
|
20
|
+
md: "size-4",
|
|
21
|
+
lg: "size-4"
|
|
22
|
+
}, b = {
|
|
23
|
+
pill: "rounded-full border",
|
|
24
|
+
badge: "rounded-md border",
|
|
25
|
+
modern: "rounded-md border shadow-xs"
|
|
26
|
+
}, t = {
|
|
27
|
+
gray: "bg-gray-v2-50 border-gray-v2-200 text-gray-v2-700",
|
|
28
|
+
brand: "bg-brand-v2-50 border-brand-v2-200 text-brand-v2-700",
|
|
29
|
+
error: "bg-error-v2-50 border-error-v2-200 text-error-v2-700",
|
|
30
|
+
warning: "bg-warning-v2-50 border-warning-v2-200 text-warning-v2-700",
|
|
31
|
+
success: "bg-success-v2-50 border-success-v2-200 text-success-v2-700",
|
|
32
|
+
"gray-blue": "bg-gray-blue-v2-50 border-gray-blue-v2-200 text-gray-blue-v2-700",
|
|
33
|
+
"blue-light": "bg-blue-light-v2-50 border-blue-light-v2-200 text-blue-light-v2-700",
|
|
34
|
+
blue: "bg-blue-v2-50 border-blue-v2-200 text-blue-v2-700",
|
|
35
|
+
indigo: "bg-indigo-v2-50 border-indigo-v2-200 text-indigo-v2-700",
|
|
36
|
+
purple: "bg-purple-v2-50 border-purple-v2-200 text-purple-v2-700",
|
|
37
|
+
pink: "bg-pink-v2-50 border-pink-v2-200 text-pink-v2-700",
|
|
38
|
+
orange: "bg-orange-v2-50 border-orange-v2-200 text-orange-v2-700"
|
|
39
|
+
}, n = {
|
|
40
|
+
gray: "text-gray-v2-700",
|
|
41
|
+
brand: "text-brand-v2-700",
|
|
42
|
+
error: "text-error-v2-700",
|
|
43
|
+
warning: "text-warning-v2-700",
|
|
44
|
+
success: "text-success-v2-700",
|
|
45
|
+
"gray-blue": "text-gray-blue-v2-700",
|
|
46
|
+
"blue-light": "text-blue-light-v2-700",
|
|
47
|
+
blue: "text-blue-v2-700",
|
|
48
|
+
indigo: "text-indigo-v2-700",
|
|
49
|
+
purple: "text-purple-v2-700",
|
|
50
|
+
pink: "text-pink-v2-700",
|
|
51
|
+
orange: "text-orange-v2-700"
|
|
52
|
+
}, v = {
|
|
53
|
+
gray: "bg-gray-v2-500",
|
|
54
|
+
brand: "bg-brand-v2-500",
|
|
55
|
+
error: "bg-error-v2-500",
|
|
56
|
+
warning: "bg-warning-v2-500",
|
|
57
|
+
success: "bg-success-v2-500",
|
|
58
|
+
"gray-blue": "bg-gray-blue-v2-500",
|
|
59
|
+
"blue-light": "bg-blue-light-v2-500",
|
|
60
|
+
blue: "bg-blue-v2-500",
|
|
61
|
+
indigo: "bg-indigo-v2-500",
|
|
62
|
+
purple: "bg-purple-v2-500",
|
|
63
|
+
pink: "bg-pink-v2-500",
|
|
64
|
+
orange: "bg-orange-v2-500"
|
|
65
|
+
};
|
|
66
|
+
export {
|
|
67
|
+
r as BADGE_COLORS,
|
|
68
|
+
t as BADGE_COLOR_CLASSES,
|
|
69
|
+
v as BADGE_DOT_CLASSES,
|
|
70
|
+
g as BADGE_ICON_SIZE_CLASSES,
|
|
71
|
+
n as BADGE_MODERN_TEXT_CLASSES,
|
|
72
|
+
e as BADGE_SIZE_CLASSES,
|
|
73
|
+
b as BADGE_TYPE_CLASSES
|
|
74
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as o, jsxs as e } from "react/jsx-runtime";
|
|
2
|
+
import { Popover as t } from "../../popover/Popover.js";
|
|
3
|
+
import { Badge as n } from "../Badge.js";
|
|
4
|
+
const c = ({ badges: r }) => r.length === 0 ? null : /* @__PURE__ */ o(t, { content: r, contentClassName: "max-w-[40vw]", children: /* @__PURE__ */ e(n, { color: "gray", size: "sm", children: [
|
|
5
|
+
"+",
|
|
6
|
+
r.length
|
|
7
|
+
] }) });
|
|
8
|
+
export {
|
|
9
|
+
c as BadgeMore
|
|
10
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { BadgeColor, BadgeSize } from '../badge-utils';
|
|
2
|
+
/** Decide the color of the badge by getting a hash from text content. */
|
|
3
|
+
export declare function getBadgeAutoColor(name: string): BadgeColor;
|
|
4
|
+
export declare const TAG_ICON_COLOR_CLASSES: Record<BadgeColor, string>;
|
|
5
|
+
interface Props {
|
|
6
|
+
name: string;
|
|
7
|
+
size?: BadgeSize;
|
|
8
|
+
}
|
|
9
|
+
export declare const BadgeTag: ({ name, size }: Props) => import("react/jsx-runtime").JSX.Element;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { jsx as o } from "react/jsx-runtime";
|
|
2
|
+
import { Tag01 as g } from "@untitled-ui/icons-react";
|
|
3
|
+
import { cn as i } from "../../../utils/cn.js";
|
|
4
|
+
import { Badge as c } from "../Badge.js";
|
|
5
|
+
import { BADGE_COLORS as n, BADGE_ICON_SIZE_CLASSES as l } from "../badge-utils.js";
|
|
6
|
+
function u(t) {
|
|
7
|
+
const r = Array.from(t).reduce(
|
|
8
|
+
(e, a) => e + a.charCodeAt(0),
|
|
9
|
+
0
|
|
10
|
+
);
|
|
11
|
+
return n[r % n.length];
|
|
12
|
+
}
|
|
13
|
+
const s = {
|
|
14
|
+
gray: "text-gray-v2-500",
|
|
15
|
+
brand: "text-brand-v2-500",
|
|
16
|
+
error: "text-error-v2-500",
|
|
17
|
+
warning: "text-warning-v2-500",
|
|
18
|
+
success: "text-success-v2-500",
|
|
19
|
+
"gray-blue": "text-gray-blue-v2-500",
|
|
20
|
+
"blue-light": "text-blue-light-v2-500",
|
|
21
|
+
blue: "text-blue-v2-500",
|
|
22
|
+
indigo: "text-indigo-v2-500",
|
|
23
|
+
purple: "text-purple-v2-500",
|
|
24
|
+
pink: "text-pink-v2-500",
|
|
25
|
+
orange: "text-orange-v2-500"
|
|
26
|
+
}, b = ({ name: t, size: r = "sm" }) => {
|
|
27
|
+
const e = u(t);
|
|
28
|
+
return /* @__PURE__ */ o(
|
|
29
|
+
c,
|
|
30
|
+
{
|
|
31
|
+
color: e,
|
|
32
|
+
size: r,
|
|
33
|
+
iconLeading: /* @__PURE__ */ o(
|
|
34
|
+
g,
|
|
35
|
+
{
|
|
36
|
+
className: i(
|
|
37
|
+
s[e],
|
|
38
|
+
l[r]
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
),
|
|
42
|
+
children: t
|
|
43
|
+
}
|
|
44
|
+
);
|
|
45
|
+
};
|
|
46
|
+
export {
|
|
47
|
+
b as BadgeTag,
|
|
48
|
+
s as TAG_ICON_COLOR_CLASSES,
|
|
49
|
+
u as getBadgeAutoColor
|
|
50
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { BadgeProps } from '../badge/Badge';
|
|
3
|
+
export interface ComboboxSuggestionItem {
|
|
4
|
+
value: string;
|
|
5
|
+
label?: string;
|
|
6
|
+
icon?: ReactNode;
|
|
7
|
+
}
|
|
8
|
+
interface Props {
|
|
9
|
+
id?: string;
|
|
10
|
+
/**
|
|
11
|
+
* Comma-joined string encoding both confirmed items and the current pending
|
|
12
|
+
* input. The last segment (after the final comma) is always the pending input;
|
|
13
|
+
* all preceding segments are confirmed items rendered as badges.
|
|
14
|
+
*
|
|
15
|
+
* Examples:
|
|
16
|
+
* "" — empty field
|
|
17
|
+
* "bob" — "bob" is pending (not yet confirmed)
|
|
18
|
+
* "bob," — "bob" is confirmed, pending is empty
|
|
19
|
+
* "bob,alice" — "bob" confirmed, "alice" pending
|
|
20
|
+
* "bob,alice," — both confirmed, pending is empty
|
|
21
|
+
*
|
|
22
|
+
* The form owns this value. Validation (shape, required, etc.) is the form's
|
|
23
|
+
* responsibility — this component does not validate.
|
|
24
|
+
*/
|
|
25
|
+
value: string;
|
|
26
|
+
/**
|
|
27
|
+
* Called on every keystroke and every confirmation.
|
|
28
|
+
*
|
|
29
|
+
* Validation pattern — the form parses the value on each change:
|
|
30
|
+
*
|
|
31
|
+
* ```
|
|
32
|
+
* const segments = value.split(",");
|
|
33
|
+
* const pending = segments.at(-1); // not yet confirmed
|
|
34
|
+
* const confirmed = segments.slice(0, -1).filter(Boolean); // confirmed badges
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* To validate item shape on confirmation: check whether the number of
|
|
38
|
+
* confirmed items increased. If the newest confirmed item is invalid, do NOT
|
|
39
|
+
* update state — the value reverts to the previous string, the invalid input
|
|
40
|
+
* stays in the pending field, and the form sets error={true} to show the ring.
|
|
41
|
+
*
|
|
42
|
+
* To validate the whole field (e.g. required, min items): run against
|
|
43
|
+
* `confirmed` on onBlur or onSubmit.
|
|
44
|
+
*
|
|
45
|
+
* The `pending` segment should generally be excluded from submit-time
|
|
46
|
+
* validation — if you want it included, append "," to the value before
|
|
47
|
+
* reading confirmed items.
|
|
48
|
+
*
|
|
49
|
+
*/
|
|
50
|
+
onChange: (value: string) => void;
|
|
51
|
+
/** Called when focus leaves the combobox. The pending input is NOT
|
|
52
|
+
* auto-confirmed — it remains in the value string as-is. The form decides
|
|
53
|
+
* whether to validate or consume the pending segment on blur. */
|
|
54
|
+
onBlur?: () => void;
|
|
55
|
+
getSuggestions?: (input: string) => ComboboxSuggestionItem[];
|
|
56
|
+
/** Characters that trigger confirmation of the pending input in addition to
|
|
57
|
+
* Enter. The character is swallowed (not inserted into the field).
|
|
58
|
+
* Default: [",", ";"] */
|
|
59
|
+
delimiter?: string[];
|
|
60
|
+
placeholder?: string;
|
|
61
|
+
error?: boolean;
|
|
62
|
+
disabled?: boolean;
|
|
63
|
+
getBadgeProps?: (value: string) => Partial<Omit<BadgeProps, "children" | "onClose" | "size">>;
|
|
64
|
+
/** Maps a confirmed value to its display label. Use when the stored value
|
|
65
|
+
* (e.g. an ID) differs from what should appear in the badge. */
|
|
66
|
+
getLabel?: (value: string) => string;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Multi-value input that renders confirmed items as badges. The `value` string
|
|
70
|
+
* encodes both confirmed items and the current pending input — see the `value`
|
|
71
|
+
* and `onChange` prop docs for the format.
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
75
|
+
*
|
|
76
|
+
* const [value, setValue] = useState("");
|
|
77
|
+
* const [error, setError] = useState(false);
|
|
78
|
+
*
|
|
79
|
+
* const handleChange = (next: string) => {
|
|
80
|
+
* const confirmed = next.split(",").slice(0, -1).filter(Boolean);
|
|
81
|
+
* const prev = value.split(",").slice(0, -1).filter(Boolean);
|
|
82
|
+
* if (confirmed.length > prev.length) {
|
|
83
|
+
* const newest = confirmed[confirmed.length - 1];
|
|
84
|
+
* if (!EMAIL.test(newest)) {
|
|
85
|
+
* setError(true);
|
|
86
|
+
* return; // reject — value reverts, invalid input stays pending
|
|
87
|
+
* }
|
|
88
|
+
* }
|
|
89
|
+
* setError(false);
|
|
90
|
+
* setValue(next);
|
|
91
|
+
* };
|
|
92
|
+
*
|
|
93
|
+
* <Combobox value={value} onChange={handleChange} error={error} />
|
|
94
|
+
*/
|
|
95
|
+
export declare const Combobox: ({ id, value, onChange, onBlur, getSuggestions, delimiter, placeholder, error, disabled, getBadgeProps, getLabel, }: Props) => import("react/jsx-runtime").JSX.Element;
|
|
96
|
+
export {};
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { jsxs as b, Fragment as _, jsx as w } from "react/jsx-runtime";
|
|
2
|
+
import { useId as B, useRef as x, useState as O, useMemo as D, useCallback as G, useEffect as k } from "react";
|
|
3
|
+
import { createPortal as J } from "react-dom";
|
|
4
|
+
import { cn as T } from "../../utils/cn.js";
|
|
5
|
+
import { Badge as Q } from "../badge/Badge.js";
|
|
6
|
+
import { usePopoverCoord as X } from "../popover/use-popover-coord.js";
|
|
7
|
+
const Y = [",", ";"];
|
|
8
|
+
function I(p) {
|
|
9
|
+
return `combobox-option-${p}`;
|
|
10
|
+
}
|
|
11
|
+
const le = ({
|
|
12
|
+
id: p,
|
|
13
|
+
value: E,
|
|
14
|
+
onChange: f,
|
|
15
|
+
onBlur: m,
|
|
16
|
+
getSuggestions: h,
|
|
17
|
+
delimiter: P = Y,
|
|
18
|
+
placeholder: S,
|
|
19
|
+
error: z = !1,
|
|
20
|
+
disabled: g = !1,
|
|
21
|
+
getBadgeProps: M,
|
|
22
|
+
getLabel: C
|
|
23
|
+
}) => {
|
|
24
|
+
const R = B(), y = x(null), j = x(null), u = x(null), [a, c] = O(!1), [l, n] = O(-1), { style: U } = X(y, u, {
|
|
25
|
+
syncWidth: !0
|
|
26
|
+
}), d = D(() => E.split(","), [E]), i = d[d.length - 1], o = D(
|
|
27
|
+
() => d.slice(0, -1).filter(Boolean),
|
|
28
|
+
[d]
|
|
29
|
+
), r = D(() => h ? h(i).filter(
|
|
30
|
+
(e) => !o.includes(e.value)
|
|
31
|
+
) : [], [h, i, o]), $ = l >= 0 && r[l] ? I(r[l].value) : void 0, v = G(() => {
|
|
32
|
+
c(!1), n(-1);
|
|
33
|
+
}, []), A = () => {
|
|
34
|
+
const e = i.trim();
|
|
35
|
+
e !== "" && f([...o, e, ""].join(","));
|
|
36
|
+
}, q = (e) => {
|
|
37
|
+
const t = e.target.value;
|
|
38
|
+
f([...o, t].join(",")), c(!0);
|
|
39
|
+
}, H = () => {
|
|
40
|
+
r.length > 0 && c(!0);
|
|
41
|
+
}, V = () => {
|
|
42
|
+
v(), m == null || m();
|
|
43
|
+
}, L = (e) => {
|
|
44
|
+
f([...o, e.value, ""].join(",")), n(-1);
|
|
45
|
+
}, N = (e) => {
|
|
46
|
+
const t = o.filter((s) => s !== e);
|
|
47
|
+
f([...t, i].join(","));
|
|
48
|
+
}, W = (e) => {
|
|
49
|
+
if (e.key === "ArrowDown") {
|
|
50
|
+
if (e.preventDefault(), !a && r.length > 0) {
|
|
51
|
+
c(!0), e.altKey || n(0);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
e.altKey || n((t) => Math.min(t + 1, r.length - 1));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (e.key === "ArrowUp") {
|
|
58
|
+
if (e.preventDefault(), e.altKey) {
|
|
59
|
+
v();
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
n((t) => Math.max(t - 1, -1));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (e.key === "Home" && a) {
|
|
66
|
+
e.preventDefault(), n(0);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (e.key === "End" && a) {
|
|
70
|
+
e.preventDefault(), n(r.length - 1);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (e.key === "ArrowLeft" || e.key === "ArrowRight" || e.key === "Delete") {
|
|
74
|
+
n(-1);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (P.includes(e.key)) {
|
|
78
|
+
e.preventDefault(), A();
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (e.key === "Enter") {
|
|
82
|
+
if (e.preventDefault(), a && l >= 0) {
|
|
83
|
+
L(r[l]);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
A();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (e.key === "Escape") {
|
|
90
|
+
a && (e.stopPropagation(), v());
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (e.key === "Tab") {
|
|
94
|
+
v();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
e.key === "Backspace" && i === "" && l < 0 && o.length > 0 && N(o[o.length - 1]);
|
|
98
|
+
};
|
|
99
|
+
return k(() => {
|
|
100
|
+
if (!a) return;
|
|
101
|
+
const e = (t) => {
|
|
102
|
+
var K, F;
|
|
103
|
+
const { target: s } = t;
|
|
104
|
+
s instanceof Node && ((K = y.current) != null && K.contains(s) || (F = u.current) != null && F.contains(s) || (c(!1), n(-1)));
|
|
105
|
+
};
|
|
106
|
+
return document.addEventListener("mousedown", e), () => document.removeEventListener("mousedown", e);
|
|
107
|
+
}, [a]), k(() => {
|
|
108
|
+
n(-1);
|
|
109
|
+
}, [r]), k(() => {
|
|
110
|
+
if (l < 0 || !u.current) return;
|
|
111
|
+
const e = r[l];
|
|
112
|
+
if (!e) return;
|
|
113
|
+
const t = u.current.querySelector(
|
|
114
|
+
`[id="${I(e.value)}"]`
|
|
115
|
+
);
|
|
116
|
+
t == null || t.scrollIntoView({ block: "nearest" });
|
|
117
|
+
}, [l, r]), /* @__PURE__ */ b(_, { children: [
|
|
118
|
+
/* @__PURE__ */ b(
|
|
119
|
+
"div",
|
|
120
|
+
{
|
|
121
|
+
ref: y,
|
|
122
|
+
onMouseDown: (e) => {
|
|
123
|
+
var t;
|
|
124
|
+
e.target === e.currentTarget && (e.preventDefault(), (t = j.current) == null || t.focus());
|
|
125
|
+
},
|
|
126
|
+
className: T(
|
|
127
|
+
"flex flex-wrap items-center gap-1.5 px-3 py-1.5 min-h-10 bg-white rounded-lg border shadow-xs transition-all outline outline-transparent",
|
|
128
|
+
g ? "bg-gray-v2-50 border-gray-v2-300" : z ? "border-error-v2-300 focus-within:border-error-v2 focus-within:outline-error-v2" : "border-gray-v2-300 focus-within:border-brand-v2 focus-within:outline-brand-v2"
|
|
129
|
+
),
|
|
130
|
+
children: [
|
|
131
|
+
o.map((e) => /* @__PURE__ */ w(
|
|
132
|
+
Q,
|
|
133
|
+
{
|
|
134
|
+
...M ? M(e) : void 0,
|
|
135
|
+
size: "sm",
|
|
136
|
+
onClose: g ? void 0 : () => N(e),
|
|
137
|
+
className: "max-w-48",
|
|
138
|
+
children: C ? C(e) : e
|
|
139
|
+
},
|
|
140
|
+
e
|
|
141
|
+
)),
|
|
142
|
+
/* @__PURE__ */ w(
|
|
143
|
+
"input",
|
|
144
|
+
{
|
|
145
|
+
id: p,
|
|
146
|
+
role: "combobox",
|
|
147
|
+
"aria-expanded": a,
|
|
148
|
+
"aria-controls": R,
|
|
149
|
+
"aria-autocomplete": "list",
|
|
150
|
+
"aria-activedescendant": $,
|
|
151
|
+
ref: j,
|
|
152
|
+
autoComplete: "off",
|
|
153
|
+
value: i,
|
|
154
|
+
onChange: q,
|
|
155
|
+
onKeyDown: W,
|
|
156
|
+
onFocus: H,
|
|
157
|
+
onBlur: V,
|
|
158
|
+
placeholder: o.length === 0 ? S : void 0,
|
|
159
|
+
disabled: g,
|
|
160
|
+
className: "flex-1 min-w-20 bg-transparent outline-none text-sm text-gray-v2-900 placeholder:text-gray-v2-500 disabled:cursor-not-allowed"
|
|
161
|
+
}
|
|
162
|
+
)
|
|
163
|
+
]
|
|
164
|
+
}
|
|
165
|
+
),
|
|
166
|
+
a && r.length > 0 ? J(
|
|
167
|
+
/* @__PURE__ */ w(
|
|
168
|
+
"div",
|
|
169
|
+
{
|
|
170
|
+
ref: u,
|
|
171
|
+
role: "listbox",
|
|
172
|
+
id: R,
|
|
173
|
+
style: U,
|
|
174
|
+
className: "z-20 rounded-lg border border-gray-v2-200 bg-white p-1 shadow-lg transition-all max-h-[40vh] overflow-auto",
|
|
175
|
+
children: r.map((e, t) => /* @__PURE__ */ b(
|
|
176
|
+
"button",
|
|
177
|
+
{
|
|
178
|
+
type: "button",
|
|
179
|
+
role: "option",
|
|
180
|
+
"aria-selected": "false",
|
|
181
|
+
tabIndex: -1,
|
|
182
|
+
id: I(e.value),
|
|
183
|
+
className: T(
|
|
184
|
+
"flex w-full items-center gap-2 rounded-md p-2 pr-2.5 text-left text-sm font-semibold text-gray-v2-700",
|
|
185
|
+
t === l ? "bg-gray-v2-100" : "hover:bg-gray-v2-50"
|
|
186
|
+
),
|
|
187
|
+
onMouseDown: (s) => s.preventDefault(),
|
|
188
|
+
onClick: () => L(e),
|
|
189
|
+
onMouseEnter: () => n(t),
|
|
190
|
+
onMouseLeave: () => n(-1),
|
|
191
|
+
children: [
|
|
192
|
+
e.icon ? e.icon : null,
|
|
193
|
+
e.label ? e.label : e.value
|
|
194
|
+
]
|
|
195
|
+
},
|
|
196
|
+
e.value
|
|
197
|
+
))
|
|
198
|
+
}
|
|
199
|
+
),
|
|
200
|
+
document.body
|
|
201
|
+
) : null
|
|
202
|
+
] });
|
|
203
|
+
};
|
|
204
|
+
export {
|
|
205
|
+
le as Combobox
|
|
206
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
interface Options {
|
|
2
|
+
pattern: RegExp;
|
|
3
|
+
initialValue?: string;
|
|
4
|
+
}
|
|
5
|
+
interface Result {
|
|
6
|
+
value: string;
|
|
7
|
+
error: boolean;
|
|
8
|
+
items: string[];
|
|
9
|
+
onChange: (next: string) => void;
|
|
10
|
+
reset: () => void;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Validates each confirmed item against a pattern and rejects invalid ones.
|
|
14
|
+
* Invalid confirmation leaves the input text in place and sets error=true.
|
|
15
|
+
*/
|
|
16
|
+
export declare function useValidatedCombobox({ pattern, initialValue, }: Options): Result;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { useState as c, useCallback as g } from "react";
|
|
2
|
+
function b({
|
|
3
|
+
pattern: l,
|
|
4
|
+
initialValue: t = ""
|
|
5
|
+
}) {
|
|
6
|
+
const [s, n] = c(t), [f, o] = c(!1), i = (e) => {
|
|
7
|
+
const r = e.split(",").slice(0, -1).filter(Boolean), m = s.split(",").slice(0, -1).filter(Boolean);
|
|
8
|
+
if (r.length > m.length) {
|
|
9
|
+
const p = r[r.length - 1];
|
|
10
|
+
if (!l.test(p)) {
|
|
11
|
+
o(!0);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
o(!1), n(e);
|
|
16
|
+
}, a = g(() => {
|
|
17
|
+
n(t), o(!1);
|
|
18
|
+
}, [t]), u = s.split(",").filter((e) => l.test(e));
|
|
19
|
+
return { value: s, error: f, items: u, onChange: i, reset: a };
|
|
20
|
+
}
|
|
21
|
+
export {
|
|
22
|
+
b as useValidatedCombobox
|
|
23
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
interface Props {
|
|
3
|
+
title: string;
|
|
4
|
+
supportingText?: string;
|
|
5
|
+
buttons?: ReactNode;
|
|
6
|
+
className?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare const ErrorState: ({ title, supportingText, buttons, className, }: Props) => import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
export {};
|