@petnow/ui 5.4.0 → 6.0.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.
@@ -0,0 +1,39 @@
1
+ import { t as e } from "./TailwindUtil-DiNvXuNl.js";
2
+ import { n as t, r as n } from "./fieldStyles-DXyDrzxD.js";
3
+ import { jsx as r, jsxs as i } from "react/jsx-runtime";
4
+ //#region src/components/ui/field.tsx
5
+ var a = ({ id: n, htmlFor: a, size: o, required: s, children: c }) => /* @__PURE__ */ i("label", {
6
+ id: n,
7
+ htmlFor: a,
8
+ className: e(t(o), "text-text-base-neutral-5"),
9
+ children: [c, s && /* @__PURE__ */ r("span", {
10
+ className: "text-text-prominent-accent-1",
11
+ "aria-hidden": "true",
12
+ children: "*"
13
+ })]
14
+ }), o = ({ id: e, size: t, errorText: i, assistiveText: a }) => !i && !a ? null : /* @__PURE__ */ r("span", {
15
+ id: e,
16
+ className: n({
17
+ size: t,
18
+ hasError: !!i
19
+ }),
20
+ children: i || a
21
+ }), s = () => /* @__PURE__ */ r("svg", {
22
+ "aria-hidden": "true",
23
+ viewBox: "0 0 20 20",
24
+ fill: "none",
25
+ className: "text-icon-prominent-accent-1 size-5",
26
+ children: /* @__PURE__ */ r("path", {
27
+ d: "m4 10 3.5 3.5L16 5",
28
+ stroke: "currentColor",
29
+ strokeWidth: "2",
30
+ strokeLinecap: "round",
31
+ strokeLinejoin: "round"
32
+ })
33
+ }), c = "border-border-base-neutral-2 bg-surface-base-neutral-1 z-50 max-h-72 overflow-y-auto rounded-[8px] border py-2 shadow-lg data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95", l = "data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95", u = (t) => e("text-text-base-neutral-5 flex w-full cursor-pointer items-center gap-2 px-4 py-3 outline-none select-none", t === "large" ? "text-body-16-regular" : "text-body-14-regular"), d = "bg-surface-base-neutral-2", f = "text-text-prominent-accent-1 font-medium", p = "cursor-not-allowed opacity-50", m = "flex size-5 shrink-0 items-center justify-center", h = (...e) => (t) => {
34
+ for (let n of e) typeof n == "function" ? n(t) : n && (n.current = t);
35
+ };
36
+ //#endregion
37
+ export { f as a, u as c, o as d, p as i, s as l, m as n, c as o, d as r, l as s, h as t, a as u };
38
+
39
+ //# sourceMappingURL=mergeRefs-CsNeQOZ6.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mergeRefs-CsNeQOZ6.js","names":[],"sources":["../src/components/ui/field.tsx","../src/components/ui/listbox.tsx","../src/components/ui/listboxStyles.ts","../src/utils/mergeRefs.ts"],"sourcesContent":["import { fieldLabelFontSize, fieldMessageVariants, type FieldSize } from \"./fieldStyles\";\n\nimport type { ReactNode } from \"react\";\n\nimport { cn } from \"@/utils/TailwindUtil\";\n\nexport const FieldLabel = ({\n id,\n htmlFor,\n size,\n required,\n children,\n}: {\n id?: string;\n htmlFor: string;\n size: FieldSize;\n required?: boolean;\n children: ReactNode;\n}) => (\n <label id={id} htmlFor={htmlFor} className={cn(fieldLabelFontSize(size), \"text-text-base-neutral-5\")}>\n {children}\n {required && (\n <span className=\"text-text-prominent-accent-1\" aria-hidden=\"true\">\n *\n </span>\n )}\n </label>\n);\n\nexport const FieldMessage = ({\n id,\n size,\n errorText,\n assistiveText,\n}: {\n id: string;\n size: FieldSize;\n errorText?: string;\n assistiveText?: ReactNode;\n}) => {\n if (!errorText && !assistiveText) return null;\n return (\n <span id={id} className={fieldMessageVariants({ size, hasError: Boolean(errorText) })}>\n {errorText || assistiveText}\n </span>\n );\n};\n","export const ListboxCheckIcon = () => (\n <svg aria-hidden=\"true\" viewBox=\"0 0 20 20\" fill=\"none\" className=\"text-icon-prominent-accent-1 size-5\">\n <path d=\"m4 10 3.5 3.5L16 5\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n </svg>\n);\n","import type { FieldSize } from \"./fieldStyles\";\n\nimport { cn } from \"@/utils/TailwindUtil\";\n\n// Select·Combobox 목록 패널(petify-admin-console Select와 같은 모양). 포털로 body에 띄우므로 스크롤·overflow\n// 조상에 잘리지 않는다. 너비는 각 컴포넌트가 Radix의 트리거 너비 변수로 맞춘다.\n// 모서리는 소비 레포의 --radius-* 설정과 무관하게 8px로 고정한다(입력 칸의 FIELD_RADIUS와 같은 이유).\nexport const LISTBOX_PANEL =\n \"border-border-base-neutral-2 bg-surface-base-neutral-1 z-50 max-h-72 overflow-y-auto rounded-[8px] border py-2 shadow-lg data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95\";\n\n// 닫힐 때 페이드 아웃. Combobox는 닫으며 검색어를 비우므로 쓰지 않는다(사라지는 동안 전체 목록이 비친다).\nexport const LISTBOX_PANEL_EXIT =\n \"data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95\";\n\n// 행 전체가 hover 배경을 받도록 좌우 여백은 패널이 아니라 행에 둔다.\nexport const listboxOptionClassName = (size: FieldSize) =>\n cn(\n \"text-text-base-neutral-5 flex w-full cursor-pointer items-center gap-2 px-4 py-3 outline-none select-none\",\n size === \"large\" ? \"text-body-16-regular\" : \"text-body-14-regular\",\n );\n\nexport const LISTBOX_OPTION_ACTIVE = \"bg-surface-base-neutral-2\";\nexport const LISTBOX_OPTION_SELECTED = \"text-text-prominent-accent-1 font-medium\";\nexport const LISTBOX_OPTION_DISABLED = \"cursor-not-allowed opacity-50\";\n\n// 고른 옵션 앞 체크 자리. 고르지 않은 행도 자리를 비워 두어 이름이 한 줄로 맞는다.\nexport const LISTBOX_CHECK_SLOT = \"flex size-5 shrink-0 items-center justify-center\";\n","import type { Ref, RefCallback } from \"react\";\n\n// 내부 ref와 소비 레포가 넘긴 ref(React 19 ref prop)를 함께 연결한다.\nexport const mergeRefs =\n <T>(...refs: (Ref<T> | undefined)[]): RefCallback<T> =>\n (node) => {\n for (const ref of refs) {\n if (typeof ref === \"function\") ref(node);\n else if (ref) ref.current = node;\n }\n };\n"],"mappings":";;;;AAMA,IAAa,KAAc,EACzB,OACA,YACA,SACA,aACA,kBAQA,kBAAC,SAAD;CAAW;CAAa;CAAS,WAAW,EAAG,EAAmB,CAAI,GAAG,0BAA0B;WAAnG,CACG,GACA,KACC,kBAAC,QAAD;EAAM,WAAU;EAA+B,eAAY;YAAO;CAE5D,CAAA,CAEH;IAGI,KAAgB,EAC3B,OACA,SACA,cACA,uBAOI,CAAC,KAAa,CAAC,IAAsB,OAEvC,kBAAC,QAAD;CAAU;CAAI,WAAW,EAAqB;EAAE;EAAM,UAAU,EAAQ;CAAW,CAAC;WACjF,KAAa;AACV,CAAA,GC5CG,UACX,kBAAC,OAAD;CAAK,eAAY;CAAO,SAAQ;CAAY,MAAK;CAAO,WAAU;WAChE,kBAAC,QAAD;EAAM,GAAE;EAAqB,QAAO;EAAe,aAAY;EAAI,eAAc;EAAQ,gBAAe;CAAS,CAAA;AAC9G,CAAA,GCIM,IACX,kNAGW,IACX,kGAGW,KAA0B,MACrC,EACE,6GACA,MAAS,UAAU,yBAAyB,sBAC9C,GAEW,IAAwB,6BACxB,IAA0B,4CAC1B,IAA0B,iCAG1B,IAAqB,oDCvBrB,KACP,GAAG,OACN,MAAS;CACR,KAAK,IAAM,KAAO,GAChB,AAAI,OAAO,KAAQ,aAAY,EAAI,CAAI,IAC9B,MAAK,EAAI,UAAU;AAEhC"}
package/dist/select.js CHANGED
@@ -1,82 +1,102 @@
1
1
  "use client";
2
2
  import { t as e } from "./TailwindUtil-DiNvXuNl.js";
3
- import { a as t, n, o as r } from "./fieldStyles-DyZu__hI.js";
3
+ import { a as t, i as n, t as r } from "./fieldStyles-DXyDrzxD.js";
4
4
  import { ChevronDownIcon as i } from "./icons/action/chevron-down.js";
5
- import { n as a, r as o, t as s } from "./mergeRefs-BHwdBwNQ.js";
6
- import { jsx as c, jsxs as l } from "react/jsx-runtime";
7
- import { useEffect as u, useId as d, useMemo as f, useRef as p, useState as m } from "react";
5
+ import { c as a, d as o, l as s, n as c, o as l, s as u, t as d, u as f } from "./mergeRefs-CsNeQOZ6.js";
6
+ import { jsx as p, jsxs as m } from "react/jsx-runtime";
7
+ import { useEffect as h, useId as g, useMemo as _, useRef as v, useState as y } from "react";
8
+ import { Select as b } from "radix-ui";
8
9
  //#region src/components/select/Select.tsx
9
- var h = ({ id: h, label: g, options: _, value: v, defaultValue: y, onChange: b, placeholder: x, errorText: S, assistiveText: C, size: w = "large", required: T, disabled: E = !1, className: D, "aria-describedby": O, ref: k, ...A }) => {
10
- let j = d(), M = h ?? j, N = `${M}-message`, P = !!S, F = !!(S || C), [I, L] = m(y ?? ""), R = v ?? I, z = _.map((e) => typeof e == "string" ? {
10
+ var x = ({ id: x, label: S, options: C, value: w, defaultValue: T, onChange: E, placeholder: D, errorText: O, assistiveText: k, size: A = "large", name: j, form: M, required: N, disabled: P = !1, className: F, "aria-describedby": I, ref: L, ...R }) => {
11
+ let z = g(), B = x ?? z, V = `${B}-message`, H = !!O, U = !!(O || k), W = C.map((e) => typeof e == "string" ? {
11
12
  value: e,
12
13
  label: e
13
- } : e), B = x === void 0 && !z.some((e) => e.value === R), V = p(null), H = f(() => s(V, k), [k]);
14
- return u(() => {
15
- let e = V.current?.form;
16
- if (v !== void 0 || !e) return;
17
- let t = () => setTimeout(() => L(V.current?.value ?? ""));
14
+ } : e).filter((e) => e.value !== ""), [G, K] = y(T ?? ""), [q, J] = y(0), Y = v(null), X = _(() => d(Y, L), [L]), Z = v({
15
+ value: w,
16
+ defaultValue: T,
17
+ onChange: E
18
+ });
19
+ return h(() => {
20
+ Z.current = {
21
+ value: w,
22
+ defaultValue: T,
23
+ onChange: E
24
+ };
25
+ }), h(() => {
26
+ let e = M ? document.getElementById(M) : Y.current?.closest("form");
27
+ if (!(e instanceof HTMLFormElement)) return;
28
+ let t = () => {
29
+ let { value: e, defaultValue: t, onChange: n } = Z.current, r = t ?? "";
30
+ e === void 0 ? K(r) : e !== r && n?.(r), J((e) => e + 1);
31
+ };
18
32
  return e.addEventListener("reset", t), () => e.removeEventListener("reset", t);
19
- }, [v]), /* @__PURE__ */ l("div", {
20
- className: e("flex w-full min-w-0 flex-col gap-2", D),
33
+ }, [M]), /* @__PURE__ */ m("div", {
34
+ className: e("flex w-full min-w-0 flex-col gap-2", F),
21
35
  children: [
22
- g && /* @__PURE__ */ c(a, {
23
- htmlFor: M,
24
- size: w,
25
- required: T,
26
- children: g
27
- }),
28
- /* @__PURE__ */ l("div", {
29
- className: "relative",
30
- children: [/* @__PURE__ */ l("select", {
31
- ...A,
32
- ref: H,
33
- id: M,
34
- value: v,
35
- defaultValue: v === void 0 ? y ?? "" : void 0,
36
- required: T,
37
- disabled: E,
38
- "aria-invalid": P || void 0,
39
- "aria-describedby": r(O, F && N),
40
- onChange: (e) => {
41
- L(e.target.value), b?.(e.target.value);
42
- },
43
- className: e(n({
44
- size: w,
45
- hasError: P,
46
- disabled: E
47
- }), "w-full min-w-0 cursor-pointer appearance-none truncate outline-none disabled:cursor-not-allowed", !E && !P && "focus:border-border-base-neutral-4", w === "large" ? "pr-12" : "pr-11", t(w), R ? "text-text-base-neutral-5" : "text-text-base-neutral-2"),
48
- children: [
49
- x !== void 0 && /* @__PURE__ */ c("option", {
50
- value: "",
51
- children: x
52
- }),
53
- B && /* @__PURE__ */ c("option", {
54
- value: R,
55
- hidden: !0,
56
- disabled: !0
57
- }),
58
- z.map((e) => /* @__PURE__ */ c("option", {
59
- value: e.value,
60
- disabled: e.disabled,
61
- className: "text-text-base-neutral-5",
62
- children: e.label
63
- }, e.value))
64
- ]
65
- }), /* @__PURE__ */ c(i, {
66
- "aria-hidden": "true",
67
- className: e("text-icon-base-neutral-3 pointer-events-none absolute top-1/2 size-6 -translate-y-1/2", w === "large" ? "right-4" : "right-3")
68
- })]
36
+ S && /* @__PURE__ */ p(f, {
37
+ htmlFor: B,
38
+ size: A,
39
+ required: N,
40
+ children: S
69
41
  }),
70
- /* @__PURE__ */ c(o, {
71
- id: N,
72
- size: w,
73
- errorText: S,
74
- assistiveText: C
42
+ /* @__PURE__ */ m(b.Root, {
43
+ value: w ?? G,
44
+ onValueChange: (e) => {
45
+ K(e), E?.(e);
46
+ },
47
+ name: j,
48
+ form: M,
49
+ required: N,
50
+ disabled: P,
51
+ children: [/* @__PURE__ */ m(b.Trigger, {
52
+ ...R,
53
+ ref: X,
54
+ id: B,
55
+ "aria-invalid": H || void 0,
56
+ "aria-describedby": t(I, U && V),
57
+ className: e(r({
58
+ size: A,
59
+ hasError: H,
60
+ disabled: P
61
+ }), "group text-text-base-neutral-5 data-[placeholder]:text-text-base-neutral-2 w-full min-w-0 cursor-pointer justify-between text-left outline-none disabled:cursor-not-allowed", !P && !H && "focus:border-border-base-neutral-4 data-[state=open]:border-border-base-neutral-4", n(A)),
62
+ children: [/* @__PURE__ */ p("span", {
63
+ className: "min-w-0 flex-1 truncate",
64
+ children: /* @__PURE__ */ p(b.Value, { placeholder: D })
65
+ }), /* @__PURE__ */ p(i, {
66
+ "aria-hidden": "true",
67
+ className: "text-icon-base-neutral-3 group-data-[state=open]:text-icon-prominent-accent-1 size-6 shrink-0 transition-transform duration-200 group-data-[state=open]:rotate-180"
68
+ })]
69
+ }), /* @__PURE__ */ p(b.Portal, { children: /* @__PURE__ */ p(b.Content, {
70
+ position: "popper",
71
+ sideOffset: 8,
72
+ className: e(l, u, "flex w-(--radix-select-trigger-width) flex-col overflow-hidden"),
73
+ children: /* @__PURE__ */ p(b.Viewport, {
74
+ className: "min-h-0 flex-1",
75
+ children: W.map((t) => /* @__PURE__ */ m(b.Item, {
76
+ value: t.value,
77
+ disabled: t.disabled,
78
+ className: e(a(A), "data-[highlighted]:bg-surface-base-neutral-2 data-[state=checked]:text-text-prominent-accent-1 data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50 data-[state=checked]:font-medium"),
79
+ children: [/* @__PURE__ */ p("span", {
80
+ className: c,
81
+ children: /* @__PURE__ */ p(b.ItemIndicator, { children: /* @__PURE__ */ p(s, {}) })
82
+ }), /* @__PURE__ */ p("span", {
83
+ className: "min-w-0 flex-1 truncate",
84
+ children: /* @__PURE__ */ p(b.ItemText, { children: t.label })
85
+ })]
86
+ }, t.value))
87
+ })
88
+ }) })]
89
+ }, q),
90
+ /* @__PURE__ */ p(o, {
91
+ id: V,
92
+ size: A,
93
+ errorText: O,
94
+ assistiveText: k
75
95
  })
76
96
  ]
77
97
  });
78
98
  };
79
99
  //#endregion
80
- export { h as Select };
100
+ export { x as Select };
81
101
 
82
102
  //# sourceMappingURL=select.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"select.js","names":[],"sources":["../src/components/select/Select.tsx"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useId, useMemo, useRef, useState } from \"react\";\n\nimport { ChevronDownIcon } from \"../icons/action/ChevronDownIcon\";\n\nimport type { ComponentPropsWithRef, ReactNode } from \"react\";\n\nimport { FieldLabel, FieldMessage } from \"@/components/ui/field\";\nimport { fieldContainerVariants, fieldValueFontSize, joinIds, type FieldSize } from \"@/components/ui/fieldStyles\";\nimport { mergeRefs } from \"@/utils/mergeRefs\";\nimport { cn } from \"@/utils/TailwindUtil\";\n\nexport interface SelectOption {\n value: string;\n label: string;\n disabled?: boolean;\n}\n\ninterface SelectProps\n extends Omit<ComponentPropsWithRef<\"select\">, \"size\" | \"value\" | \"defaultValue\" | \"onChange\" | \"children\"> {\n /** 칸 위에 보이는 이름. `<label>`로 `<select>`에 연결된다. */\n label?: string;\n /** 문자열 옵션은 값과 표시 이름이 같다. */\n options: (string | SelectOption)[];\n value?: string;\n defaultValue?: string;\n onChange?: (value: string) => void;\n /** 값이 없을 때 보이는 첫 옵션(값 `\"\"`). `required`이면 이 옵션으로는 제출되지 않는다. */\n placeholder?: string;\n errorText?: string;\n assistiveText?: ReactNode;\n size?: FieldSize;\n}\n\n/** 목록에서 하나를 고르는 입력. 브라우저 기본 `<select>`라 모바일 선택기·폼 검증·키보드 조작을 그대로 쓴다. */\nconst Select = ({\n id,\n label,\n options,\n value,\n defaultValue,\n onChange,\n placeholder,\n errorText,\n assistiveText,\n size = \"large\",\n required,\n disabled = false,\n className,\n \"aria-describedby\": ariaDescribedBy,\n ref,\n ...selectProps\n}: SelectProps) => {\n const generatedId = useId();\n const selectId = id ?? generatedId;\n const messageId = `${selectId}-message`;\n const hasError = Boolean(errorText);\n const hasMessage = Boolean(errorText || assistiveText);\n const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue ?? \"\");\n const currentValue = value ?? uncontrolledValue;\n const normalized = options.map((option) => (typeof option === \"string\" ? { value: option, label: option } : option));\n // placeholder 없이 옵션에 없는 값(예: \"\")이면 브라우저가 첫 옵션을 보여 주므로, 숨긴 빈 옵션을 고른 상태로 둔다.\n const needsEmptyOption = placeholder === undefined && !normalized.some((option) => option.value === currentValue);\n const selectRef = useRef<HTMLSelectElement | null>(null);\n const setRef = useMemo(() => mergeRefs(selectRef, ref), [ref]);\n\n // 비제어일 때 form.reset()은 onChange 없이 값을 되돌리므로 placeholder 색을 다시 맞춘다.\n useEffect(() => {\n const form = selectRef.current?.form;\n if (value !== undefined || !form) return;\n const sync = () => setTimeout(() => setUncontrolledValue(selectRef.current?.value ?? \"\"));\n form.addEventListener(\"reset\", sync);\n return () => form.removeEventListener(\"reset\", sync);\n }, [value]);\n\n return (\n <div className={cn(\"flex w-full min-w-0 flex-col gap-2\", className)}>\n {label && (\n <FieldLabel htmlFor={selectId} size={size} required={required}>\n {label}\n </FieldLabel>\n )}\n <div className=\"relative\">\n <select\n {...selectProps}\n ref={setRef}\n id={selectId}\n value={value}\n defaultValue={value === undefined ? (defaultValue ?? \"\") : undefined}\n required={required}\n disabled={disabled}\n aria-invalid={hasError || undefined}\n aria-describedby={joinIds(ariaDescribedBy, hasMessage && messageId)}\n onChange={(event) => {\n setUncontrolledValue(event.target.value);\n onChange?.(event.target.value);\n }}\n className={cn(\n fieldContainerVariants({ size, hasError, disabled }),\n \"w-full min-w-0 cursor-pointer appearance-none truncate outline-none disabled:cursor-not-allowed\",\n !disabled && !hasError && \"focus:border-border-base-neutral-4\",\n size === \"large\" ? \"pr-12\" : \"pr-11\",\n fieldValueFontSize(size),\n currentValue ? \"text-text-base-neutral-5\" : \"text-text-base-neutral-2\",\n )}\n >\n {placeholder !== undefined && <option value=\"\">{placeholder}</option>}\n {needsEmptyOption && <option value={currentValue} hidden disabled />}\n {normalized.map((option) => (\n <option\n key={option.value}\n value={option.value}\n disabled={option.disabled}\n className=\"text-text-base-neutral-5\"\n >\n {option.label}\n </option>\n ))}\n </select>\n <ChevronDownIcon\n aria-hidden=\"true\"\n className={cn(\n \"text-icon-base-neutral-3 pointer-events-none absolute top-1/2 size-6 -translate-y-1/2\",\n size === \"large\" ? \"right-4\" : \"right-3\",\n )}\n />\n </div>\n <FieldMessage id={messageId} size={size} errorText={errorText} assistiveText={assistiveText} />\n </div>\n );\n};\n\nexport { Select };\n"],"mappings":";;;;;;;;AAoCA,IAAM,KAAU,EACd,OACA,UACA,YACA,UACA,iBACA,aACA,gBACA,cACA,kBACA,UAAO,SACP,aACA,cAAW,IACX,cACA,oBAAoB,GACpB,QACA,GAAG,QACc;CACjB,IAAM,IAAc,EAAM,GACpB,IAAW,KAAM,GACjB,IAAY,GAAG,EAAS,WACxB,IAAW,EAAQ,GACnB,IAAa,GAAQ,KAAa,IAClC,CAAC,GAAmB,KAAwB,EAAS,KAAgB,EAAE,GACvE,IAAe,KAAS,GACxB,IAAa,EAAQ,KAAK,MAAY,OAAO,KAAW,WAAW;EAAE,OAAO;EAAQ,OAAO;CAAO,IAAI,CAAO,GAE7G,IAAmB,MAAgB,KAAA,KAAa,CAAC,EAAW,MAAM,MAAW,EAAO,UAAU,CAAY,GAC1G,IAAY,EAAiC,IAAI,GACjD,IAAS,QAAc,EAAU,GAAW,CAAG,GAAG,CAAC,CAAG,CAAC;CAW7D,OARA,QAAgB;EACd,IAAM,IAAO,EAAU,SAAS;EAChC,IAAI,MAAU,KAAA,KAAa,CAAC,GAAM;EAClC,IAAM,UAAa,iBAAiB,EAAqB,EAAU,SAAS,SAAS,EAAE,CAAC;EAExF,OADA,EAAK,iBAAiB,SAAS,CAAI,SACtB,EAAK,oBAAoB,SAAS,CAAI;CACrD,GAAG,CAAC,CAAK,CAAC,GAGR,kBAAC,OAAD;EAAK,WAAW,EAAG,sCAAsC,CAAS;YAAlE;GACG,KACC,kBAAC,GAAD;IAAY,SAAS;IAAgB;IAAgB;cAClD;GACS,CAAA;GAEd,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,UAAD;KACE,GAAI;KACJ,KAAK;KACL,IAAI;KACG;KACP,cAAc,MAAU,KAAA,IAAa,KAAgB,KAAM,KAAA;KACjD;KACA;KACV,gBAAc,KAAY,KAAA;KAC1B,oBAAkB,EAAQ,GAAiB,KAAc,CAAS;KAClE,WAAW,MAAU;MAEnB,AADA,EAAqB,EAAM,OAAO,KAAK,GACvC,IAAW,EAAM,OAAO,KAAK;KAC/B;KACA,WAAW,EACT,EAAuB;MAAE;MAAM;MAAU;KAAS,CAAC,GACnD,mGACA,CAAC,KAAY,CAAC,KAAY,sCAC1B,MAAS,UAAU,UAAU,SAC7B,EAAmB,CAAI,GACvB,IAAe,6BAA6B,0BAC9C;eArBF;MAuBG,MAAgB,KAAA,KAAa,kBAAC,UAAD;OAAQ,OAAM;iBAAI;MAAoB,CAAA;MACnE,KAAoB,kBAAC,UAAD;OAAQ,OAAO;OAAc,QAAA;OAAO,UAAA;MAAU,CAAA;MAClE,EAAW,KAAK,MACf,kBAAC,UAAD;OAEE,OAAO,EAAO;OACd,UAAU,EAAO;OACjB,WAAU;iBAET,EAAO;MACF,GAND,EAAO,KAMN,CACT;KACK;QACR,kBAAC,GAAD;KACE,eAAY;KACZ,WAAW,EACT,yFACA,MAAS,UAAU,YAAY,SACjC;IACD,CAAA,CACE;;GACL,kBAAC,GAAD;IAAc,IAAI;IAAiB;IAAiB;IAA0B;GAAgB,CAAA;EAC3F;;AAET"}
1
+ {"version":3,"file":"select.js","names":[],"sources":["../src/components/select/Select.tsx"],"sourcesContent":["\"use client\";\n\nimport { Select as SelectPrimitive } from \"radix-ui\";\nimport { useEffect, useId, useMemo, useRef, useState } from \"react\";\n\nimport { ChevronDownIcon } from \"../icons/action/ChevronDownIcon\";\n\nimport type { ComponentPropsWithRef, ReactNode } from \"react\";\n\nimport { FieldLabel, FieldMessage } from \"@/components/ui/field\";\nimport { fieldContainerVariants, fieldValueFontSize, joinIds, type FieldSize } from \"@/components/ui/fieldStyles\";\nimport { ListboxCheckIcon } from \"@/components/ui/listbox\";\nimport {\n LISTBOX_CHECK_SLOT,\n LISTBOX_PANEL,\n LISTBOX_PANEL_EXIT,\n listboxOptionClassName,\n} from \"@/components/ui/listboxStyles\";\nimport { mergeRefs } from \"@/utils/mergeRefs\";\nimport { cn } from \"@/utils/TailwindUtil\";\n\nexport interface SelectOption {\n value: string;\n label: string;\n disabled?: boolean;\n}\n\ninterface SelectProps\n extends Omit<ComponentPropsWithRef<\"button\">, \"value\" | \"defaultValue\" | \"onChange\" | \"children\" | \"type\" | \"dir\"> {\n /** 칸 위에 보이는 이름. `<label>`로 트리거에 연결된다. */\n label?: string;\n /** 문자열 옵션은 값과 표시 이름이 같다. 값으로 `\"\"`는 쓸 수 없다(고르지 않은 상태). */\n options: (string | SelectOption)[];\n /** 고른 옵션의 값. 고르지 않았으면 `\"\"`. */\n value?: string;\n defaultValue?: string;\n onChange?: (value: string) => void;\n /** 고르지 않았을 때 트리거에 보이는 문구. */\n placeholder?: string;\n errorText?: string;\n assistiveText?: ReactNode;\n size?: FieldSize;\n /** 고르지 않으면 폼 제출을 막는다. 폼 안에서는 숨은 `<select>`로 브라우저 검증·FormData에 들어간다. */\n required?: boolean;\n}\n\n/**\n * 목록에서 하나를 고르는 입력(Radix Select). petify-admin-console의 Select처럼 트리거 아래 포털 패널에 옵션을\n * 띄우고, 고른 옵션 앞에 체크를 보인다. 키보드(방향키·Enter·Esc·타이핑 검색)와 접근성은 Radix가 맡는다.\n * 폼 안에 있으면 Radix가 숨은 `<select>`를 그려 `name`·`required`가 FormData·브라우저 검증에 들어간다.\n */\nconst Select = ({\n id,\n label,\n options,\n value,\n defaultValue,\n onChange,\n placeholder,\n errorText,\n assistiveText,\n size = \"large\",\n name,\n form,\n required,\n disabled = false,\n className,\n \"aria-describedby\": ariaDescribedBy,\n ref,\n ...triggerProps\n}: SelectProps) => {\n const generatedId = useId();\n const triggerId = id ?? generatedId;\n const messageId = `${triggerId}-message`;\n const hasError = Boolean(errorText);\n const hasMessage = Boolean(errorText || assistiveText);\n // 빈 값은 '고르지 않음'이고 Radix는 빈 값 옵션을 그리면 throw하므로 옵션에서 뺀다.\n const normalized = options\n .map((option) => (typeof option === \"string\" ? { value: option, label: option } : option))\n .filter((option) => option.value !== \"\");\n const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue ?? \"\");\n const [resetCount, setResetCount] = useState(0);\n const triggerRef = useRef<HTMLButtonElement | null>(null);\n const setRef = useMemo(() => mergeRefs(triggerRef, ref), [ref]);\n const latest = useRef({ value, defaultValue, onChange });\n useEffect(() => {\n latest.current = { value, defaultValue, onChange };\n });\n\n // Radix는 폼 reset을 듣지 않아 숨은 <select>만 처음 값으로 돌아가고 화면 값은 그대로 남는다.\n // 화면 값을 defaultValue로 되돌리고, Root를 다시 마운트해 숨은 <select>도 그 값에서 시작하게 한다.\n useEffect(() => {\n const formElement = form ? document.getElementById(form) : triggerRef.current?.closest(\"form\");\n if (!(formElement instanceof HTMLFormElement)) return;\n const handleReset = () => {\n const { value: controlled, defaultValue: initial, onChange: notify } = latest.current;\n const next = initial ?? \"\";\n if (controlled === undefined) setUncontrolledValue(next);\n else if (controlled !== next) notify?.(next);\n setResetCount((count) => count + 1);\n };\n formElement.addEventListener(\"reset\", handleReset);\n return () => formElement.removeEventListener(\"reset\", handleReset);\n }, [form]);\n\n return (\n <div className={cn(\"flex w-full min-w-0 flex-col gap-2\", className)}>\n {label && (\n <FieldLabel htmlFor={triggerId} size={size} required={required}>\n {label}\n </FieldLabel>\n )}\n <SelectPrimitive.Root\n key={resetCount}\n value={value ?? uncontrolledValue}\n onValueChange={(next) => {\n setUncontrolledValue(next);\n onChange?.(next);\n }}\n name={name}\n form={form}\n required={required}\n disabled={disabled}\n >\n <SelectPrimitive.Trigger\n {...triggerProps}\n ref={setRef}\n id={triggerId}\n aria-invalid={hasError || undefined}\n aria-describedby={joinIds(ariaDescribedBy, hasMessage && messageId)}\n className={cn(\n fieldContainerVariants({ size, hasError, disabled }),\n \"group text-text-base-neutral-5 data-[placeholder]:text-text-base-neutral-2 w-full min-w-0 cursor-pointer justify-between text-left outline-none disabled:cursor-not-allowed\",\n !disabled &&\n !hasError &&\n \"focus:border-border-base-neutral-4 data-[state=open]:border-border-base-neutral-4\",\n fieldValueFontSize(size),\n )}\n >\n <span className=\"min-w-0 flex-1 truncate\">\n <SelectPrimitive.Value placeholder={placeholder} />\n </span>\n <ChevronDownIcon\n aria-hidden=\"true\"\n className=\"text-icon-base-neutral-3 group-data-[state=open]:text-icon-prominent-accent-1 size-6 shrink-0 transition-transform duration-200 group-data-[state=open]:rotate-180\"\n />\n </SelectPrimitive.Trigger>\n <SelectPrimitive.Portal>\n <SelectPrimitive.Content\n position=\"popper\"\n sideOffset={8}\n // 스크롤은 Viewport가 맡아야 Radix가 키보드로 옮긴 옵션을 보이게 스크롤한다.\n className={cn(\n LISTBOX_PANEL,\n LISTBOX_PANEL_EXIT,\n \"flex w-(--radix-select-trigger-width) flex-col overflow-hidden\",\n )}\n >\n <SelectPrimitive.Viewport className=\"min-h-0 flex-1\">\n {normalized.map((option) => (\n <SelectPrimitive.Item\n key={option.value}\n value={option.value}\n disabled={option.disabled}\n className={cn(\n listboxOptionClassName(size),\n \"data-[highlighted]:bg-surface-base-neutral-2 data-[state=checked]:text-text-prominent-accent-1 data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50 data-[state=checked]:font-medium\",\n )}\n >\n <span className={LISTBOX_CHECK_SLOT}>\n <SelectPrimitive.ItemIndicator>\n <ListboxCheckIcon />\n </SelectPrimitive.ItemIndicator>\n </span>\n <span className=\"min-w-0 flex-1 truncate\">\n <SelectPrimitive.ItemText>{option.label}</SelectPrimitive.ItemText>\n </span>\n </SelectPrimitive.Item>\n ))}\n </SelectPrimitive.Viewport>\n </SelectPrimitive.Content>\n </SelectPrimitive.Portal>\n </SelectPrimitive.Root>\n <FieldMessage id={messageId} size={size} errorText={errorText} assistiveText={assistiveText} />\n </div>\n );\n};\n\nexport { Select };\n"],"mappings":"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"}
@@ -30,6 +30,7 @@ interface ComboboxProps extends Omit<ComponentPropsWithRef<"input">, "size" | "v
30
30
  * 입력해서 검색하고 목록에서 하나를 고르는 입력(WAI-ARIA combobox, list autocomplete).
31
31
  * 포커스는 입력에 남고 목록의 현재 항목은 `aria-activedescendant`로 알린다.
32
32
  * 고른 값만 `value`가 되고, 입력한 글자는 고르기 전까지 값이 아니다.
33
+ * 목록은 `Select`와 같은 포털 패널(Radix Popover)이라 스크롤·overflow 조상에 잘리지 않는다.
33
34
  */
34
35
  declare const Combobox: ({ id, label, options, value, onChange, filter, emptyText, toggleLabel, invalidSelectionText, errorText, assistiveText, size, name, required, disabled, placeholder, className, autoComplete, "aria-describedby": ariaDescribedBy, onBlur, onKeyDown, onClick, ref, ...inputProps }: ComboboxProps) => import("react").JSX.Element;
35
36
  export { Combobox };
@@ -5,20 +5,27 @@ export interface SelectOption {
5
5
  label: string;
6
6
  disabled?: boolean;
7
7
  }
8
- interface SelectProps extends Omit<ComponentPropsWithRef<"select">, "size" | "value" | "defaultValue" | "onChange" | "children"> {
9
- /** 칸 위에 보이는 이름. `<label>`로 `<select>`에 연결된다. */
8
+ interface SelectProps extends Omit<ComponentPropsWithRef<"button">, "value" | "defaultValue" | "onChange" | "children" | "type" | "dir"> {
9
+ /** 칸 위에 보이는 이름. `<label>`로 트리거에 연결된다. */
10
10
  label?: string;
11
- /** 문자열 옵션은 값과 표시 이름이 같다. */
11
+ /** 문자열 옵션은 값과 표시 이름이 같다. 값으로 `""`는 쓸 수 없다(고르지 않은 상태). */
12
12
  options: (string | SelectOption)[];
13
+ /** 고른 옵션의 값. 고르지 않았으면 `""`. */
13
14
  value?: string;
14
15
  defaultValue?: string;
15
16
  onChange?: (value: string) => void;
16
- /** 값이 없을 때 보이는 첫 옵션(값 `""`). `required`이면 이 옵션으로는 제출되지 않는다. */
17
+ /** 고르지 않았을 때 트리거에 보이는 문구. */
17
18
  placeholder?: string;
18
19
  errorText?: string;
19
20
  assistiveText?: ReactNode;
20
21
  size?: FieldSize;
22
+ /** 고르지 않으면 폼 제출을 막는다. 폼 안에서는 숨은 `<select>`로 브라우저 검증·FormData에 들어간다. */
23
+ required?: boolean;
21
24
  }
22
- /** 목록에서 하나를 고르는 입력. 브라우저 기본 `<select>`라 모바일 선택기·폼 검증·키보드 조작을 그대로 쓴다. */
23
- declare const Select: ({ id, label, options, value, defaultValue, onChange, placeholder, errorText, assistiveText, size, required, disabled, className, "aria-describedby": ariaDescribedBy, ref, ...selectProps }: SelectProps) => import("react").JSX.Element;
25
+ /**
26
+ * 목록에서 하나를 고르는 입력(Radix Select). petify-admin-console의 Select처럼 트리거 아래 포털 패널에 옵션을
27
+ * 띄우고, 고른 옵션 앞에 체크를 보인다. 키보드(방향키·Enter·Esc·타이핑 검색)와 접근성은 Radix가 맡는다.
28
+ * 폼 안에 있으면 Radix가 숨은 `<select>`를 그려 `name`·`required`가 FormData·브라우저 검증에 들어간다.
29
+ */
30
+ declare const Select: ({ id, label, options, value, defaultValue, onChange, placeholder, errorText, assistiveText, size, name, form, required, disabled, className, "aria-describedby": ariaDescribedBy, ref, ...triggerProps }: SelectProps) => import("react").JSX.Element;
24
31
  export { Select };
@@ -1,4 +1,3 @@
1
- import { VariantProps } from 'class-variance-authority';
2
1
  import * as React from "react";
3
2
  interface TextFieldNormalProps extends Omit<React.InputHTMLAttributes<HTMLInputElement | HTMLTextAreaElement>, "size"> {
4
3
  subtitle?: string;
@@ -13,24 +12,6 @@ interface TextFieldNormalProps extends Omit<React.InputHTMLAttributes<HTMLInputE
13
12
  size?: "large" | "small";
14
13
  }
15
14
  declare const TextFieldNormal: React.ForwardRefExoticComponent<TextFieldNormalProps & React.RefAttributes<HTMLInputElement | HTMLTextAreaElement>>;
16
- declare const textFieldSelectVariants: (props?: ({
17
- size?: "small" | "large" | null | undefined;
18
- hasError?: boolean | null | undefined;
19
- disabled?: boolean | null | undefined;
20
- } & import('class-variance-authority/types').ClassProp) | undefined) => string;
21
- interface TextFieldSelectProps extends VariantProps<typeof textFieldSelectVariants> {
22
- label?: string;
23
- placeholder: string;
24
- value?: string;
25
- isRequired?: boolean;
26
- disabled?: boolean;
27
- assistiveText?: string | React.ReactNode;
28
- errorText?: string;
29
- onPress?: () => void;
30
- className?: string;
31
- size?: "large" | "small";
32
- }
33
- declare const TextFieldSelect: ({ label, placeholder, value, isRequired, disabled, assistiveText, errorText, onPress, className, size, }: TextFieldSelectProps) => React.JSX.Element;
34
15
  interface TextFieldWithButtonProps extends Omit<React.InputHTMLAttributes<HTMLInputElement | HTMLTextAreaElement>, "size"> {
35
16
  subtitle?: string;
36
17
  isRequired?: boolean;
@@ -47,4 +28,4 @@ interface TextFieldWithButtonProps extends Omit<React.InputHTMLAttributes<HTMLIn
47
28
  buttonDisabled?: boolean;
48
29
  }
49
30
  declare const TextFieldWithButton: React.ForwardRefExoticComponent<TextFieldWithButtonProps & React.RefAttributes<HTMLInputElement | HTMLTextAreaElement>>;
50
- export { TextFieldNormal, TextFieldSelect, TextFieldWithButton };
31
+ export { TextFieldNormal, TextFieldWithButton };
@@ -0,0 +1 @@
1
+ export declare const ListboxCheckIcon: () => import("react").JSX.Element;
@@ -0,0 +1,8 @@
1
+ import { FieldSize } from './fieldStyles';
2
+ export declare const LISTBOX_PANEL = "border-border-base-neutral-2 bg-surface-base-neutral-1 z-50 max-h-72 overflow-y-auto rounded-[8px] border py-2 shadow-lg data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95";
3
+ export declare const LISTBOX_PANEL_EXIT = "data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95";
4
+ export declare const listboxOptionClassName: (size: FieldSize) => string;
5
+ export declare const LISTBOX_OPTION_ACTIVE = "bg-surface-base-neutral-2";
6
+ export declare const LISTBOX_OPTION_SELECTED = "text-text-prominent-accent-1 font-medium";
7
+ export declare const LISTBOX_OPTION_DISABLED = "cursor-not-allowed opacity-50";
8
+ export declare const LISTBOX_CHECK_SLOT = "flex size-5 shrink-0 items-center justify-center";