sh-ui-cli 0.113.0 → 0.115.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/data/changelog/versions.json +27 -0
- package/data/registry/react/components/form/field.test.tsx +106 -1
- package/data/registry/react/components/form/field.tsx +179 -23
- package/data/registry/react/components/form/use-sh-ui-form.ts +14 -0
- package/data/registry/react/components/form-rhf/README.md +138 -8
- package/data/registry/react/components/form-rhf/index.tsx +75 -0
- package/data/registry/react/components/form-rhf/rhf.test.tsx +53 -1
- package/data/registry/react/components/label/index.tailwind.tsx +5 -1
- package/data/registry/react/components/label/styles.css +9 -5
- package/data/registry/react/components/label/styles.module.css +7 -5
- package/data/registry/react/components/rich-text-editor/index.module.tsx +523 -171
- package/data/registry/react/components/rich-text-editor/index.tailwind.tsx +596 -70
- package/data/registry/react/components/rich-text-editor/index.tsx +523 -171
- package/data/registry/react/components/rich-text-editor/styles.css +103 -5
- package/data/registry/react/components/rich-text-editor/styles.module.css +103 -5
- package/data/registry/react/components/sidebar/index.module.tsx +57 -0
- package/data/registry/react/components/sidebar/index.tailwind.tsx +68 -1
- package/data/registry/react/components/sidebar/index.tsx +57 -0
- package/data/registry/react/components/sidebar/styles.css +77 -0
- package/data/registry/react/components/sidebar/styles.module.css +77 -0
- package/data/registry/react/registry.json +319 -963
- package/data/registry/react/tokens-used.json +4 -1
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@ import { useForm } from "react-hook-form";
|
|
|
6
6
|
import { Form } from "../form";
|
|
7
7
|
import { Field } from "../form/field";
|
|
8
8
|
import { FormControl, FormError } from "../form/field";
|
|
9
|
-
import { adaptReactHookForm } from "./index";
|
|
9
|
+
import { adaptReactHookForm, useReactHookFormAdapter } from "./index";
|
|
10
10
|
|
|
11
11
|
function TestForm() {
|
|
12
12
|
const rhf = useForm({ defaultValues: { email: "" }, mode: "onBlur" });
|
|
@@ -40,3 +40,55 @@ describe("adaptReactHookForm", () => {
|
|
|
40
40
|
await screen.findByText("bad");
|
|
41
41
|
});
|
|
42
42
|
});
|
|
43
|
+
|
|
44
|
+
// ─────────────────────────────────────────────
|
|
45
|
+
// useReactHookFormAdapter — v0.114+ (안정화 hook)
|
|
46
|
+
// ─────────────────────────────────────────────
|
|
47
|
+
describe("useReactHookFormAdapter", () => {
|
|
48
|
+
it("returns same store instance across re-renders", () => {
|
|
49
|
+
const stores: any[] = [];
|
|
50
|
+
function Probe() {
|
|
51
|
+
const rhf = useForm({ defaultValues: { x: "" } });
|
|
52
|
+
const form = useReactHookFormAdapter(rhf);
|
|
53
|
+
stores.push(form);
|
|
54
|
+
const [, force] = React.useReducer((s: number) => s + 1, 0);
|
|
55
|
+
return <button onClick={force as any} data-testid="rerender">rerender</button>;
|
|
56
|
+
}
|
|
57
|
+
const { getByTestId } = render(<Probe />);
|
|
58
|
+
// 강제 re-render 3회
|
|
59
|
+
for (let i = 0; i < 3; i++) {
|
|
60
|
+
getByTestId("rerender").click();
|
|
61
|
+
}
|
|
62
|
+
// 최초 + re-render 들 모두 같은 인스턴스
|
|
63
|
+
expect(stores.length).toBeGreaterThanOrEqual(2);
|
|
64
|
+
for (const s of stores) {
|
|
65
|
+
expect(s).toBe(stores[0]);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("works with render prop and propagates value through RHF", async () => {
|
|
70
|
+
const user = userEvent.setup();
|
|
71
|
+
function HookForm() {
|
|
72
|
+
const rhf = useForm({ defaultValues: { email: "" }, mode: "onBlur" });
|
|
73
|
+
const form = useReactHookFormAdapter(rhf);
|
|
74
|
+
return (
|
|
75
|
+
<Form form={form}>
|
|
76
|
+
<Field name="email">
|
|
77
|
+
{(field) => (
|
|
78
|
+
<input
|
|
79
|
+
data-testid="i"
|
|
80
|
+
value={(field.value as string) ?? ""}
|
|
81
|
+
onChange={(e) => field.handleChange(e.target.value)}
|
|
82
|
+
onBlur={field.handleBlur}
|
|
83
|
+
/>
|
|
84
|
+
)}
|
|
85
|
+
</Field>
|
|
86
|
+
</Form>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
render(<HookForm />);
|
|
90
|
+
const input = screen.getByTestId("i") as HTMLInputElement;
|
|
91
|
+
await user.type(input, "kim@studio");
|
|
92
|
+
expect(input.value).toBe("kim@studio");
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -17,7 +17,11 @@ export const Label = React.forwardRef<HTMLLabelElement, LabelProps>(
|
|
|
17
17
|
<label
|
|
18
18
|
ref={ref}
|
|
19
19
|
className={cn(
|
|
20
|
-
|
|
20
|
+
// 기본 — typography + 인터랙션만. display 는 label 의 native(inline) 유지.
|
|
21
|
+
// sub-part(LabelTitle/Subtitle/Description/Caption) 가 자식에 있을 때만
|
|
22
|
+
// 자동으로 flex-col stack — 사용자가 가로 정렬(inline-flex items-center)
|
|
23
|
+
// 을 명시할 수 있도록 기본 display 를 강제하지 않는다.
|
|
24
|
+
"text-[length:var(--text-sm)] font-medium leading-snug text-foreground cursor-pointer select-none has-[[data-sh-ui-label-part]]:flex has-[[data-sh-ui-label-part]]:flex-col has-[[data-sh-ui-label-part]]:gap-0.5",
|
|
21
25
|
// 필수 표시 — title 이 있으면 title 뒤, 없으면 label 뒤에 * 부착
|
|
22
26
|
isRequired &&
|
|
23
27
|
"has-[[data-sh-ui-label-part='title']]:[&>[data-sh-ui-label-part='title']]:after:content-['_*'] has-[[data-sh-ui-label-part='title']]:[&>[data-sh-ui-label-part='title']]:after:text-danger has-[[data-sh-ui-label-part='title']]:[&>[data-sh-ui-label-part='title']]:after:font-semibold not-has-[[data-sh-ui-label-part='title']]:after:content-['_*'] not-has-[[data-sh-ui-label-part='title']]:after:text-danger not-has-[[data-sh-ui-label-part='title']]:after:font-semibold",
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
+
/* 기본 — typography + 인터랙션만. display 는 label 의 native (inline) 유지.
|
|
2
|
+
사용자가 `inline-flex items-center gap-2` 같이 가로 정렬을 명시할 수 있다. */
|
|
1
3
|
.sh-ui-label {
|
|
2
|
-
display: flex;
|
|
3
|
-
flex-direction: column;
|
|
4
|
-
gap: 0.125rem;
|
|
5
4
|
font-size: var(--text-sm);
|
|
6
5
|
font-weight: var(--weight-medium);
|
|
7
6
|
line-height: 1.4;
|
|
@@ -10,8 +9,13 @@
|
|
|
10
9
|
user-select: none;
|
|
11
10
|
}
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
/* sub-part (Title/Subtitle/Description/Caption) 가 자식에 있을 때만 세로 stack.
|
|
13
|
+
사용자가 `<Label><LabelTitle .../><LabelDescription .../></Label>` 형태로
|
|
14
|
+
쓸 때 자동으로 정렬. */
|
|
15
|
+
.sh-ui-label:has(.sh-ui-label__title, .sh-ui-label__subtitle, .sh-ui-label__description, .sh-ui-label__caption) {
|
|
16
|
+
display: flex;
|
|
17
|
+
flex-direction: column;
|
|
18
|
+
gap: 0.125rem;
|
|
15
19
|
}
|
|
16
20
|
|
|
17
21
|
/* ───── 텍스트 계층 ───── */
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
+
/* 기본 — typography + 인터랙션만. display 는 label 의 native (inline) 유지.
|
|
2
|
+
사용자가 `inline-flex items-center gap-2` 같이 가로 정렬을 명시할 수 있다. */
|
|
1
3
|
.label {
|
|
2
|
-
display: flex;
|
|
3
|
-
flex-direction: column;
|
|
4
|
-
gap: 0.125rem;
|
|
5
4
|
font-size: var(--text-sm);
|
|
6
5
|
font-weight: var(--weight-medium);
|
|
7
6
|
line-height: 1.4;
|
|
@@ -10,8 +9,11 @@
|
|
|
10
9
|
user-select: none;
|
|
11
10
|
}
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
/* sub-part (Title/Subtitle/Description/Caption) 가 자식에 있을 때만 세로 stack. */
|
|
13
|
+
.label:has(.label__title, .label__subtitle, .label__description, .label__caption) {
|
|
14
|
+
display: flex;
|
|
15
|
+
flex-direction: column;
|
|
16
|
+
gap: 0.125rem;
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
/* ───── 텍스트 계층 ───── */
|