@uniai-fe/uds-primitives 0.4.8 → 0.5.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/README.md CHANGED
@@ -153,6 +153,9 @@ export default function Page() {
153
153
  - `Slot.Text`
154
154
  - `SlotComponentProps`
155
155
  - `SlotTextProps`
156
+ - `Switch`
157
+ - `SwitchProps`
158
+ - `SwitchSize`
156
159
  - `TabRoot`
157
160
  - `TabList`
158
161
  - `TabTrigger`
package/dist/styles.css CHANGED
@@ -691,6 +691,23 @@
691
691
  --select-multiple-chip-summary-font-weight: 400;
692
692
  /* Typography tokens */
693
693
  --select-text-font-family: var(--font-body-medium-family);
694
+ --switch-track-width-small: 48px;
695
+ --switch-track-height-small: 24px;
696
+ --switch-track-padding-small: 3px;
697
+ --switch-handle-size-small: 18px;
698
+ --switch-track-width-medium: 68px;
699
+ --switch-track-height-medium: 34px;
700
+ --switch-track-padding-medium: 2px;
701
+ --switch-handle-size-medium: 30px;
702
+ --switch-track-background-unchecked: var(--color-surface-strong);
703
+ --switch-track-background-checked: var(--color-primary-standard);
704
+ --switch-thumb-background: var(--color-surface-static-white);
705
+ --switch-thumb-shadow-unchecked:
706
+ 0px 2px 8px rgba(0, 0, 0, 0.04), 0px 0px 4px rgba(0, 0, 0, 0.08);
707
+ --switch-thumb-shadow-checked:
708
+ 0px 2px 8px rgba(0, 0, 0, 0.08), 0px 0px 4px rgba(0, 0, 0, 0.12);
709
+ --switch-focus-ring: var(--color-primary-focus);
710
+ --switch-disabled-opacity: 0.4;
694
711
  --tab-height-small: 40px;
695
712
  --tab-height-medium: 48px;
696
713
  --tab-height-large: 56px;
@@ -4419,6 +4436,64 @@ figure.chip {
4419
4436
 
4420
4437
 
4421
4438
 
4439
+ .switch {
4440
+ --switch-track-width: var(--switch-track-width-small);
4441
+ --switch-track-height: var(--switch-track-height-small);
4442
+ --switch-track-padding: var(--switch-track-padding-small);
4443
+ --switch-handle-size: var(--switch-handle-size-small);
4444
+ display: flex;
4445
+ align-items: center;
4446
+ justify-content: flex-start;
4447
+ width: var(--switch-track-width);
4448
+ min-width: var(--switch-track-width);
4449
+ height: var(--switch-track-height);
4450
+ padding: var(--switch-track-padding);
4451
+ border: none;
4452
+ border-radius: 999px;
4453
+ background-color: var(--switch-track-background-unchecked);
4454
+ cursor: pointer;
4455
+ transition: background-color 0.2s ease, box-shadow 0.2s ease, opacity 0.2s ease;
4456
+ }
4457
+
4458
+ .switch:where([data-size=medium]) {
4459
+ --switch-track-width: var(--switch-track-width-medium);
4460
+ --switch-track-height: var(--switch-track-height-medium);
4461
+ --switch-track-padding: var(--switch-track-padding-medium);
4462
+ --switch-handle-size: var(--switch-handle-size-medium);
4463
+ }
4464
+
4465
+ .switch:where([data-state=checked]) {
4466
+ background-color: var(--switch-track-background-checked);
4467
+ }
4468
+
4469
+ .switch:where(:focus-visible) {
4470
+ box-shadow: 0 0 0 2px var(--switch-focus-ring);
4471
+ }
4472
+
4473
+ .switch:where(:disabled) {
4474
+ cursor: not-allowed;
4475
+ opacity: var(--switch-disabled-opacity);
4476
+ }
4477
+
4478
+ .switch-handle {
4479
+ display: flex;
4480
+ width: var(--switch-handle-size);
4481
+ min-width: var(--switch-handle-size);
4482
+ height: var(--switch-handle-size);
4483
+ border-radius: 999px;
4484
+ background-color: var(--switch-thumb-background);
4485
+ box-shadow: var(--switch-thumb-shadow-unchecked);
4486
+ transition: transform 0.2s ease;
4487
+ }
4488
+
4489
+ .switch:where([data-state=checked]) .switch-handle {
4490
+ /* 변경: handle 이동 거리는 frame width - handle size - 양쪽 padding 합으로 고정한다. */
4491
+ box-shadow: var(--switch-thumb-shadow-checked);
4492
+ transform: translateX(calc(var(--switch-track-width) - var(--switch-handle-size) - var(--switch-track-padding) * 2));
4493
+ }
4494
+
4495
+
4496
+
4422
4497
  .tab-root {
4423
4498
  --tab-height: var(--tab-height-medium);
4424
4499
  --tab-label-font-size: var(--tab-label-font-size-medium);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniai-fe/uds-primitives",
3
- "version": "0.4.8",
3
+ "version": "0.5.0",
4
4
  "description": "UNIAI Design System; Primitives Components Package",
5
5
  "type": "module",
6
6
  "private": false,
@@ -0,0 +1 @@
1
+ @use "./styles/index.scss";
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Switch; standard toggle leaf 카테고리 배럴
3
+ * @desc
4
+ * - `Switch`: on/off 전환만 담당하는 leaf switch 컴포넌트다.
5
+ * - `SwitchProps`, `SwitchSize`: public 계약 타입이다.
6
+ */
7
+ import "./index.scss";
8
+
9
+ export * from "./markup";
10
+ export type * from "./types";
@@ -0,0 +1,76 @@
1
+ import clsx from "clsx";
2
+ import { forwardRef, useState } from "react";
3
+ import type { MouseEvent } from "react";
4
+ import type { SwitchProps } from "../types";
5
+
6
+ /**
7
+ * Switch; standard on/off toggle leaf 컴포넌트
8
+ * @component
9
+ * @param {SwitchProps} props
10
+ * @param {"small" | "medium"} [props.size="small"] Figma size 축이다.
11
+ * @param {boolean} [props.checked] 제어형 checked 상태다.
12
+ * @param {boolean} [props.defaultChecked=false] 비제어 초기 checked 상태다.
13
+ * @param {(checked: boolean, event: MouseEvent<HTMLButtonElement>) => void} [props.onCheckedChange]
14
+ * checked 상태 변경 핸들러다.
15
+ * @param {boolean} [props.disabled] 비활성 여부다.
16
+ * @param {string} [props.className] button root className override다.
17
+ * @example
18
+ * <Switch aria-label="알림 수신" defaultChecked />
19
+ */
20
+ const Switch = forwardRef<HTMLButtonElement, SwitchProps>(function Switch(
21
+ {
22
+ size = "small",
23
+ checked,
24
+ defaultChecked = false,
25
+ disabled = false,
26
+ className,
27
+ onCheckedChange,
28
+ onClick,
29
+ ...restProps
30
+ },
31
+ ref,
32
+ ) {
33
+ const [uncontrolledChecked, setUncontrolledChecked] =
34
+ useState(defaultChecked);
35
+ const isControlled = typeof checked === "boolean";
36
+ const isChecked = isControlled ? checked : uncontrolledChecked;
37
+
38
+ const onToggle = (event: MouseEvent<HTMLButtonElement>) => {
39
+ // 변경: 소비자가 defaultPrevented로 토글을 막을 수 있도록 onClick을 먼저 실행한다.
40
+ onClick?.(event);
41
+
42
+ if (event.defaultPrevented || disabled) {
43
+ return;
44
+ }
45
+
46
+ const nextChecked = !isChecked;
47
+
48
+ if (!isControlled) {
49
+ setUncontrolledChecked(nextChecked);
50
+ }
51
+
52
+ onCheckedChange?.(nextChecked, event);
53
+ };
54
+
55
+ return (
56
+ <button
57
+ {...restProps}
58
+ ref={ref}
59
+ type="button"
60
+ role="switch"
61
+ aria-checked={isChecked}
62
+ disabled={disabled}
63
+ className={clsx("switch", className)}
64
+ data-size={size}
65
+ data-state={isChecked ? "checked" : "unchecked"}
66
+ onClick={onToggle}
67
+ >
68
+ {/* 변경: standard variant는 handle만 렌더링하고, with text는 후속 확장 범위로 보류한다. */}
69
+ <span className="switch-handle" aria-hidden="true" />
70
+ </button>
71
+ );
72
+ });
73
+
74
+ Switch.displayName = "Switch";
75
+
76
+ export { Switch };
@@ -0,0 +1 @@
1
+ export { Switch } from "./Switch";
@@ -0,0 +1,2 @@
1
+ @use "./variables.scss";
2
+ @use "./switch.scss";
@@ -0,0 +1,63 @@
1
+ .switch {
2
+ --switch-track-width: var(--switch-track-width-small);
3
+ --switch-track-height: var(--switch-track-height-small);
4
+ --switch-track-padding: var(--switch-track-padding-small);
5
+ --switch-handle-size: var(--switch-handle-size-small);
6
+ display: flex;
7
+ align-items: center;
8
+ justify-content: flex-start;
9
+ width: var(--switch-track-width);
10
+ min-width: var(--switch-track-width);
11
+ height: var(--switch-track-height);
12
+ padding: var(--switch-track-padding);
13
+ border: none;
14
+ border-radius: 999px;
15
+ background-color: var(--switch-track-background-unchecked);
16
+ cursor: pointer;
17
+ transition:
18
+ background-color 0.2s ease,
19
+ box-shadow 0.2s ease,
20
+ opacity 0.2s ease;
21
+ }
22
+
23
+ .switch:where([data-size="medium"]) {
24
+ --switch-track-width: var(--switch-track-width-medium);
25
+ --switch-track-height: var(--switch-track-height-medium);
26
+ --switch-track-padding: var(--switch-track-padding-medium);
27
+ --switch-handle-size: var(--switch-handle-size-medium);
28
+ }
29
+
30
+ .switch:where([data-state="checked"]) {
31
+ background-color: var(--switch-track-background-checked);
32
+ }
33
+
34
+ .switch:where(:focus-visible) {
35
+ box-shadow: 0 0 0 2px var(--switch-focus-ring);
36
+ }
37
+
38
+ .switch:where(:disabled) {
39
+ cursor: not-allowed;
40
+ opacity: var(--switch-disabled-opacity);
41
+ }
42
+
43
+ .switch-handle {
44
+ display: flex;
45
+ width: var(--switch-handle-size);
46
+ min-width: var(--switch-handle-size);
47
+ height: var(--switch-handle-size);
48
+ border-radius: 999px;
49
+ background-color: var(--switch-thumb-background);
50
+ box-shadow: var(--switch-thumb-shadow-unchecked);
51
+ transition: transform 0.2s ease;
52
+ }
53
+
54
+ .switch:where([data-state="checked"]) .switch-handle {
55
+ /* 변경: handle 이동 거리는 frame width - handle size - 양쪽 padding 합으로 고정한다. */
56
+ box-shadow: var(--switch-thumb-shadow-checked);
57
+ transform: translateX(
58
+ calc(
59
+ var(--switch-track-width) - var(--switch-handle-size) -
60
+ (var(--switch-track-padding) * 2)
61
+ )
62
+ );
63
+ }
@@ -0,0 +1,21 @@
1
+ :root {
2
+ --switch-track-width-small: 48px;
3
+ --switch-track-height-small: 24px;
4
+ --switch-track-padding-small: 3px;
5
+ --switch-handle-size-small: 18px;
6
+
7
+ --switch-track-width-medium: 68px;
8
+ --switch-track-height-medium: 34px;
9
+ --switch-track-padding-medium: 2px;
10
+ --switch-handle-size-medium: 30px;
11
+
12
+ --switch-track-background-unchecked: var(--color-surface-strong);
13
+ --switch-track-background-checked: var(--color-primary-standard);
14
+ --switch-thumb-background: var(--color-surface-static-white);
15
+ --switch-thumb-shadow-unchecked:
16
+ 0px 2px 8px rgba(0, 0, 0, 0.04), 0px 0px 4px rgba(0, 0, 0, 0.08);
17
+ --switch-thumb-shadow-checked:
18
+ 0px 2px 8px rgba(0, 0, 0, 0.08), 0px 0px 4px rgba(0, 0, 0, 0.12);
19
+ --switch-focus-ring: var(--color-primary-focus);
20
+ --switch-disabled-opacity: 0.4;
21
+ }
@@ -0,0 +1 @@
1
+ export type * from "./switch";
@@ -0,0 +1,46 @@
1
+ import type { ComponentPropsWithoutRef, MouseEvent } from "react";
2
+
3
+ type NativeButtonProps = Omit<
4
+ ComponentPropsWithoutRef<"button">,
5
+ "aria-checked" | "children" | "onChange" | "role" | "type"
6
+ >;
7
+
8
+ /**
9
+ * Switch size; standard switch size 축
10
+ * @typedef {"small" | "medium"} SwitchSize
11
+ */
12
+ export type SwitchSize = "small" | "medium";
13
+
14
+ /**
15
+ * Switch props; standard toggle leaf public props
16
+ * @property {"small" | "medium"} [size] Figma size 축이다.
17
+ * @property {boolean} [checked] 제어형 checked 상태다.
18
+ * @property {boolean} [defaultChecked] 비제어 초기 checked 상태다.
19
+ * @property {(checked: boolean, event: MouseEvent<HTMLButtonElement>) => void} [onCheckedChange] checked 상태 변경 핸들러다.
20
+ * @property {boolean} [disabled] 비활성 여부다.
21
+ */
22
+ export interface SwitchProps extends NativeButtonProps {
23
+ /**
24
+ * Figma size 축이다.
25
+ */
26
+ size?: SwitchSize;
27
+ /**
28
+ * 제어형 checked 상태다.
29
+ */
30
+ checked?: boolean;
31
+ /**
32
+ * 비제어 초기 checked 상태다.
33
+ */
34
+ defaultChecked?: boolean;
35
+ /**
36
+ * checked 상태 변경 핸들러다.
37
+ */
38
+ onCheckedChange?: (
39
+ checked: boolean,
40
+ event: MouseEvent<HTMLButtonElement>,
41
+ ) => void;
42
+ /**
43
+ * 비활성 여부다.
44
+ */
45
+ disabled?: boolean;
46
+ }
package/src/index.scss CHANGED
@@ -19,6 +19,7 @@
19
19
  @use "./components/segmented-control";
20
20
  @use "./components/select";
21
21
  @use "./components/spinner";
22
+ @use "./components/switch";
22
23
  @use "./components/tab";
23
24
  @use "./components/table";
24
25
  @use "./components/tooltip";
package/src/index.tsx CHANGED
@@ -25,6 +25,7 @@ export * from "./components/segmented-control";
25
25
  export * from "./components/select";
26
26
  export * from "./components/slot";
27
27
  export * from "./components/spinner";
28
+ export * from "./components/switch";
28
29
  export * from "./components/tab";
29
30
  export * from "./components/table";
30
31
  export * from "./components/tooltip";