sellmate-design-system-react 0.1.3 → 0.1.4
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/components/SActionModal/SActionModal.cjs +129 -1
- package/dist/components/SActionModal/SActionModal.cjs.map +1 -1
- package/dist/components/SActionModal/SActionModal.js +129 -1
- package/dist/components/SActionModal/SActionModal.js.map +1 -1
- package/dist/components/SBarcodeInput/SBarcodeInput.cjs +156 -3
- package/dist/components/SBarcodeInput/SBarcodeInput.cjs.map +1 -1
- package/dist/components/SBarcodeInput/SBarcodeInput.js +157 -4
- package/dist/components/SBarcodeInput/SBarcodeInput.js.map +1 -1
- package/dist/components/SButton/SButton.cjs +129 -1
- package/dist/components/SButton/SButton.cjs.map +1 -1
- package/dist/components/SButton/SButton.js +129 -1
- package/dist/components/SButton/SButton.js.map +1 -1
- package/dist/components/SCalendar/SCalendar.cjs +131 -3
- package/dist/components/SCalendar/SCalendar.cjs.map +1 -1
- package/dist/components/SCalendar/SCalendar.d.ts +3 -1
- package/dist/components/SCalendar/SCalendar.js +131 -3
- package/dist/components/SCalendar/SCalendar.js.map +1 -1
- package/dist/components/SCallout/SCallout.cjs +129 -1
- package/dist/components/SCallout/SCallout.cjs.map +1 -1
- package/dist/components/SCallout/SCallout.js +129 -1
- package/dist/components/SCallout/SCallout.js.map +1 -1
- package/dist/components/SCheckbox/SCheckbox.cjs +129 -1
- package/dist/components/SCheckbox/SCheckbox.cjs.map +1 -1
- package/dist/components/SCheckbox/SCheckbox.js +129 -1
- package/dist/components/SCheckbox/SCheckbox.js.map +1 -1
- package/dist/components/SChip/SChip.cjs +130 -3
- package/dist/components/SChip/SChip.cjs.map +1 -1
- package/dist/components/SChip/SChip.js +130 -3
- package/dist/components/SChip/SChip.js.map +1 -1
- package/dist/components/SChipInput/SChipInput.cjs +153 -3
- package/dist/components/SChipInput/SChipInput.cjs.map +1 -1
- package/dist/components/SChipInput/SChipInput.js +153 -3
- package/dist/components/SChipInput/SChipInput.js.map +1 -1
- package/dist/components/SConfirmModal/SConfirmModal.cjs +129 -1
- package/dist/components/SConfirmModal/SConfirmModal.cjs.map +1 -1
- package/dist/components/SConfirmModal/SConfirmModal.js +129 -1
- package/dist/components/SConfirmModal/SConfirmModal.js.map +1 -1
- package/dist/components/SDatePicker/SDatePicker.cjs +158 -5
- package/dist/components/SDatePicker/SDatePicker.cjs.map +1 -1
- package/dist/components/SDatePicker/SDatePicker.js +158 -5
- package/dist/components/SDatePicker/SDatePicker.js.map +1 -1
- package/dist/components/SDateRangePicker/SDateRangePicker.cjs +156 -3
- package/dist/components/SDateRangePicker/SDateRangePicker.cjs.map +1 -1
- package/dist/components/SDateRangePicker/SDateRangePicker.js +156 -3
- package/dist/components/SDateRangePicker/SDateRangePicker.js.map +1 -1
- package/dist/components/SDropdownButton/SDropdownButton.cjs +129 -1
- package/dist/components/SDropdownButton/SDropdownButton.cjs.map +1 -1
- package/dist/components/SDropdownButton/SDropdownButton.js +129 -1
- package/dist/components/SDropdownButton/SDropdownButton.js.map +1 -1
- package/dist/components/SField/SField.cjs +129 -1
- package/dist/components/SField/SField.cjs.map +1 -1
- package/dist/components/SField/SField.js +129 -1
- package/dist/components/SField/SField.js.map +1 -1
- package/dist/components/SFilePicker/SFilePicker.cjs +156 -3
- package/dist/components/SFilePicker/SFilePicker.cjs.map +1 -1
- package/dist/components/SFilePicker/SFilePicker.js +156 -3
- package/dist/components/SFilePicker/SFilePicker.js.map +1 -1
- package/dist/components/SForm/SForm.cjs +91 -0
- package/dist/components/SForm/SForm.cjs.map +1 -0
- package/dist/components/SForm/SForm.d.ts +33 -0
- package/dist/components/SForm/SForm.js +89 -0
- package/dist/components/SForm/SForm.js.map +1 -0
- package/dist/components/SForm/index.d.ts +1 -0
- package/dist/components/SGhostButton/SGhostButton.cjs +129 -1
- package/dist/components/SGhostButton/SGhostButton.cjs.map +1 -1
- package/dist/components/SGhostButton/SGhostButton.js +129 -1
- package/dist/components/SGhostButton/SGhostButton.js.map +1 -1
- package/dist/components/SGuide/SGuide.cjs +129 -1
- package/dist/components/SGuide/SGuide.cjs.map +1 -1
- package/dist/components/SGuide/SGuide.js +129 -1
- package/dist/components/SGuide/SGuide.js.map +1 -1
- package/dist/components/SIcon/SIcon.cjs +129 -1
- package/dist/components/SIcon/SIcon.cjs.map +1 -1
- package/dist/components/SIcon/SIcon.d.ts +3 -2
- package/dist/components/SIcon/SIcon.js +129 -1
- package/dist/components/SIcon/SIcon.js.map +1 -1
- package/dist/components/SInput/SInput.cjs +156 -3
- package/dist/components/SInput/SInput.cjs.map +1 -1
- package/dist/components/SInput/SInput.js +157 -4
- package/dist/components/SInput/SInput.js.map +1 -1
- package/dist/components/SKeyValueTable/SKeyValueTable.cjs +212 -5
- package/dist/components/SKeyValueTable/SKeyValueTable.cjs.map +1 -1
- package/dist/components/SKeyValueTable/SKeyValueTable.js +212 -5
- package/dist/components/SKeyValueTable/SKeyValueTable.js.map +1 -1
- package/dist/components/SLoadingModal/SLoadingModal.cjs +129 -1
- package/dist/components/SLoadingModal/SLoadingModal.cjs.map +1 -1
- package/dist/components/SLoadingModal/SLoadingModal.js +129 -1
- package/dist/components/SLoadingModal/SLoadingModal.js.map +1 -1
- package/dist/components/SModalContainer/SModalContainer.cjs +129 -1
- package/dist/components/SModalContainer/SModalContainer.cjs.map +1 -1
- package/dist/components/SModalContainer/SModalContainer.js +129 -1
- package/dist/components/SModalContainer/SModalContainer.js.map +1 -1
- package/dist/components/SNumberInput/SNumberInput.cjs +156 -3
- package/dist/components/SNumberInput/SNumberInput.cjs.map +1 -1
- package/dist/components/SNumberInput/SNumberInput.js +156 -3
- package/dist/components/SNumberInput/SNumberInput.js.map +1 -1
- package/dist/components/SPagination/SPagination.cjs +129 -1
- package/dist/components/SPagination/SPagination.cjs.map +1 -1
- package/dist/components/SPagination/SPagination.js +129 -1
- package/dist/components/SPagination/SPagination.js.map +1 -1
- package/dist/components/SPopover/SPopover.cjs +129 -1
- package/dist/components/SPopover/SPopover.cjs.map +1 -1
- package/dist/components/SPopover/SPopover.d.ts +3 -1
- package/dist/components/SPopover/SPopover.js +129 -1
- package/dist/components/SPopover/SPopover.js.map +1 -1
- package/dist/components/SPopup/SPopup.cjs +129 -1
- package/dist/components/SPopup/SPopup.cjs.map +1 -1
- package/dist/components/SPopup/SPopup.js +129 -1
- package/dist/components/SPopup/SPopup.js.map +1 -1
- package/dist/components/SSelect/SSelect.cjs +156 -3
- package/dist/components/SSelect/SSelect.cjs.map +1 -1
- package/dist/components/SSelect/SSelect.js +157 -4
- package/dist/components/SSelect/SSelect.js.map +1 -1
- package/dist/components/STable/STable.cjs +156 -3
- package/dist/components/STable/STable.cjs.map +1 -1
- package/dist/components/STable/STable.d.ts +3 -1
- package/dist/components/STable/STable.js +156 -3
- package/dist/components/STable/STable.js.map +1 -1
- package/dist/components/STabs/STabs.cjs +129 -1
- package/dist/components/STabs/STabs.cjs.map +1 -1
- package/dist/components/STabs/STabs.js +129 -1
- package/dist/components/STabs/STabs.js.map +1 -1
- package/dist/components/STag/STag.cjs +129 -1
- package/dist/components/STag/STag.cjs.map +1 -1
- package/dist/components/STag/STag.js +129 -1
- package/dist/components/STag/STag.js.map +1 -1
- package/dist/components/STextLink/STextLink.cjs +129 -1
- package/dist/components/STextLink/STextLink.cjs.map +1 -1
- package/dist/components/STextLink/STextLink.d.ts +3 -2
- package/dist/components/STextLink/STextLink.js +129 -1
- package/dist/components/STextLink/STextLink.js.map +1 -1
- package/dist/components/STextarea/STextarea.cjs +156 -3
- package/dist/components/STextarea/STextarea.cjs.map +1 -1
- package/dist/components/STextarea/STextarea.js +157 -4
- package/dist/components/STextarea/STextarea.js.map +1 -1
- package/dist/components/SToast/SToast.cjs +129 -1
- package/dist/components/SToast/SToast.cjs.map +1 -1
- package/dist/components/SToast/SToast.js +129 -1
- package/dist/components/SToast/SToast.js.map +1 -1
- package/dist/components/SToast/SToastContainer.cjs +129 -1
- package/dist/components/SToast/SToastContainer.cjs.map +1 -1
- package/dist/components/SToast/SToastContainer.js +129 -1
- package/dist/components/SToast/SToastContainer.js.map +1 -1
- package/dist/components/STooltip/STooltip.cjs +129 -1
- package/dist/components/STooltip/STooltip.cjs.map +1 -1
- package/dist/components/STooltip/STooltip.d.ts +3 -1
- package/dist/components/STooltip/STooltip.js +129 -1
- package/dist/components/STooltip/STooltip.js.map +1 -1
- package/dist/index.cjs +306 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +303 -6
- package/dist/index.js.map +1 -1
- package/dist/lib/color.cjs +135 -0
- package/dist/lib/color.cjs.map +1 -0
- package/dist/lib/color.d.ts +28 -0
- package/dist/lib/color.js +131 -0
- package/dist/lib/color.js.map +1 -0
- package/dist/lib/form.cjs +23 -0
- package/dist/lib/form.cjs.map +1 -1
- package/dist/lib/form.d.ts +25 -0
- package/dist/lib/form.js +21 -1
- package/dist/lib/form.js.map +1 -1
- package/dist/styles.css +25 -0
- package/package.json +1 -1
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
var react = require('react');
|
|
5
|
+
var clsx = require('clsx');
|
|
6
|
+
var tailwindMerge = require('tailwind-merge');
|
|
7
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
8
|
+
|
|
9
|
+
function cn(...inputs) {
|
|
10
|
+
return tailwindMerge.twMerge(clsx.clsx(inputs));
|
|
11
|
+
}
|
|
12
|
+
var FormContext = react.createContext(null);
|
|
13
|
+
var SForm = react.forwardRef(function SForm2({ formClass, onSubmit, onValidationError, children, className, style }, ref) {
|
|
14
|
+
const formRef = react.useRef(null);
|
|
15
|
+
const fieldsRef = react.useRef(/* @__PURE__ */ new Set());
|
|
16
|
+
const register = react.useCallback((entry) => {
|
|
17
|
+
fieldsRef.current.add(entry);
|
|
18
|
+
return () => {
|
|
19
|
+
fieldsRef.current.delete(entry);
|
|
20
|
+
};
|
|
21
|
+
}, []);
|
|
22
|
+
const ctx = react.useMemo(() => ({ register }), [register]);
|
|
23
|
+
const orderByDom = react.useCallback((names) => {
|
|
24
|
+
const form = formRef.current;
|
|
25
|
+
if (!form || names.length < 2) return names;
|
|
26
|
+
const domOrder = /* @__PURE__ */ new Map();
|
|
27
|
+
form.querySelectorAll("[name],[data-name]").forEach((el, i) => {
|
|
28
|
+
const n = el.getAttribute("name") || el.getAttribute("data-name");
|
|
29
|
+
if (n && !domOrder.has(n)) domOrder.set(n, i);
|
|
30
|
+
});
|
|
31
|
+
return [...names].sort((a, b) => (domOrder.get(a) ?? Infinity) - (domOrder.get(b) ?? Infinity));
|
|
32
|
+
}, []);
|
|
33
|
+
const focusByName = react.useCallback((name) => {
|
|
34
|
+
const form = formRef.current;
|
|
35
|
+
if (!form || !name) return;
|
|
36
|
+
const host = form.querySelector(`[name="${CSS.escape(name)}"]`) ?? form.querySelector(`[data-name="${CSS.escape(name)}"]`);
|
|
37
|
+
if (!host) return;
|
|
38
|
+
const focusable = host.matches("input,textarea,select,button,[tabindex]") ? host : host.querySelector("input,textarea,select,button,[tabindex]");
|
|
39
|
+
focusable?.focus();
|
|
40
|
+
(focusable ?? host).scrollIntoView({ block: "nearest" });
|
|
41
|
+
}, []);
|
|
42
|
+
const runValidation = react.useCallback(() => {
|
|
43
|
+
let valid = true;
|
|
44
|
+
const invalidNames = [];
|
|
45
|
+
fieldsRef.current.forEach((f) => {
|
|
46
|
+
if (!f.validate()) {
|
|
47
|
+
valid = false;
|
|
48
|
+
if (f.name) invalidNames.push(f.name);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
return { valid, names: orderByDom(invalidNames) };
|
|
52
|
+
}, [orderByDom]);
|
|
53
|
+
react.useImperativeHandle(
|
|
54
|
+
ref,
|
|
55
|
+
() => ({
|
|
56
|
+
validate: () => runValidation().valid,
|
|
57
|
+
resetValidation: () => fieldsRef.current.forEach((f) => f.resetValidation()),
|
|
58
|
+
reset: () => fieldsRef.current.forEach((f) => f.resetValidation()),
|
|
59
|
+
focusFirstInvalid: () => {
|
|
60
|
+
const { names } = runValidation();
|
|
61
|
+
if (names.length > 0) focusByName(names[0]);
|
|
62
|
+
}
|
|
63
|
+
}),
|
|
64
|
+
[runValidation, focusByName]
|
|
65
|
+
);
|
|
66
|
+
const handleSubmit = (e) => {
|
|
67
|
+
e.preventDefault();
|
|
68
|
+
const { valid, names } = runValidation();
|
|
69
|
+
if (!valid) {
|
|
70
|
+
if (names.length > 0) focusByName(names[0]);
|
|
71
|
+
onValidationError?.({ names });
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
onSubmit?.();
|
|
75
|
+
};
|
|
76
|
+
return /* @__PURE__ */ jsxRuntime.jsx(FormContext.Provider, { value: ctx, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
77
|
+
"form",
|
|
78
|
+
{
|
|
79
|
+
ref: formRef,
|
|
80
|
+
onSubmit: handleSubmit,
|
|
81
|
+
className: cn(formClass, className),
|
|
82
|
+
style,
|
|
83
|
+
noValidate: true,
|
|
84
|
+
children
|
|
85
|
+
}
|
|
86
|
+
) });
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
exports.SForm = SForm;
|
|
90
|
+
//# sourceMappingURL=SForm.cjs.map
|
|
91
|
+
//# sourceMappingURL=SForm.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/lib/cn.ts","../../../src/lib/form.ts","../../../src/components/SForm/SForm.tsx"],"names":["twMerge","clsx","createContext","forwardRef","SForm","useRef","useCallback","useMemo","useImperativeHandle","jsx"],"mappings":"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","file":"SForm.cjs","sourcesContent":["import { clsx, type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\n/**\n * className 병합 유틸.\n * clsx로 조건부 클래스를 합치고, tailwind-merge로 Tailwind 클래스 충돌을 해소한다.\n */\nexport function cn(...inputs: ClassValue[]): string {\n return twMerge(clsx(inputs));\n}\n","import { createContext, useContext, useEffect, useRef } from 'react';\n\n/** 유효성 규칙: 유효하면 true, 아니면 에러 메시지 문자열 반환 (sd types/form Rule 포팅) */\nexport type Rule = (value: unknown) => true | string;\n\n/** rules를 순서대로 실행해 첫 실패 메시지를 반환. 모두 통과하면 \"\" */\nexport function runRules(value: unknown, rules?: Rule[]): string {\n if (!rules || rules.length === 0) return '';\n for (const rule of rules) {\n const result = rule(value ?? '');\n if (result !== true) return typeof result === 'string' ? result : '유효하지 않은 값입니다.';\n }\n return '';\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// SForm 연동 — Context 기반 필드 등록 (sd-form 검증 서브시스템 포팅)\n// 각 rule-bearing 컨트롤이 useFormField로 자신을 등록하면, SForm이 submit 시\n// 등록된 모든 필드의 validate()를 실행해 하나라도 실패하면 제출을 막는다.\n// SForm 밖에서 쓰이면 context가 없어 no-op (form-agnostic 사용 유지).\n// ─────────────────────────────────────────────────────────────────────────────\n\n/** SForm에 등록되는 필드 항목 */\nexport interface FormFieldEntry {\n /** 폼 연동용 name (validationError names·focus 대상 식별) */\n name: string;\n /** 현재 값에 rules를 실행하고 에러 상태를 반영한 뒤 valid 여부 반환 */\n validate: () => boolean;\n /** 검증 에러 상태만 초기화 (값은 소비자 소유) */\n resetValidation: () => void;\n}\n\nexport interface FormContextValue {\n /** 필드를 등록하고 해제 함수를 반환 */\n register: (entry: FormFieldEntry) => () => void;\n}\n\nexport const FormContext = createContext<FormContextValue | null>(null);\n\n/** 가장 가까운 SForm의 context (없으면 null) */\nexport function useFormContext(): FormContextValue | null {\n return useContext(FormContext);\n}\n\n/**\n * 폼 필드 등록 훅. SForm 내부이면 등록되고, 밖이면 no-op.\n * validate/resetValidation은 항상 최신 클로저를 참조하도록 ref로 래핑한다.\n */\nexport function useFormField(field: {\n name?: string;\n validate: () => boolean;\n resetValidation: () => void;\n}): void {\n const ctx = useFormContext();\n const cbRef = useRef(field);\n cbRef.current = field;\n const name = field.name ?? '';\n\n useEffect(() => {\n if (!ctx) return;\n return ctx.register({\n name,\n validate: () => cbRef.current.validate(),\n resetValidation: () => cbRef.current.resetValidation(),\n });\n }, [ctx, name]);\n}\n","'use client';\n\nimport {\n forwardRef,\n useCallback,\n useImperativeHandle,\n useMemo,\n useRef,\n type CSSProperties,\n type FormEvent,\n type ReactNode,\n} from 'react';\nimport { cn } from '../../lib/cn';\nimport { FormContext, type FormContextValue, type FormFieldEntry } from '../../lib/form';\n\nexport interface SFormValidationError {\n /** 검증에 실패한 필드 name 목록 (DOM 순서) */\n names: string[];\n}\n\n/** ref로 노출되는 폼 제어 메서드 (sd-form @Method 대응) */\nexport interface SFormHandle {\n /** 등록된 모든 필드를 검증하고 통과 여부를 반환 */\n validate: () => boolean;\n /** 모든 필드의 검증 상태만 초기화 (값은 소비자 소유) */\n resetValidation: () => void;\n /** resetValidation 별칭 — sd-form sdReset 대응. 값 리셋은 소비자(value/onChange) 책임 */\n reset: () => void;\n /** 첫 번째 실패 필드로 포커스 이동 */\n focusFirstInvalid: () => void;\n}\n\nexport interface SFormProps {\n /** form 엘리먼트에 추가할 클래스 (sd-form formClass 대응) */\n formClass?: string;\n /** 모든 필드 검증 통과 후 발생 (sd-form sdSubmit 대응) */\n onSubmit?: () => void;\n /** 검증 실패 시 실패한 필드 name 목록 전달 (sd-form sdValidationError 대응) */\n onValidationError?: (error: SFormValidationError) => void;\n children?: ReactNode;\n className?: string;\n style?: CSSProperties;\n}\n\n/**\n * SForm — sd-form 포팅. 폼 안의 rule-bearing 컨트롤(useFormField로 등록)을 모아\n * submit 시 일괄 검증한다. 하나라도 실패하면 제출을 막고 첫 실패 필드에 포커스한 뒤\n * onValidationError를 발생시킨다. 값은 여전히 소비자 소유(form-agnostic 유지).\n */\nexport const SForm = forwardRef<SFormHandle, SFormProps>(function SForm(\n { formClass, onSubmit, onValidationError, children, className, style },\n ref,\n) {\n const formRef = useRef<HTMLFormElement>(null);\n const fieldsRef = useRef<Set<FormFieldEntry>>(new Set());\n\n const register = useCallback((entry: FormFieldEntry) => {\n fieldsRef.current.add(entry);\n return () => {\n fieldsRef.current.delete(entry);\n };\n }, []);\n\n const ctx = useMemo<FormContextValue>(() => ({ register }), [register]);\n\n /** 실패 필드 name들을 DOM 등장 순서로 정렬 (name / data-name 기준) */\n const orderByDom = useCallback((names: string[]): string[] => {\n const form = formRef.current;\n if (!form || names.length < 2) return names;\n const domOrder = new Map<string, number>();\n form.querySelectorAll<HTMLElement>('[name],[data-name]').forEach((el, i) => {\n const n = el.getAttribute('name') || el.getAttribute('data-name');\n if (n && !domOrder.has(n)) domOrder.set(n, i);\n });\n return [...names].sort((a, b) => (domOrder.get(a) ?? Infinity) - (domOrder.get(b) ?? Infinity));\n }, []);\n\n const focusByName = useCallback((name: string) => {\n const form = formRef.current;\n if (!form || !name) return;\n const host =\n form.querySelector<HTMLElement>(`[name=\"${CSS.escape(name)}\"]`) ??\n form.querySelector<HTMLElement>(`[data-name=\"${CSS.escape(name)}\"]`);\n if (!host) return;\n const focusable = host.matches('input,textarea,select,button,[tabindex]')\n ? host\n : host.querySelector<HTMLElement>('input,textarea,select,button,[tabindex]');\n focusable?.focus();\n (focusable ?? host).scrollIntoView({ block: 'nearest' });\n }, []);\n\n /**\n * 등록된 모든 필드를 검증한다.\n * - valid: 전체 통과 여부 (이름 없는 필드도 실패하면 false → 제출 차단)\n * - names: 실패한 필드 중 name이 있는 것들 (DOM 순서, onValidationError용)\n */\n const runValidation = useCallback((): { valid: boolean; names: string[] } => {\n let valid = true;\n const invalidNames: string[] = [];\n fieldsRef.current.forEach(f => {\n if (!f.validate()) {\n valid = false;\n if (f.name) invalidNames.push(f.name);\n }\n });\n return { valid, names: orderByDom(invalidNames) };\n }, [orderByDom]);\n\n useImperativeHandle(\n ref,\n () => ({\n validate: () => runValidation().valid,\n resetValidation: () => fieldsRef.current.forEach(f => f.resetValidation()),\n reset: () => fieldsRef.current.forEach(f => f.resetValidation()),\n focusFirstInvalid: () => {\n const { names } = runValidation();\n if (names.length > 0) focusByName(names[0]);\n },\n }),\n [runValidation, focusByName],\n );\n\n const handleSubmit = (e: FormEvent<HTMLFormElement>) => {\n e.preventDefault();\n const { valid, names } = runValidation();\n if (!valid) {\n if (names.length > 0) focusByName(names[0]);\n onValidationError?.({ names });\n return;\n }\n onSubmit?.();\n };\n\n return (\n <FormContext.Provider value={ctx}>\n <form\n ref={formRef}\n onSubmit={handleSubmit}\n className={cn(formClass, className)}\n style={style}\n noValidate\n >\n {children}\n </form>\n </FormContext.Provider>\n );\n});\n"]}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type CSSProperties, type ReactNode } from 'react';
|
|
2
|
+
export interface SFormValidationError {
|
|
3
|
+
/** 검증에 실패한 필드 name 목록 (DOM 순서) */
|
|
4
|
+
names: string[];
|
|
5
|
+
}
|
|
6
|
+
/** ref로 노출되는 폼 제어 메서드 (sd-form @Method 대응) */
|
|
7
|
+
export interface SFormHandle {
|
|
8
|
+
/** 등록된 모든 필드를 검증하고 통과 여부를 반환 */
|
|
9
|
+
validate: () => boolean;
|
|
10
|
+
/** 모든 필드의 검증 상태만 초기화 (값은 소비자 소유) */
|
|
11
|
+
resetValidation: () => void;
|
|
12
|
+
/** resetValidation 별칭 — sd-form sdReset 대응. 값 리셋은 소비자(value/onChange) 책임 */
|
|
13
|
+
reset: () => void;
|
|
14
|
+
/** 첫 번째 실패 필드로 포커스 이동 */
|
|
15
|
+
focusFirstInvalid: () => void;
|
|
16
|
+
}
|
|
17
|
+
export interface SFormProps {
|
|
18
|
+
/** form 엘리먼트에 추가할 클래스 (sd-form formClass 대응) */
|
|
19
|
+
formClass?: string;
|
|
20
|
+
/** 모든 필드 검증 통과 후 발생 (sd-form sdSubmit 대응) */
|
|
21
|
+
onSubmit?: () => void;
|
|
22
|
+
/** 검증 실패 시 실패한 필드 name 목록 전달 (sd-form sdValidationError 대응) */
|
|
23
|
+
onValidationError?: (error: SFormValidationError) => void;
|
|
24
|
+
children?: ReactNode;
|
|
25
|
+
className?: string;
|
|
26
|
+
style?: CSSProperties;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* SForm — sd-form 포팅. 폼 안의 rule-bearing 컨트롤(useFormField로 등록)을 모아
|
|
30
|
+
* submit 시 일괄 검증한다. 하나라도 실패하면 제출을 막고 첫 실패 필드에 포커스한 뒤
|
|
31
|
+
* onValidationError를 발생시킨다. 값은 여전히 소비자 소유(form-agnostic 유지).
|
|
32
|
+
*/
|
|
33
|
+
export declare const SForm: import("react").ForwardRefExoticComponent<SFormProps & import("react").RefAttributes<SFormHandle>>;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { createContext, forwardRef, useRef, useCallback, useMemo, useImperativeHandle } from 'react';
|
|
3
|
+
import { clsx } from 'clsx';
|
|
4
|
+
import { twMerge } from 'tailwind-merge';
|
|
5
|
+
import { jsx } from 'react/jsx-runtime';
|
|
6
|
+
|
|
7
|
+
function cn(...inputs) {
|
|
8
|
+
return twMerge(clsx(inputs));
|
|
9
|
+
}
|
|
10
|
+
var FormContext = createContext(null);
|
|
11
|
+
var SForm = forwardRef(function SForm2({ formClass, onSubmit, onValidationError, children, className, style }, ref) {
|
|
12
|
+
const formRef = useRef(null);
|
|
13
|
+
const fieldsRef = useRef(/* @__PURE__ */ new Set());
|
|
14
|
+
const register = useCallback((entry) => {
|
|
15
|
+
fieldsRef.current.add(entry);
|
|
16
|
+
return () => {
|
|
17
|
+
fieldsRef.current.delete(entry);
|
|
18
|
+
};
|
|
19
|
+
}, []);
|
|
20
|
+
const ctx = useMemo(() => ({ register }), [register]);
|
|
21
|
+
const orderByDom = useCallback((names) => {
|
|
22
|
+
const form = formRef.current;
|
|
23
|
+
if (!form || names.length < 2) return names;
|
|
24
|
+
const domOrder = /* @__PURE__ */ new Map();
|
|
25
|
+
form.querySelectorAll("[name],[data-name]").forEach((el, i) => {
|
|
26
|
+
const n = el.getAttribute("name") || el.getAttribute("data-name");
|
|
27
|
+
if (n && !domOrder.has(n)) domOrder.set(n, i);
|
|
28
|
+
});
|
|
29
|
+
return [...names].sort((a, b) => (domOrder.get(a) ?? Infinity) - (domOrder.get(b) ?? Infinity));
|
|
30
|
+
}, []);
|
|
31
|
+
const focusByName = useCallback((name) => {
|
|
32
|
+
const form = formRef.current;
|
|
33
|
+
if (!form || !name) return;
|
|
34
|
+
const host = form.querySelector(`[name="${CSS.escape(name)}"]`) ?? form.querySelector(`[data-name="${CSS.escape(name)}"]`);
|
|
35
|
+
if (!host) return;
|
|
36
|
+
const focusable = host.matches("input,textarea,select,button,[tabindex]") ? host : host.querySelector("input,textarea,select,button,[tabindex]");
|
|
37
|
+
focusable?.focus();
|
|
38
|
+
(focusable ?? host).scrollIntoView({ block: "nearest" });
|
|
39
|
+
}, []);
|
|
40
|
+
const runValidation = useCallback(() => {
|
|
41
|
+
let valid = true;
|
|
42
|
+
const invalidNames = [];
|
|
43
|
+
fieldsRef.current.forEach((f) => {
|
|
44
|
+
if (!f.validate()) {
|
|
45
|
+
valid = false;
|
|
46
|
+
if (f.name) invalidNames.push(f.name);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
return { valid, names: orderByDom(invalidNames) };
|
|
50
|
+
}, [orderByDom]);
|
|
51
|
+
useImperativeHandle(
|
|
52
|
+
ref,
|
|
53
|
+
() => ({
|
|
54
|
+
validate: () => runValidation().valid,
|
|
55
|
+
resetValidation: () => fieldsRef.current.forEach((f) => f.resetValidation()),
|
|
56
|
+
reset: () => fieldsRef.current.forEach((f) => f.resetValidation()),
|
|
57
|
+
focusFirstInvalid: () => {
|
|
58
|
+
const { names } = runValidation();
|
|
59
|
+
if (names.length > 0) focusByName(names[0]);
|
|
60
|
+
}
|
|
61
|
+
}),
|
|
62
|
+
[runValidation, focusByName]
|
|
63
|
+
);
|
|
64
|
+
const handleSubmit = (e) => {
|
|
65
|
+
e.preventDefault();
|
|
66
|
+
const { valid, names } = runValidation();
|
|
67
|
+
if (!valid) {
|
|
68
|
+
if (names.length > 0) focusByName(names[0]);
|
|
69
|
+
onValidationError?.({ names });
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
onSubmit?.();
|
|
73
|
+
};
|
|
74
|
+
return /* @__PURE__ */ jsx(FormContext.Provider, { value: ctx, children: /* @__PURE__ */ jsx(
|
|
75
|
+
"form",
|
|
76
|
+
{
|
|
77
|
+
ref: formRef,
|
|
78
|
+
onSubmit: handleSubmit,
|
|
79
|
+
className: cn(formClass, className),
|
|
80
|
+
style,
|
|
81
|
+
noValidate: true,
|
|
82
|
+
children
|
|
83
|
+
}
|
|
84
|
+
) });
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
export { SForm };
|
|
88
|
+
//# sourceMappingURL=SForm.js.map
|
|
89
|
+
//# sourceMappingURL=SForm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/lib/cn.ts","../../../src/lib/form.ts","../../../src/components/SForm/SForm.tsx"],"names":["SForm","useRef"],"mappings":"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","file":"SForm.js","sourcesContent":["import { clsx, type ClassValue } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\n/**\n * className 병합 유틸.\n * clsx로 조건부 클래스를 합치고, tailwind-merge로 Tailwind 클래스 충돌을 해소한다.\n */\nexport function cn(...inputs: ClassValue[]): string {\n return twMerge(clsx(inputs));\n}\n","import { createContext, useContext, useEffect, useRef } from 'react';\n\n/** 유효성 규칙: 유효하면 true, 아니면 에러 메시지 문자열 반환 (sd types/form Rule 포팅) */\nexport type Rule = (value: unknown) => true | string;\n\n/** rules를 순서대로 실행해 첫 실패 메시지를 반환. 모두 통과하면 \"\" */\nexport function runRules(value: unknown, rules?: Rule[]): string {\n if (!rules || rules.length === 0) return '';\n for (const rule of rules) {\n const result = rule(value ?? '');\n if (result !== true) return typeof result === 'string' ? result : '유효하지 않은 값입니다.';\n }\n return '';\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// SForm 연동 — Context 기반 필드 등록 (sd-form 검증 서브시스템 포팅)\n// 각 rule-bearing 컨트롤이 useFormField로 자신을 등록하면, SForm이 submit 시\n// 등록된 모든 필드의 validate()를 실행해 하나라도 실패하면 제출을 막는다.\n// SForm 밖에서 쓰이면 context가 없어 no-op (form-agnostic 사용 유지).\n// ─────────────────────────────────────────────────────────────────────────────\n\n/** SForm에 등록되는 필드 항목 */\nexport interface FormFieldEntry {\n /** 폼 연동용 name (validationError names·focus 대상 식별) */\n name: string;\n /** 현재 값에 rules를 실행하고 에러 상태를 반영한 뒤 valid 여부 반환 */\n validate: () => boolean;\n /** 검증 에러 상태만 초기화 (값은 소비자 소유) */\n resetValidation: () => void;\n}\n\nexport interface FormContextValue {\n /** 필드를 등록하고 해제 함수를 반환 */\n register: (entry: FormFieldEntry) => () => void;\n}\n\nexport const FormContext = createContext<FormContextValue | null>(null);\n\n/** 가장 가까운 SForm의 context (없으면 null) */\nexport function useFormContext(): FormContextValue | null {\n return useContext(FormContext);\n}\n\n/**\n * 폼 필드 등록 훅. SForm 내부이면 등록되고, 밖이면 no-op.\n * validate/resetValidation은 항상 최신 클로저를 참조하도록 ref로 래핑한다.\n */\nexport function useFormField(field: {\n name?: string;\n validate: () => boolean;\n resetValidation: () => void;\n}): void {\n const ctx = useFormContext();\n const cbRef = useRef(field);\n cbRef.current = field;\n const name = field.name ?? '';\n\n useEffect(() => {\n if (!ctx) return;\n return ctx.register({\n name,\n validate: () => cbRef.current.validate(),\n resetValidation: () => cbRef.current.resetValidation(),\n });\n }, [ctx, name]);\n}\n","'use client';\n\nimport {\n forwardRef,\n useCallback,\n useImperativeHandle,\n useMemo,\n useRef,\n type CSSProperties,\n type FormEvent,\n type ReactNode,\n} from 'react';\nimport { cn } from '../../lib/cn';\nimport { FormContext, type FormContextValue, type FormFieldEntry } from '../../lib/form';\n\nexport interface SFormValidationError {\n /** 검증에 실패한 필드 name 목록 (DOM 순서) */\n names: string[];\n}\n\n/** ref로 노출되는 폼 제어 메서드 (sd-form @Method 대응) */\nexport interface SFormHandle {\n /** 등록된 모든 필드를 검증하고 통과 여부를 반환 */\n validate: () => boolean;\n /** 모든 필드의 검증 상태만 초기화 (값은 소비자 소유) */\n resetValidation: () => void;\n /** resetValidation 별칭 — sd-form sdReset 대응. 값 리셋은 소비자(value/onChange) 책임 */\n reset: () => void;\n /** 첫 번째 실패 필드로 포커스 이동 */\n focusFirstInvalid: () => void;\n}\n\nexport interface SFormProps {\n /** form 엘리먼트에 추가할 클래스 (sd-form formClass 대응) */\n formClass?: string;\n /** 모든 필드 검증 통과 후 발생 (sd-form sdSubmit 대응) */\n onSubmit?: () => void;\n /** 검증 실패 시 실패한 필드 name 목록 전달 (sd-form sdValidationError 대응) */\n onValidationError?: (error: SFormValidationError) => void;\n children?: ReactNode;\n className?: string;\n style?: CSSProperties;\n}\n\n/**\n * SForm — sd-form 포팅. 폼 안의 rule-bearing 컨트롤(useFormField로 등록)을 모아\n * submit 시 일괄 검증한다. 하나라도 실패하면 제출을 막고 첫 실패 필드에 포커스한 뒤\n * onValidationError를 발생시킨다. 값은 여전히 소비자 소유(form-agnostic 유지).\n */\nexport const SForm = forwardRef<SFormHandle, SFormProps>(function SForm(\n { formClass, onSubmit, onValidationError, children, className, style },\n ref,\n) {\n const formRef = useRef<HTMLFormElement>(null);\n const fieldsRef = useRef<Set<FormFieldEntry>>(new Set());\n\n const register = useCallback((entry: FormFieldEntry) => {\n fieldsRef.current.add(entry);\n return () => {\n fieldsRef.current.delete(entry);\n };\n }, []);\n\n const ctx = useMemo<FormContextValue>(() => ({ register }), [register]);\n\n /** 실패 필드 name들을 DOM 등장 순서로 정렬 (name / data-name 기준) */\n const orderByDom = useCallback((names: string[]): string[] => {\n const form = formRef.current;\n if (!form || names.length < 2) return names;\n const domOrder = new Map<string, number>();\n form.querySelectorAll<HTMLElement>('[name],[data-name]').forEach((el, i) => {\n const n = el.getAttribute('name') || el.getAttribute('data-name');\n if (n && !domOrder.has(n)) domOrder.set(n, i);\n });\n return [...names].sort((a, b) => (domOrder.get(a) ?? Infinity) - (domOrder.get(b) ?? Infinity));\n }, []);\n\n const focusByName = useCallback((name: string) => {\n const form = formRef.current;\n if (!form || !name) return;\n const host =\n form.querySelector<HTMLElement>(`[name=\"${CSS.escape(name)}\"]`) ??\n form.querySelector<HTMLElement>(`[data-name=\"${CSS.escape(name)}\"]`);\n if (!host) return;\n const focusable = host.matches('input,textarea,select,button,[tabindex]')\n ? host\n : host.querySelector<HTMLElement>('input,textarea,select,button,[tabindex]');\n focusable?.focus();\n (focusable ?? host).scrollIntoView({ block: 'nearest' });\n }, []);\n\n /**\n * 등록된 모든 필드를 검증한다.\n * - valid: 전체 통과 여부 (이름 없는 필드도 실패하면 false → 제출 차단)\n * - names: 실패한 필드 중 name이 있는 것들 (DOM 순서, onValidationError용)\n */\n const runValidation = useCallback((): { valid: boolean; names: string[] } => {\n let valid = true;\n const invalidNames: string[] = [];\n fieldsRef.current.forEach(f => {\n if (!f.validate()) {\n valid = false;\n if (f.name) invalidNames.push(f.name);\n }\n });\n return { valid, names: orderByDom(invalidNames) };\n }, [orderByDom]);\n\n useImperativeHandle(\n ref,\n () => ({\n validate: () => runValidation().valid,\n resetValidation: () => fieldsRef.current.forEach(f => f.resetValidation()),\n reset: () => fieldsRef.current.forEach(f => f.resetValidation()),\n focusFirstInvalid: () => {\n const { names } = runValidation();\n if (names.length > 0) focusByName(names[0]);\n },\n }),\n [runValidation, focusByName],\n );\n\n const handleSubmit = (e: FormEvent<HTMLFormElement>) => {\n e.preventDefault();\n const { valid, names } = runValidation();\n if (!valid) {\n if (names.length > 0) focusByName(names[0]);\n onValidationError?.({ names });\n return;\n }\n onSubmit?.();\n };\n\n return (\n <FormContext.Provider value={ctx}>\n <form\n ref={formRef}\n onSubmit={handleSubmit}\n className={cn(formClass, className)}\n style={style}\n noValidate\n >\n {children}\n </form>\n </FormContext.Provider>\n );\n});\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { SForm, type SFormProps, type SFormHandle, type SFormValidationError } from './SForm';
|
|
@@ -35,6 +35,134 @@ PortalContainerContext.Provider;
|
|
|
35
35
|
function usePortalContainer() {
|
|
36
36
|
return react.useContext(PortalContainerContext) ?? void 0;
|
|
37
37
|
}
|
|
38
|
+
|
|
39
|
+
// src/lib/color.ts
|
|
40
|
+
var COLOR_KEYS = [
|
|
41
|
+
"white",
|
|
42
|
+
"black",
|
|
43
|
+
"grey_10",
|
|
44
|
+
"grey_20",
|
|
45
|
+
"grey_25",
|
|
46
|
+
"grey_30",
|
|
47
|
+
"grey_35",
|
|
48
|
+
"grey_45",
|
|
49
|
+
"grey_50",
|
|
50
|
+
"grey_55",
|
|
51
|
+
"grey_60",
|
|
52
|
+
"grey_65",
|
|
53
|
+
"grey_70",
|
|
54
|
+
"grey_80",
|
|
55
|
+
"grey_85",
|
|
56
|
+
"grey_90",
|
|
57
|
+
"grey_95",
|
|
58
|
+
"grey_05",
|
|
59
|
+
"red_15",
|
|
60
|
+
"red_20",
|
|
61
|
+
"red_30",
|
|
62
|
+
"red_45",
|
|
63
|
+
"red_60",
|
|
64
|
+
"red_70",
|
|
65
|
+
"red_75",
|
|
66
|
+
"red_80",
|
|
67
|
+
"red_85",
|
|
68
|
+
"red_90",
|
|
69
|
+
"red_95",
|
|
70
|
+
"red_99",
|
|
71
|
+
"orange_10",
|
|
72
|
+
"orange_20",
|
|
73
|
+
"orange_35",
|
|
74
|
+
"orange_45",
|
|
75
|
+
"orange_55",
|
|
76
|
+
"orange_60",
|
|
77
|
+
"orange_65",
|
|
78
|
+
"orange_75",
|
|
79
|
+
"orange_85",
|
|
80
|
+
"orange_90",
|
|
81
|
+
"orange_95",
|
|
82
|
+
"orange_99",
|
|
83
|
+
"yellow_10",
|
|
84
|
+
"yellow_20",
|
|
85
|
+
"yellow_25",
|
|
86
|
+
"yellow_30",
|
|
87
|
+
"yellow_40",
|
|
88
|
+
"yellow_45",
|
|
89
|
+
"yellow_50",
|
|
90
|
+
"yellow_60",
|
|
91
|
+
"yellow_70",
|
|
92
|
+
"yellow_80",
|
|
93
|
+
"yellow_90",
|
|
94
|
+
"yellow_95",
|
|
95
|
+
"olive_10",
|
|
96
|
+
"olive_15",
|
|
97
|
+
"olive_20",
|
|
98
|
+
"olive_30",
|
|
99
|
+
"olive_45",
|
|
100
|
+
"olive_55",
|
|
101
|
+
"olive_65",
|
|
102
|
+
"olive_70",
|
|
103
|
+
"olive_80",
|
|
104
|
+
"olive_90",
|
|
105
|
+
"olive_95",
|
|
106
|
+
"olive_05",
|
|
107
|
+
"green_15",
|
|
108
|
+
"green_25",
|
|
109
|
+
"green_45",
|
|
110
|
+
"green_55",
|
|
111
|
+
"green_65",
|
|
112
|
+
"green_70",
|
|
113
|
+
"green_75",
|
|
114
|
+
"green_80",
|
|
115
|
+
"green_85",
|
|
116
|
+
"green_90",
|
|
117
|
+
"green_95",
|
|
118
|
+
"green_99",
|
|
119
|
+
"steelblue_10",
|
|
120
|
+
"steelblue_25",
|
|
121
|
+
"steelblue_45",
|
|
122
|
+
"steelblue_60",
|
|
123
|
+
"steelblue_65",
|
|
124
|
+
"steelblue_70",
|
|
125
|
+
"steelblue_75",
|
|
126
|
+
"steelblue_80",
|
|
127
|
+
"steelblue_85",
|
|
128
|
+
"steelblue_90",
|
|
129
|
+
"steelblue_95",
|
|
130
|
+
"steelblue_99",
|
|
131
|
+
"oceanblue_15",
|
|
132
|
+
"oceanblue_25",
|
|
133
|
+
"oceanblue_50",
|
|
134
|
+
"oceanblue_60",
|
|
135
|
+
"oceanblue_65",
|
|
136
|
+
"oceanblue_70",
|
|
137
|
+
"oceanblue_75",
|
|
138
|
+
"oceanblue_80",
|
|
139
|
+
"oceanblue_85",
|
|
140
|
+
"oceanblue_90",
|
|
141
|
+
"oceanblue_95",
|
|
142
|
+
"oceanblue_99",
|
|
143
|
+
"brilliantblue_10",
|
|
144
|
+
"brilliantblue_20",
|
|
145
|
+
"brilliantblue_25",
|
|
146
|
+
"brilliantblue_40",
|
|
147
|
+
"brilliantblue_50",
|
|
148
|
+
"brilliantblue_60",
|
|
149
|
+
"brilliantblue_70",
|
|
150
|
+
"brilliantblue_75",
|
|
151
|
+
"brilliantblue_80",
|
|
152
|
+
"brilliantblue_85",
|
|
153
|
+
"brilliantblue_90",
|
|
154
|
+
"brilliantblue_95",
|
|
155
|
+
"brilliantblue_99",
|
|
156
|
+
"brilliantblue_05"
|
|
157
|
+
];
|
|
158
|
+
var KEY_SET = new Set(COLOR_KEYS);
|
|
159
|
+
function isColorKey(input) {
|
|
160
|
+
return KEY_SET.has(input);
|
|
161
|
+
}
|
|
162
|
+
function resolveColor(input, fallback) {
|
|
163
|
+
if (input === null || input === void 0 || input === "") return fallback;
|
|
164
|
+
return isColorKey(input) ? `var(--color-${input.replace(/_/g, "-")})` : input;
|
|
165
|
+
}
|
|
38
166
|
var Icon_add = (p) => {
|
|
39
167
|
const { size = 24, ...rest } = p;
|
|
40
168
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -2734,7 +2862,7 @@ var SIcon = react.forwardRef(function SIcon2({ name, size = 24, color, rotate, l
|
|
|
2734
2862
|
{
|
|
2735
2863
|
ref,
|
|
2736
2864
|
className: cn("inline-flex items-center justify-center", rotateClass, className),
|
|
2737
|
-
style: { color, ...style },
|
|
2865
|
+
style: { color: resolveColor(color), ...style },
|
|
2738
2866
|
role: label ? "img" : void 0,
|
|
2739
2867
|
"aria-label": label,
|
|
2740
2868
|
"aria-hidden": label ? void 0 : true,
|