@nurimedia/react-ui 0.1.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/LICENSE +21 -0
- package/README.md +274 -0
- package/dist/AlertDialog/index.cjs +185 -0
- package/dist/AlertDialog/index.cjs.map +1 -0
- package/dist/AlertDialog/index.d.cts +49 -0
- package/dist/AlertDialog/index.d.ts +49 -0
- package/dist/AlertDialog/index.js +148 -0
- package/dist/AlertDialog/index.js.map +1 -0
- package/dist/AlertDialog/styles.css +187 -0
- package/dist/Button/index.cjs +136 -0
- package/dist/Button/index.cjs.map +1 -0
- package/dist/Button/index.d.cts +65 -0
- package/dist/Button/index.d.ts +65 -0
- package/dist/Button/index.js +109 -0
- package/dist/Button/index.js.map +1 -0
- package/dist/Button/styles.css +181 -0
- package/dist/Checkbox/index.cjs +83 -0
- package/dist/Checkbox/index.cjs.map +1 -0
- package/dist/Checkbox/index.d.cts +40 -0
- package/dist/Checkbox/index.d.ts +40 -0
- package/dist/Checkbox/index.js +46 -0
- package/dist/Checkbox/index.js.map +1 -0
- package/dist/Checkbox/styles.css +78 -0
- package/dist/Input/index.cjs +103 -0
- package/dist/Input/index.cjs.map +1 -0
- package/dist/Input/index.d.cts +53 -0
- package/dist/Input/index.d.ts +53 -0
- package/dist/Input/index.js +76 -0
- package/dist/Input/index.js.map +1 -0
- package/dist/Input/styles.css +135 -0
- package/dist/Modal/index.cjs +120 -0
- package/dist/Modal/index.cjs.map +1 -0
- package/dist/Modal/index.d.cts +62 -0
- package/dist/Modal/index.d.ts +62 -0
- package/dist/Modal/index.js +83 -0
- package/dist/Modal/index.js.map +1 -0
- package/dist/Modal/styles.css +229 -0
- package/dist/Radio/index.cjs +86 -0
- package/dist/Radio/index.cjs.map +1 -0
- package/dist/Radio/index.d.cts +46 -0
- package/dist/Radio/index.d.ts +46 -0
- package/dist/Radio/index.js +48 -0
- package/dist/Radio/index.js.map +1 -0
- package/dist/Radio/styles.css +76 -0
- package/dist/Select/index.cjs +116 -0
- package/dist/Select/index.cjs.map +1 -0
- package/dist/Select/index.d.cts +61 -0
- package/dist/Select/index.d.ts +61 -0
- package/dist/Select/index.js +79 -0
- package/dist/Select/index.js.map +1 -0
- package/dist/Select/styles.css +157 -0
- package/dist/Switch/index.cjs +71 -0
- package/dist/Switch/index.cjs.map +1 -0
- package/dist/Switch/index.d.cts +34 -0
- package/dist/Switch/index.d.ts +34 -0
- package/dist/Switch/index.js +34 -0
- package/dist/Switch/index.js.map +1 -0
- package/dist/Switch/styles.css +71 -0
- package/dist/Textarea/index.cjs +127 -0
- package/dist/Textarea/index.cjs.map +1 -0
- package/dist/Textarea/index.d.cts +48 -0
- package/dist/Textarea/index.d.ts +48 -0
- package/dist/Textarea/index.js +100 -0
- package/dist/Textarea/index.js.map +1 -0
- package/dist/Textarea/styles.css +95 -0
- package/dist/Toast/index.cjs +184 -0
- package/dist/Toast/index.cjs.map +1 -0
- package/dist/Toast/index.d.cts +67 -0
- package/dist/Toast/index.d.ts +67 -0
- package/dist/Toast/index.js +146 -0
- package/dist/Toast/index.js.map +1 -0
- package/dist/Toast/styles.css +122 -0
- package/dist/Tooltip/index.cjs +105 -0
- package/dist/Tooltip/index.cjs.map +1 -0
- package/dist/Tooltip/index.d.cts +65 -0
- package/dist/Tooltip/index.d.ts +65 -0
- package/dist/Tooltip/index.js +67 -0
- package/dist/Tooltip/index.js.map +1 -0
- package/dist/Tooltip/styles.css +42 -0
- package/dist/index.cjs +900 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +16 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +850 -0
- package/dist/index.js.map +1 -0
- package/dist/styles.css +11 -0
- package/dist/tokens.css +232 -0
- package/dist/typography.css +104 -0
- package/llms.txt +369 -0
- package/package.json +183 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/* 보더는 상태와 무관하게 #818181 고정, 선택 시 가운데 브랜드 점만 생김 (디자인 확인: 사용자 실측 2026-07) */
|
|
2
|
+
.nuri-radio-group {
|
|
3
|
+
display: flex;
|
|
4
|
+
gap: 12px;
|
|
5
|
+
}
|
|
6
|
+
.nuri-radio-group--vertical {
|
|
7
|
+
flex-direction: column;
|
|
8
|
+
}
|
|
9
|
+
.nuri-radio-group--horizontal {
|
|
10
|
+
flex-direction: row;
|
|
11
|
+
flex-wrap: wrap;
|
|
12
|
+
column-gap: 20px;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.nuri-radio {
|
|
16
|
+
display: inline-flex;
|
|
17
|
+
align-items: center;
|
|
18
|
+
gap: var(--nuri-control-gap);
|
|
19
|
+
font-family: inherit;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.nuri-radio__control {
|
|
23
|
+
box-sizing: border-box;
|
|
24
|
+
display: inline-flex;
|
|
25
|
+
align-items: center;
|
|
26
|
+
justify-content: center;
|
|
27
|
+
flex-shrink: 0;
|
|
28
|
+
width: var(--nuri-control-radio-size);
|
|
29
|
+
height: var(--nuri-control-radio-size);
|
|
30
|
+
padding: 0;
|
|
31
|
+
background-color: var(--nuri-control-bg);
|
|
32
|
+
border: 1px solid var(--nuri-control-radio-border);
|
|
33
|
+
border-radius: 50%;
|
|
34
|
+
cursor: pointer;
|
|
35
|
+
transition:
|
|
36
|
+
background-color 120ms ease,
|
|
37
|
+
border-color 120ms ease;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* 선택돼도 보더는 변하지 않는다 — 안쪽 점이 유일한 상태 표시 */
|
|
41
|
+
|
|
42
|
+
.nuri-radio__indicator {
|
|
43
|
+
display: block;
|
|
44
|
+
width: var(--nuri-control-radio-dot-size);
|
|
45
|
+
height: var(--nuri-control-radio-dot-size);
|
|
46
|
+
border-radius: 50%;
|
|
47
|
+
/* 가운데 점 = 브랜드 색 */
|
|
48
|
+
background-color: var(--nuri-control-bg-checked, var(--nuri-color-brand-default));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.nuri-radio__control:focus-visible {
|
|
52
|
+
outline: var(--nuri-button-focus-ring-width) solid
|
|
53
|
+
var(--nuri-button-focus-ring-color, var(--nuri-color-brand-default));
|
|
54
|
+
outline-offset: var(--nuri-button-focus-ring-offset);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.nuri-radio__control:disabled {
|
|
58
|
+
background-color: var(--nuri-control-bg-disabled);
|
|
59
|
+
border-color: var(--nuri-control-border-disabled);
|
|
60
|
+
cursor: not-allowed;
|
|
61
|
+
}
|
|
62
|
+
.nuri-radio__control:disabled .nuri-radio__indicator {
|
|
63
|
+
background-color: var(--nuri-control-border-disabled);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.nuri-radio__label {
|
|
67
|
+
font-size: var(--nuri-control-label-size);
|
|
68
|
+
line-height: var(--nuri-control-line-height);
|
|
69
|
+
color: var(--nuri-control-label-color);
|
|
70
|
+
cursor: pointer;
|
|
71
|
+
user-select: none;
|
|
72
|
+
}
|
|
73
|
+
.nuri-radio:has(.nuri-radio__control:disabled) .nuri-radio__label {
|
|
74
|
+
color: var(--nuri-control-label-disabled);
|
|
75
|
+
cursor: not-allowed;
|
|
76
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/Select/index.ts
|
|
31
|
+
var Select_exports = {};
|
|
32
|
+
__export(Select_exports, {
|
|
33
|
+
Select: () => Select
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(Select_exports);
|
|
36
|
+
|
|
37
|
+
// src/Select/Select.tsx
|
|
38
|
+
var import_styles = require("./styles.css");
|
|
39
|
+
var RadixSelect = __toESM(require("@radix-ui/react-select"), 1);
|
|
40
|
+
var import_react = require("react");
|
|
41
|
+
|
|
42
|
+
// src/utils/cx.ts
|
|
43
|
+
function cx(...classes) {
|
|
44
|
+
return classes.filter(Boolean).join(" ");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// src/Select/Select.tsx
|
|
48
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
49
|
+
var CaretIcon = () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { width: "8", height: "8", viewBox: "0 0 8 8", fill: "none", "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M1.1 2.5h5.8L4 5.9z", fill: "currentColor" }) });
|
|
50
|
+
var Select = (0, import_react.forwardRef)(function Select2({
|
|
51
|
+
options,
|
|
52
|
+
size = "md",
|
|
53
|
+
placeholder,
|
|
54
|
+
value,
|
|
55
|
+
defaultValue,
|
|
56
|
+
onValueChange,
|
|
57
|
+
open,
|
|
58
|
+
defaultOpen,
|
|
59
|
+
onOpenChange,
|
|
60
|
+
disabled = false,
|
|
61
|
+
block = false,
|
|
62
|
+
name,
|
|
63
|
+
required,
|
|
64
|
+
container,
|
|
65
|
+
className,
|
|
66
|
+
"aria-label": ariaLabel
|
|
67
|
+
}, ref) {
|
|
68
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
69
|
+
RadixSelect.Root,
|
|
70
|
+
{
|
|
71
|
+
value,
|
|
72
|
+
defaultValue,
|
|
73
|
+
onValueChange,
|
|
74
|
+
open,
|
|
75
|
+
defaultOpen,
|
|
76
|
+
onOpenChange,
|
|
77
|
+
disabled,
|
|
78
|
+
name,
|
|
79
|
+
required,
|
|
80
|
+
children: [
|
|
81
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
82
|
+
RadixSelect.Trigger,
|
|
83
|
+
{
|
|
84
|
+
ref,
|
|
85
|
+
"aria-label": ariaLabel,
|
|
86
|
+
className: cx(
|
|
87
|
+
"nuri-select__trigger",
|
|
88
|
+
`nuri-select__trigger--${size}`,
|
|
89
|
+
block && "nuri-select__trigger--block",
|
|
90
|
+
className
|
|
91
|
+
),
|
|
92
|
+
children: [
|
|
93
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "nuri-select__value", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RadixSelect.Value, { placeholder }) }),
|
|
94
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(RadixSelect.Icon, { className: "nuri-select__caret", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CaretIcon, {}) })
|
|
95
|
+
]
|
|
96
|
+
}
|
|
97
|
+
),
|
|
98
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(RadixSelect.Portal, { container, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RadixSelect.Content, { className: "nuri-select__content", position: "popper", sideOffset: 6, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RadixSelect.Viewport, { className: "nuri-select__viewport", children: options.map((option) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
99
|
+
RadixSelect.Item,
|
|
100
|
+
{
|
|
101
|
+
value: option.value,
|
|
102
|
+
disabled: option.disabled,
|
|
103
|
+
className: "nuri-select__item",
|
|
104
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RadixSelect.ItemText, { children: option.label })
|
|
105
|
+
},
|
|
106
|
+
option.value
|
|
107
|
+
)) }) }) })
|
|
108
|
+
]
|
|
109
|
+
}
|
|
110
|
+
);
|
|
111
|
+
});
|
|
112
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
113
|
+
0 && (module.exports = {
|
|
114
|
+
Select
|
|
115
|
+
});
|
|
116
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/Select/index.ts","../../src/Select/Select.tsx","../../src/utils/cx.ts"],"sourcesContent":["export { Select } from './Select.js';\nexport type { SelectProps, SelectOption } from './Select.js';\n","'use client';\n\nimport './styles.css';\nimport * as RadixSelect from '@radix-ui/react-select';\nimport { forwardRef } from 'react';\nimport type { ReactNode } from 'react';\nimport { cx } from '../utils/cx.js';\n\ntype Size = 'sm' | 'md';\n\nexport interface SelectOption {\n value: string;\n label: ReactNode;\n disabled?: boolean;\n}\n\nexport interface SelectProps {\n /** 선택지 목록. */\n options: SelectOption[];\n /**\n * 사이즈. sm 36 / md 42 px 높이 (디자인 시스템의 Small/Large).\n * @default 'md'\n */\n size?: Size;\n /** 값이 없을 때 트리거에 표시할 문구. */\n placeholder?: ReactNode;\n /** 선택된 값 (제어형). */\n value?: string;\n /** 초기 값 (비제어형). */\n defaultValue?: string;\n onValueChange?: (value: string) => void;\n /** 열림 상태 제어 (선택). */\n open?: boolean;\n defaultOpen?: boolean;\n onOpenChange?: (open: boolean) => void;\n disabled?: boolean;\n /** 컨테이너 전체 너비로 확장. @default false */\n block?: boolean;\n /** 폼 전송용 name — 네이티브 select처럼 폼에 참여. */\n name?: string;\n required?: boolean;\n /** 포털 렌더 대상. 기본 document.body. */\n container?: HTMLElement | null;\n /** 트리거에 추가할 className. */\n className?: string;\n /** 시각 라벨이 없을 때 접근 가능한 이름. */\n 'aria-label'?: string;\n}\n\n/** 디자인 시스템의 8px 삼각 캐럿 (열림 시 CSS로 회전). */\nconst CaretIcon = () => (\n <svg width=\"8\" height=\"8\" viewBox=\"0 0 8 8\" fill=\"none\" aria-hidden=\"true\">\n <path d=\"M1.1 2.5h5.8L4 5.9z\" fill=\"currentColor\" />\n </svg>\n);\n\n/**\n * 셀렉트 박스 (단일 선택). Radix Select 기반 — 키보드 탐색·타입어헤드·\n * 포지셔닝·aria 연결을 자동 제공. 옵션 목록은 `options` 배열로 전달한다.\n *\n * 다중 선택(체크박스 옵션)·서브메뉴 변형은 별도 컴포넌트(DropdownMenu) 범위.\n *\n * @example\n * <Select\n * aria-label=\"정렬\"\n * placeholder=\"정렬 선택\"\n * options={[\n * { value: 'latest', label: '최신순' },\n * { value: 'edited', label: '수정순' },\n * ]}\n * onValueChange={setSort}\n * />\n */\nexport const Select = forwardRef<HTMLButtonElement, SelectProps>(function Select(\n {\n options,\n size = 'md',\n placeholder,\n value,\n defaultValue,\n onValueChange,\n open,\n defaultOpen,\n onOpenChange,\n disabled = false,\n block = false,\n name,\n required,\n container,\n className,\n 'aria-label': ariaLabel,\n },\n ref,\n) {\n return (\n <RadixSelect.Root\n value={value}\n defaultValue={defaultValue}\n onValueChange={onValueChange}\n open={open}\n defaultOpen={defaultOpen}\n onOpenChange={onOpenChange}\n disabled={disabled}\n name={name}\n required={required}\n >\n <RadixSelect.Trigger\n ref={ref}\n aria-label={ariaLabel}\n className={cx(\n 'nuri-select__trigger',\n `nuri-select__trigger--${size}`,\n block && 'nuri-select__trigger--block',\n className,\n )}\n >\n <span className=\"nuri-select__value\">\n <RadixSelect.Value placeholder={placeholder} />\n </span>\n <RadixSelect.Icon className=\"nuri-select__caret\">\n <CaretIcon />\n </RadixSelect.Icon>\n </RadixSelect.Trigger>\n\n <RadixSelect.Portal container={container}>\n <RadixSelect.Content className=\"nuri-select__content\" position=\"popper\" sideOffset={6}>\n <RadixSelect.Viewport className=\"nuri-select__viewport\">\n {options.map((option) => (\n <RadixSelect.Item\n key={option.value}\n value={option.value}\n disabled={option.disabled}\n className=\"nuri-select__item\"\n >\n <RadixSelect.ItemText>{option.label}</RadixSelect.ItemText>\n </RadixSelect.Item>\n ))}\n </RadixSelect.Viewport>\n </RadixSelect.Content>\n </RadixSelect.Portal>\n </RadixSelect.Root>\n );\n});\n","/**\n * 조건부 className 합성 유틸. falsy 값(undefined/false/null/'')은 제거.\n * @example cx('btn', isActive && 'btn--active', className)\n */\nexport function cx(...classes: Array<string | false | null | undefined>): string {\n return classes.filter(Boolean).join(' ');\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACEA,oBAAO;AACP,kBAA6B;AAC7B,mBAA2B;;;ACApB,SAAS,MAAM,SAA2D;AAC/E,SAAO,QAAQ,OAAO,OAAO,EAAE,KAAK,GAAG;AACzC;;;AD8CI;AAFJ,IAAM,YAAY,MAChB,4CAAC,SAAI,OAAM,KAAI,QAAO,KAAI,SAAQ,WAAU,MAAK,QAAO,eAAY,QAClE,sDAAC,UAAK,GAAE,uBAAsB,MAAK,gBAAe,GACpD;AAoBK,IAAM,aAAS,yBAA2C,SAASA,QACxE;AAAA,EACE;AAAA,EACA,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,cAAc;AAChB,GACA,KACA;AACA,SACE;AAAA,IAAa;AAAA,IAAZ;AAAA,MACC;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MAEA;AAAA;AAAA,UAAa;AAAA,UAAZ;AAAA,YACC;AAAA,YACA,cAAY;AAAA,YACZ,WAAW;AAAA,cACT;AAAA,cACA,yBAAyB,IAAI;AAAA,cAC7B,SAAS;AAAA,cACT;AAAA,YACF;AAAA,YAEA;AAAA,0DAAC,UAAK,WAAU,sBACd,sDAAa,mBAAZ,EAAkB,aAA0B,GAC/C;AAAA,cACA,4CAAa,kBAAZ,EAAiB,WAAU,sBAC1B,sDAAC,aAAU,GACb;AAAA;AAAA;AAAA,QACF;AAAA,QAEA,4CAAa,oBAAZ,EAAmB,WAClB,sDAAa,qBAAZ,EAAoB,WAAU,wBAAuB,UAAS,UAAS,YAAY,GAClF,sDAAa,sBAAZ,EAAqB,WAAU,yBAC7B,kBAAQ,IAAI,CAAC,WACZ;AAAA,UAAa;AAAA,UAAZ;AAAA,YAEC,OAAO,OAAO;AAAA,YACd,UAAU,OAAO;AAAA,YACjB,WAAU;AAAA,YAEV,sDAAa,sBAAZ,EAAsB,iBAAO,OAAM;AAAA;AAAA,UAL/B,OAAO;AAAA,QAMd,CACD,GACH,GACF,GACF;AAAA;AAAA;AAAA,EACF;AAEJ,CAAC;","names":["Select"]}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
type Size = 'sm' | 'md';
|
|
5
|
+
interface SelectOption {
|
|
6
|
+
value: string;
|
|
7
|
+
label: ReactNode;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
}
|
|
10
|
+
interface SelectProps {
|
|
11
|
+
/** 선택지 목록. */
|
|
12
|
+
options: SelectOption[];
|
|
13
|
+
/**
|
|
14
|
+
* 사이즈. sm 36 / md 42 px 높이 (디자인 시스템의 Small/Large).
|
|
15
|
+
* @default 'md'
|
|
16
|
+
*/
|
|
17
|
+
size?: Size;
|
|
18
|
+
/** 값이 없을 때 트리거에 표시할 문구. */
|
|
19
|
+
placeholder?: ReactNode;
|
|
20
|
+
/** 선택된 값 (제어형). */
|
|
21
|
+
value?: string;
|
|
22
|
+
/** 초기 값 (비제어형). */
|
|
23
|
+
defaultValue?: string;
|
|
24
|
+
onValueChange?: (value: string) => void;
|
|
25
|
+
/** 열림 상태 제어 (선택). */
|
|
26
|
+
open?: boolean;
|
|
27
|
+
defaultOpen?: boolean;
|
|
28
|
+
onOpenChange?: (open: boolean) => void;
|
|
29
|
+
disabled?: boolean;
|
|
30
|
+
/** 컨테이너 전체 너비로 확장. @default false */
|
|
31
|
+
block?: boolean;
|
|
32
|
+
/** 폼 전송용 name — 네이티브 select처럼 폼에 참여. */
|
|
33
|
+
name?: string;
|
|
34
|
+
required?: boolean;
|
|
35
|
+
/** 포털 렌더 대상. 기본 document.body. */
|
|
36
|
+
container?: HTMLElement | null;
|
|
37
|
+
/** 트리거에 추가할 className. */
|
|
38
|
+
className?: string;
|
|
39
|
+
/** 시각 라벨이 없을 때 접근 가능한 이름. */
|
|
40
|
+
'aria-label'?: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 셀렉트 박스 (단일 선택). Radix Select 기반 — 키보드 탐색·타입어헤드·
|
|
44
|
+
* 포지셔닝·aria 연결을 자동 제공. 옵션 목록은 `options` 배열로 전달한다.
|
|
45
|
+
*
|
|
46
|
+
* 다중 선택(체크박스 옵션)·서브메뉴 변형은 별도 컴포넌트(DropdownMenu) 범위.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* <Select
|
|
50
|
+
* aria-label="정렬"
|
|
51
|
+
* placeholder="정렬 선택"
|
|
52
|
+
* options={[
|
|
53
|
+
* { value: 'latest', label: '최신순' },
|
|
54
|
+
* { value: 'edited', label: '수정순' },
|
|
55
|
+
* ]}
|
|
56
|
+
* onValueChange={setSort}
|
|
57
|
+
* />
|
|
58
|
+
*/
|
|
59
|
+
declare const Select: react.ForwardRefExoticComponent<SelectProps & react.RefAttributes<HTMLButtonElement>>;
|
|
60
|
+
|
|
61
|
+
export { Select, type SelectOption, type SelectProps };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
type Size = 'sm' | 'md';
|
|
5
|
+
interface SelectOption {
|
|
6
|
+
value: string;
|
|
7
|
+
label: ReactNode;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
}
|
|
10
|
+
interface SelectProps {
|
|
11
|
+
/** 선택지 목록. */
|
|
12
|
+
options: SelectOption[];
|
|
13
|
+
/**
|
|
14
|
+
* 사이즈. sm 36 / md 42 px 높이 (디자인 시스템의 Small/Large).
|
|
15
|
+
* @default 'md'
|
|
16
|
+
*/
|
|
17
|
+
size?: Size;
|
|
18
|
+
/** 값이 없을 때 트리거에 표시할 문구. */
|
|
19
|
+
placeholder?: ReactNode;
|
|
20
|
+
/** 선택된 값 (제어형). */
|
|
21
|
+
value?: string;
|
|
22
|
+
/** 초기 값 (비제어형). */
|
|
23
|
+
defaultValue?: string;
|
|
24
|
+
onValueChange?: (value: string) => void;
|
|
25
|
+
/** 열림 상태 제어 (선택). */
|
|
26
|
+
open?: boolean;
|
|
27
|
+
defaultOpen?: boolean;
|
|
28
|
+
onOpenChange?: (open: boolean) => void;
|
|
29
|
+
disabled?: boolean;
|
|
30
|
+
/** 컨테이너 전체 너비로 확장. @default false */
|
|
31
|
+
block?: boolean;
|
|
32
|
+
/** 폼 전송용 name — 네이티브 select처럼 폼에 참여. */
|
|
33
|
+
name?: string;
|
|
34
|
+
required?: boolean;
|
|
35
|
+
/** 포털 렌더 대상. 기본 document.body. */
|
|
36
|
+
container?: HTMLElement | null;
|
|
37
|
+
/** 트리거에 추가할 className. */
|
|
38
|
+
className?: string;
|
|
39
|
+
/** 시각 라벨이 없을 때 접근 가능한 이름. */
|
|
40
|
+
'aria-label'?: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 셀렉트 박스 (단일 선택). Radix Select 기반 — 키보드 탐색·타입어헤드·
|
|
44
|
+
* 포지셔닝·aria 연결을 자동 제공. 옵션 목록은 `options` 배열로 전달한다.
|
|
45
|
+
*
|
|
46
|
+
* 다중 선택(체크박스 옵션)·서브메뉴 변형은 별도 컴포넌트(DropdownMenu) 범위.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* <Select
|
|
50
|
+
* aria-label="정렬"
|
|
51
|
+
* placeholder="정렬 선택"
|
|
52
|
+
* options={[
|
|
53
|
+
* { value: 'latest', label: '최신순' },
|
|
54
|
+
* { value: 'edited', label: '수정순' },
|
|
55
|
+
* ]}
|
|
56
|
+
* onValueChange={setSort}
|
|
57
|
+
* />
|
|
58
|
+
*/
|
|
59
|
+
declare const Select: react.ForwardRefExoticComponent<SelectProps & react.RefAttributes<HTMLButtonElement>>;
|
|
60
|
+
|
|
61
|
+
export { Select, type SelectOption, type SelectProps };
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// src/Select/Select.tsx
|
|
2
|
+
import "./styles.css";
|
|
3
|
+
import * as RadixSelect from "@radix-ui/react-select";
|
|
4
|
+
import { forwardRef } from "react";
|
|
5
|
+
|
|
6
|
+
// src/utils/cx.ts
|
|
7
|
+
function cx(...classes) {
|
|
8
|
+
return classes.filter(Boolean).join(" ");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// src/Select/Select.tsx
|
|
12
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
13
|
+
var CaretIcon = () => /* @__PURE__ */ jsx("svg", { width: "8", height: "8", viewBox: "0 0 8 8", fill: "none", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M1.1 2.5h5.8L4 5.9z", fill: "currentColor" }) });
|
|
14
|
+
var Select = forwardRef(function Select2({
|
|
15
|
+
options,
|
|
16
|
+
size = "md",
|
|
17
|
+
placeholder,
|
|
18
|
+
value,
|
|
19
|
+
defaultValue,
|
|
20
|
+
onValueChange,
|
|
21
|
+
open,
|
|
22
|
+
defaultOpen,
|
|
23
|
+
onOpenChange,
|
|
24
|
+
disabled = false,
|
|
25
|
+
block = false,
|
|
26
|
+
name,
|
|
27
|
+
required,
|
|
28
|
+
container,
|
|
29
|
+
className,
|
|
30
|
+
"aria-label": ariaLabel
|
|
31
|
+
}, ref) {
|
|
32
|
+
return /* @__PURE__ */ jsxs(
|
|
33
|
+
RadixSelect.Root,
|
|
34
|
+
{
|
|
35
|
+
value,
|
|
36
|
+
defaultValue,
|
|
37
|
+
onValueChange,
|
|
38
|
+
open,
|
|
39
|
+
defaultOpen,
|
|
40
|
+
onOpenChange,
|
|
41
|
+
disabled,
|
|
42
|
+
name,
|
|
43
|
+
required,
|
|
44
|
+
children: [
|
|
45
|
+
/* @__PURE__ */ jsxs(
|
|
46
|
+
RadixSelect.Trigger,
|
|
47
|
+
{
|
|
48
|
+
ref,
|
|
49
|
+
"aria-label": ariaLabel,
|
|
50
|
+
className: cx(
|
|
51
|
+
"nuri-select__trigger",
|
|
52
|
+
`nuri-select__trigger--${size}`,
|
|
53
|
+
block && "nuri-select__trigger--block",
|
|
54
|
+
className
|
|
55
|
+
),
|
|
56
|
+
children: [
|
|
57
|
+
/* @__PURE__ */ jsx("span", { className: "nuri-select__value", children: /* @__PURE__ */ jsx(RadixSelect.Value, { placeholder }) }),
|
|
58
|
+
/* @__PURE__ */ jsx(RadixSelect.Icon, { className: "nuri-select__caret", children: /* @__PURE__ */ jsx(CaretIcon, {}) })
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
),
|
|
62
|
+
/* @__PURE__ */ jsx(RadixSelect.Portal, { container, children: /* @__PURE__ */ jsx(RadixSelect.Content, { className: "nuri-select__content", position: "popper", sideOffset: 6, children: /* @__PURE__ */ jsx(RadixSelect.Viewport, { className: "nuri-select__viewport", children: options.map((option) => /* @__PURE__ */ jsx(
|
|
63
|
+
RadixSelect.Item,
|
|
64
|
+
{
|
|
65
|
+
value: option.value,
|
|
66
|
+
disabled: option.disabled,
|
|
67
|
+
className: "nuri-select__item",
|
|
68
|
+
children: /* @__PURE__ */ jsx(RadixSelect.ItemText, { children: option.label })
|
|
69
|
+
},
|
|
70
|
+
option.value
|
|
71
|
+
)) }) }) })
|
|
72
|
+
]
|
|
73
|
+
}
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
export {
|
|
77
|
+
Select
|
|
78
|
+
};
|
|
79
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/Select/Select.tsx","../../src/utils/cx.ts"],"sourcesContent":["'use client';\n\nimport './styles.css';\nimport * as RadixSelect from '@radix-ui/react-select';\nimport { forwardRef } from 'react';\nimport type { ReactNode } from 'react';\nimport { cx } from '../utils/cx.js';\n\ntype Size = 'sm' | 'md';\n\nexport interface SelectOption {\n value: string;\n label: ReactNode;\n disabled?: boolean;\n}\n\nexport interface SelectProps {\n /** 선택지 목록. */\n options: SelectOption[];\n /**\n * 사이즈. sm 36 / md 42 px 높이 (디자인 시스템의 Small/Large).\n * @default 'md'\n */\n size?: Size;\n /** 값이 없을 때 트리거에 표시할 문구. */\n placeholder?: ReactNode;\n /** 선택된 값 (제어형). */\n value?: string;\n /** 초기 값 (비제어형). */\n defaultValue?: string;\n onValueChange?: (value: string) => void;\n /** 열림 상태 제어 (선택). */\n open?: boolean;\n defaultOpen?: boolean;\n onOpenChange?: (open: boolean) => void;\n disabled?: boolean;\n /** 컨테이너 전체 너비로 확장. @default false */\n block?: boolean;\n /** 폼 전송용 name — 네이티브 select처럼 폼에 참여. */\n name?: string;\n required?: boolean;\n /** 포털 렌더 대상. 기본 document.body. */\n container?: HTMLElement | null;\n /** 트리거에 추가할 className. */\n className?: string;\n /** 시각 라벨이 없을 때 접근 가능한 이름. */\n 'aria-label'?: string;\n}\n\n/** 디자인 시스템의 8px 삼각 캐럿 (열림 시 CSS로 회전). */\nconst CaretIcon = () => (\n <svg width=\"8\" height=\"8\" viewBox=\"0 0 8 8\" fill=\"none\" aria-hidden=\"true\">\n <path d=\"M1.1 2.5h5.8L4 5.9z\" fill=\"currentColor\" />\n </svg>\n);\n\n/**\n * 셀렉트 박스 (단일 선택). Radix Select 기반 — 키보드 탐색·타입어헤드·\n * 포지셔닝·aria 연결을 자동 제공. 옵션 목록은 `options` 배열로 전달한다.\n *\n * 다중 선택(체크박스 옵션)·서브메뉴 변형은 별도 컴포넌트(DropdownMenu) 범위.\n *\n * @example\n * <Select\n * aria-label=\"정렬\"\n * placeholder=\"정렬 선택\"\n * options={[\n * { value: 'latest', label: '최신순' },\n * { value: 'edited', label: '수정순' },\n * ]}\n * onValueChange={setSort}\n * />\n */\nexport const Select = forwardRef<HTMLButtonElement, SelectProps>(function Select(\n {\n options,\n size = 'md',\n placeholder,\n value,\n defaultValue,\n onValueChange,\n open,\n defaultOpen,\n onOpenChange,\n disabled = false,\n block = false,\n name,\n required,\n container,\n className,\n 'aria-label': ariaLabel,\n },\n ref,\n) {\n return (\n <RadixSelect.Root\n value={value}\n defaultValue={defaultValue}\n onValueChange={onValueChange}\n open={open}\n defaultOpen={defaultOpen}\n onOpenChange={onOpenChange}\n disabled={disabled}\n name={name}\n required={required}\n >\n <RadixSelect.Trigger\n ref={ref}\n aria-label={ariaLabel}\n className={cx(\n 'nuri-select__trigger',\n `nuri-select__trigger--${size}`,\n block && 'nuri-select__trigger--block',\n className,\n )}\n >\n <span className=\"nuri-select__value\">\n <RadixSelect.Value placeholder={placeholder} />\n </span>\n <RadixSelect.Icon className=\"nuri-select__caret\">\n <CaretIcon />\n </RadixSelect.Icon>\n </RadixSelect.Trigger>\n\n <RadixSelect.Portal container={container}>\n <RadixSelect.Content className=\"nuri-select__content\" position=\"popper\" sideOffset={6}>\n <RadixSelect.Viewport className=\"nuri-select__viewport\">\n {options.map((option) => (\n <RadixSelect.Item\n key={option.value}\n value={option.value}\n disabled={option.disabled}\n className=\"nuri-select__item\"\n >\n <RadixSelect.ItemText>{option.label}</RadixSelect.ItemText>\n </RadixSelect.Item>\n ))}\n </RadixSelect.Viewport>\n </RadixSelect.Content>\n </RadixSelect.Portal>\n </RadixSelect.Root>\n );\n});\n","/**\n * 조건부 className 합성 유틸. falsy 값(undefined/false/null/'')은 제거.\n * @example cx('btn', isActive && 'btn--active', className)\n */\nexport function cx(...classes: Array<string | false | null | undefined>): string {\n return classes.filter(Boolean).join(' ');\n}\n"],"mappings":";AAEA,OAAO;AACP,YAAY,iBAAiB;AAC7B,SAAS,kBAAkB;;;ACApB,SAAS,MAAM,SAA2D;AAC/E,SAAO,QAAQ,OAAO,OAAO,EAAE,KAAK,GAAG;AACzC;;;AD8CI,cAsDE,YAtDF;AAFJ,IAAM,YAAY,MAChB,oBAAC,SAAI,OAAM,KAAI,QAAO,KAAI,SAAQ,WAAU,MAAK,QAAO,eAAY,QAClE,8BAAC,UAAK,GAAE,uBAAsB,MAAK,gBAAe,GACpD;AAoBK,IAAM,SAAS,WAA2C,SAASA,QACxE;AAAA,EACE;AAAA,EACA,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,WAAW;AAAA,EACX,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,cAAc;AAChB,GACA,KACA;AACA,SACE;AAAA,IAAa;AAAA,IAAZ;AAAA,MACC;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MAEA;AAAA;AAAA,UAAa;AAAA,UAAZ;AAAA,YACC;AAAA,YACA,cAAY;AAAA,YACZ,WAAW;AAAA,cACT;AAAA,cACA,yBAAyB,IAAI;AAAA,cAC7B,SAAS;AAAA,cACT;AAAA,YACF;AAAA,YAEA;AAAA,kCAAC,UAAK,WAAU,sBACd,8BAAa,mBAAZ,EAAkB,aAA0B,GAC/C;AAAA,cACA,oBAAa,kBAAZ,EAAiB,WAAU,sBAC1B,8BAAC,aAAU,GACb;AAAA;AAAA;AAAA,QACF;AAAA,QAEA,oBAAa,oBAAZ,EAAmB,WAClB,8BAAa,qBAAZ,EAAoB,WAAU,wBAAuB,UAAS,UAAS,YAAY,GAClF,8BAAa,sBAAZ,EAAqB,WAAU,yBAC7B,kBAAQ,IAAI,CAAC,WACZ;AAAA,UAAa;AAAA,UAAZ;AAAA,YAEC,OAAO,OAAO;AAAA,YACd,UAAU,OAAO;AAAA,YACjB,WAAU;AAAA,YAEV,8BAAa,sBAAZ,EAAsB,iBAAO,OAAM;AAAA;AAAA,UAL/B,OAAO;AAAA,QAMd,CACD,GACH,GACF,GACF;AAAA;AAAA;AAAA,EACF;AAEJ,CAAC;","names":["Select"]}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/* ── 트리거 ── */
|
|
2
|
+
.nuri-select__trigger {
|
|
3
|
+
box-sizing: border-box;
|
|
4
|
+
display: inline-flex;
|
|
5
|
+
align-items: center;
|
|
6
|
+
gap: 4px;
|
|
7
|
+
padding: 0 var(--nuri-select-padding-right) 0 var(--nuri-select-padding-left);
|
|
8
|
+
background-color: var(--nuri-select-bg);
|
|
9
|
+
border: 1px solid var(--nuri-select-border);
|
|
10
|
+
border-radius: var(--nuri-select-radius);
|
|
11
|
+
font-family: inherit; /* 앱 폰트 상속 */
|
|
12
|
+
font-size: var(--nuri-select-font-size);
|
|
13
|
+
font-weight: var(--nuri-select-font-weight);
|
|
14
|
+
line-height: var(--nuri-select-line-height);
|
|
15
|
+
color: var(--nuri-select-text);
|
|
16
|
+
cursor: pointer;
|
|
17
|
+
user-select: none;
|
|
18
|
+
transition:
|
|
19
|
+
border-color 120ms ease,
|
|
20
|
+
color 120ms ease;
|
|
21
|
+
}
|
|
22
|
+
.nuri-select__trigger--sm {
|
|
23
|
+
height: var(--nuri-select-size-sm-height);
|
|
24
|
+
}
|
|
25
|
+
.nuri-select__trigger--md {
|
|
26
|
+
height: var(--nuri-select-size-md-height);
|
|
27
|
+
}
|
|
28
|
+
.nuri-select__trigger--block {
|
|
29
|
+
display: flex;
|
|
30
|
+
width: 100%;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* 포커스 — 링 대신 보더 색 변화 (Input의 focus 문법과 통일).
|
|
34
|
+
열림 상태와는 텍스트색·캐럿 회전·패널 유무로 구분된다. */
|
|
35
|
+
.nuri-select__trigger:focus-visible {
|
|
36
|
+
outline: none;
|
|
37
|
+
border-color: var(--nuri-select-border-open, var(--nuri-color-brand-default));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* 열림(Spread) — 브랜드색 보더·텍스트 + 캐럿 회전 */
|
|
41
|
+
.nuri-select__trigger[data-state='open'] {
|
|
42
|
+
border-color: var(--nuri-select-border-open, var(--nuri-color-brand-default));
|
|
43
|
+
color: var(--nuri-select-text-open, var(--nuri-color-brand-default));
|
|
44
|
+
}
|
|
45
|
+
.nuri-select__trigger[data-state='open'] .nuri-select__caret > svg {
|
|
46
|
+
transform: rotate(180deg);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.nuri-select__trigger[data-disabled] {
|
|
50
|
+
background-color: var(--nuri-select-bg-disabled);
|
|
51
|
+
border-color: var(--nuri-select-border-disabled);
|
|
52
|
+
color: var(--nuri-select-text-disabled);
|
|
53
|
+
cursor: not-allowed;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.nuri-select__value {
|
|
57
|
+
flex: 1 1 auto;
|
|
58
|
+
min-width: 0;
|
|
59
|
+
overflow: hidden;
|
|
60
|
+
text-overflow: ellipsis;
|
|
61
|
+
white-space: nowrap;
|
|
62
|
+
text-align: left;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.nuri-select__caret {
|
|
66
|
+
position: relative;
|
|
67
|
+
display: inline-flex;
|
|
68
|
+
align-items: center;
|
|
69
|
+
justify-content: center;
|
|
70
|
+
flex-shrink: 0;
|
|
71
|
+
width: var(--nuri-select-caret-size);
|
|
72
|
+
height: var(--nuri-select-caret-size);
|
|
73
|
+
color: var(--nuri-select-caret-color);
|
|
74
|
+
}
|
|
75
|
+
.nuri-select__caret > svg {
|
|
76
|
+
transition: transform 150ms ease;
|
|
77
|
+
}
|
|
78
|
+
.nuri-select__trigger[data-state='open'] .nuri-select__caret {
|
|
79
|
+
color: var(--nuri-select-text-open, var(--nuri-color-brand-default));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* ── 드롭다운 패널 ── */
|
|
83
|
+
.nuri-select__content {
|
|
84
|
+
box-sizing: border-box;
|
|
85
|
+
z-index: var(--nuri-select-content-z-index);
|
|
86
|
+
min-width: var(--radix-select-trigger-width);
|
|
87
|
+
max-height: var(--radix-select-content-available-height);
|
|
88
|
+
padding: var(--nuri-select-content-padding);
|
|
89
|
+
background-color: var(--nuri-select-content-bg);
|
|
90
|
+
border: 1px solid var(--nuri-select-content-border);
|
|
91
|
+
border-radius: var(--nuri-select-content-radius);
|
|
92
|
+
box-shadow: var(--nuri-select-content-shadow);
|
|
93
|
+
overflow: hidden;
|
|
94
|
+
}
|
|
95
|
+
.nuri-select__viewport {
|
|
96
|
+
display: flex;
|
|
97
|
+
flex-direction: column;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/* ── 옵션 ── */
|
|
101
|
+
.nuri-select__item {
|
|
102
|
+
box-sizing: border-box;
|
|
103
|
+
display: flex;
|
|
104
|
+
align-items: center;
|
|
105
|
+
height: var(--nuri-select-item-height);
|
|
106
|
+
padding: 0 var(--nuri-select-item-padding-x);
|
|
107
|
+
border-radius: var(--nuri-select-item-radius);
|
|
108
|
+
font-family: inherit;
|
|
109
|
+
font-size: var(--nuri-select-item-font-size);
|
|
110
|
+
font-weight: var(--nuri-select-item-font-weight);
|
|
111
|
+
line-height: var(--nuri-select-line-height);
|
|
112
|
+
color: var(--nuri-select-item-text);
|
|
113
|
+
cursor: pointer;
|
|
114
|
+
user-select: none;
|
|
115
|
+
outline: none;
|
|
116
|
+
white-space: nowrap;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* 키보드/호버 하이라이트 */
|
|
120
|
+
.nuri-select__item[data-highlighted] {
|
|
121
|
+
background-color: var(--nuri-select-item-bg-highlighted);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/* 선택됨 — 옅은 브랜드 배경 + 브랜드 텍스트 */
|
|
125
|
+
.nuri-select__item[data-state='checked'] {
|
|
126
|
+
background-color: var(--nuri-select-item-bg-selected, var(--nuri-color-brand-subtle));
|
|
127
|
+
color: var(--nuri-select-item-text-selected, var(--nuri-color-brand-default));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.nuri-select__item[data-disabled] {
|
|
131
|
+
color: var(--nuri-select-item-text-disabled);
|
|
132
|
+
cursor: not-allowed;
|
|
133
|
+
pointer-events: none;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/* ── 애니메이션 ── */
|
|
137
|
+
.nuri-select__content[data-state='open'] {
|
|
138
|
+
animation: nuri-select-in 150ms ease;
|
|
139
|
+
}
|
|
140
|
+
@keyframes nuri-select-in {
|
|
141
|
+
from {
|
|
142
|
+
opacity: 0;
|
|
143
|
+
transform: translateY(-4px);
|
|
144
|
+
}
|
|
145
|
+
to {
|
|
146
|
+
opacity: 1;
|
|
147
|
+
transform: translateY(0);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
@media (prefers-reduced-motion: reduce) {
|
|
151
|
+
.nuri-select__content[data-state='open'] {
|
|
152
|
+
animation-duration: 1ms;
|
|
153
|
+
}
|
|
154
|
+
.nuri-select__caret > svg {
|
|
155
|
+
transition: none;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/Switch/index.ts
|
|
31
|
+
var Switch_exports = {};
|
|
32
|
+
__export(Switch_exports, {
|
|
33
|
+
Switch: () => Switch
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(Switch_exports);
|
|
36
|
+
|
|
37
|
+
// src/Switch/Switch.tsx
|
|
38
|
+
var import_styles = require("./styles.css");
|
|
39
|
+
var RadixSwitch = __toESM(require("@radix-ui/react-switch"), 1);
|
|
40
|
+
var import_react = require("react");
|
|
41
|
+
|
|
42
|
+
// src/utils/cx.ts
|
|
43
|
+
function cx(...classes) {
|
|
44
|
+
return classes.filter(Boolean).join(" ");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// src/Switch/Switch.tsx
|
|
48
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
49
|
+
var Switch = (0, import_react.forwardRef)(function Switch2({ label, disabled = false, id: idProp, className, ...rest }, ref) {
|
|
50
|
+
const reactId = (0, import_react.useId)();
|
|
51
|
+
const id = idProp ?? `nuri-switch-${reactId}`;
|
|
52
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: cx("nuri-switch", className), children: [
|
|
53
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
54
|
+
RadixSwitch.Root,
|
|
55
|
+
{
|
|
56
|
+
ref,
|
|
57
|
+
id,
|
|
58
|
+
className: "nuri-switch__control",
|
|
59
|
+
disabled,
|
|
60
|
+
...rest,
|
|
61
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RadixSwitch.Thumb, { className: "nuri-switch__thumb" })
|
|
62
|
+
}
|
|
63
|
+
),
|
|
64
|
+
label != null && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("label", { htmlFor: id, className: "nuri-switch__label", children: label })
|
|
65
|
+
] });
|
|
66
|
+
});
|
|
67
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
68
|
+
0 && (module.exports = {
|
|
69
|
+
Switch
|
|
70
|
+
});
|
|
71
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/Switch/index.ts","../../src/Switch/Switch.tsx","../../src/utils/cx.ts"],"sourcesContent":["export { Switch } from './Switch.js';\nexport type { SwitchProps } from './Switch.js';\n","'use client';\n\nimport './styles.css';\nimport * as RadixSwitch from '@radix-ui/react-switch';\nimport { forwardRef, useId } from 'react';\nimport type { ReactNode } from 'react';\nimport { cx } from '../utils/cx.js';\n\nexport interface SwitchProps {\n /**\n * 라벨 (컨트롤 우측). 디자인 시스템의 Toggle은 컨트롤 단독 정의지만,\n * 범용 사용을 위해 라벨을 지원한다(Checkbox 라벨 스타일 준용). 없으면 `aria-label` 필수.\n */\n label?: ReactNode;\n /** 켜짐 상태 (제어형). */\n checked?: boolean;\n /** 초기 상태 (비제어형). */\n defaultChecked?: boolean;\n onCheckedChange?: (checked: boolean) => void;\n disabled?: boolean;\n /** 폼 전송용 name. */\n name?: string;\n /** 폼 전송 시 값. @default 'on' */\n value?: string;\n required?: boolean;\n id?: string;\n className?: string;\n 'aria-label'?: string;\n}\n\n/**\n * 스위치(토글). Radix Switch 기반 — `role=\"switch\"`·Space/Enter 토글·폼 참여 자동.\n * 즉시 적용되는 켬/끔 설정에 사용한다 (제출이 필요한 선택은 Checkbox).\n *\n * @example\n * <Switch label=\"알림 받기\" checked={on} onCheckedChange={setOn} />\n */\nexport const Switch = forwardRef<HTMLButtonElement, SwitchProps>(function Switch(\n { label, disabled = false, id: idProp, className, ...rest },\n ref,\n) {\n const reactId = useId();\n const id = idProp ?? `nuri-switch-${reactId}`;\n\n return (\n <div className={cx('nuri-switch', className)}>\n <RadixSwitch.Root\n ref={ref}\n id={id}\n className=\"nuri-switch__control\"\n disabled={disabled}\n {...rest}\n >\n <RadixSwitch.Thumb className=\"nuri-switch__thumb\" />\n </RadixSwitch.Root>\n {label != null && (\n <label htmlFor={id} className=\"nuri-switch__label\">\n {label}\n </label>\n )}\n </div>\n );\n});\n","/**\n * 조건부 className 합성 유틸. falsy 값(undefined/false/null/'')은 제거.\n * @example cx('btn', isActive && 'btn--active', className)\n */\nexport function cx(...classes: Array<string | false | null | undefined>): string {\n return classes.filter(Boolean).join(' ');\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACEA,oBAAO;AACP,kBAA6B;AAC7B,mBAAkC;;;ACA3B,SAAS,MAAM,SAA2D;AAC/E,SAAO,QAAQ,OAAO,OAAO,EAAE,KAAK,GAAG;AACzC;;;ADuCI;AARG,IAAM,aAAS,yBAA2C,SAASA,QACxE,EAAE,OAAO,WAAW,OAAO,IAAI,QAAQ,WAAW,GAAG,KAAK,GAC1D,KACA;AACA,QAAM,cAAU,oBAAM;AACtB,QAAM,KAAK,UAAU,eAAe,OAAO;AAE3C,SACE,6CAAC,SAAI,WAAW,GAAG,eAAe,SAAS,GACzC;AAAA;AAAA,MAAa;AAAA,MAAZ;AAAA,QACC;AAAA,QACA;AAAA,QACA,WAAU;AAAA,QACV;AAAA,QACC,GAAG;AAAA,QAEJ,sDAAa,mBAAZ,EAAkB,WAAU,sBAAqB;AAAA;AAAA,IACpD;AAAA,IACC,SAAS,QACR,4CAAC,WAAM,SAAS,IAAI,WAAU,sBAC3B,iBACH;AAAA,KAEJ;AAEJ,CAAC;","names":["Switch"]}
|