@tide-tds/components 0.1.0 → 0.2.1

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/Link.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ import { Role } from "./types.js";
2
+ import { ComponentProps, ReactElement } from "react";
3
+ export interface LinkProps extends Omit<ComponentProps<"a">, "color"> {
4
+ /**
5
+ * Role. Tide Role 6개와 앱이 `RoleRegistry`에 더한 Role을 받는다. 기본값은 `primary`다.
6
+ * 색 계열 이름(`blue` 등)은 받지 않는다. 행동의 뜻을 색으로 전하기 때문이다(ADR 0016).
7
+ * 루트에 `data-color`로 붙어 글자색(`text-accent`)을 정한다. 바깥 요소의 색을 물려받지 않는다.
8
+ */
9
+ color?: Role;
10
+ /**
11
+ * 다른 요소로 렌더한다. 예: `render={<NextLink href="/x" />}`.
12
+ * props, ref, className을 그 요소에 병합한다.
13
+ */
14
+ render?: ReactElement;
15
+ }
16
+ /**
17
+ * 문장 안에서 다른 페이지나 위치로 이동하는 텍스트 링크.
18
+ * Next.js에서는 `render={<NextLink href="/x" />}`로 클라이언트 이동을 쓴다.
19
+ */
20
+ export declare function Link({ color, render, ...props }: LinkProps): ReactElement;
package/dist/Link.js ADDED
@@ -0,0 +1,21 @@
1
+ import { renderElement } from "./render.js";
2
+
3
+ // src/Link.tsx
4
+ const baseClass = "cursor-pointer rounded-sm text-accent underline decoration-1 underline-offset-2 hover:decoration-2 focus-visible:focus-ring";
5
+ /**
6
+ * 문장 안에서 다른 페이지나 위치로 이동하는 텍스트 링크.
7
+ * Next.js에서는 `render={<NextLink href="/x" />}`로 클라이언트 이동을 쓴다.
8
+ */
9
+ function Link({ color = "primary", render, ...props }) {
10
+ return renderElement({
11
+ defaultElement: "a",
12
+ render,
13
+ baseClassName: baseClass,
14
+ props: {
15
+ ...props,
16
+ "data-color": color
17
+ }
18
+ });
19
+ }
20
+
21
+ export { Link };
@@ -0,0 +1,27 @@
1
+ import { ChoiceSize } from "./choice-style.js";
2
+ import { Role } from "./types.js";
3
+ import { CSSProperties, ComponentProps, ReactElement, ReactNode } from "react";
4
+ export interface RadioProps extends Omit<ComponentProps<"input">, "type" | "size" | "color" | "children" | "className" | "style" | "value"> {
5
+ /** 이 라디오의 값. RadioGroup의 `value`·`onValueChange`가 이 값을 쓰고, form에 제출된다. */
6
+ value: string;
7
+ /** 원 옆에 보이는 label. 라디오의 이름이 된다. 없으면 `aria-label`을 준다. */
8
+ children?: ReactNode;
9
+ /** 원 16px·글자 14px(`sm`) 또는 원 20px·글자 16px(`md`). 누르는 영역은 둘 다 24×24px이다. 기본값은 `md`다. */
10
+ size?: ChoiceSize;
11
+ /** 선택했을 때 원 색(Role). 루트에 `data-color`로 붙는다. 기본값은 `primary`다. */
12
+ color?: Role;
13
+ /**
14
+ * 오류 상태. input에 `aria-invalid="true"`와 `data-invalid`가 붙고 경계가 danger 색이 된다.
15
+ * Field·RadioGroup 안에서는 그 값을 따르고, 여기에 준 값이 이긴다.
16
+ */
17
+ invalid?: boolean;
18
+ /** 루트 label의 class. 사용자 class가 기본 class를 이긴다. 상자 모양은 바꾸지 않는다(`size`·`color`가 맡는다). */
19
+ className?: string;
20
+ /** 루트 label의 style */
21
+ style?: CSSProperties;
22
+ }
23
+ /**
24
+ * 네이티브 라디오 버튼. 보통 `RadioGroup` 안에 두고, 묶음이 `name`과 선택 값을 정한다.
25
+ * 묶음 밖에서는 `name`·`checked`/`defaultChecked`·`onChange`를 직접 준다.
26
+ */
27
+ export declare function Radio({ value, children, size, color, invalid, id, disabled, required, name, checked, defaultChecked, onChange, className, style, "aria-describedby": ariaDescribedBy, "aria-invalid": ariaInvalid, ...props }: RadioProps): ReactElement;
package/dist/Radio.js ADDED
@@ -0,0 +1,67 @@
1
+ "use client";
2
+
3
+ import { RadioGroupContext } from "./choice-group.js";
4
+ import { choiceAreaClass, choiceInputClass, choiceMarkClass, choiceRootClass, choiceTextClass } from "./choice-style.js";
5
+ import { cn } from "./cn.js";
6
+ import { useFieldControl } from "./field-context.js";
7
+ import { jsx, jsxs } from "react/jsx-runtime";
8
+ import { use } from "react";
9
+ import { CircleIcon } from "@tide-tds/icons";
10
+
11
+ // src/Radio.tsx
12
+ const boxClass = {
13
+ sm: "size-4 rounded-full",
14
+ md: "size-5 rounded-full"
15
+ };
16
+ /** 가운데 점 크기(px). 원 아이콘을 채워 그리고 선 두께 2px이 더해진다. */
17
+ const dotSize = {
18
+ sm: 6,
19
+ md: 8
20
+ };
21
+ /**
22
+ * 네이티브 라디오 버튼. 보통 `RadioGroup` 안에 두고, 묶음이 `name`과 선택 값을 정한다.
23
+ * 묶음 밖에서는 `name`·`checked`/`defaultChecked`·`onChange`를 직접 준다.
24
+ */
25
+ function Radio({ value, children, size = "md", color = "primary", invalid, id, disabled, required, name, checked, defaultChecked, onChange, className, style, "aria-describedby": ariaDescribedBy, "aria-invalid": ariaInvalid, ...props }) {
26
+ const group = use(RadioGroupContext);
27
+ const control = useFieldControl({
28
+ id,
29
+ invalid: invalid ?? group?.invalid,
30
+ disabled: disabled ?? group?.disabled,
31
+ required: required ?? (group?.required || void 0),
32
+ "aria-describedby": ariaDescribedBy,
33
+ "aria-invalid": ariaInvalid
34
+ });
35
+ const selection = group?.value !== void 0 ? { checked: group.value === value } : group?.defaultValue !== void 0 ? { defaultChecked: group.defaultValue === value } : checked !== void 0 ? { checked } : { defaultChecked };
36
+ const handleChange = (event) => {
37
+ onChange?.(event);
38
+ if (event.target.checked) group?.onValueChange?.(value);
39
+ };
40
+ return /* @__PURE__ */ jsxs("label", {
41
+ "data-color": color,
42
+ "data-disabled": control.disabled ? "" : void 0,
43
+ "data-invalid": control["data-invalid"],
44
+ style,
45
+ className: cn(choiceRootClass, choiceTextClass[size], className),
46
+ children: [/* @__PURE__ */ jsxs("span", {
47
+ className: choiceAreaClass,
48
+ children: [/* @__PURE__ */ jsx("input", {
49
+ ...props,
50
+ ...control,
51
+ ...selection,
52
+ type: "radio",
53
+ name: name ?? group?.name,
54
+ value,
55
+ onChange: handleChange,
56
+ className: cn(boxClass[size], choiceInputClass)
57
+ }), /* @__PURE__ */ jsx(CircleIcon, {
58
+ size: dotSize[size],
59
+ fill: "currentColor",
60
+ "data-mark": "dot",
61
+ className: cn(choiceMarkClass, "peer-checked:block")
62
+ })]
63
+ }), children === void 0 || children === null ? null : /* @__PURE__ */ jsx("span", { children })]
64
+ });
65
+ }
66
+
67
+ export { Radio };
@@ -0,0 +1,28 @@
1
+ import { ChoiceOrientation } from "./CheckboxGroup.js";
2
+ import { ComponentProps, ReactElement } from "react";
3
+ export interface RadioGroupProps extends Omit<ComponentProps<"div">, "defaultValue"> {
4
+ /** 안의 Radio가 함께 쓰는 `name`. form에 이 이름으로 제출된다. 주지 않으면 자동으로 만든다. */
5
+ name?: string;
6
+ /** 선택된 Radio의 값(제어). `onValueChange`와 함께 쓴다. */
7
+ value?: string;
8
+ /** 처음 선택된 Radio의 값(비제어). */
9
+ defaultValue?: string;
10
+ /** 선택이 바뀌면 새 값 하나로 불린다. */
11
+ onValueChange?: (value: string) => void;
12
+ /** 항목을 세로(`vertical`)로 쌓거나 가로(`horizontal`)로 늘어놓는다. 기본값은 `vertical`이다. 화살표 키는 둘 다 동작한다. */
13
+ orientation?: ChoiceOrientation;
14
+ /**
15
+ * 오류 상태. 안의 Radio마다 `aria-invalid="true"`가 붙고, 묶음에 `data-invalid`가 붙는다.
16
+ * Field 안에서는 Field의 값을 따르고, 여기에 준 값이 이긴다.
17
+ */
18
+ invalid?: boolean;
19
+ /** 안의 Radio를 모두 비활성으로 만든다. Field 안에서는 Field의 값을 따른다. */
20
+ disabled?: boolean;
21
+ /** 하나를 꼭 골라야 한다. 안의 Radio에 네이티브 `required`가 붙고 묶음에 `aria-required`가 붙는다. */
22
+ required?: boolean;
23
+ }
24
+ /**
25
+ * Radio 여러 개를 하나의 이름과 선택 값으로 묶는다. `<Field label group>` 안에 두면 label이 묶음의 이름,
26
+ * 설명·오류가 묶음의 설명이 된다. Field 밖에서는 `aria-label`이나 `aria-labelledby`를 준다.
27
+ */
28
+ export declare function RadioGroup({ name, value, defaultValue, onValueChange, orientation, invalid, disabled, required, id, className, children, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, ...props }: RadioGroupProps): ReactElement;
@@ -0,0 +1,50 @@
1
+ "use client";
2
+
3
+ import { orientationClass } from "./CheckboxGroup.js";
4
+ import { RadioGroupContext, useChoiceGroup } from "./choice-group.js";
5
+ import { cn } from "./cn.js";
6
+ import { FieldContext } from "./field-context.js";
7
+ import { jsx } from "react/jsx-runtime";
8
+ import { useId } from "react";
9
+
10
+ // src/RadioGroup.tsx
11
+ /**
12
+ * Radio 여러 개를 하나의 이름과 선택 값으로 묶는다. `<Field label group>` 안에 두면 label이 묶음의 이름,
13
+ * 설명·오류가 묶음의 설명이 된다. Field 밖에서는 `aria-label`이나 `aria-labelledby`를 준다.
14
+ */
15
+ function RadioGroup({ name, value, defaultValue, onValueChange, orientation = "vertical", invalid, disabled, required, id, className, children, "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, ...props }) {
16
+ const autoName = useId();
17
+ const connection = useChoiceGroup({
18
+ id,
19
+ invalid,
20
+ disabled,
21
+ required,
22
+ "aria-labelledby": ariaLabelledBy,
23
+ "aria-describedby": ariaDescribedBy
24
+ });
25
+ const state = {
26
+ name: name ?? autoName,
27
+ value,
28
+ defaultValue,
29
+ onValueChange,
30
+ invalid: connection.invalid,
31
+ disabled: connection.disabled,
32
+ required: connection.required
33
+ };
34
+ return /* @__PURE__ */ jsx("div", {
35
+ role: "radiogroup",
36
+ "aria-required": connection.required || void 0,
37
+ ...props,
38
+ ...connection.rootProps,
39
+ className: cn(orientationClass[orientation], className),
40
+ children: /* @__PURE__ */ jsx(FieldContext, {
41
+ value: null,
42
+ children: /* @__PURE__ */ jsx(RadioGroupContext, {
43
+ value: state,
44
+ children
45
+ })
46
+ })
47
+ });
48
+ }
49
+
50
+ export { RadioGroup };
@@ -0,0 +1,18 @@
1
+ import { ComponentProps, ReactElement } from "react";
2
+ /** Separator의 방향 */
3
+ type SeparatorOrientation = "horizontal" | "vertical";
4
+ export interface SeparatorProps extends Omit<ComponentProps<"hr">, "children" | "role"> {
5
+ /** `horizontal`(기본값)은 위아래 내용을, `vertical`은 한 줄 안의 양옆 내용을 나눈다. */
6
+ orientation?: SeparatorOrientation;
7
+ /**
8
+ * 보기만 나누는 선이면 `true`. `role="none"`이 붙어 보조 기술에서 빠진다. 기본값은 `false`(separator 역할)다.
9
+ * 내용의 주제가 바뀌는 곳이면 `false`로 둔다.
10
+ */
11
+ decorative?: boolean;
12
+ }
13
+ /**
14
+ * 내용 묶음 사이를 선으로 나눈다. 기본은 의미 구분(separator 역할)이고, `decorative`면 보조 기술에서 빠진다.
15
+ * 서버 컴포넌트로 쓸 수 있다.
16
+ */
17
+ export declare function Separator({ orientation, decorative, className, ...props }: SeparatorProps): ReactElement;
18
+ export {};
@@ -0,0 +1,24 @@
1
+ import { cn } from "./cn.js";
2
+ import { jsx } from "react/jsx-runtime";
3
+
4
+ // src/Separator.tsx
5
+ const orientationClass = {
6
+ horizontal: "h-0 w-full border-t",
7
+ vertical: "h-auto w-0 self-stretch border-s"
8
+ };
9
+ const baseClass = "m-0 shrink-0 border-0 border-default";
10
+ /**
11
+ * 내용 묶음 사이를 선으로 나눈다. 기본은 의미 구분(separator 역할)이고, `decorative`면 보조 기술에서 빠진다.
12
+ * 서버 컴포넌트로 쓸 수 있다.
13
+ */
14
+ function Separator({ orientation = "horizontal", decorative = false, className, ...props }) {
15
+ return /* @__PURE__ */ jsx("hr", {
16
+ ...props,
17
+ role: decorative ? "none" : void 0,
18
+ "aria-orientation": !decorative && orientation === "vertical" ? "vertical" : void 0,
19
+ "data-orientation": orientation,
20
+ className: cn(baseClass, orientationClass[orientation], className)
21
+ });
22
+ }
23
+
24
+ export { Separator };
@@ -0,0 +1,30 @@
1
+ import { Role, Size } from "./types.js";
2
+ import { CSSProperties, ComponentProps, ReactElement, ReactNode } from "react";
3
+ /** Switch의 size. `xs`는 track이 너무 작아 받지 않는다. */
4
+ type SwitchSize = Extract<Size, "sm" | "md" | "lg">;
5
+ export interface SwitchProps extends Omit<ComponentProps<"input">, "type" | "size" | "color" | "role" | "children"> {
6
+ /** 스위치 옆에 보이는 label. 스위치의 이름이 된다. Field 안에서는 Field의 `label`을 쓰고 비워 둔다. */
7
+ children?: ReactNode;
8
+ /** track 크기 `sm` 28×16px, `md` 36×20px, `lg` 44×24px. label 텍스트 스타일도 함께 바뀐다. 기본값은 `md`다. */
9
+ size?: SwitchSize;
10
+ /**
11
+ * 켜졌을 때 track 색. Role(Tide Role 6개와 앱이 `RoleRegistry`에 더한 Role)을 받는다. 기본값은 `primary`다.
12
+ * 루트에 `data-color`로 붙어 accent 색을 정한다. 바깥 요소의 색을 물려받지 않는다.
13
+ */
14
+ color?: Role;
15
+ /**
16
+ * 오류 상태. `aria-invalid="true"`와 `data-invalid`가 붙고 꺼진 track 경계가 danger 색이 된다.
17
+ * Field 안에서는 Field의 값을 따르고, 여기에 준 값이 이긴다.
18
+ */
19
+ invalid?: boolean;
20
+ /** 루트 `<label>`의 class. 기본 class를 이긴다. track과 thumb 모양은 바꾸지 않는다. */
21
+ className?: string;
22
+ /** 루트 `<label>`의 style */
23
+ style?: CSSProperties;
24
+ }
25
+ /**
26
+ * 켜고 끄는 설정을 바꾸는 스위치. 네이티브 체크박스라 form 제출이 그대로 되고, 스크린리더는 「스위치」로 읽는다.
27
+ * `children`이 label이 된다. Field 안에 두면 Field의 label·설명·오류가 연결된다.
28
+ */
29
+ export declare function Switch({ children, size, color, invalid, id, disabled, required, className, style, "aria-describedby": ariaDescribedBy, "aria-invalid": ariaInvalid, ...props }: SwitchProps): ReactElement;
30
+ export {};
package/dist/Switch.js ADDED
@@ -0,0 +1,65 @@
1
+ "use client";
2
+
3
+ import { cn } from "./cn.js";
4
+ import { useFieldControl } from "./field-context.js";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
6
+
7
+ // src/Switch.tsx
8
+ /** 루트 높이는 최소 24px라 track이 작아도 누르는 영역이 24×24px 이상이다. */
9
+ const rootSizeClass = {
10
+ sm: "gap-1.5 text-label-sm",
11
+ md: "gap-2 text-label-md",
12
+ lg: "gap-2.5 text-label-lg"
13
+ };
14
+ /** track 크기(border 1px 포함). thumb은 track 안쪽에서 2px 떨어진다. */
15
+ const trackSizeClass = {
16
+ sm: "h-4 w-7",
17
+ md: "h-5 w-9",
18
+ lg: "h-6 w-11"
19
+ };
20
+ /** thumb 크기와 켜졌을 때 이동 거리(track 폭 - border 2px - 여백 4px - thumb 크기). RTL에서는 반대로 움직인다. */
21
+ const thumbSizeClass = {
22
+ sm: "size-2.5 peer-checked:translate-x-3 rtl:peer-checked:-translate-x-3",
23
+ md: "size-3.5 peer-checked:translate-x-4 rtl:peer-checked:-translate-x-4",
24
+ lg: "size-4.5 peer-checked:translate-x-5 rtl:peer-checked:-translate-x-5"
25
+ };
26
+ const trackClass = "peer m-0 shrink-0 cursor-pointer appearance-none rounded-full border border-strong bg-transparent transition-colors aria-invalid:border-danger focus-visible:focus-ring checked:enabled:border-accent checked:enabled:bg-accent-solid group-hover/switch:checked:enabled:bg-accent-solid-hover group-active/switch:checked:enabled:bg-accent-solid-active group-hover/switch:not-checked:enabled:bg-accent-subtle group-active/switch:not-checked:enabled:bg-accent-subtle-active disabled:cursor-not-allowed disabled:border-default disabled:bg-disabled";
27
+ const thumbClass = "pointer-events-none absolute start-0.75 top-0.75 rounded-full bg-current text-muted shadow-sm transition-colors motion-safe:transition-[color,background-color,translate] peer-checked:peer-enabled:text-accent-on-solid group-hover/switch:peer-enabled:peer-not-checked:text-accent group-active/switch:peer-enabled:peer-not-checked:text-accent peer-disabled:text-disabled";
28
+ const trackForcedClass = "forced-colors:forced-color-adjust-none forced-colors:[--tide-border-strong:CanvasText] forced-colors:[--tide-border-danger:CanvasText] forced-colors:[--tide-accent-border:Highlight] forced-colors:[--tide-accent-solid:Highlight] forced-colors:[--tide-accent-solid-hover:Highlight] forced-colors:[--tide-accent-solid-active:Highlight] forced-colors:[--tide-accent-subtle:Canvas] forced-colors:[--tide-accent-subtle-active:Canvas] forced-colors:[--tide-border-default:GrayText] forced-colors:[--tide-bg-disabled:Canvas] forced-colors:[--tide-outline-focus:Highlight]";
29
+ const thumbForcedClass = "forced-colors:forced-color-adjust-none forced-colors:[--tide-text-muted:CanvasText] forced-colors:[--tide-accent-text:CanvasText] forced-colors:[--tide-accent-on-solid:HighlightText] forced-colors:[--tide-text-disabled:GrayText]";
30
+ /**
31
+ * 켜고 끄는 설정을 바꾸는 스위치. 네이티브 체크박스라 form 제출이 그대로 되고, 스크린리더는 「스위치」로 읽는다.
32
+ * `children`이 label이 된다. Field 안에 두면 Field의 label·설명·오류가 연결된다.
33
+ */
34
+ function Switch({ children, size = "md", color = "primary", invalid, id, disabled, required, className, style, "aria-describedby": ariaDescribedBy, "aria-invalid": ariaInvalid, ...props }) {
35
+ const control = useFieldControl({
36
+ id,
37
+ invalid,
38
+ disabled,
39
+ required,
40
+ "aria-describedby": ariaDescribedBy,
41
+ "aria-invalid": ariaInvalid
42
+ });
43
+ return /* @__PURE__ */ jsxs("label", {
44
+ "data-color": color,
45
+ "data-disabled": control.disabled ? "" : void 0,
46
+ "data-invalid": control["data-invalid"],
47
+ style,
48
+ className: cn("group/switch inline-flex min-h-6 cursor-pointer items-center text-default select-none has-disabled:cursor-not-allowed has-disabled:text-disabled", rootSizeClass[size], className),
49
+ children: [/* @__PURE__ */ jsxs("span", {
50
+ className: "relative inline-flex shrink-0",
51
+ children: [/* @__PURE__ */ jsx("input", {
52
+ ...props,
53
+ ...control,
54
+ type: "checkbox",
55
+ role: "switch",
56
+ className: cn(trackSizeClass[size], trackClass, trackForcedClass)
57
+ }), /* @__PURE__ */ jsx("span", {
58
+ "aria-hidden": "true",
59
+ className: cn(thumbSizeClass[size], thumbClass, thumbForcedClass)
60
+ })]
61
+ }), children != null && children !== false ? /* @__PURE__ */ jsx("span", { children }) : null]
62
+ });
63
+ }
64
+
65
+ export { Switch };
@@ -0,0 +1,19 @@
1
+ import { Size } from "./types.js";
2
+ import { ComponentProps, ReactElement } from "react";
3
+ /** Textarea의 size. Input과 같이 `sm`/`md`/`lg`만 받는다. */
4
+ type TextareaSize = Extract<Size, "sm" | "md" | "lg">;
5
+ export interface TextareaProps extends ComponentProps<"textarea"> {
6
+ /** 글자 크기와 padding. 글자 크기는 같은 size의 Input과 같다(12/14/16px). 기본값은 `md`다. */
7
+ size?: TextareaSize;
8
+ /**
9
+ * 오류 상태. `aria-invalid="true"`와 `data-invalid`가 붙고 경계가 danger 색이 된다.
10
+ * Field 안에서는 Field의 값을 따르고, 여기에 준 값이 이긴다.
11
+ */
12
+ invalid?: boolean;
13
+ }
14
+ /**
15
+ * 여러 줄 텍스트를 입력하는 네이티브 `<textarea>`. 기본 3줄 높이이고 세로로만 크기를 바꿀 수 있다.
16
+ * Field 안에 두면 label, 설명, 오류 메시지가 자동으로 연결된다.
17
+ */
18
+ export declare function Textarea({ size, invalid, id, disabled, required, rows, className, "aria-describedby": ariaDescribedBy, "aria-invalid": ariaInvalid, ...props }: TextareaProps): ReactElement;
19
+ export {};
@@ -0,0 +1,36 @@
1
+ "use client";
2
+
3
+ import { cn } from "./cn.js";
4
+ import { useFieldControl } from "./field-context.js";
5
+ import { inputBaseClass } from "./input-style.js";
6
+ import { jsx } from "react/jsx-runtime";
7
+
8
+ // src/Textarea.tsx
9
+ /** 좌우·세로 padding, 텍스트 스타일 */
10
+ const sizeClass = {
11
+ sm: "px-2.5 py-1.5 text-caption",
12
+ md: "px-3 py-2 text-body-sm",
13
+ lg: "px-4 py-3 text-body-md"
14
+ };
15
+ /**
16
+ * 여러 줄 텍스트를 입력하는 네이티브 `<textarea>`. 기본 3줄 높이이고 세로로만 크기를 바꿀 수 있다.
17
+ * Field 안에 두면 label, 설명, 오류 메시지가 자동으로 연결된다.
18
+ */
19
+ function Textarea({ size = "md", invalid, id, disabled, required, rows = 3, className, "aria-describedby": ariaDescribedBy, "aria-invalid": ariaInvalid, ...props }) {
20
+ const control = useFieldControl({
21
+ id,
22
+ invalid,
23
+ disabled,
24
+ required,
25
+ "aria-describedby": ariaDescribedBy,
26
+ "aria-invalid": ariaInvalid
27
+ });
28
+ return /* @__PURE__ */ jsx("textarea", {
29
+ ...props,
30
+ ...control,
31
+ rows,
32
+ className: cn(sizeClass[size], inputBaseClass, "resize-y", className)
33
+ });
34
+ }
35
+
36
+ export { Textarea };
@@ -0,0 +1,34 @@
1
+ "use client";
2
+
3
+ import { FieldContext } from "./field-context.js";
4
+ import { createContext, use } from "react";
5
+
6
+ // src/choice-group.ts
7
+ /** CheckboxGroup 안에서만 값이 있다. */
8
+ const CheckboxGroupContext = createContext(null);
9
+ /** RadioGroup 안에서만 값이 있다. */
10
+ const RadioGroupContext = createContext(null);
11
+ /**
12
+ * Field 연결 정보와 묶음 자신의 props를 합친다. 묶음에 직접 준 값이 Field 값을 이긴다(useFieldControl과 같은 규칙).
13
+ * `aria-labelledby`를 직접 주면 Field label 대신 그것을 쓴다. `aria-describedby`는 Field 것 뒤에 이어 붙인다.
14
+ */
15
+ function useChoiceGroup(props) {
16
+ const field = use(FieldContext);
17
+ const invalid = props.invalid ?? field?.invalid ?? false;
18
+ const disabled = props.disabled ?? field?.disabled ?? false;
19
+ const required = props.required ?? field?.required ?? false;
20
+ return {
21
+ rootProps: {
22
+ id: props.id ?? field?.controlId,
23
+ "aria-labelledby": props["aria-labelledby"] ?? field?.labelId,
24
+ "aria-describedby": [field?.describedBy, props["aria-describedby"]].filter(Boolean).join(" ") || void 0,
25
+ "data-invalid": invalid ? "" : void 0,
26
+ "data-disabled": disabled ? "" : void 0
27
+ },
28
+ invalid,
29
+ disabled,
30
+ required
31
+ };
32
+ }
33
+
34
+ export { CheckboxGroupContext, RadioGroupContext, useChoiceGroup };
@@ -0,0 +1,2 @@
1
+ /** Checkbox·Radio의 size. 상자 크기와 글자 크기만 바뀌고 조작 영역은 24×24로 같다. */
2
+ export type ChoiceSize = "sm" | "md";
@@ -0,0 +1,16 @@
1
+ // src/choice-style.ts
2
+ /** 루트 label. 글자 크기 class와 사용자 className은 뒤에 붙인다. */
3
+ const choiceRootClass = "group/choice inline-flex items-start gap-2 align-top text-default cursor-pointer has-disabled:cursor-not-allowed has-disabled:text-disabled";
4
+ /** 상자와 아이콘을 겹쳐 두는 24×24 조작 영역 */
5
+ const choiceAreaClass = "grid size-6 shrink-0 place-items-center";
6
+ /** 네이티브 input을 상자로 그린다. 모양(rounded)과 크기 class는 Component가 앞에 붙인다. */
7
+ const choiceInputClass = "peer col-start-1 row-start-1 m-0 appearance-none border border-strong bg-canvas cursor-pointer transition-colors checked:border-accent checked:bg-accent-solid enabled:not-checked:not-indeterminate:group-hover/choice:bg-accent-subtle enabled:checked:group-hover/choice:bg-accent-solid-hover indeterminate:border-accent indeterminate:bg-accent-solid enabled:indeterminate:group-hover/choice:bg-accent-solid-hover enabled:not-checked:not-indeterminate:group-active/choice:bg-accent-subtle-active enabled:checked:group-active/choice:bg-accent-solid-active enabled:indeterminate:group-active/choice:bg-accent-solid-active aria-invalid:border-danger focus-visible:focus-ring disabled:cursor-not-allowed disabled:border-default disabled:bg-disabled forced-colors:forced-color-adjust-none forced-colors:[--tide-bg-canvas:Canvas] forced-colors:[--tide-border-strong:CanvasText] forced-colors:[--tide-border-danger:CanvasText] forced-colors:[--tide-accent-border:Highlight] forced-colors:[--tide-accent-solid:Highlight] forced-colors:[--tide-accent-solid-hover:Highlight] forced-colors:[--tide-accent-solid-active:Highlight] forced-colors:[--tide-accent-subtle:Canvas] forced-colors:[--tide-accent-subtle-active:Canvas] forced-colors:[--tide-border-default:GrayText] forced-colors:[--tide-bg-disabled:Canvas] forced-colors:[--tide-outline-focus:Highlight]";
8
+ /** 상자 위에 겹치는 표시 아이콘. 누름은 input이 받는다. 보이는 조건 class는 Component가 붙인다. */
9
+ const choiceMarkClass = "pointer-events-none col-start-1 row-start-1 hidden text-accent-on-solid peer-disabled:text-disabled forced-colors:forced-color-adjust-none forced-colors:[--tide-accent-on-solid:HighlightText] forced-colors:[--tide-text-disabled:GrayText]";
10
+ /** 글자 스타일. md 행간 24px은 조작 영역과 같고, sm 행간 20px은 위아래 2px을 둬 첫 줄을 상자 가운데에 맞춘다. */
11
+ const choiceTextClass = {
12
+ sm: "py-0.5 text-body-sm",
13
+ md: "text-body-md"
14
+ };
15
+
16
+ export { choiceAreaClass, choiceInputClass, choiceMarkClass, choiceRootClass, choiceTextClass };
@@ -0,0 +1,26 @@
1
+ "use client";
2
+
3
+ import { createContext, use } from "react";
4
+
5
+ // src/field-context.ts
6
+ /** Field 밖에서는 `null`이다. */
7
+ const FieldContext = createContext(null);
8
+ /**
9
+ * Field 연결 정보와 입력 Component 자신의 props를 합친다.
10
+ * 입력 Component에 직접 준 값(id, disabled 등)이 Field 값보다 이긴다. `aria-describedby`는 Field 것 뒤에 이어 붙인다.
11
+ */
12
+ function useFieldControl(props) {
13
+ const field = use(FieldContext);
14
+ const invalid = props.invalid ?? field?.invalid ?? false;
15
+ const describedBy = [field?.describedBy, props["aria-describedby"]].filter(Boolean).join(" ") || void 0;
16
+ return {
17
+ id: props.id ?? field?.controlId,
18
+ "aria-describedby": describedBy,
19
+ "aria-invalid": invalid ? true : props["aria-invalid"],
20
+ "data-invalid": invalid ? "" : void 0,
21
+ disabled: (props.disabled ?? field?.disabled) || void 0,
22
+ required: (props.required ?? field?.required) || void 0
23
+ };
24
+ }
25
+
26
+ export { FieldContext, useFieldControl };
package/dist/index.d.ts CHANGED
@@ -2,5 +2,19 @@ import { cn } from "./cn.js";
2
2
  import { VisuallyHidden, VisuallyHiddenProps } from "./VisuallyHidden.js";
3
3
  import { Typography, TypographyProps } from "./Typography.js";
4
4
  import { Spinner, SpinnerProps } from "./Spinner.js";
5
- import { Size } from "./types.js";
6
- export { type Size, Spinner, type SpinnerProps, Typography, type TypographyProps, VisuallyHidden, type VisuallyHiddenProps, cn };
5
+ import { Button, ButtonProps } from "./Button.js";
6
+ import { IconButton, IconButtonProps } from "./IconButton.js";
7
+ import { Link, LinkProps } from "./Link.js";
8
+ import { Field, FieldProps } from "./Field.js";
9
+ import { Input, InputProps } from "./Input.js";
10
+ import { Textarea, TextareaProps } from "./Textarea.js";
11
+ import { Checkbox, CheckboxProps } from "./Checkbox.js";
12
+ import { CheckboxGroup, CheckboxGroupProps, ChoiceOrientation } from "./CheckboxGroup.js";
13
+ import { Radio, RadioProps } from "./Radio.js";
14
+ import { RadioGroup, RadioGroupProps } from "./RadioGroup.js";
15
+ import { Switch, SwitchProps } from "./Switch.js";
16
+ import { Badge, BadgeProps } from "./Badge.js";
17
+ import { Card, CardProps } from "./Card.js";
18
+ import { Separator, SeparatorProps } from "./Separator.js";
19
+ import { Color, ColorFamilyName, ColorFamilyRegistry, FillVariant, Role, RoleRegistry, Size } from "./types.js";
20
+ export { Badge, type BadgeProps, Button, type ButtonProps, Card, type CardProps, Checkbox, CheckboxGroup, type CheckboxGroupProps, type CheckboxProps, type ChoiceOrientation, type Color, type ColorFamilyName, type ColorFamilyRegistry, Field, type FieldProps, type FillVariant, IconButton, type IconButtonProps, Input, type InputProps, Link, type LinkProps, Radio, RadioGroup, type RadioGroupProps, type RadioProps, type Role, type RoleRegistry, Separator, type SeparatorProps, type Size, Spinner, type SpinnerProps, Switch, type SwitchProps, Textarea, type TextareaProps, Typography, type TypographyProps, VisuallyHidden, type VisuallyHiddenProps, cn };
package/dist/index.js CHANGED
@@ -2,5 +2,19 @@ import { cn } from "./cn.js";
2
2
  import { VisuallyHidden } from "./VisuallyHidden.js";
3
3
  import { Typography } from "./Typography.js";
4
4
  import { Spinner } from "./Spinner.js";
5
+ import { Button } from "./Button.js";
6
+ import { IconButton } from "./IconButton.js";
7
+ import { Link } from "./Link.js";
8
+ import { Field } from "./Field.js";
9
+ import { Input } from "./Input.js";
10
+ import { Textarea } from "./Textarea.js";
11
+ import { Checkbox } from "./Checkbox.js";
12
+ import { CheckboxGroup } from "./CheckboxGroup.js";
13
+ import { Radio } from "./Radio.js";
14
+ import { RadioGroup } from "./RadioGroup.js";
15
+ import { Switch } from "./Switch.js";
16
+ import { Badge } from "./Badge.js";
17
+ import { Card } from "./Card.js";
18
+ import { Separator } from "./Separator.js";
5
19
 
6
- export { Spinner, Typography, VisuallyHidden, cn };
20
+ export { Badge, Button, Card, Checkbox, CheckboxGroup, Field, IconButton, Input, Link, Radio, RadioGroup, Separator, Spinner, Switch, Textarea, Typography, VisuallyHidden, cn };
@@ -0,0 +1,5 @@
1
+ // src/input-style.ts
2
+ /** 입력창의 공통 모양. size class 뒤에 넣는다. */
3
+ const inputBaseClass = "w-full min-w-0 rounded-md border border-strong bg-canvas font-normal text-default transition-colors placeholder:text-muted aria-invalid:border-danger focus:focus-field disabled:cursor-not-allowed disabled:border-default disabled:bg-disabled disabled:text-disabled disabled:placeholder:text-disabled";
4
+
5
+ export { inputBaseClass };
@@ -0,0 +1,65 @@
1
+ // src/pressable.ts
2
+ /** 기본 색(배경, 글자, 경계). 색 이름마다 Foundation이 같은 대비 규칙으로 검사한다. */
3
+ const colorClass = {
4
+ solid: "border-transparent bg-accent-solid text-accent-on-solid",
5
+ subtle: "border-transparent bg-accent-subtle text-accent",
6
+ outline: "border-accent text-accent",
7
+ ghost: "border-transparent text-accent"
8
+ };
9
+ /** hover·누름 색. 활성 상태일 때만 붙인다(비활성·로딩에는 붙이지 않아 CSS 순서에 기대지 않는다). */
10
+ const interactionClass = {
11
+ solid: "hover:bg-accent-solid-hover active:bg-accent-solid-active",
12
+ subtle: "hover:bg-accent-subtle-hover active:bg-accent-subtle-active",
13
+ outline: "hover:bg-accent-subtle-hover active:bg-accent-subtle-active",
14
+ ghost: "hover:bg-accent-subtle-hover active:bg-accent-subtle-active"
15
+ };
16
+ /** 비활성 색. 색 이름과 관계없이 같다. disabled Token은 대비 검사 대상이 아니다. */
17
+ const disabledClass = {
18
+ solid: "cursor-not-allowed border-transparent bg-disabled text-disabled",
19
+ subtle: "cursor-not-allowed border-transparent bg-disabled text-disabled",
20
+ outline: "cursor-not-allowed border-default text-disabled",
21
+ ghost: "cursor-not-allowed border-transparent text-disabled"
22
+ };
23
+ /** 모양의 공통 부분. radius는 모든 size에서 `md`다. 높이·폭·padding은 Component가 정한다. */
24
+ const pressableBaseClass = "inline-flex shrink-0 items-center justify-center rounded-md border whitespace-nowrap select-none transition-colors focus-visible:focus-ring";
25
+ const pressScaleClass = "motion-safe:transition-[color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,scale] motion-safe:active:scale-96";
26
+ /** `disabled`를 네이티브로 지원하는 요소인지. Button류가 렌더할 만한 요소 중에서는 `button`뿐이다. */
27
+ const supportsNativeDisabled = (render) => render === void 0 || render.type === "button";
28
+ /**
29
+ * 채움 방식·색·상태에서 class와 루트 props를 정한다.
30
+ * - disabled: 기본 요소와 `render={<button />}`는 네이티브 `disabled`, 그 밖의 요소는 `aria-disabled`·`data-disabled`와 클릭 차단
31
+ * - loading: 포커스를 남긴 채 `aria-disabled`·`data-loading`과 클릭 차단. disabled와 함께 오면 disabled가 이긴다
32
+ * - 커서: 활성 pointer, 로딩 wait, 비활성 not-allowed
33
+ * - hover·누름 색과 누름 축소는 활성 상태에서만 붙인다
34
+ */
35
+ function pressableState({ variant, color, disabled, loading, noPressScale, render, type, onClick, ariaDisabled }) {
36
+ const native = supportsNativeDisabled(render);
37
+ const softDisabled = disabled && !native;
38
+ const blocked = softDisabled || loading && !disabled;
39
+ const interactive = !disabled && !loading;
40
+ const showSpinner = loading && !disabled;
41
+ return {
42
+ className: [
43
+ disabled ? disabledClass[variant] : colorClass[variant],
44
+ interactive ? interactionClass[variant] : void 0,
45
+ interactive && !noPressScale ? pressScaleClass : void 0,
46
+ interactive ? "cursor-pointer" : void 0,
47
+ showSpinner ? "cursor-wait" : void 0
48
+ ].filter(Boolean).join(" "),
49
+ showSpinner,
50
+ rootProps: {
51
+ type: render === void 0 ? type ?? "button" : type,
52
+ disabled: native && disabled ? true : void 0,
53
+ "data-color": color,
54
+ "aria-disabled": blocked ? true : ariaDisabled,
55
+ "data-disabled": softDisabled ? "" : void 0,
56
+ "data-loading": showSpinner ? "" : void 0,
57
+ onClick: blocked ? (event) => {
58
+ event.preventDefault();
59
+ event.stopPropagation();
60
+ } : onClick
61
+ }
62
+ };
63
+ }
64
+
65
+ export { pressableBaseClass, pressableState };
package/dist/types.d.ts CHANGED
@@ -1,2 +1,9 @@
1
+ import { Color, ColorFamilyName, ColorFamilyRegistry, Role, RoleRegistry } from "@tide-tds/foundation";
1
2
  /** Component size. 높이는 `xs` 24px, `sm` 32px, `md` 40px, `lg` 48px다(spec size 표). */
2
- export type Size = "xs" | "sm" | "md" | "lg";
3
+ export type Size = "xs" | "sm" | "md" | "lg";
4
+ /**
5
+ * Button류의 채움 방식. `solid`·`subtle`은 Token의 강도와 이름이 같고, `outline`·`ghost`는 Component에만 있다.
6
+ * 스타일 축 이름이 `variant`라도 값 집합은 채움 방식이라 `Variant`가 아닌 이 이름을 쓴다(GLOSSARY 「채움 방식」).
7
+ */
8
+ export type FillVariant = "solid" | "subtle" | "outline" | "ghost";
9
+ export type { Color, ColorFamilyName, ColorFamilyRegistry, Role, RoleRegistry };