@nurimedia/react-ui 0.1.0 → 0.1.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/AlertDialog/index.cjs +1 -0
- package/dist/AlertDialog/index.cjs.map +1 -1
- package/dist/AlertDialog/index.js +2 -0
- package/dist/AlertDialog/index.js.map +1 -1
- package/dist/Button/index.cjs +1 -0
- package/dist/Button/index.cjs.map +1 -1
- package/dist/Button/index.js +2 -0
- package/dist/Button/index.js.map +1 -1
- package/dist/Checkbox/index.cjs +1 -0
- package/dist/Checkbox/index.cjs.map +1 -1
- package/dist/Checkbox/index.js +2 -0
- package/dist/Checkbox/index.js.map +1 -1
- package/dist/Input/index.cjs +1 -0
- package/dist/Input/index.cjs.map +1 -1
- package/dist/Input/index.js +2 -0
- package/dist/Input/index.js.map +1 -1
- package/dist/Modal/index.cjs +1 -0
- package/dist/Modal/index.cjs.map +1 -1
- package/dist/Modal/index.js +2 -0
- package/dist/Modal/index.js.map +1 -1
- package/dist/Radio/index.cjs +1 -0
- package/dist/Radio/index.cjs.map +1 -1
- package/dist/Radio/index.js +2 -0
- package/dist/Radio/index.js.map +1 -1
- package/dist/Select/index.cjs +1 -0
- package/dist/Select/index.cjs.map +1 -1
- package/dist/Select/index.js +2 -0
- package/dist/Select/index.js.map +1 -1
- package/dist/Switch/index.cjs +1 -0
- package/dist/Switch/index.cjs.map +1 -1
- package/dist/Switch/index.js +2 -0
- package/dist/Switch/index.js.map +1 -1
- package/dist/Textarea/index.cjs +1 -0
- package/dist/Textarea/index.cjs.map +1 -1
- package/dist/Textarea/index.js +2 -0
- package/dist/Textarea/index.js.map +1 -1
- package/dist/Toast/index.cjs +1 -0
- package/dist/Toast/index.cjs.map +1 -1
- package/dist/Toast/index.js +2 -0
- package/dist/Toast/index.js.map +1 -1
- package/dist/Tooltip/index.cjs +1 -0
- package/dist/Tooltip/index.cjs.map +1 -1
- package/dist/Tooltip/index.js +2 -0
- package/dist/Tooltip/index.js.map +1 -1
- package/dist/index.cjs +1 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/AlertDialog/index.ts","../../src/AlertDialog/AlertDialog.tsx","../../src/utils/cx.ts","../../src/AlertDialog/icons.tsx"],"sourcesContent":["export { AlertDialog } from './AlertDialog.js';\nexport type { AlertDialogContentProps, AlertDialogFooterProps } from './AlertDialog.js';\n","'use client';\n\nimport './styles.css';\nimport * as Alert from '@radix-ui/react-alert-dialog';\nimport { forwardRef } from 'react';\nimport type { ComponentPropsWithoutRef, ElementRef, ReactNode } from 'react';\nimport { cx } from '../utils/cx.js';\nimport { defaultToneIcon } from './icons.js';\n\ntype Tone = 'info' | 'success' | 'warning';\n\n/**\n * 확인 강제형 다이얼로그 (alertdialog). Radix AlertDialog 기반.\n *\n * 일반 Modal과의 차이:\n * - `role=\"alertdialog\"` — 스크린리더가 더 강하게 안내\n * - **바깥(백드롭) 클릭으로 닫히지 않음** — 실수로 닫히는 것 방지\n * - 포커스가 기본적으로 Cancel(취소)에 위치 — 실수로 위험 액션 방지\n * - **중앙 정렬 + 상단 아이콘** (디자인 시스템 표준 레이아웃)\n *\n * Esc 키는 닫힘(키보드 탈출구 — 접근성). 막으려면 Content에 `onEscapeKeyDown` 전달.\n *\n * @example\n * <AlertDialog>\n * <AlertDialog.Trigger asChild><Button>삭제</Button></AlertDialog.Trigger>\n * <AlertDialog.Content tone=\"warning\">\n * <AlertDialog.Header>\n * <AlertDialog.Title>정말 삭제할까요?</AlertDialog.Title>\n * <AlertDialog.Description>되돌릴 수 없습니다.</AlertDialog.Description>\n * </AlertDialog.Header>\n * <AlertDialog.Footer>\n * <AlertDialog.Cancel asChild><Button variant=\"outline\">취소</Button></AlertDialog.Cancel>\n * <AlertDialog.Action asChild><Button onClick={onDelete}>삭제</Button></AlertDialog.Action>\n * </AlertDialog.Footer>\n * </AlertDialog.Content>\n * </AlertDialog>\n */\nconst Root = (props: ComponentPropsWithoutRef<typeof Alert.Root>) => <Alert.Root {...props} />;\nRoot.displayName = 'AlertDialog';\n\n/** 다이얼로그를 여는 트리거. `asChild`로 임의 엘리먼트에 위임. */\nconst Trigger = Alert.Trigger;\n\nexport interface AlertDialogContentProps extends ComponentPropsWithoutRef<typeof Alert.Content> {\n /**\n * 상태 톤 — 기본 아이콘과 그 색을 결정.\n * @default 'info'\n */\n tone?: Tone;\n /**\n * 상단 아이콘.\n * - 미지정: `tone`에 맞는 **기본 아이콘** 자동 렌더\n * - `ReactNode`: 커스텀 아이콘으로 교체\n * - `null`: 아이콘 제거\n */\n icon?: ReactNode | null;\n /** 백드롭(오버레이) 표시 여부. @default true */\n overlay?: boolean;\n /** 포털 렌더 대상. 기본은 document.body. 다른 요소에 렌더하려면 지정. */\n container?: HTMLElement | null;\n}\n\n/** 오버레이 + 포털 + 패널을 한 번에 렌더. 중앙 정렬 + 상단 아이콘 레이아웃. */\nconst Content = forwardRef<ElementRef<typeof Alert.Content>, AlertDialogContentProps>(\n function AlertDialogContent(\n { tone = 'info', icon, overlay = true, container, className, children, ...rest },\n ref,\n ) {\n // icon 미지정(undefined) → 기본 tone 아이콘 / null → 제거 / ReactNode → 교체\n const resolvedIcon = icon === undefined ? defaultToneIcon[tone] : icon;\n return (\n <Alert.Portal container={container}>\n {overlay && <Alert.Overlay className=\"nuri-alert__overlay\" />}\n <Alert.Content\n ref={ref}\n className={cx('nuri-alert', `nuri-alert--${tone}`, className)}\n {...rest}\n >\n {resolvedIcon != null && (\n <div className=\"nuri-alert__icon\" aria-hidden=\"true\">\n {resolvedIcon}\n </div>\n )}\n {children}\n </Alert.Content>\n </Alert.Portal>\n );\n },\n);\n\n/** 헤더 슬롯 (Title/Description 묶음, 중앙 정렬). */\nconst Header = forwardRef<HTMLDivElement, ComponentPropsWithoutRef<'div'>>(\n function AlertDialogHeader({ className, ...rest }, ref) {\n return <div ref={ref} className={cx('nuri-alert__header', className)} {...rest} />;\n },\n);\n\n/** 제목. 자동으로 `aria-labelledby`로 연결됨. */\nconst Title = forwardRef<\n ElementRef<typeof Alert.Title>,\n ComponentPropsWithoutRef<typeof Alert.Title>\n>(function AlertDialogTitle({ className, ...rest }, ref) {\n return <Alert.Title ref={ref} className={cx('nuri-alert__title', className)} {...rest} />;\n});\n\n/** 설명. 자동으로 `aria-describedby`로 연결됨. */\nconst Description = forwardRef<\n ElementRef<typeof Alert.Description>,\n ComponentPropsWithoutRef<typeof Alert.Description>\n>(function AlertDialogDescription({ className, ...rest }, ref) {\n return (\n <Alert.Description ref={ref} className={cx('nuri-alert__description', className)} {...rest} />\n );\n});\n\n/** 본문 슬롯 (추가 콘텐츠). */\nconst Body = forwardRef<HTMLDivElement, ComponentPropsWithoutRef<'div'>>(function AlertDialogBody(\n { className, ...rest },\n ref,\n) {\n return <div ref={ref} className={cx('nuri-alert__body', className)} {...rest} />;\n});\n\nexport interface AlertDialogFooterProps extends ComponentPropsWithoutRef<'div'> {\n /**\n * 버튼 정렬. 디자인 표준은 전체너비 버튼이라 기본값 `full`.\n * @default 'full'\n */\n align?: 'start' | 'center' | 'end' | 'between' | 'full';\n}\n\n/** 푸터 슬롯 (Cancel/Action 배치). 기본 전체너비(`full`). */\nconst Footer = forwardRef<HTMLDivElement, AlertDialogFooterProps>(function AlertDialogFooter(\n { align = 'full', className, ...rest },\n ref,\n) {\n return (\n <div\n ref={ref}\n className={cx('nuri-alert__footer', `nuri-alert__footer--${align}`, className)}\n {...rest}\n />\n );\n});\n\n/**\n * 확인(주 액션) 버튼. 클릭 시 닫힘. `asChild`로 Button 위임.\n * @example <AlertDialog.Action asChild><Button onClick={onConfirm}>삭제</Button></AlertDialog.Action>\n */\nconst Action = Alert.Action;\n\n/**\n * 취소 버튼. 클릭 시 닫힘. 포커스가 기본적으로 여기 위치.\n * @example <AlertDialog.Cancel asChild><Button variant=\"outline\">취소</Button></AlertDialog.Cancel>\n */\nconst Cancel = Alert.Cancel;\n\nconst CloseIcon = () => (\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 20 20\" fill=\"none\" aria-hidden=\"true\">\n <path d=\"M5 5l10 10M15 5L5 15\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" />\n </svg>\n);\n\n/**\n * 우상단 X 닫기 버튼 (옵션). 내부적으로 Cancel과 동일하게 닫힘. 기본 `aria-label=\"닫기\"`.\n * 확인을 강제하려면 사용하지 않는 편이 좋습니다 (Cancel/Action으로 명시적 선택 유도).\n */\nconst CloseButton = forwardRef<\n HTMLButtonElement,\n Omit<ComponentPropsWithoutRef<'button'>, 'children'> & { children?: ReactNode }\n>(function AlertDialogCloseButton(\n { className, 'aria-label': ariaLabel = '닫기', children, ...rest },\n ref,\n) {\n return (\n <Alert.Cancel asChild>\n <button\n ref={ref}\n type=\"button\"\n aria-label={ariaLabel}\n className={cx('nuri-alert__close', className)}\n {...rest}\n >\n {children ?? <CloseIcon />}\n </button>\n </Alert.Cancel>\n );\n});\n\nexport const AlertDialog = Object.assign(Root, {\n Trigger,\n Content,\n Header,\n Title,\n Description,\n Body,\n Footer,\n Action,\n Cancel,\n CloseButton,\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","/**\n * tone별 기본 아이콘 — 디자이너 시스템(Figma) 에셋을 그대로 inline SVG로 재현.\n * 구조: 틴트 원 배경 + tone 색 심볼. 색은 토큰(`--nuri-alert-tone-*`)으로 분리.\n * AlertDialog.Content의 `icon` 미지정 시 tone에 맞춰 자동 렌더.\n * `icon={<Custom/>}`로 교체, `icon={null}`로 제거 가능.\n */\nimport type { ReactNode } from 'react';\n\nconst SuccessIcon = () => (\n <svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden=\"true\">\n <circle cx=\"12\" cy=\"12\" r=\"12\" fill=\"var(--nuri-alert-tone-success-bg)\" />\n <path\n d=\"M15.77 9.58L10.65 14.70L8.90 12.95\"\n stroke=\"var(--nuri-alert-tone-success-fg)\"\n strokeWidth=\"2.5\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n);\n\nconst WarningIcon = () => (\n <svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden=\"true\">\n <circle cx=\"12\" cy=\"12\" r=\"12\" fill=\"var(--nuri-alert-tone-warning-bg)\" />\n <path\n d=\"M12 6.75V12\"\n stroke=\"var(--nuri-alert-tone-warning-fg)\"\n strokeWidth=\"2.5\"\n strokeLinecap=\"round\"\n />\n <circle cx=\"12\" cy=\"16.31\" r=\"1.1\" fill=\"var(--nuri-alert-tone-warning-fg)\" />\n </svg>\n);\n\nconst InfoIcon = () => (\n <svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden=\"true\">\n <circle cx=\"12\" cy=\"12\" r=\"12\" fill=\"var(--nuri-alert-tone-info-bg)\" />\n <path\n d=\"M12.25 7h0.01\"\n stroke=\"var(--nuri-alert-tone-info-fg)\"\n strokeWidth=\"3\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n <path\n d=\"M11 12h1.25v5h1.25\"\n stroke=\"var(--nuri-alert-tone-info-fg)\"\n strokeWidth=\"2.5\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n);\n\nexport const defaultToneIcon: Record<'info' | 'success' | 'warning', ReactNode> = {\n info: <InfoIcon />,\n success: <SuccessIcon />,\n warning: <WarningIcon />,\n};\n"],"mappings":"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","names":["import_jsx_runtime","Root","Trigger","Content","Title","Description","Action","Cancel"]}
|
|
1
|
+
{"version":3,"sources":["../../src/AlertDialog/index.ts","../../src/AlertDialog/AlertDialog.tsx","../../src/utils/cx.ts","../../src/AlertDialog/icons.tsx"],"sourcesContent":["export { AlertDialog } from './AlertDialog.js';\nexport type { AlertDialogContentProps, AlertDialogFooterProps } from './AlertDialog.js';\n","'use client';\n\nimport './styles.css';\nimport * as Alert from '@radix-ui/react-alert-dialog';\nimport { forwardRef } from 'react';\nimport type { ComponentPropsWithoutRef, ElementRef, ReactNode } from 'react';\nimport { cx } from '../utils/cx.js';\nimport { defaultToneIcon } from './icons.js';\n\ntype Tone = 'info' | 'success' | 'warning';\n\n/**\n * 확인 강제형 다이얼로그 (alertdialog). Radix AlertDialog 기반.\n *\n * 일반 Modal과의 차이:\n * - `role=\"alertdialog\"` — 스크린리더가 더 강하게 안내\n * - **바깥(백드롭) 클릭으로 닫히지 않음** — 실수로 닫히는 것 방지\n * - 포커스가 기본적으로 Cancel(취소)에 위치 — 실수로 위험 액션 방지\n * - **중앙 정렬 + 상단 아이콘** (디자인 시스템 표준 레이아웃)\n *\n * Esc 키는 닫힘(키보드 탈출구 — 접근성). 막으려면 Content에 `onEscapeKeyDown` 전달.\n *\n * @example\n * <AlertDialog>\n * <AlertDialog.Trigger asChild><Button>삭제</Button></AlertDialog.Trigger>\n * <AlertDialog.Content tone=\"warning\">\n * <AlertDialog.Header>\n * <AlertDialog.Title>정말 삭제할까요?</AlertDialog.Title>\n * <AlertDialog.Description>되돌릴 수 없습니다.</AlertDialog.Description>\n * </AlertDialog.Header>\n * <AlertDialog.Footer>\n * <AlertDialog.Cancel asChild><Button variant=\"outline\">취소</Button></AlertDialog.Cancel>\n * <AlertDialog.Action asChild><Button onClick={onDelete}>삭제</Button></AlertDialog.Action>\n * </AlertDialog.Footer>\n * </AlertDialog.Content>\n * </AlertDialog>\n */\nconst Root = (props: ComponentPropsWithoutRef<typeof Alert.Root>) => <Alert.Root {...props} />;\nRoot.displayName = 'AlertDialog';\n\n/** 다이얼로그를 여는 트리거. `asChild`로 임의 엘리먼트에 위임. */\nconst Trigger = Alert.Trigger;\n\nexport interface AlertDialogContentProps extends ComponentPropsWithoutRef<typeof Alert.Content> {\n /**\n * 상태 톤 — 기본 아이콘과 그 색을 결정.\n * @default 'info'\n */\n tone?: Tone;\n /**\n * 상단 아이콘.\n * - 미지정: `tone`에 맞는 **기본 아이콘** 자동 렌더\n * - `ReactNode`: 커스텀 아이콘으로 교체\n * - `null`: 아이콘 제거\n */\n icon?: ReactNode | null;\n /** 백드롭(오버레이) 표시 여부. @default true */\n overlay?: boolean;\n /** 포털 렌더 대상. 기본은 document.body. 다른 요소에 렌더하려면 지정. */\n container?: HTMLElement | null;\n}\n\n/** 오버레이 + 포털 + 패널을 한 번에 렌더. 중앙 정렬 + 상단 아이콘 레이아웃. */\nconst Content = forwardRef<ElementRef<typeof Alert.Content>, AlertDialogContentProps>(\n function AlertDialogContent(\n { tone = 'info', icon, overlay = true, container, className, children, ...rest },\n ref,\n ) {\n // icon 미지정(undefined) → 기본 tone 아이콘 / null → 제거 / ReactNode → 교체\n const resolvedIcon = icon === undefined ? defaultToneIcon[tone] : icon;\n return (\n <Alert.Portal container={container}>\n {overlay && <Alert.Overlay className=\"nuri-alert__overlay\" />}\n <Alert.Content\n ref={ref}\n className={cx('nuri-alert', `nuri-alert--${tone}`, className)}\n {...rest}\n >\n {resolvedIcon != null && (\n <div className=\"nuri-alert__icon\" aria-hidden=\"true\">\n {resolvedIcon}\n </div>\n )}\n {children}\n </Alert.Content>\n </Alert.Portal>\n );\n },\n);\n\n/** 헤더 슬롯 (Title/Description 묶음, 중앙 정렬). */\nconst Header = forwardRef<HTMLDivElement, ComponentPropsWithoutRef<'div'>>(\n function AlertDialogHeader({ className, ...rest }, ref) {\n return <div ref={ref} className={cx('nuri-alert__header', className)} {...rest} />;\n },\n);\n\n/** 제목. 자동으로 `aria-labelledby`로 연결됨. */\nconst Title = forwardRef<\n ElementRef<typeof Alert.Title>,\n ComponentPropsWithoutRef<typeof Alert.Title>\n>(function AlertDialogTitle({ className, ...rest }, ref) {\n return <Alert.Title ref={ref} className={cx('nuri-alert__title', className)} {...rest} />;\n});\n\n/** 설명. 자동으로 `aria-describedby`로 연결됨. */\nconst Description = forwardRef<\n ElementRef<typeof Alert.Description>,\n ComponentPropsWithoutRef<typeof Alert.Description>\n>(function AlertDialogDescription({ className, ...rest }, ref) {\n return (\n <Alert.Description ref={ref} className={cx('nuri-alert__description', className)} {...rest} />\n );\n});\n\n/** 본문 슬롯 (추가 콘텐츠). */\nconst Body = forwardRef<HTMLDivElement, ComponentPropsWithoutRef<'div'>>(function AlertDialogBody(\n { className, ...rest },\n ref,\n) {\n return <div ref={ref} className={cx('nuri-alert__body', className)} {...rest} />;\n});\n\nexport interface AlertDialogFooterProps extends ComponentPropsWithoutRef<'div'> {\n /**\n * 버튼 정렬. 디자인 표준은 전체너비 버튼이라 기본값 `full`.\n * @default 'full'\n */\n align?: 'start' | 'center' | 'end' | 'between' | 'full';\n}\n\n/** 푸터 슬롯 (Cancel/Action 배치). 기본 전체너비(`full`). */\nconst Footer = forwardRef<HTMLDivElement, AlertDialogFooterProps>(function AlertDialogFooter(\n { align = 'full', className, ...rest },\n ref,\n) {\n return (\n <div\n ref={ref}\n className={cx('nuri-alert__footer', `nuri-alert__footer--${align}`, className)}\n {...rest}\n />\n );\n});\n\n/**\n * 확인(주 액션) 버튼. 클릭 시 닫힘. `asChild`로 Button 위임.\n * @example <AlertDialog.Action asChild><Button onClick={onConfirm}>삭제</Button></AlertDialog.Action>\n */\nconst Action = Alert.Action;\n\n/**\n * 취소 버튼. 클릭 시 닫힘. 포커스가 기본적으로 여기 위치.\n * @example <AlertDialog.Cancel asChild><Button variant=\"outline\">취소</Button></AlertDialog.Cancel>\n */\nconst Cancel = Alert.Cancel;\n\nconst CloseIcon = () => (\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 20 20\" fill=\"none\" aria-hidden=\"true\">\n <path d=\"M5 5l10 10M15 5L5 15\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" />\n </svg>\n);\n\n/**\n * 우상단 X 닫기 버튼 (옵션). 내부적으로 Cancel과 동일하게 닫힘. 기본 `aria-label=\"닫기\"`.\n * 확인을 강제하려면 사용하지 않는 편이 좋습니다 (Cancel/Action으로 명시적 선택 유도).\n */\nconst CloseButton = forwardRef<\n HTMLButtonElement,\n Omit<ComponentPropsWithoutRef<'button'>, 'children'> & { children?: ReactNode }\n>(function AlertDialogCloseButton(\n { className, 'aria-label': ariaLabel = '닫기', children, ...rest },\n ref,\n) {\n return (\n <Alert.Cancel asChild>\n <button\n ref={ref}\n type=\"button\"\n aria-label={ariaLabel}\n className={cx('nuri-alert__close', className)}\n {...rest}\n >\n {children ?? <CloseIcon />}\n </button>\n </Alert.Cancel>\n );\n});\n\nexport const AlertDialog = Object.assign(Root, {\n Trigger,\n Content,\n Header,\n Title,\n Description,\n Body,\n Footer,\n Action,\n Cancel,\n CloseButton,\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","/**\n * tone별 기본 아이콘 — 디자이너 시스템(Figma) 에셋을 그대로 inline SVG로 재현.\n * 구조: 틴트 원 배경 + tone 색 심볼. 색은 토큰(`--nuri-alert-tone-*`)으로 분리.\n * AlertDialog.Content의 `icon` 미지정 시 tone에 맞춰 자동 렌더.\n * `icon={<Custom/>}`로 교체, `icon={null}`로 제거 가능.\n */\nimport type { ReactNode } from 'react';\n\nconst SuccessIcon = () => (\n <svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden=\"true\">\n <circle cx=\"12\" cy=\"12\" r=\"12\" fill=\"var(--nuri-alert-tone-success-bg)\" />\n <path\n d=\"M15.77 9.58L10.65 14.70L8.90 12.95\"\n stroke=\"var(--nuri-alert-tone-success-fg)\"\n strokeWidth=\"2.5\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n);\n\nconst WarningIcon = () => (\n <svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden=\"true\">\n <circle cx=\"12\" cy=\"12\" r=\"12\" fill=\"var(--nuri-alert-tone-warning-bg)\" />\n <path\n d=\"M12 6.75V12\"\n stroke=\"var(--nuri-alert-tone-warning-fg)\"\n strokeWidth=\"2.5\"\n strokeLinecap=\"round\"\n />\n <circle cx=\"12\" cy=\"16.31\" r=\"1.1\" fill=\"var(--nuri-alert-tone-warning-fg)\" />\n </svg>\n);\n\nconst InfoIcon = () => (\n <svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden=\"true\">\n <circle cx=\"12\" cy=\"12\" r=\"12\" fill=\"var(--nuri-alert-tone-info-bg)\" />\n <path\n d=\"M12.25 7h0.01\"\n stroke=\"var(--nuri-alert-tone-info-fg)\"\n strokeWidth=\"3\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n <path\n d=\"M11 12h1.25v5h1.25\"\n stroke=\"var(--nuri-alert-tone-info-fg)\"\n strokeWidth=\"2.5\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n);\n\nexport const defaultToneIcon: Record<'info' | 'success' | 'warning', ReactNode> = {\n info: <InfoIcon />,\n success: <SuccessIcon />,\n warning: <WarningIcon />,\n};\n"],"mappings":"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","names":["import_jsx_runtime","Root","Trigger","Content","Title","Description","Action","Cancel"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/AlertDialog/AlertDialog.tsx","../../src/utils/cx.ts","../../src/AlertDialog/icons.tsx"],"sourcesContent":["'use client';\n\nimport './styles.css';\nimport * as Alert from '@radix-ui/react-alert-dialog';\nimport { forwardRef } from 'react';\nimport type { ComponentPropsWithoutRef, ElementRef, ReactNode } from 'react';\nimport { cx } from '../utils/cx.js';\nimport { defaultToneIcon } from './icons.js';\n\ntype Tone = 'info' | 'success' | 'warning';\n\n/**\n * 확인 강제형 다이얼로그 (alertdialog). Radix AlertDialog 기반.\n *\n * 일반 Modal과의 차이:\n * - `role=\"alertdialog\"` — 스크린리더가 더 강하게 안내\n * - **바깥(백드롭) 클릭으로 닫히지 않음** — 실수로 닫히는 것 방지\n * - 포커스가 기본적으로 Cancel(취소)에 위치 — 실수로 위험 액션 방지\n * - **중앙 정렬 + 상단 아이콘** (디자인 시스템 표준 레이아웃)\n *\n * Esc 키는 닫힘(키보드 탈출구 — 접근성). 막으려면 Content에 `onEscapeKeyDown` 전달.\n *\n * @example\n * <AlertDialog>\n * <AlertDialog.Trigger asChild><Button>삭제</Button></AlertDialog.Trigger>\n * <AlertDialog.Content tone=\"warning\">\n * <AlertDialog.Header>\n * <AlertDialog.Title>정말 삭제할까요?</AlertDialog.Title>\n * <AlertDialog.Description>되돌릴 수 없습니다.</AlertDialog.Description>\n * </AlertDialog.Header>\n * <AlertDialog.Footer>\n * <AlertDialog.Cancel asChild><Button variant=\"outline\">취소</Button></AlertDialog.Cancel>\n * <AlertDialog.Action asChild><Button onClick={onDelete}>삭제</Button></AlertDialog.Action>\n * </AlertDialog.Footer>\n * </AlertDialog.Content>\n * </AlertDialog>\n */\nconst Root = (props: ComponentPropsWithoutRef<typeof Alert.Root>) => <Alert.Root {...props} />;\nRoot.displayName = 'AlertDialog';\n\n/** 다이얼로그를 여는 트리거. `asChild`로 임의 엘리먼트에 위임. */\nconst Trigger = Alert.Trigger;\n\nexport interface AlertDialogContentProps extends ComponentPropsWithoutRef<typeof Alert.Content> {\n /**\n * 상태 톤 — 기본 아이콘과 그 색을 결정.\n * @default 'info'\n */\n tone?: Tone;\n /**\n * 상단 아이콘.\n * - 미지정: `tone`에 맞는 **기본 아이콘** 자동 렌더\n * - `ReactNode`: 커스텀 아이콘으로 교체\n * - `null`: 아이콘 제거\n */\n icon?: ReactNode | null;\n /** 백드롭(오버레이) 표시 여부. @default true */\n overlay?: boolean;\n /** 포털 렌더 대상. 기본은 document.body. 다른 요소에 렌더하려면 지정. */\n container?: HTMLElement | null;\n}\n\n/** 오버레이 + 포털 + 패널을 한 번에 렌더. 중앙 정렬 + 상단 아이콘 레이아웃. */\nconst Content = forwardRef<ElementRef<typeof Alert.Content>, AlertDialogContentProps>(\n function AlertDialogContent(\n { tone = 'info', icon, overlay = true, container, className, children, ...rest },\n ref,\n ) {\n // icon 미지정(undefined) → 기본 tone 아이콘 / null → 제거 / ReactNode → 교체\n const resolvedIcon = icon === undefined ? defaultToneIcon[tone] : icon;\n return (\n <Alert.Portal container={container}>\n {overlay && <Alert.Overlay className=\"nuri-alert__overlay\" />}\n <Alert.Content\n ref={ref}\n className={cx('nuri-alert', `nuri-alert--${tone}`, className)}\n {...rest}\n >\n {resolvedIcon != null && (\n <div className=\"nuri-alert__icon\" aria-hidden=\"true\">\n {resolvedIcon}\n </div>\n )}\n {children}\n </Alert.Content>\n </Alert.Portal>\n );\n },\n);\n\n/** 헤더 슬롯 (Title/Description 묶음, 중앙 정렬). */\nconst Header = forwardRef<HTMLDivElement, ComponentPropsWithoutRef<'div'>>(\n function AlertDialogHeader({ className, ...rest }, ref) {\n return <div ref={ref} className={cx('nuri-alert__header', className)} {...rest} />;\n },\n);\n\n/** 제목. 자동으로 `aria-labelledby`로 연결됨. */\nconst Title = forwardRef<\n ElementRef<typeof Alert.Title>,\n ComponentPropsWithoutRef<typeof Alert.Title>\n>(function AlertDialogTitle({ className, ...rest }, ref) {\n return <Alert.Title ref={ref} className={cx('nuri-alert__title', className)} {...rest} />;\n});\n\n/** 설명. 자동으로 `aria-describedby`로 연결됨. */\nconst Description = forwardRef<\n ElementRef<typeof Alert.Description>,\n ComponentPropsWithoutRef<typeof Alert.Description>\n>(function AlertDialogDescription({ className, ...rest }, ref) {\n return (\n <Alert.Description ref={ref} className={cx('nuri-alert__description', className)} {...rest} />\n );\n});\n\n/** 본문 슬롯 (추가 콘텐츠). */\nconst Body = forwardRef<HTMLDivElement, ComponentPropsWithoutRef<'div'>>(function AlertDialogBody(\n { className, ...rest },\n ref,\n) {\n return <div ref={ref} className={cx('nuri-alert__body', className)} {...rest} />;\n});\n\nexport interface AlertDialogFooterProps extends ComponentPropsWithoutRef<'div'> {\n /**\n * 버튼 정렬. 디자인 표준은 전체너비 버튼이라 기본값 `full`.\n * @default 'full'\n */\n align?: 'start' | 'center' | 'end' | 'between' | 'full';\n}\n\n/** 푸터 슬롯 (Cancel/Action 배치). 기본 전체너비(`full`). */\nconst Footer = forwardRef<HTMLDivElement, AlertDialogFooterProps>(function AlertDialogFooter(\n { align = 'full', className, ...rest },\n ref,\n) {\n return (\n <div\n ref={ref}\n className={cx('nuri-alert__footer', `nuri-alert__footer--${align}`, className)}\n {...rest}\n />\n );\n});\n\n/**\n * 확인(주 액션) 버튼. 클릭 시 닫힘. `asChild`로 Button 위임.\n * @example <AlertDialog.Action asChild><Button onClick={onConfirm}>삭제</Button></AlertDialog.Action>\n */\nconst Action = Alert.Action;\n\n/**\n * 취소 버튼. 클릭 시 닫힘. 포커스가 기본적으로 여기 위치.\n * @example <AlertDialog.Cancel asChild><Button variant=\"outline\">취소</Button></AlertDialog.Cancel>\n */\nconst Cancel = Alert.Cancel;\n\nconst CloseIcon = () => (\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 20 20\" fill=\"none\" aria-hidden=\"true\">\n <path d=\"M5 5l10 10M15 5L5 15\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" />\n </svg>\n);\n\n/**\n * 우상단 X 닫기 버튼 (옵션). 내부적으로 Cancel과 동일하게 닫힘. 기본 `aria-label=\"닫기\"`.\n * 확인을 강제하려면 사용하지 않는 편이 좋습니다 (Cancel/Action으로 명시적 선택 유도).\n */\nconst CloseButton = forwardRef<\n HTMLButtonElement,\n Omit<ComponentPropsWithoutRef<'button'>, 'children'> & { children?: ReactNode }\n>(function AlertDialogCloseButton(\n { className, 'aria-label': ariaLabel = '닫기', children, ...rest },\n ref,\n) {\n return (\n <Alert.Cancel asChild>\n <button\n ref={ref}\n type=\"button\"\n aria-label={ariaLabel}\n className={cx('nuri-alert__close', className)}\n {...rest}\n >\n {children ?? <CloseIcon />}\n </button>\n </Alert.Cancel>\n );\n});\n\nexport const AlertDialog = Object.assign(Root, {\n Trigger,\n Content,\n Header,\n Title,\n Description,\n Body,\n Footer,\n Action,\n Cancel,\n CloseButton,\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","/**\n * tone별 기본 아이콘 — 디자이너 시스템(Figma) 에셋을 그대로 inline SVG로 재현.\n * 구조: 틴트 원 배경 + tone 색 심볼. 색은 토큰(`--nuri-alert-tone-*`)으로 분리.\n * AlertDialog.Content의 `icon` 미지정 시 tone에 맞춰 자동 렌더.\n * `icon={<Custom/>}`로 교체, `icon={null}`로 제거 가능.\n */\nimport type { ReactNode } from 'react';\n\nconst SuccessIcon = () => (\n <svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden=\"true\">\n <circle cx=\"12\" cy=\"12\" r=\"12\" fill=\"var(--nuri-alert-tone-success-bg)\" />\n <path\n d=\"M15.77 9.58L10.65 14.70L8.90 12.95\"\n stroke=\"var(--nuri-alert-tone-success-fg)\"\n strokeWidth=\"2.5\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n);\n\nconst WarningIcon = () => (\n <svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden=\"true\">\n <circle cx=\"12\" cy=\"12\" r=\"12\" fill=\"var(--nuri-alert-tone-warning-bg)\" />\n <path\n d=\"M12 6.75V12\"\n stroke=\"var(--nuri-alert-tone-warning-fg)\"\n strokeWidth=\"2.5\"\n strokeLinecap=\"round\"\n />\n <circle cx=\"12\" cy=\"16.31\" r=\"1.1\" fill=\"var(--nuri-alert-tone-warning-fg)\" />\n </svg>\n);\n\nconst InfoIcon = () => (\n <svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden=\"true\">\n <circle cx=\"12\" cy=\"12\" r=\"12\" fill=\"var(--nuri-alert-tone-info-bg)\" />\n <path\n d=\"M12.25 7h0.01\"\n stroke=\"var(--nuri-alert-tone-info-fg)\"\n strokeWidth=\"3\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n <path\n d=\"M11 12h1.25v5h1.25\"\n stroke=\"var(--nuri-alert-tone-info-fg)\"\n strokeWidth=\"2.5\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n);\n\nexport const defaultToneIcon: Record<'info' | 'success' | 'warning', ReactNode> = {\n info: <InfoIcon />,\n success: <SuccessIcon />,\n warning: <WarningIcon />,\n};\n"],"mappings":"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","names":["jsx","jsxs","Root","Trigger","Content","Title","Description","Action","Cancel"]}
|
|
1
|
+
{"version":3,"sources":["../../src/AlertDialog/AlertDialog.tsx","../../src/utils/cx.ts","../../src/AlertDialog/icons.tsx"],"sourcesContent":["'use client';\n\nimport './styles.css';\nimport * as Alert from '@radix-ui/react-alert-dialog';\nimport { forwardRef } from 'react';\nimport type { ComponentPropsWithoutRef, ElementRef, ReactNode } from 'react';\nimport { cx } from '../utils/cx.js';\nimport { defaultToneIcon } from './icons.js';\n\ntype Tone = 'info' | 'success' | 'warning';\n\n/**\n * 확인 강제형 다이얼로그 (alertdialog). Radix AlertDialog 기반.\n *\n * 일반 Modal과의 차이:\n * - `role=\"alertdialog\"` — 스크린리더가 더 강하게 안내\n * - **바깥(백드롭) 클릭으로 닫히지 않음** — 실수로 닫히는 것 방지\n * - 포커스가 기본적으로 Cancel(취소)에 위치 — 실수로 위험 액션 방지\n * - **중앙 정렬 + 상단 아이콘** (디자인 시스템 표준 레이아웃)\n *\n * Esc 키는 닫힘(키보드 탈출구 — 접근성). 막으려면 Content에 `onEscapeKeyDown` 전달.\n *\n * @example\n * <AlertDialog>\n * <AlertDialog.Trigger asChild><Button>삭제</Button></AlertDialog.Trigger>\n * <AlertDialog.Content tone=\"warning\">\n * <AlertDialog.Header>\n * <AlertDialog.Title>정말 삭제할까요?</AlertDialog.Title>\n * <AlertDialog.Description>되돌릴 수 없습니다.</AlertDialog.Description>\n * </AlertDialog.Header>\n * <AlertDialog.Footer>\n * <AlertDialog.Cancel asChild><Button variant=\"outline\">취소</Button></AlertDialog.Cancel>\n * <AlertDialog.Action asChild><Button onClick={onDelete}>삭제</Button></AlertDialog.Action>\n * </AlertDialog.Footer>\n * </AlertDialog.Content>\n * </AlertDialog>\n */\nconst Root = (props: ComponentPropsWithoutRef<typeof Alert.Root>) => <Alert.Root {...props} />;\nRoot.displayName = 'AlertDialog';\n\n/** 다이얼로그를 여는 트리거. `asChild`로 임의 엘리먼트에 위임. */\nconst Trigger = Alert.Trigger;\n\nexport interface AlertDialogContentProps extends ComponentPropsWithoutRef<typeof Alert.Content> {\n /**\n * 상태 톤 — 기본 아이콘과 그 색을 결정.\n * @default 'info'\n */\n tone?: Tone;\n /**\n * 상단 아이콘.\n * - 미지정: `tone`에 맞는 **기본 아이콘** 자동 렌더\n * - `ReactNode`: 커스텀 아이콘으로 교체\n * - `null`: 아이콘 제거\n */\n icon?: ReactNode | null;\n /** 백드롭(오버레이) 표시 여부. @default true */\n overlay?: boolean;\n /** 포털 렌더 대상. 기본은 document.body. 다른 요소에 렌더하려면 지정. */\n container?: HTMLElement | null;\n}\n\n/** 오버레이 + 포털 + 패널을 한 번에 렌더. 중앙 정렬 + 상단 아이콘 레이아웃. */\nconst Content = forwardRef<ElementRef<typeof Alert.Content>, AlertDialogContentProps>(\n function AlertDialogContent(\n { tone = 'info', icon, overlay = true, container, className, children, ...rest },\n ref,\n ) {\n // icon 미지정(undefined) → 기본 tone 아이콘 / null → 제거 / ReactNode → 교체\n const resolvedIcon = icon === undefined ? defaultToneIcon[tone] : icon;\n return (\n <Alert.Portal container={container}>\n {overlay && <Alert.Overlay className=\"nuri-alert__overlay\" />}\n <Alert.Content\n ref={ref}\n className={cx('nuri-alert', `nuri-alert--${tone}`, className)}\n {...rest}\n >\n {resolvedIcon != null && (\n <div className=\"nuri-alert__icon\" aria-hidden=\"true\">\n {resolvedIcon}\n </div>\n )}\n {children}\n </Alert.Content>\n </Alert.Portal>\n );\n },\n);\n\n/** 헤더 슬롯 (Title/Description 묶음, 중앙 정렬). */\nconst Header = forwardRef<HTMLDivElement, ComponentPropsWithoutRef<'div'>>(\n function AlertDialogHeader({ className, ...rest }, ref) {\n return <div ref={ref} className={cx('nuri-alert__header', className)} {...rest} />;\n },\n);\n\n/** 제목. 자동으로 `aria-labelledby`로 연결됨. */\nconst Title = forwardRef<\n ElementRef<typeof Alert.Title>,\n ComponentPropsWithoutRef<typeof Alert.Title>\n>(function AlertDialogTitle({ className, ...rest }, ref) {\n return <Alert.Title ref={ref} className={cx('nuri-alert__title', className)} {...rest} />;\n});\n\n/** 설명. 자동으로 `aria-describedby`로 연결됨. */\nconst Description = forwardRef<\n ElementRef<typeof Alert.Description>,\n ComponentPropsWithoutRef<typeof Alert.Description>\n>(function AlertDialogDescription({ className, ...rest }, ref) {\n return (\n <Alert.Description ref={ref} className={cx('nuri-alert__description', className)} {...rest} />\n );\n});\n\n/** 본문 슬롯 (추가 콘텐츠). */\nconst Body = forwardRef<HTMLDivElement, ComponentPropsWithoutRef<'div'>>(function AlertDialogBody(\n { className, ...rest },\n ref,\n) {\n return <div ref={ref} className={cx('nuri-alert__body', className)} {...rest} />;\n});\n\nexport interface AlertDialogFooterProps extends ComponentPropsWithoutRef<'div'> {\n /**\n * 버튼 정렬. 디자인 표준은 전체너비 버튼이라 기본값 `full`.\n * @default 'full'\n */\n align?: 'start' | 'center' | 'end' | 'between' | 'full';\n}\n\n/** 푸터 슬롯 (Cancel/Action 배치). 기본 전체너비(`full`). */\nconst Footer = forwardRef<HTMLDivElement, AlertDialogFooterProps>(function AlertDialogFooter(\n { align = 'full', className, ...rest },\n ref,\n) {\n return (\n <div\n ref={ref}\n className={cx('nuri-alert__footer', `nuri-alert__footer--${align}`, className)}\n {...rest}\n />\n );\n});\n\n/**\n * 확인(주 액션) 버튼. 클릭 시 닫힘. `asChild`로 Button 위임.\n * @example <AlertDialog.Action asChild><Button onClick={onConfirm}>삭제</Button></AlertDialog.Action>\n */\nconst Action = Alert.Action;\n\n/**\n * 취소 버튼. 클릭 시 닫힘. 포커스가 기본적으로 여기 위치.\n * @example <AlertDialog.Cancel asChild><Button variant=\"outline\">취소</Button></AlertDialog.Cancel>\n */\nconst Cancel = Alert.Cancel;\n\nconst CloseIcon = () => (\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 20 20\" fill=\"none\" aria-hidden=\"true\">\n <path d=\"M5 5l10 10M15 5L5 15\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" />\n </svg>\n);\n\n/**\n * 우상단 X 닫기 버튼 (옵션). 내부적으로 Cancel과 동일하게 닫힘. 기본 `aria-label=\"닫기\"`.\n * 확인을 강제하려면 사용하지 않는 편이 좋습니다 (Cancel/Action으로 명시적 선택 유도).\n */\nconst CloseButton = forwardRef<\n HTMLButtonElement,\n Omit<ComponentPropsWithoutRef<'button'>, 'children'> & { children?: ReactNode }\n>(function AlertDialogCloseButton(\n { className, 'aria-label': ariaLabel = '닫기', children, ...rest },\n ref,\n) {\n return (\n <Alert.Cancel asChild>\n <button\n ref={ref}\n type=\"button\"\n aria-label={ariaLabel}\n className={cx('nuri-alert__close', className)}\n {...rest}\n >\n {children ?? <CloseIcon />}\n </button>\n </Alert.Cancel>\n );\n});\n\nexport const AlertDialog = Object.assign(Root, {\n Trigger,\n Content,\n Header,\n Title,\n Description,\n Body,\n Footer,\n Action,\n Cancel,\n CloseButton,\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","/**\n * tone별 기본 아이콘 — 디자이너 시스템(Figma) 에셋을 그대로 inline SVG로 재현.\n * 구조: 틴트 원 배경 + tone 색 심볼. 색은 토큰(`--nuri-alert-tone-*`)으로 분리.\n * AlertDialog.Content의 `icon` 미지정 시 tone에 맞춰 자동 렌더.\n * `icon={<Custom/>}`로 교체, `icon={null}`로 제거 가능.\n */\nimport type { ReactNode } from 'react';\n\nconst SuccessIcon = () => (\n <svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden=\"true\">\n <circle cx=\"12\" cy=\"12\" r=\"12\" fill=\"var(--nuri-alert-tone-success-bg)\" />\n <path\n d=\"M15.77 9.58L10.65 14.70L8.90 12.95\"\n stroke=\"var(--nuri-alert-tone-success-fg)\"\n strokeWidth=\"2.5\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n);\n\nconst WarningIcon = () => (\n <svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden=\"true\">\n <circle cx=\"12\" cy=\"12\" r=\"12\" fill=\"var(--nuri-alert-tone-warning-bg)\" />\n <path\n d=\"M12 6.75V12\"\n stroke=\"var(--nuri-alert-tone-warning-fg)\"\n strokeWidth=\"2.5\"\n strokeLinecap=\"round\"\n />\n <circle cx=\"12\" cy=\"16.31\" r=\"1.1\" fill=\"var(--nuri-alert-tone-warning-fg)\" />\n </svg>\n);\n\nconst InfoIcon = () => (\n <svg width=\"24\" height=\"24\" viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden=\"true\">\n <circle cx=\"12\" cy=\"12\" r=\"12\" fill=\"var(--nuri-alert-tone-info-bg)\" />\n <path\n d=\"M12.25 7h0.01\"\n stroke=\"var(--nuri-alert-tone-info-fg)\"\n strokeWidth=\"3\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n <path\n d=\"M11 12h1.25v5h1.25\"\n stroke=\"var(--nuri-alert-tone-info-fg)\"\n strokeWidth=\"2.5\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n);\n\nexport const defaultToneIcon: Record<'info' | 'success' | 'warning', ReactNode> = {\n info: <InfoIcon />,\n success: <SuccessIcon />,\n warning: <WarningIcon />,\n};\n"],"mappings":"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","names":["jsx","jsxs","Root","Trigger","Content","Title","Description","Action","Cancel"]}
|
package/dist/Button/index.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/Button/index.ts","../../src/Button/Button.tsx","../../src/Button/Spinner.tsx","../../src/utils/cx.ts"],"sourcesContent":["export { Button } from './Button.js';\nexport type { ButtonProps } from './Button.js';\n","'use client';\n\nimport './styles.css';\nimport { Slot } from '@radix-ui/react-slot';\nimport { Children, cloneElement, forwardRef, isValidElement } from 'react';\nimport type { ButtonHTMLAttributes, MouseEvent, ReactElement, ReactNode } from 'react';\nimport { Spinner } from './Spinner.js';\nimport { cx } from '../utils/cx.js';\n\ntype Variant = 'primary' | 'secondary' | 'outline' | 'ghost';\ntype Size = 'sm' | 'md' | 'lg';\n\nexport interface ButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'type'> {\n /**\n * 시각 유형.\n * - `primary`: 브랜드 색 배경 (기본)\n * - `secondary`: 브랜드 옅은 톤 배경\n * - `outline`: 투명 배경 + 테두리\n * - `ghost`: 투명 배경, hover 시에만 배경 채움\n * @default 'primary'\n */\n variant?: Variant;\n\n /**\n * 사이즈.\n * - `sm`: 36px 높이 — 밀집된 UI, 테이블 액션\n * - `md`: 42px 높이 — 일반 콘텐츠 영역 (기본)\n * - `lg`: 48px 높이 — 주요 CTA, 모바일 터치\n * @default 'md'\n */\n size?: Size;\n\n /**\n * 라벨 좌측에 표시할 아이콘.\n * @example <Button iconLeft={<SearchIcon />}>검색</Button>\n */\n iconLeft?: ReactNode;\n\n /**\n * 라벨 우측에 표시할 아이콘.\n */\n iconRight?: ReactNode;\n\n /**\n * 아이콘만 표시하는 정사각 버튼. **`aria-label` 필수**\n * (icon-only는 시각적으로만 의미가 있으므로 스크린리더용 라벨이 없으면 사용자가 용도를 알 수 없음).\n * children은 단일 아이콘 엘리먼트.\n * @example <Button iconOnly aria-label=\"닫기\"><CloseIcon /></Button>\n */\n iconOnly?: boolean;\n\n /**\n * 컨테이너 전체 너비로 확장.\n */\n block?: boolean;\n\n /**\n * 로딩 중 표시. 스피너를 좌측 아이콘 자리에 표시하고, 클릭을 차단하며 `aria-busy=\"true\"`를 부여.\n */\n loading?: boolean;\n\n /**\n * Radix Slot으로 자식 엘리먼트를 렌더 — 자식이 Button의 className·ref·이벤트 핸들러를 모두 물려받음.\n * 링크를 버튼처럼 보이게 할 때 가장 자주 사용 (next/link, react-router Link 등).\n * @example\n * <Button asChild><Link href=\"/about\">소개</Link></Button>\n */\n asChild?: boolean;\n\n /**\n * 폼 버튼 타입. 의도치 않은 form submit 방지를 위해 기본값 `'button'`.\n * 폼 제출 버튼이면 명시적으로 `type=\"submit\"` 지정.\n * @default 'button'\n */\n type?: 'button' | 'submit' | 'reset';\n\n children?: ReactNode;\n}\n\nexport const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(\n {\n variant = 'primary',\n size = 'md',\n iconLeft,\n iconRight,\n iconOnly = false,\n block = false,\n loading = false,\n disabled = false,\n asChild = false,\n type = 'button',\n className,\n children,\n onClick,\n ...rest\n },\n ref,\n) {\n const isBlocked = disabled || loading;\n\n if (\n process.env.NODE_ENV !== 'production' &&\n iconOnly &&\n !rest['aria-label'] &&\n !rest['aria-labelledby']\n ) {\n // iconOnly 버튼은 텍스트가 없어 스크린리더가 용도를 알 수 없음 → 개발 모드 경고\n console.warn(\n '[nuri/Button] `iconOnly` 버튼은 `aria-label`(또는 `aria-labelledby`)이 필요합니다.',\n );\n }\n\n const hasLeftSlot = !iconOnly && (loading || iconLeft != null);\n const hasRightSlot = !iconOnly && !loading && iconRight != null;\n\n const classes = cx(\n 'nuri-button',\n `nuri-button--${variant}`,\n `nuri-button--${size}`,\n block && 'nuri-button--block',\n iconOnly && 'nuri-button--icon-only',\n isBlocked && 'nuri-button--disabled',\n loading && 'nuri-button--loading',\n hasLeftSlot && 'nuri-button--has-left',\n hasRightSlot && 'nuri-button--has-right',\n className,\n );\n\n const handleClick = (e: MouseEvent<HTMLButtonElement>) => {\n if (isBlocked) {\n e.preventDefault();\n e.stopPropagation();\n return;\n }\n onClick?.(e);\n };\n\n const renderContent = (content: ReactNode) => {\n if (iconOnly) {\n return (\n <span className=\"nuri-button__icon\" aria-hidden=\"true\">\n {loading ? <Spinner /> : content}\n </span>\n );\n }\n return (\n <>\n {loading ? (\n <Spinner />\n ) : (\n iconLeft != null && (\n <span className=\"nuri-button__icon nuri-button__icon--left\" aria-hidden=\"true\">\n {iconLeft}\n </span>\n )\n )}\n {content}\n {hasRightSlot && (\n <span className=\"nuri-button__icon nuri-button__icon--right\" aria-hidden=\"true\">\n {iconRight}\n </span>\n )}\n </>\n );\n };\n\n if (asChild) {\n const child = Children.only(children);\n if (!isValidElement(child)) return null;\n const childElement = child as ReactElement<{ children?: ReactNode }>;\n return (\n <Slot\n ref={ref}\n className={classes}\n aria-disabled={isBlocked || undefined}\n aria-busy={loading || undefined}\n onClick={handleClick}\n {...rest}\n >\n {cloneElement(childElement, undefined, renderContent(childElement.props.children))}\n </Slot>\n );\n }\n\n return (\n <button\n ref={ref}\n type={type}\n className={classes}\n // disabled = 진짜 비활성 → native disabled (포커스 불가, 의도된 동작)\n // loading = 일시적 처리중 → aria-disabled로 포커스 유지 (클릭은 handleClick이 가드)\n disabled={disabled}\n aria-disabled={loading || undefined}\n aria-busy={loading || undefined}\n onClick={handleClick}\n {...rest}\n >\n {renderContent(children)}\n </button>\n );\n});\n","export const Spinner = () => (\n <span className=\"nuri-button__icon\" aria-hidden=\"true\">\n <svg className=\"nuri-button__spinner\" viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" fill=\"none\">\n <circle cx=\"8\" cy=\"8\" r=\"6\" stroke=\"currentColor\" strokeOpacity=\"0.25\" strokeWidth=\"2\" />\n <path d=\"M14 8a6 6 0 0 0-6-6\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" />\n </svg>\n </span>\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":"
|
|
1
|
+
{"version":3,"sources":["../../src/Button/index.ts","../../src/Button/Button.tsx","../../src/Button/Spinner.tsx","../../src/utils/cx.ts"],"sourcesContent":["export { Button } from './Button.js';\nexport type { ButtonProps } from './Button.js';\n","'use client';\n\nimport './styles.css';\nimport { Slot } from '@radix-ui/react-slot';\nimport { Children, cloneElement, forwardRef, isValidElement } from 'react';\nimport type { ButtonHTMLAttributes, MouseEvent, ReactElement, ReactNode } from 'react';\nimport { Spinner } from './Spinner.js';\nimport { cx } from '../utils/cx.js';\n\ntype Variant = 'primary' | 'secondary' | 'outline' | 'ghost';\ntype Size = 'sm' | 'md' | 'lg';\n\nexport interface ButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'type'> {\n /**\n * 시각 유형.\n * - `primary`: 브랜드 색 배경 (기본)\n * - `secondary`: 브랜드 옅은 톤 배경\n * - `outline`: 투명 배경 + 테두리\n * - `ghost`: 투명 배경, hover 시에만 배경 채움\n * @default 'primary'\n */\n variant?: Variant;\n\n /**\n * 사이즈.\n * - `sm`: 36px 높이 — 밀집된 UI, 테이블 액션\n * - `md`: 42px 높이 — 일반 콘텐츠 영역 (기본)\n * - `lg`: 48px 높이 — 주요 CTA, 모바일 터치\n * @default 'md'\n */\n size?: Size;\n\n /**\n * 라벨 좌측에 표시할 아이콘.\n * @example <Button iconLeft={<SearchIcon />}>검색</Button>\n */\n iconLeft?: ReactNode;\n\n /**\n * 라벨 우측에 표시할 아이콘.\n */\n iconRight?: ReactNode;\n\n /**\n * 아이콘만 표시하는 정사각 버튼. **`aria-label` 필수**\n * (icon-only는 시각적으로만 의미가 있으므로 스크린리더용 라벨이 없으면 사용자가 용도를 알 수 없음).\n * children은 단일 아이콘 엘리먼트.\n * @example <Button iconOnly aria-label=\"닫기\"><CloseIcon /></Button>\n */\n iconOnly?: boolean;\n\n /**\n * 컨테이너 전체 너비로 확장.\n */\n block?: boolean;\n\n /**\n * 로딩 중 표시. 스피너를 좌측 아이콘 자리에 표시하고, 클릭을 차단하며 `aria-busy=\"true\"`를 부여.\n */\n loading?: boolean;\n\n /**\n * Radix Slot으로 자식 엘리먼트를 렌더 — 자식이 Button의 className·ref·이벤트 핸들러를 모두 물려받음.\n * 링크를 버튼처럼 보이게 할 때 가장 자주 사용 (next/link, react-router Link 등).\n * @example\n * <Button asChild><Link href=\"/about\">소개</Link></Button>\n */\n asChild?: boolean;\n\n /**\n * 폼 버튼 타입. 의도치 않은 form submit 방지를 위해 기본값 `'button'`.\n * 폼 제출 버튼이면 명시적으로 `type=\"submit\"` 지정.\n * @default 'button'\n */\n type?: 'button' | 'submit' | 'reset';\n\n children?: ReactNode;\n}\n\nexport const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(\n {\n variant = 'primary',\n size = 'md',\n iconLeft,\n iconRight,\n iconOnly = false,\n block = false,\n loading = false,\n disabled = false,\n asChild = false,\n type = 'button',\n className,\n children,\n onClick,\n ...rest\n },\n ref,\n) {\n const isBlocked = disabled || loading;\n\n if (\n process.env.NODE_ENV !== 'production' &&\n iconOnly &&\n !rest['aria-label'] &&\n !rest['aria-labelledby']\n ) {\n // iconOnly 버튼은 텍스트가 없어 스크린리더가 용도를 알 수 없음 → 개발 모드 경고\n console.warn(\n '[nuri/Button] `iconOnly` 버튼은 `aria-label`(또는 `aria-labelledby`)이 필요합니다.',\n );\n }\n\n const hasLeftSlot = !iconOnly && (loading || iconLeft != null);\n const hasRightSlot = !iconOnly && !loading && iconRight != null;\n\n const classes = cx(\n 'nuri-button',\n `nuri-button--${variant}`,\n `nuri-button--${size}`,\n block && 'nuri-button--block',\n iconOnly && 'nuri-button--icon-only',\n isBlocked && 'nuri-button--disabled',\n loading && 'nuri-button--loading',\n hasLeftSlot && 'nuri-button--has-left',\n hasRightSlot && 'nuri-button--has-right',\n className,\n );\n\n const handleClick = (e: MouseEvent<HTMLButtonElement>) => {\n if (isBlocked) {\n e.preventDefault();\n e.stopPropagation();\n return;\n }\n onClick?.(e);\n };\n\n const renderContent = (content: ReactNode) => {\n if (iconOnly) {\n return (\n <span className=\"nuri-button__icon\" aria-hidden=\"true\">\n {loading ? <Spinner /> : content}\n </span>\n );\n }\n return (\n <>\n {loading ? (\n <Spinner />\n ) : (\n iconLeft != null && (\n <span className=\"nuri-button__icon nuri-button__icon--left\" aria-hidden=\"true\">\n {iconLeft}\n </span>\n )\n )}\n {content}\n {hasRightSlot && (\n <span className=\"nuri-button__icon nuri-button__icon--right\" aria-hidden=\"true\">\n {iconRight}\n </span>\n )}\n </>\n );\n };\n\n if (asChild) {\n const child = Children.only(children);\n if (!isValidElement(child)) return null;\n const childElement = child as ReactElement<{ children?: ReactNode }>;\n return (\n <Slot\n ref={ref}\n className={classes}\n aria-disabled={isBlocked || undefined}\n aria-busy={loading || undefined}\n onClick={handleClick}\n {...rest}\n >\n {cloneElement(childElement, undefined, renderContent(childElement.props.children))}\n </Slot>\n );\n }\n\n return (\n <button\n ref={ref}\n type={type}\n className={classes}\n // disabled = 진짜 비활성 → native disabled (포커스 불가, 의도된 동작)\n // loading = 일시적 처리중 → aria-disabled로 포커스 유지 (클릭은 handleClick이 가드)\n disabled={disabled}\n aria-disabled={loading || undefined}\n aria-busy={loading || undefined}\n onClick={handleClick}\n {...rest}\n >\n {renderContent(children)}\n </button>\n );\n});\n","export const Spinner = () => (\n <span className=\"nuri-button__icon\" aria-hidden=\"true\">\n <svg className=\"nuri-button__spinner\" viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" fill=\"none\">\n <circle cx=\"8\" cy=\"8\" r=\"6\" stroke=\"currentColor\" strokeOpacity=\"0.25\" strokeWidth=\"2\" />\n <path d=\"M14 8a6 6 0 0 0-6-6\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" />\n </svg>\n </span>\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":"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","names":["import_jsx_runtime","Button"]}
|
package/dist/Button/index.js
CHANGED
package/dist/Button/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/Button/Button.tsx","../../src/Button/Spinner.tsx","../../src/utils/cx.ts"],"sourcesContent":["'use client';\n\nimport './styles.css';\nimport { Slot } from '@radix-ui/react-slot';\nimport { Children, cloneElement, forwardRef, isValidElement } from 'react';\nimport type { ButtonHTMLAttributes, MouseEvent, ReactElement, ReactNode } from 'react';\nimport { Spinner } from './Spinner.js';\nimport { cx } from '../utils/cx.js';\n\ntype Variant = 'primary' | 'secondary' | 'outline' | 'ghost';\ntype Size = 'sm' | 'md' | 'lg';\n\nexport interface ButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'type'> {\n /**\n * 시각 유형.\n * - `primary`: 브랜드 색 배경 (기본)\n * - `secondary`: 브랜드 옅은 톤 배경\n * - `outline`: 투명 배경 + 테두리\n * - `ghost`: 투명 배경, hover 시에만 배경 채움\n * @default 'primary'\n */\n variant?: Variant;\n\n /**\n * 사이즈.\n * - `sm`: 36px 높이 — 밀집된 UI, 테이블 액션\n * - `md`: 42px 높이 — 일반 콘텐츠 영역 (기본)\n * - `lg`: 48px 높이 — 주요 CTA, 모바일 터치\n * @default 'md'\n */\n size?: Size;\n\n /**\n * 라벨 좌측에 표시할 아이콘.\n * @example <Button iconLeft={<SearchIcon />}>검색</Button>\n */\n iconLeft?: ReactNode;\n\n /**\n * 라벨 우측에 표시할 아이콘.\n */\n iconRight?: ReactNode;\n\n /**\n * 아이콘만 표시하는 정사각 버튼. **`aria-label` 필수**\n * (icon-only는 시각적으로만 의미가 있으므로 스크린리더용 라벨이 없으면 사용자가 용도를 알 수 없음).\n * children은 단일 아이콘 엘리먼트.\n * @example <Button iconOnly aria-label=\"닫기\"><CloseIcon /></Button>\n */\n iconOnly?: boolean;\n\n /**\n * 컨테이너 전체 너비로 확장.\n */\n block?: boolean;\n\n /**\n * 로딩 중 표시. 스피너를 좌측 아이콘 자리에 표시하고, 클릭을 차단하며 `aria-busy=\"true\"`를 부여.\n */\n loading?: boolean;\n\n /**\n * Radix Slot으로 자식 엘리먼트를 렌더 — 자식이 Button의 className·ref·이벤트 핸들러를 모두 물려받음.\n * 링크를 버튼처럼 보이게 할 때 가장 자주 사용 (next/link, react-router Link 등).\n * @example\n * <Button asChild><Link href=\"/about\">소개</Link></Button>\n */\n asChild?: boolean;\n\n /**\n * 폼 버튼 타입. 의도치 않은 form submit 방지를 위해 기본값 `'button'`.\n * 폼 제출 버튼이면 명시적으로 `type=\"submit\"` 지정.\n * @default 'button'\n */\n type?: 'button' | 'submit' | 'reset';\n\n children?: ReactNode;\n}\n\nexport const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(\n {\n variant = 'primary',\n size = 'md',\n iconLeft,\n iconRight,\n iconOnly = false,\n block = false,\n loading = false,\n disabled = false,\n asChild = false,\n type = 'button',\n className,\n children,\n onClick,\n ...rest\n },\n ref,\n) {\n const isBlocked = disabled || loading;\n\n if (\n process.env.NODE_ENV !== 'production' &&\n iconOnly &&\n !rest['aria-label'] &&\n !rest['aria-labelledby']\n ) {\n // iconOnly 버튼은 텍스트가 없어 스크린리더가 용도를 알 수 없음 → 개발 모드 경고\n console.warn(\n '[nuri/Button] `iconOnly` 버튼은 `aria-label`(또는 `aria-labelledby`)이 필요합니다.',\n );\n }\n\n const hasLeftSlot = !iconOnly && (loading || iconLeft != null);\n const hasRightSlot = !iconOnly && !loading && iconRight != null;\n\n const classes = cx(\n 'nuri-button',\n `nuri-button--${variant}`,\n `nuri-button--${size}`,\n block && 'nuri-button--block',\n iconOnly && 'nuri-button--icon-only',\n isBlocked && 'nuri-button--disabled',\n loading && 'nuri-button--loading',\n hasLeftSlot && 'nuri-button--has-left',\n hasRightSlot && 'nuri-button--has-right',\n className,\n );\n\n const handleClick = (e: MouseEvent<HTMLButtonElement>) => {\n if (isBlocked) {\n e.preventDefault();\n e.stopPropagation();\n return;\n }\n onClick?.(e);\n };\n\n const renderContent = (content: ReactNode) => {\n if (iconOnly) {\n return (\n <span className=\"nuri-button__icon\" aria-hidden=\"true\">\n {loading ? <Spinner /> : content}\n </span>\n );\n }\n return (\n <>\n {loading ? (\n <Spinner />\n ) : (\n iconLeft != null && (\n <span className=\"nuri-button__icon nuri-button__icon--left\" aria-hidden=\"true\">\n {iconLeft}\n </span>\n )\n )}\n {content}\n {hasRightSlot && (\n <span className=\"nuri-button__icon nuri-button__icon--right\" aria-hidden=\"true\">\n {iconRight}\n </span>\n )}\n </>\n );\n };\n\n if (asChild) {\n const child = Children.only(children);\n if (!isValidElement(child)) return null;\n const childElement = child as ReactElement<{ children?: ReactNode }>;\n return (\n <Slot\n ref={ref}\n className={classes}\n aria-disabled={isBlocked || undefined}\n aria-busy={loading || undefined}\n onClick={handleClick}\n {...rest}\n >\n {cloneElement(childElement, undefined, renderContent(childElement.props.children))}\n </Slot>\n );\n }\n\n return (\n <button\n ref={ref}\n type={type}\n className={classes}\n // disabled = 진짜 비활성 → native disabled (포커스 불가, 의도된 동작)\n // loading = 일시적 처리중 → aria-disabled로 포커스 유지 (클릭은 handleClick이 가드)\n disabled={disabled}\n aria-disabled={loading || undefined}\n aria-busy={loading || undefined}\n onClick={handleClick}\n {...rest}\n >\n {renderContent(children)}\n </button>\n );\n});\n","export const Spinner = () => (\n <span className=\"nuri-button__icon\" aria-hidden=\"true\">\n <svg className=\"nuri-button__spinner\" viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" fill=\"none\">\n <circle cx=\"8\" cy=\"8\" r=\"6\" stroke=\"currentColor\" strokeOpacity=\"0.25\" strokeWidth=\"2\" />\n <path d=\"M14 8a6 6 0 0 0-6-6\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" />\n </svg>\n </span>\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":"
|
|
1
|
+
{"version":3,"sources":["../../src/Button/Button.tsx","../../src/Button/Spinner.tsx","../../src/utils/cx.ts"],"sourcesContent":["'use client';\n\nimport './styles.css';\nimport { Slot } from '@radix-ui/react-slot';\nimport { Children, cloneElement, forwardRef, isValidElement } from 'react';\nimport type { ButtonHTMLAttributes, MouseEvent, ReactElement, ReactNode } from 'react';\nimport { Spinner } from './Spinner.js';\nimport { cx } from '../utils/cx.js';\n\ntype Variant = 'primary' | 'secondary' | 'outline' | 'ghost';\ntype Size = 'sm' | 'md' | 'lg';\n\nexport interface ButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'type'> {\n /**\n * 시각 유형.\n * - `primary`: 브랜드 색 배경 (기본)\n * - `secondary`: 브랜드 옅은 톤 배경\n * - `outline`: 투명 배경 + 테두리\n * - `ghost`: 투명 배경, hover 시에만 배경 채움\n * @default 'primary'\n */\n variant?: Variant;\n\n /**\n * 사이즈.\n * - `sm`: 36px 높이 — 밀집된 UI, 테이블 액션\n * - `md`: 42px 높이 — 일반 콘텐츠 영역 (기본)\n * - `lg`: 48px 높이 — 주요 CTA, 모바일 터치\n * @default 'md'\n */\n size?: Size;\n\n /**\n * 라벨 좌측에 표시할 아이콘.\n * @example <Button iconLeft={<SearchIcon />}>검색</Button>\n */\n iconLeft?: ReactNode;\n\n /**\n * 라벨 우측에 표시할 아이콘.\n */\n iconRight?: ReactNode;\n\n /**\n * 아이콘만 표시하는 정사각 버튼. **`aria-label` 필수**\n * (icon-only는 시각적으로만 의미가 있으므로 스크린리더용 라벨이 없으면 사용자가 용도를 알 수 없음).\n * children은 단일 아이콘 엘리먼트.\n * @example <Button iconOnly aria-label=\"닫기\"><CloseIcon /></Button>\n */\n iconOnly?: boolean;\n\n /**\n * 컨테이너 전체 너비로 확장.\n */\n block?: boolean;\n\n /**\n * 로딩 중 표시. 스피너를 좌측 아이콘 자리에 표시하고, 클릭을 차단하며 `aria-busy=\"true\"`를 부여.\n */\n loading?: boolean;\n\n /**\n * Radix Slot으로 자식 엘리먼트를 렌더 — 자식이 Button의 className·ref·이벤트 핸들러를 모두 물려받음.\n * 링크를 버튼처럼 보이게 할 때 가장 자주 사용 (next/link, react-router Link 등).\n * @example\n * <Button asChild><Link href=\"/about\">소개</Link></Button>\n */\n asChild?: boolean;\n\n /**\n * 폼 버튼 타입. 의도치 않은 form submit 방지를 위해 기본값 `'button'`.\n * 폼 제출 버튼이면 명시적으로 `type=\"submit\"` 지정.\n * @default 'button'\n */\n type?: 'button' | 'submit' | 'reset';\n\n children?: ReactNode;\n}\n\nexport const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(\n {\n variant = 'primary',\n size = 'md',\n iconLeft,\n iconRight,\n iconOnly = false,\n block = false,\n loading = false,\n disabled = false,\n asChild = false,\n type = 'button',\n className,\n children,\n onClick,\n ...rest\n },\n ref,\n) {\n const isBlocked = disabled || loading;\n\n if (\n process.env.NODE_ENV !== 'production' &&\n iconOnly &&\n !rest['aria-label'] &&\n !rest['aria-labelledby']\n ) {\n // iconOnly 버튼은 텍스트가 없어 스크린리더가 용도를 알 수 없음 → 개발 모드 경고\n console.warn(\n '[nuri/Button] `iconOnly` 버튼은 `aria-label`(또는 `aria-labelledby`)이 필요합니다.',\n );\n }\n\n const hasLeftSlot = !iconOnly && (loading || iconLeft != null);\n const hasRightSlot = !iconOnly && !loading && iconRight != null;\n\n const classes = cx(\n 'nuri-button',\n `nuri-button--${variant}`,\n `nuri-button--${size}`,\n block && 'nuri-button--block',\n iconOnly && 'nuri-button--icon-only',\n isBlocked && 'nuri-button--disabled',\n loading && 'nuri-button--loading',\n hasLeftSlot && 'nuri-button--has-left',\n hasRightSlot && 'nuri-button--has-right',\n className,\n );\n\n const handleClick = (e: MouseEvent<HTMLButtonElement>) => {\n if (isBlocked) {\n e.preventDefault();\n e.stopPropagation();\n return;\n }\n onClick?.(e);\n };\n\n const renderContent = (content: ReactNode) => {\n if (iconOnly) {\n return (\n <span className=\"nuri-button__icon\" aria-hidden=\"true\">\n {loading ? <Spinner /> : content}\n </span>\n );\n }\n return (\n <>\n {loading ? (\n <Spinner />\n ) : (\n iconLeft != null && (\n <span className=\"nuri-button__icon nuri-button__icon--left\" aria-hidden=\"true\">\n {iconLeft}\n </span>\n )\n )}\n {content}\n {hasRightSlot && (\n <span className=\"nuri-button__icon nuri-button__icon--right\" aria-hidden=\"true\">\n {iconRight}\n </span>\n )}\n </>\n );\n };\n\n if (asChild) {\n const child = Children.only(children);\n if (!isValidElement(child)) return null;\n const childElement = child as ReactElement<{ children?: ReactNode }>;\n return (\n <Slot\n ref={ref}\n className={classes}\n aria-disabled={isBlocked || undefined}\n aria-busy={loading || undefined}\n onClick={handleClick}\n {...rest}\n >\n {cloneElement(childElement, undefined, renderContent(childElement.props.children))}\n </Slot>\n );\n }\n\n return (\n <button\n ref={ref}\n type={type}\n className={classes}\n // disabled = 진짜 비활성 → native disabled (포커스 불가, 의도된 동작)\n // loading = 일시적 처리중 → aria-disabled로 포커스 유지 (클릭은 handleClick이 가드)\n disabled={disabled}\n aria-disabled={loading || undefined}\n aria-busy={loading || undefined}\n onClick={handleClick}\n {...rest}\n >\n {renderContent(children)}\n </button>\n );\n});\n","export const Spinner = () => (\n <span className=\"nuri-button__icon\" aria-hidden=\"true\">\n <svg className=\"nuri-button__spinner\" viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" fill=\"none\">\n <circle cx=\"8\" cy=\"8\" r=\"6\" stroke=\"currentColor\" strokeOpacity=\"0.25\" strokeWidth=\"2\" />\n <path d=\"M14 8a6 6 0 0 0-6-6\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" />\n </svg>\n </span>\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":"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","names":["jsx","jsxs","Button"]}
|
package/dist/Checkbox/index.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/Checkbox/index.ts","../../src/Checkbox/Checkbox.tsx","../../src/utils/cx.ts"],"sourcesContent":["export { Checkbox } from './Checkbox.js';\nexport type { CheckboxProps } from './Checkbox.js';\n","'use client';\n\nimport './styles.css';\nimport * as RadixCheckbox from '@radix-ui/react-checkbox';\nimport { forwardRef, useId } from 'react';\nimport type { ReactNode } from 'react';\nimport { cx } from '../utils/cx.js';\n\ntype Size = 'sm' | 'md';\n\nexport interface CheckboxProps {\n /** 라벨 (컨트롤 우측). 지정 시 클릭 영역에 포함되고 자동 연결됨. 없으면 `aria-label` 필수. */\n label?: ReactNode;\n /**\n * 사이즈. md 16px(디자인 Large, 기본) / sm 12px(디자인 Small).\n * @default 'md'\n */\n size?: Size;\n /** 체크 상태 (제어형). `'indeterminate'`는 부분 선택 표시. */\n checked?: boolean | 'indeterminate';\n /** 초기 체크 상태 (비제어형). */\n defaultChecked?: boolean;\n onCheckedChange?: (checked: boolean | 'indeterminate') => void;\n disabled?: boolean;\n /** 폼 전송용 name — 네이티브 checkbox처럼 폼에 참여. */\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/** 체크마크. TODO(Figma 실측): 디자이너 아이콘으로 교체 예정 — 잠정 표준형. */\nconst CheckIcon = () => (\n <svg viewBox=\"0 0 12 12\" fill=\"none\" aria-hidden=\"true\">\n <path\n d=\"M2.5 6.2l2.4 2.4 4.6-5\"\n stroke=\"currentColor\"\n strokeWidth=\"1.6\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n);\n\n/** 부분 선택(indeterminate) 표시 — 가로 막대. */\nconst IndeterminateIcon = () => (\n <svg viewBox=\"0 0 12 12\" fill=\"none\" aria-hidden=\"true\">\n <path d=\"M2.8 6h6.4\" stroke=\"currentColor\" strokeWidth=\"1.6\" strokeLinecap=\"round\" />\n </svg>\n);\n\n/**\n * 체크박스. Radix Checkbox 기반 — Space 토글·aria-checked·폼 참여 자동.\n * 라벨을 지정하면 라벨 클릭으로도 토글된다.\n *\n * @example\n * <Checkbox label=\"이용약관에 동의합니다\" checked={agreed} onCheckedChange={setAgreed} />\n *\n * @example 부분 선택 (전체선택 헤더 등)\n * <Checkbox label=\"전체 선택\" checked={someChecked ? 'indeterminate' : allChecked} … />\n */\nexport const Checkbox = forwardRef<HTMLButtonElement, CheckboxProps>(function Checkbox(\n { label, size = 'md', disabled = false, id: idProp, className, checked, ...rest },\n ref,\n) {\n const reactId = useId();\n const id = idProp ?? `nuri-checkbox-${reactId}`;\n\n return (\n <div className={cx('nuri-checkbox', `nuri-checkbox--${size}`, className)}>\n <RadixCheckbox.Root\n ref={ref}\n id={id}\n className=\"nuri-checkbox__control\"\n disabled={disabled}\n checked={checked}\n {...rest}\n >\n <RadixCheckbox.Indicator className=\"nuri-checkbox__indicator\">\n {checked === 'indeterminate' ? <IndeterminateIcon /> : <CheckIcon />}\n </RadixCheckbox.Indicator>\n </RadixCheckbox.Root>\n {label != null && (\n <label htmlFor={id} className=\"nuri-checkbox__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":"
|
|
1
|
+
{"version":3,"sources":["../../src/Checkbox/index.ts","../../src/Checkbox/Checkbox.tsx","../../src/utils/cx.ts"],"sourcesContent":["export { Checkbox } from './Checkbox.js';\nexport type { CheckboxProps } from './Checkbox.js';\n","'use client';\n\nimport './styles.css';\nimport * as RadixCheckbox from '@radix-ui/react-checkbox';\nimport { forwardRef, useId } from 'react';\nimport type { ReactNode } from 'react';\nimport { cx } from '../utils/cx.js';\n\ntype Size = 'sm' | 'md';\n\nexport interface CheckboxProps {\n /** 라벨 (컨트롤 우측). 지정 시 클릭 영역에 포함되고 자동 연결됨. 없으면 `aria-label` 필수. */\n label?: ReactNode;\n /**\n * 사이즈. md 16px(디자인 Large, 기본) / sm 12px(디자인 Small).\n * @default 'md'\n */\n size?: Size;\n /** 체크 상태 (제어형). `'indeterminate'`는 부분 선택 표시. */\n checked?: boolean | 'indeterminate';\n /** 초기 체크 상태 (비제어형). */\n defaultChecked?: boolean;\n onCheckedChange?: (checked: boolean | 'indeterminate') => void;\n disabled?: boolean;\n /** 폼 전송용 name — 네이티브 checkbox처럼 폼에 참여. */\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/** 체크마크. TODO(Figma 실측): 디자이너 아이콘으로 교체 예정 — 잠정 표준형. */\nconst CheckIcon = () => (\n <svg viewBox=\"0 0 12 12\" fill=\"none\" aria-hidden=\"true\">\n <path\n d=\"M2.5 6.2l2.4 2.4 4.6-5\"\n stroke=\"currentColor\"\n strokeWidth=\"1.6\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n);\n\n/** 부분 선택(indeterminate) 표시 — 가로 막대. */\nconst IndeterminateIcon = () => (\n <svg viewBox=\"0 0 12 12\" fill=\"none\" aria-hidden=\"true\">\n <path d=\"M2.8 6h6.4\" stroke=\"currentColor\" strokeWidth=\"1.6\" strokeLinecap=\"round\" />\n </svg>\n);\n\n/**\n * 체크박스. Radix Checkbox 기반 — Space 토글·aria-checked·폼 참여 자동.\n * 라벨을 지정하면 라벨 클릭으로도 토글된다.\n *\n * @example\n * <Checkbox label=\"이용약관에 동의합니다\" checked={agreed} onCheckedChange={setAgreed} />\n *\n * @example 부분 선택 (전체선택 헤더 등)\n * <Checkbox label=\"전체 선택\" checked={someChecked ? 'indeterminate' : allChecked} … />\n */\nexport const Checkbox = forwardRef<HTMLButtonElement, CheckboxProps>(function Checkbox(\n { label, size = 'md', disabled = false, id: idProp, className, checked, ...rest },\n ref,\n) {\n const reactId = useId();\n const id = idProp ?? `nuri-checkbox-${reactId}`;\n\n return (\n <div className={cx('nuri-checkbox', `nuri-checkbox--${size}`, className)}>\n <RadixCheckbox.Root\n ref={ref}\n id={id}\n className=\"nuri-checkbox__control\"\n disabled={disabled}\n checked={checked}\n {...rest}\n >\n <RadixCheckbox.Indicator className=\"nuri-checkbox__indicator\">\n {checked === 'indeterminate' ? <IndeterminateIcon /> : <CheckIcon />}\n </RadixCheckbox.Indicator>\n </RadixCheckbox.Root>\n {label != null && (\n <label htmlFor={id} className=\"nuri-checkbox__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,oBAA+B;AAC/B,mBAAkC;;;ACA3B,SAAS,MAAM,SAA2D;AAC/E,SAAO,QAAQ,OAAO,OAAO,EAAE,KAAK,GAAG;AACzC;;;AD+BI;AAFJ,IAAM,YAAY,MAChB,4CAAC,SAAI,SAAQ,aAAY,MAAK,QAAO,eAAY,QAC/C;AAAA,EAAC;AAAA;AAAA,IACC,GAAE;AAAA,IACF,QAAO;AAAA,IACP,aAAY;AAAA,IACZ,eAAc;AAAA,IACd,gBAAe;AAAA;AACjB,GACF;AAIF,IAAM,oBAAoB,MACxB,4CAAC,SAAI,SAAQ,aAAY,MAAK,QAAO,eAAY,QAC/C,sDAAC,UAAK,GAAE,cAAa,QAAO,gBAAe,aAAY,OAAM,eAAc,SAAQ,GACrF;AAaK,IAAM,eAAW,yBAA6C,SAASA,UAC5E,EAAE,OAAO,OAAO,MAAM,WAAW,OAAO,IAAI,QAAQ,WAAW,SAAS,GAAG,KAAK,GAChF,KACA;AACA,QAAM,cAAU,oBAAM;AACtB,QAAM,KAAK,UAAU,iBAAiB,OAAO;AAE7C,SACE,6CAAC,SAAI,WAAW,GAAG,iBAAiB,kBAAkB,IAAI,IAAI,SAAS,GACrE;AAAA;AAAA,MAAe;AAAA,MAAd;AAAA,QACC;AAAA,QACA;AAAA,QACA,WAAU;AAAA,QACV;AAAA,QACA;AAAA,QACC,GAAG;AAAA,QAEJ,sDAAe,yBAAd,EAAwB,WAAU,4BAChC,sBAAY,kBAAkB,4CAAC,qBAAkB,IAAK,4CAAC,aAAU,GACpE;AAAA;AAAA,IACF;AAAA,IACC,SAAS,QACR,4CAAC,WAAM,SAAS,IAAI,WAAU,wBAC3B,iBACH;AAAA,KAEJ;AAEJ,CAAC;","names":["Checkbox"]}
|
package/dist/Checkbox/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/Checkbox/Checkbox.tsx","../../src/utils/cx.ts"],"sourcesContent":["'use client';\n\nimport './styles.css';\nimport * as RadixCheckbox from '@radix-ui/react-checkbox';\nimport { forwardRef, useId } from 'react';\nimport type { ReactNode } from 'react';\nimport { cx } from '../utils/cx.js';\n\ntype Size = 'sm' | 'md';\n\nexport interface CheckboxProps {\n /** 라벨 (컨트롤 우측). 지정 시 클릭 영역에 포함되고 자동 연결됨. 없으면 `aria-label` 필수. */\n label?: ReactNode;\n /**\n * 사이즈. md 16px(디자인 Large, 기본) / sm 12px(디자인 Small).\n * @default 'md'\n */\n size?: Size;\n /** 체크 상태 (제어형). `'indeterminate'`는 부분 선택 표시. */\n checked?: boolean | 'indeterminate';\n /** 초기 체크 상태 (비제어형). */\n defaultChecked?: boolean;\n onCheckedChange?: (checked: boolean | 'indeterminate') => void;\n disabled?: boolean;\n /** 폼 전송용 name — 네이티브 checkbox처럼 폼에 참여. */\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/** 체크마크. TODO(Figma 실측): 디자이너 아이콘으로 교체 예정 — 잠정 표준형. */\nconst CheckIcon = () => (\n <svg viewBox=\"0 0 12 12\" fill=\"none\" aria-hidden=\"true\">\n <path\n d=\"M2.5 6.2l2.4 2.4 4.6-5\"\n stroke=\"currentColor\"\n strokeWidth=\"1.6\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n);\n\n/** 부분 선택(indeterminate) 표시 — 가로 막대. */\nconst IndeterminateIcon = () => (\n <svg viewBox=\"0 0 12 12\" fill=\"none\" aria-hidden=\"true\">\n <path d=\"M2.8 6h6.4\" stroke=\"currentColor\" strokeWidth=\"1.6\" strokeLinecap=\"round\" />\n </svg>\n);\n\n/**\n * 체크박스. Radix Checkbox 기반 — Space 토글·aria-checked·폼 참여 자동.\n * 라벨을 지정하면 라벨 클릭으로도 토글된다.\n *\n * @example\n * <Checkbox label=\"이용약관에 동의합니다\" checked={agreed} onCheckedChange={setAgreed} />\n *\n * @example 부분 선택 (전체선택 헤더 등)\n * <Checkbox label=\"전체 선택\" checked={someChecked ? 'indeterminate' : allChecked} … />\n */\nexport const Checkbox = forwardRef<HTMLButtonElement, CheckboxProps>(function Checkbox(\n { label, size = 'md', disabled = false, id: idProp, className, checked, ...rest },\n ref,\n) {\n const reactId = useId();\n const id = idProp ?? `nuri-checkbox-${reactId}`;\n\n return (\n <div className={cx('nuri-checkbox', `nuri-checkbox--${size}`, className)}>\n <RadixCheckbox.Root\n ref={ref}\n id={id}\n className=\"nuri-checkbox__control\"\n disabled={disabled}\n checked={checked}\n {...rest}\n >\n <RadixCheckbox.Indicator className=\"nuri-checkbox__indicator\">\n {checked === 'indeterminate' ? <IndeterminateIcon /> : <CheckIcon />}\n </RadixCheckbox.Indicator>\n </RadixCheckbox.Root>\n {label != null && (\n <label htmlFor={id} className=\"nuri-checkbox__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":"
|
|
1
|
+
{"version":3,"sources":["../../src/Checkbox/Checkbox.tsx","../../src/utils/cx.ts"],"sourcesContent":["'use client';\n\nimport './styles.css';\nimport * as RadixCheckbox from '@radix-ui/react-checkbox';\nimport { forwardRef, useId } from 'react';\nimport type { ReactNode } from 'react';\nimport { cx } from '../utils/cx.js';\n\ntype Size = 'sm' | 'md';\n\nexport interface CheckboxProps {\n /** 라벨 (컨트롤 우측). 지정 시 클릭 영역에 포함되고 자동 연결됨. 없으면 `aria-label` 필수. */\n label?: ReactNode;\n /**\n * 사이즈. md 16px(디자인 Large, 기본) / sm 12px(디자인 Small).\n * @default 'md'\n */\n size?: Size;\n /** 체크 상태 (제어형). `'indeterminate'`는 부분 선택 표시. */\n checked?: boolean | 'indeterminate';\n /** 초기 체크 상태 (비제어형). */\n defaultChecked?: boolean;\n onCheckedChange?: (checked: boolean | 'indeterminate') => void;\n disabled?: boolean;\n /** 폼 전송용 name — 네이티브 checkbox처럼 폼에 참여. */\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/** 체크마크. TODO(Figma 실측): 디자이너 아이콘으로 교체 예정 — 잠정 표준형. */\nconst CheckIcon = () => (\n <svg viewBox=\"0 0 12 12\" fill=\"none\" aria-hidden=\"true\">\n <path\n d=\"M2.5 6.2l2.4 2.4 4.6-5\"\n stroke=\"currentColor\"\n strokeWidth=\"1.6\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n </svg>\n);\n\n/** 부분 선택(indeterminate) 표시 — 가로 막대. */\nconst IndeterminateIcon = () => (\n <svg viewBox=\"0 0 12 12\" fill=\"none\" aria-hidden=\"true\">\n <path d=\"M2.8 6h6.4\" stroke=\"currentColor\" strokeWidth=\"1.6\" strokeLinecap=\"round\" />\n </svg>\n);\n\n/**\n * 체크박스. Radix Checkbox 기반 — Space 토글·aria-checked·폼 참여 자동.\n * 라벨을 지정하면 라벨 클릭으로도 토글된다.\n *\n * @example\n * <Checkbox label=\"이용약관에 동의합니다\" checked={agreed} onCheckedChange={setAgreed} />\n *\n * @example 부분 선택 (전체선택 헤더 등)\n * <Checkbox label=\"전체 선택\" checked={someChecked ? 'indeterminate' : allChecked} … />\n */\nexport const Checkbox = forwardRef<HTMLButtonElement, CheckboxProps>(function Checkbox(\n { label, size = 'md', disabled = false, id: idProp, className, checked, ...rest },\n ref,\n) {\n const reactId = useId();\n const id = idProp ?? `nuri-checkbox-${reactId}`;\n\n return (\n <div className={cx('nuri-checkbox', `nuri-checkbox--${size}`, className)}>\n <RadixCheckbox.Root\n ref={ref}\n id={id}\n className=\"nuri-checkbox__control\"\n disabled={disabled}\n checked={checked}\n {...rest}\n >\n <RadixCheckbox.Indicator className=\"nuri-checkbox__indicator\">\n {checked === 'indeterminate' ? <IndeterminateIcon /> : <CheckIcon />}\n </RadixCheckbox.Indicator>\n </RadixCheckbox.Root>\n {label != null && (\n <label htmlFor={id} className=\"nuri-checkbox__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":";;;AAEA,OAAO;AACP,YAAY,mBAAmB;AAC/B,SAAS,YAAY,aAAa;;;ACA3B,SAAS,MAAM,SAA2D;AAC/E,SAAO,QAAQ,OAAO,OAAO,EAAE,KAAK,GAAG;AACzC;;;AD+BI,cAmCA,YAnCA;AAFJ,IAAM,YAAY,MAChB,oBAAC,SAAI,SAAQ,aAAY,MAAK,QAAO,eAAY,QAC/C;AAAA,EAAC;AAAA;AAAA,IACC,GAAE;AAAA,IACF,QAAO;AAAA,IACP,aAAY;AAAA,IACZ,eAAc;AAAA,IACd,gBAAe;AAAA;AACjB,GACF;AAIF,IAAM,oBAAoB,MACxB,oBAAC,SAAI,SAAQ,aAAY,MAAK,QAAO,eAAY,QAC/C,8BAAC,UAAK,GAAE,cAAa,QAAO,gBAAe,aAAY,OAAM,eAAc,SAAQ,GACrF;AAaK,IAAM,WAAW,WAA6C,SAASA,UAC5E,EAAE,OAAO,OAAO,MAAM,WAAW,OAAO,IAAI,QAAQ,WAAW,SAAS,GAAG,KAAK,GAChF,KACA;AACA,QAAM,UAAU,MAAM;AACtB,QAAM,KAAK,UAAU,iBAAiB,OAAO;AAE7C,SACE,qBAAC,SAAI,WAAW,GAAG,iBAAiB,kBAAkB,IAAI,IAAI,SAAS,GACrE;AAAA;AAAA,MAAe;AAAA,MAAd;AAAA,QACC;AAAA,QACA;AAAA,QACA,WAAU;AAAA,QACV;AAAA,QACA;AAAA,QACC,GAAG;AAAA,QAEJ,8BAAe,yBAAd,EAAwB,WAAU,4BAChC,sBAAY,kBAAkB,oBAAC,qBAAkB,IAAK,oBAAC,aAAU,GACpE;AAAA;AAAA,IACF;AAAA,IACC,SAAS,QACR,oBAAC,WAAM,SAAS,IAAI,WAAU,wBAC3B,iBACH;AAAA,KAEJ;AAEJ,CAAC;","names":["Checkbox"]}
|
package/dist/Input/index.cjs
CHANGED
package/dist/Input/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/Input/index.ts","../../src/Input/Input.tsx","../../src/utils/cx.ts"],"sourcesContent":["export { Input } from './Input.js';\nexport type { InputProps } from './Input.js';\n","'use client';\n\nimport './styles.css';\nimport { forwardRef, useId } from 'react';\nimport type { InputHTMLAttributes, ReactNode } from 'react';\nimport { cx } from '../utils/cx.js';\n\ntype Size = 'sm' | 'md' | 'lg';\n\nexport interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size'> {\n /**\n * 사이즈. sm 36 / md 42 / lg 48 px 높이.\n * @default 'md'\n */\n size?: Size;\n /**\n * 라벨 (필드 위). 지정 시 `<label>`로 렌더되어 자동 연결됨.\n * 라벨을 두지 않으면 `aria-label`로 접근 가능한 이름을 주세요\n * (placeholder는 이름을 대체하지 못함 — 입력 시 사라지고 대비가 낮음).\n */\n label?: ReactNode;\n /**\n * 라벨을 시각적으로만 숨김 (스크린리더에는 노출). 디자인상 라벨을 안 보이게 하되\n * 접근성 이름은 유지할 때 사용.\n * @default false\n */\n hideLabel?: boolean;\n /** 평상시 힌트 (회색, 필드 아래). `aria-describedby`로 연결됨. */\n description?: ReactNode;\n /** 에러 상태 — 보더·텍스트·배경이 에러색으로 바뀌고 `aria-invalid`가 설정됨. */\n invalid?: boolean;\n /** `invalid`일 때 표시할 에러 메시지 (빨강). 있으면 description을 대체. */\n errorMessage?: ReactNode;\n /** 좌측 어도먼트 (아이콘 등). */\n iconLeft?: ReactNode;\n /** 우측 어도먼트 (아이콘·clear·버튼 등). */\n iconRight?: ReactNode;\n /** 컨테이너 전체 너비로 확장. @default false */\n block?: boolean;\n}\n\n/**\n * 텍스트 입력 필드. 옵션으로 라벨(위)·헬퍼/에러 메시지(아래)를 포함하는 자체 완결형.\n * 상태(default/focus/invalid/disabled)는 CSS로, 값 채움(filled)은 네이티브로 처리.\n *\n * @example 기본\n * <Input placeholder=\"검색어\" aria-label=\"검색\" />\n *\n * @example 라벨 + 헬퍼 + 에러\n * <Input\n * label=\"비밀번호\"\n * description=\"8자 이상 입력하세요\"\n * invalid\n * errorMessage=\"비밀번호가 일치하지 않습니다\"\n * />\n */\nexport const Input = forwardRef<HTMLInputElement, InputProps>(function Input(\n {\n size = 'md',\n label,\n hideLabel = false,\n description,\n invalid = false,\n errorMessage,\n iconLeft,\n iconRight,\n block = false,\n disabled = false,\n className,\n id: idProp,\n 'aria-describedby': ariaDescribedby,\n ...rest\n },\n ref,\n) {\n const reactId = useId();\n const id = idProp ?? `nuri-input-${reactId}`;\n const messageId = `${id}-message`;\n\n // invalid + errorMessage → 에러 메시지 / 아니면 힌트. 시각 슬롯은 항상 한 줄.\n const message = invalid && errorMessage != null ? errorMessage : description;\n const hasMessage = message != null;\n const describedBy =\n cx(hasMessage ? messageId : undefined, ariaDescribedby as string | undefined) || undefined;\n\n return (\n <div className={cx('nuri-input', block && 'nuri-input--block', className)}>\n {label != null && (\n <label htmlFor={id} className={cx('nuri-input__label', hideLabel && 'nuri-sr-only')}>\n {label}\n </label>\n )}\n\n <div\n className={cx(\n 'nuri-input__field',\n `nuri-input__field--${size}`,\n invalid && 'nuri-input__field--invalid',\n disabled && 'nuri-input__field--disabled',\n )}\n >\n {iconLeft != null && (\n <span className=\"nuri-input__icon\" aria-hidden=\"true\">\n {iconLeft}\n </span>\n )}\n <input\n ref={ref}\n id={id}\n className=\"nuri-input__control\"\n disabled={disabled}\n aria-invalid={invalid || undefined}\n aria-describedby={describedBy}\n {...rest}\n />\n {iconRight != null && (\n <span className=\"nuri-input__icon\" aria-hidden=\"true\">\n {iconRight}\n </span>\n )}\n </div>\n\n {hasMessage && (\n <p\n id={messageId}\n className={cx('nuri-input__message', invalid && 'nuri-input__message--error')}\n >\n {message}\n </p>\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":"
|
|
1
|
+
{"version":3,"sources":["../../src/Input/index.ts","../../src/Input/Input.tsx","../../src/utils/cx.ts"],"sourcesContent":["export { Input } from './Input.js';\nexport type { InputProps } from './Input.js';\n","'use client';\n\nimport './styles.css';\nimport { forwardRef, useId } from 'react';\nimport type { InputHTMLAttributes, ReactNode } from 'react';\nimport { cx } from '../utils/cx.js';\n\ntype Size = 'sm' | 'md' | 'lg';\n\nexport interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size'> {\n /**\n * 사이즈. sm 36 / md 42 / lg 48 px 높이.\n * @default 'md'\n */\n size?: Size;\n /**\n * 라벨 (필드 위). 지정 시 `<label>`로 렌더되어 자동 연결됨.\n * 라벨을 두지 않으면 `aria-label`로 접근 가능한 이름을 주세요\n * (placeholder는 이름을 대체하지 못함 — 입력 시 사라지고 대비가 낮음).\n */\n label?: ReactNode;\n /**\n * 라벨을 시각적으로만 숨김 (스크린리더에는 노출). 디자인상 라벨을 안 보이게 하되\n * 접근성 이름은 유지할 때 사용.\n * @default false\n */\n hideLabel?: boolean;\n /** 평상시 힌트 (회색, 필드 아래). `aria-describedby`로 연결됨. */\n description?: ReactNode;\n /** 에러 상태 — 보더·텍스트·배경이 에러색으로 바뀌고 `aria-invalid`가 설정됨. */\n invalid?: boolean;\n /** `invalid`일 때 표시할 에러 메시지 (빨강). 있으면 description을 대체. */\n errorMessage?: ReactNode;\n /** 좌측 어도먼트 (아이콘 등). */\n iconLeft?: ReactNode;\n /** 우측 어도먼트 (아이콘·clear·버튼 등). */\n iconRight?: ReactNode;\n /** 컨테이너 전체 너비로 확장. @default false */\n block?: boolean;\n}\n\n/**\n * 텍스트 입력 필드. 옵션으로 라벨(위)·헬퍼/에러 메시지(아래)를 포함하는 자체 완결형.\n * 상태(default/focus/invalid/disabled)는 CSS로, 값 채움(filled)은 네이티브로 처리.\n *\n * @example 기본\n * <Input placeholder=\"검색어\" aria-label=\"검색\" />\n *\n * @example 라벨 + 헬퍼 + 에러\n * <Input\n * label=\"비밀번호\"\n * description=\"8자 이상 입력하세요\"\n * invalid\n * errorMessage=\"비밀번호가 일치하지 않습니다\"\n * />\n */\nexport const Input = forwardRef<HTMLInputElement, InputProps>(function Input(\n {\n size = 'md',\n label,\n hideLabel = false,\n description,\n invalid = false,\n errorMessage,\n iconLeft,\n iconRight,\n block = false,\n disabled = false,\n className,\n id: idProp,\n 'aria-describedby': ariaDescribedby,\n ...rest\n },\n ref,\n) {\n const reactId = useId();\n const id = idProp ?? `nuri-input-${reactId}`;\n const messageId = `${id}-message`;\n\n // invalid + errorMessage → 에러 메시지 / 아니면 힌트. 시각 슬롯은 항상 한 줄.\n const message = invalid && errorMessage != null ? errorMessage : description;\n const hasMessage = message != null;\n const describedBy =\n cx(hasMessage ? messageId : undefined, ariaDescribedby as string | undefined) || undefined;\n\n return (\n <div className={cx('nuri-input', block && 'nuri-input--block', className)}>\n {label != null && (\n <label htmlFor={id} className={cx('nuri-input__label', hideLabel && 'nuri-sr-only')}>\n {label}\n </label>\n )}\n\n <div\n className={cx(\n 'nuri-input__field',\n `nuri-input__field--${size}`,\n invalid && 'nuri-input__field--invalid',\n disabled && 'nuri-input__field--disabled',\n )}\n >\n {iconLeft != null && (\n <span className=\"nuri-input__icon\" aria-hidden=\"true\">\n {iconLeft}\n </span>\n )}\n <input\n ref={ref}\n id={id}\n className=\"nuri-input__control\"\n disabled={disabled}\n aria-invalid={invalid || undefined}\n aria-describedby={describedBy}\n {...rest}\n />\n {iconRight != null && (\n <span className=\"nuri-input__icon\" aria-hidden=\"true\">\n {iconRight}\n </span>\n )}\n </div>\n\n {hasMessage && (\n <p\n id={messageId}\n className={cx('nuri-input__message', invalid && 'nuri-input__message--error')}\n >\n {message}\n </p>\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,mBAAkC;;;ACC3B,SAAS,MAAM,SAA2D;AAC/E,SAAO,QAAQ,OAAO,OAAO,EAAE,KAAK,GAAG;AACzC;;;ADkFQ;AAhCD,IAAM,YAAQ,yBAAyC,SAASA,OACrE;AAAA,EACE,OAAO;AAAA,EACP;AAAA,EACA,YAAY;AAAA,EACZ;AAAA,EACA,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA;AAAA,EACA,QAAQ;AAAA,EACR,WAAW;AAAA,EACX;AAAA,EACA,IAAI;AAAA,EACJ,oBAAoB;AAAA,EACpB,GAAG;AACL,GACA,KACA;AACA,QAAM,cAAU,oBAAM;AACtB,QAAM,KAAK,UAAU,cAAc,OAAO;AAC1C,QAAM,YAAY,GAAG,EAAE;AAGvB,QAAM,UAAU,WAAW,gBAAgB,OAAO,eAAe;AACjE,QAAM,aAAa,WAAW;AAC9B,QAAM,cACJ,GAAG,aAAa,YAAY,QAAW,eAAqC,KAAK;AAEnF,SACE,6CAAC,SAAI,WAAW,GAAG,cAAc,SAAS,qBAAqB,SAAS,GACrE;AAAA,aAAS,QACR,4CAAC,WAAM,SAAS,IAAI,WAAW,GAAG,qBAAqB,aAAa,cAAc,GAC/E,iBACH;AAAA,IAGF;AAAA,MAAC;AAAA;AAAA,QACC,WAAW;AAAA,UACT;AAAA,UACA,sBAAsB,IAAI;AAAA,UAC1B,WAAW;AAAA,UACX,YAAY;AAAA,QACd;AAAA,QAEC;AAAA,sBAAY,QACX,4CAAC,UAAK,WAAU,oBAAmB,eAAY,QAC5C,oBACH;AAAA,UAEF;AAAA,YAAC;AAAA;AAAA,cACC;AAAA,cACA;AAAA,cACA,WAAU;AAAA,cACV;AAAA,cACA,gBAAc,WAAW;AAAA,cACzB,oBAAkB;AAAA,cACjB,GAAG;AAAA;AAAA,UACN;AAAA,UACC,aAAa,QACZ,4CAAC,UAAK,WAAU,oBAAmB,eAAY,QAC5C,qBACH;AAAA;AAAA;AAAA,IAEJ;AAAA,IAEC,cACC;AAAA,MAAC;AAAA;AAAA,QACC,IAAI;AAAA,QACJ,WAAW,GAAG,uBAAuB,WAAW,4BAA4B;AAAA,QAE3E;AAAA;AAAA,IACH;AAAA,KAEJ;AAEJ,CAAC;","names":["Input"]}
|
package/dist/Input/index.js
CHANGED
package/dist/Input/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/Input/Input.tsx","../../src/utils/cx.ts"],"sourcesContent":["'use client';\n\nimport './styles.css';\nimport { forwardRef, useId } from 'react';\nimport type { InputHTMLAttributes, ReactNode } from 'react';\nimport { cx } from '../utils/cx.js';\n\ntype Size = 'sm' | 'md' | 'lg';\n\nexport interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size'> {\n /**\n * 사이즈. sm 36 / md 42 / lg 48 px 높이.\n * @default 'md'\n */\n size?: Size;\n /**\n * 라벨 (필드 위). 지정 시 `<label>`로 렌더되어 자동 연결됨.\n * 라벨을 두지 않으면 `aria-label`로 접근 가능한 이름을 주세요\n * (placeholder는 이름을 대체하지 못함 — 입력 시 사라지고 대비가 낮음).\n */\n label?: ReactNode;\n /**\n * 라벨을 시각적으로만 숨김 (스크린리더에는 노출). 디자인상 라벨을 안 보이게 하되\n * 접근성 이름은 유지할 때 사용.\n * @default false\n */\n hideLabel?: boolean;\n /** 평상시 힌트 (회색, 필드 아래). `aria-describedby`로 연결됨. */\n description?: ReactNode;\n /** 에러 상태 — 보더·텍스트·배경이 에러색으로 바뀌고 `aria-invalid`가 설정됨. */\n invalid?: boolean;\n /** `invalid`일 때 표시할 에러 메시지 (빨강). 있으면 description을 대체. */\n errorMessage?: ReactNode;\n /** 좌측 어도먼트 (아이콘 등). */\n iconLeft?: ReactNode;\n /** 우측 어도먼트 (아이콘·clear·버튼 등). */\n iconRight?: ReactNode;\n /** 컨테이너 전체 너비로 확장. @default false */\n block?: boolean;\n}\n\n/**\n * 텍스트 입력 필드. 옵션으로 라벨(위)·헬퍼/에러 메시지(아래)를 포함하는 자체 완결형.\n * 상태(default/focus/invalid/disabled)는 CSS로, 값 채움(filled)은 네이티브로 처리.\n *\n * @example 기본\n * <Input placeholder=\"검색어\" aria-label=\"검색\" />\n *\n * @example 라벨 + 헬퍼 + 에러\n * <Input\n * label=\"비밀번호\"\n * description=\"8자 이상 입력하세요\"\n * invalid\n * errorMessage=\"비밀번호가 일치하지 않습니다\"\n * />\n */\nexport const Input = forwardRef<HTMLInputElement, InputProps>(function Input(\n {\n size = 'md',\n label,\n hideLabel = false,\n description,\n invalid = false,\n errorMessage,\n iconLeft,\n iconRight,\n block = false,\n disabled = false,\n className,\n id: idProp,\n 'aria-describedby': ariaDescribedby,\n ...rest\n },\n ref,\n) {\n const reactId = useId();\n const id = idProp ?? `nuri-input-${reactId}`;\n const messageId = `${id}-message`;\n\n // invalid + errorMessage → 에러 메시지 / 아니면 힌트. 시각 슬롯은 항상 한 줄.\n const message = invalid && errorMessage != null ? errorMessage : description;\n const hasMessage = message != null;\n const describedBy =\n cx(hasMessage ? messageId : undefined, ariaDescribedby as string | undefined) || undefined;\n\n return (\n <div className={cx('nuri-input', block && 'nuri-input--block', className)}>\n {label != null && (\n <label htmlFor={id} className={cx('nuri-input__label', hideLabel && 'nuri-sr-only')}>\n {label}\n </label>\n )}\n\n <div\n className={cx(\n 'nuri-input__field',\n `nuri-input__field--${size}`,\n invalid && 'nuri-input__field--invalid',\n disabled && 'nuri-input__field--disabled',\n )}\n >\n {iconLeft != null && (\n <span className=\"nuri-input__icon\" aria-hidden=\"true\">\n {iconLeft}\n </span>\n )}\n <input\n ref={ref}\n id={id}\n className=\"nuri-input__control\"\n disabled={disabled}\n aria-invalid={invalid || undefined}\n aria-describedby={describedBy}\n {...rest}\n />\n {iconRight != null && (\n <span className=\"nuri-input__icon\" aria-hidden=\"true\">\n {iconRight}\n </span>\n )}\n </div>\n\n {hasMessage && (\n <p\n id={messageId}\n className={cx('nuri-input__message', invalid && 'nuri-input__message--error')}\n >\n {message}\n </p>\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":"
|
|
1
|
+
{"version":3,"sources":["../../src/Input/Input.tsx","../../src/utils/cx.ts"],"sourcesContent":["'use client';\n\nimport './styles.css';\nimport { forwardRef, useId } from 'react';\nimport type { InputHTMLAttributes, ReactNode } from 'react';\nimport { cx } from '../utils/cx.js';\n\ntype Size = 'sm' | 'md' | 'lg';\n\nexport interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size'> {\n /**\n * 사이즈. sm 36 / md 42 / lg 48 px 높이.\n * @default 'md'\n */\n size?: Size;\n /**\n * 라벨 (필드 위). 지정 시 `<label>`로 렌더되어 자동 연결됨.\n * 라벨을 두지 않으면 `aria-label`로 접근 가능한 이름을 주세요\n * (placeholder는 이름을 대체하지 못함 — 입력 시 사라지고 대비가 낮음).\n */\n label?: ReactNode;\n /**\n * 라벨을 시각적으로만 숨김 (스크린리더에는 노출). 디자인상 라벨을 안 보이게 하되\n * 접근성 이름은 유지할 때 사용.\n * @default false\n */\n hideLabel?: boolean;\n /** 평상시 힌트 (회색, 필드 아래). `aria-describedby`로 연결됨. */\n description?: ReactNode;\n /** 에러 상태 — 보더·텍스트·배경이 에러색으로 바뀌고 `aria-invalid`가 설정됨. */\n invalid?: boolean;\n /** `invalid`일 때 표시할 에러 메시지 (빨강). 있으면 description을 대체. */\n errorMessage?: ReactNode;\n /** 좌측 어도먼트 (아이콘 등). */\n iconLeft?: ReactNode;\n /** 우측 어도먼트 (아이콘·clear·버튼 등). */\n iconRight?: ReactNode;\n /** 컨테이너 전체 너비로 확장. @default false */\n block?: boolean;\n}\n\n/**\n * 텍스트 입력 필드. 옵션으로 라벨(위)·헬퍼/에러 메시지(아래)를 포함하는 자체 완결형.\n * 상태(default/focus/invalid/disabled)는 CSS로, 값 채움(filled)은 네이티브로 처리.\n *\n * @example 기본\n * <Input placeholder=\"검색어\" aria-label=\"검색\" />\n *\n * @example 라벨 + 헬퍼 + 에러\n * <Input\n * label=\"비밀번호\"\n * description=\"8자 이상 입력하세요\"\n * invalid\n * errorMessage=\"비밀번호가 일치하지 않습니다\"\n * />\n */\nexport const Input = forwardRef<HTMLInputElement, InputProps>(function Input(\n {\n size = 'md',\n label,\n hideLabel = false,\n description,\n invalid = false,\n errorMessage,\n iconLeft,\n iconRight,\n block = false,\n disabled = false,\n className,\n id: idProp,\n 'aria-describedby': ariaDescribedby,\n ...rest\n },\n ref,\n) {\n const reactId = useId();\n const id = idProp ?? `nuri-input-${reactId}`;\n const messageId = `${id}-message`;\n\n // invalid + errorMessage → 에러 메시지 / 아니면 힌트. 시각 슬롯은 항상 한 줄.\n const message = invalid && errorMessage != null ? errorMessage : description;\n const hasMessage = message != null;\n const describedBy =\n cx(hasMessage ? messageId : undefined, ariaDescribedby as string | undefined) || undefined;\n\n return (\n <div className={cx('nuri-input', block && 'nuri-input--block', className)}>\n {label != null && (\n <label htmlFor={id} className={cx('nuri-input__label', hideLabel && 'nuri-sr-only')}>\n {label}\n </label>\n )}\n\n <div\n className={cx(\n 'nuri-input__field',\n `nuri-input__field--${size}`,\n invalid && 'nuri-input__field--invalid',\n disabled && 'nuri-input__field--disabled',\n )}\n >\n {iconLeft != null && (\n <span className=\"nuri-input__icon\" aria-hidden=\"true\">\n {iconLeft}\n </span>\n )}\n <input\n ref={ref}\n id={id}\n className=\"nuri-input__control\"\n disabled={disabled}\n aria-invalid={invalid || undefined}\n aria-describedby={describedBy}\n {...rest}\n />\n {iconRight != null && (\n <span className=\"nuri-input__icon\" aria-hidden=\"true\">\n {iconRight}\n </span>\n )}\n </div>\n\n {hasMessage && (\n <p\n id={messageId}\n className={cx('nuri-input__message', invalid && 'nuri-input__message--error')}\n >\n {message}\n </p>\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":";;;AAEA,OAAO;AACP,SAAS,YAAY,aAAa;;;ACC3B,SAAS,MAAM,SAA2D;AAC/E,SAAO,QAAQ,OAAO,OAAO,EAAE,KAAK,GAAG;AACzC;;;ADkFQ,cAKF,YALE;AAhCD,IAAM,QAAQ,WAAyC,SAASA,OACrE;AAAA,EACE,OAAO;AAAA,EACP;AAAA,EACA,YAAY;AAAA,EACZ;AAAA,EACA,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA;AAAA,EACA,QAAQ;AAAA,EACR,WAAW;AAAA,EACX;AAAA,EACA,IAAI;AAAA,EACJ,oBAAoB;AAAA,EACpB,GAAG;AACL,GACA,KACA;AACA,QAAM,UAAU,MAAM;AACtB,QAAM,KAAK,UAAU,cAAc,OAAO;AAC1C,QAAM,YAAY,GAAG,EAAE;AAGvB,QAAM,UAAU,WAAW,gBAAgB,OAAO,eAAe;AACjE,QAAM,aAAa,WAAW;AAC9B,QAAM,cACJ,GAAG,aAAa,YAAY,QAAW,eAAqC,KAAK;AAEnF,SACE,qBAAC,SAAI,WAAW,GAAG,cAAc,SAAS,qBAAqB,SAAS,GACrE;AAAA,aAAS,QACR,oBAAC,WAAM,SAAS,IAAI,WAAW,GAAG,qBAAqB,aAAa,cAAc,GAC/E,iBACH;AAAA,IAGF;AAAA,MAAC;AAAA;AAAA,QACC,WAAW;AAAA,UACT;AAAA,UACA,sBAAsB,IAAI;AAAA,UAC1B,WAAW;AAAA,UACX,YAAY;AAAA,QACd;AAAA,QAEC;AAAA,sBAAY,QACX,oBAAC,UAAK,WAAU,oBAAmB,eAAY,QAC5C,oBACH;AAAA,UAEF;AAAA,YAAC;AAAA;AAAA,cACC;AAAA,cACA;AAAA,cACA,WAAU;AAAA,cACV;AAAA,cACA,gBAAc,WAAW;AAAA,cACzB,oBAAkB;AAAA,cACjB,GAAG;AAAA;AAAA,UACN;AAAA,UACC,aAAa,QACZ,oBAAC,UAAK,WAAU,oBAAmB,eAAY,QAC5C,qBACH;AAAA;AAAA;AAAA,IAEJ;AAAA,IAEC,cACC;AAAA,MAAC;AAAA;AAAA,QACC,IAAI;AAAA,QACJ,WAAW,GAAG,uBAAuB,WAAW,4BAA4B;AAAA,QAE3E;AAAA;AAAA,IACH;AAAA,KAEJ;AAEJ,CAAC;","names":["Input"]}
|
package/dist/Modal/index.cjs
CHANGED
package/dist/Modal/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/Modal/index.ts","../../src/Modal/Modal.tsx","../../src/utils/cx.ts"],"sourcesContent":["export { Modal } from './Modal.js';\nexport type { ModalContentProps, ModalFooterProps, ModalTitleProps } from './Modal.js';\n","'use client';\n\nimport './styles.css';\nimport * as Dialog from '@radix-ui/react-dialog';\nimport { forwardRef } from 'react';\nimport type { ComponentPropsWithoutRef, ElementRef, ReactNode } from 'react';\nimport { cx } from '../utils/cx.js';\n\ntype Placement = 'center' | 'bottom';\ntype Size = 'sm' | 'md' | 'lg';\n\n/**\n * 모달 루트. open/onOpenChange로 제어하거나, Modal.Trigger와 함께 비제어로도 사용.\n * Radix Dialog 기반 — 포커스 트랩·Esc·스크롤 락·포커스 복귀·aria 연결을 자동 제공.\n *\n * @example 제어형\n * <Modal open={open} onOpenChange={setOpen}>\n * <Modal.Content>...</Modal.Content>\n * </Modal>\n *\n * @example 비제어 (트리거)\n * <Modal>\n * <Modal.Trigger asChild><Button>열기</Button></Modal.Trigger>\n * <Modal.Content>...</Modal.Content>\n * </Modal>\n */\nconst Root = (props: ComponentPropsWithoutRef<typeof Dialog.Root>) => <Dialog.Root {...props} />;\nRoot.displayName = 'Modal';\n\n/** 모달을 여는 트리거. `asChild`로 임의 엘리먼트(Button 등)에 위임. */\nconst Trigger = Dialog.Trigger;\n\nexport interface ModalContentProps extends ComponentPropsWithoutRef<typeof Dialog.Content> {\n /**\n * 표시 위치.\n * - `center`: 화면 중앙 다이얼로그 (기본)\n * - `bottom`: 하단 바텀시트 (모바일에서 아래에서 올라옴)\n * @default 'center'\n */\n placement?: Placement;\n /**\n * 최대 너비. **`placement=\"center\"`에서만 적용** (bottom 바텀시트는 항상 풀 width).\n * @default 'md'\n */\n size?: Size;\n /** 백드롭(오버레이) 표시 여부. @default true */\n overlay?: boolean;\n /** 포털 렌더 대상. 기본은 document.body. 다른 요소에 렌더하려면 지정. */\n container?: HTMLElement | null;\n}\n\n/**\n * 오버레이 + 포털 + 다이얼로그 패널을 한 번에 렌더.\n * 내부에 Modal.Header / Modal.Body / Modal.Footer를 자유롭게 합성.\n */\nconst Content = forwardRef<ElementRef<typeof Dialog.Content>, ModalContentProps>(\n function ModalContent(\n { placement = 'center', size = 'md', overlay = true, container, className, children, ...rest },\n ref,\n ) {\n const classes = cx('nuri-modal', `nuri-modal--${placement}`, `nuri-modal--${size}`, className);\n\n return (\n <Dialog.Portal container={container}>\n {overlay && <Dialog.Overlay className=\"nuri-modal__overlay\" />}\n <Dialog.Content ref={ref} className={classes} {...rest}>\n {children}\n </Dialog.Content>\n </Dialog.Portal>\n );\n },\n);\n\n/** 헤더 영역 (Title/Description/CloseButton을 담는 레이아웃 슬롯). */\nconst Header = forwardRef<HTMLDivElement, ComponentPropsWithoutRef<'div'>>(function ModalHeader(\n { className, ...rest },\n ref,\n) {\n return <div ref={ref} className={cx('nuri-modal__header', className)} {...rest} />;\n});\n\nexport interface ModalTitleProps extends ComponentPropsWithoutRef<typeof Dialog.Title> {\n /**\n * 시각적으로는 숨기되 스크린리더에는 노출. 시각적 제목이 없는 모달도\n * 접근 가능한 이름을 가져야 하므로(Radix Dialog는 Title 필수), 이 옵션으로 처리.\n * @example <Modal.Title visuallyHidden>이미지 미리보기</Modal.Title>\n */\n visuallyHidden?: boolean;\n}\n\n/** 모달 제목. 렌더 시 자동으로 `aria-labelledby`로 연결됨 (Radix Dialog.Title). */\nconst Title = forwardRef<ElementRef<typeof Dialog.Title>, ModalTitleProps>(function ModalTitle(\n { visuallyHidden = false, className, ...rest },\n ref,\n) {\n return (\n <Dialog.Title\n ref={ref}\n className={cx('nuri-modal__title', visuallyHidden && 'nuri-sr-only', className)}\n {...rest}\n />\n );\n});\n\n/** 모달 설명. 렌더 시 자동으로 `aria-describedby`로 연결됨 (Radix Dialog.Description). */\nconst Description = forwardRef<\n ElementRef<typeof Dialog.Description>,\n ComponentPropsWithoutRef<typeof Dialog.Description>\n>(function ModalDescription({ className, ...rest }, ref) {\n return (\n <Dialog.Description ref={ref} className={cx('nuri-modal__description', className)} {...rest} />\n );\n});\n\n/** 본문 영역 (스크롤 가능). */\nconst Body = forwardRef<HTMLDivElement, ComponentPropsWithoutRef<'div'>>(function ModalBody(\n { className, ...rest },\n ref,\n) {\n return <div ref={ref} className={cx('nuri-modal__body', className)} {...rest} />;\n});\n\nexport interface ModalFooterProps extends ComponentPropsWithoutRef<'div'> {\n /**\n * 액션 버튼 정렬.\n * - `end`: 우측 (기본, 데스크탑 관례)\n * - `start`: 좌측\n * - `center`: 가운데\n * - `between`: 양끝 분산 (예: 좌측 위험 액션 + 우측 확인)\n * - `full`: 버튼들이 가로를 균등하게 채움 (모바일 전체너비)\n * @default 'end'\n */\n align?: 'start' | 'center' | 'end' | 'between' | 'full';\n}\n\n/** 푸터 영역 (액션 버튼 배치). 정렬은 `align` prop으로 제어. */\nconst Footer = forwardRef<HTMLDivElement, ModalFooterProps>(function ModalFooter(\n { align = 'end', className, ...rest },\n ref,\n) {\n return (\n <div\n ref={ref}\n className={cx('nuri-modal__footer', `nuri-modal__footer--${align}`, className)}\n {...rest}\n />\n );\n});\n\n/**\n * 모달을 닫는 액션. `asChild`로 임의 엘리먼트에 위임.\n * @example <Modal.Close asChild><Button variant=\"outline\">취소</Button></Modal.Close>\n */\nconst Close = Dialog.Close;\n\nconst CloseIcon = () => (\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 20 20\" fill=\"none\" aria-hidden=\"true\">\n <path d=\"M5 5l10 10M15 5L5 15\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" />\n </svg>\n);\n\n/** 우상단 X 닫기 버튼 (미리 스타일링됨). 기본 `aria-label=\"닫기\"`. */\nconst CloseButton = forwardRef<\n HTMLButtonElement,\n Omit<ComponentPropsWithoutRef<'button'>, 'children'> & { children?: ReactNode }\n>(function ModalCloseButton(\n { className, 'aria-label': ariaLabel = '닫기', children, ...rest },\n ref,\n) {\n return (\n <Dialog.Close asChild>\n <button\n ref={ref}\n type=\"button\"\n aria-label={ariaLabel}\n className={cx('nuri-modal__close', className)}\n {...rest}\n >\n {children ?? <CloseIcon />}\n </button>\n </Dialog.Close>\n );\n});\n\nexport const Modal = Object.assign(Root, {\n Trigger,\n Content,\n Header,\n Title,\n Description,\n Body,\n Footer,\n Close,\n CloseButton,\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":"
|
|
1
|
+
{"version":3,"sources":["../../src/Modal/index.ts","../../src/Modal/Modal.tsx","../../src/utils/cx.ts"],"sourcesContent":["export { Modal } from './Modal.js';\nexport type { ModalContentProps, ModalFooterProps, ModalTitleProps } from './Modal.js';\n","'use client';\n\nimport './styles.css';\nimport * as Dialog from '@radix-ui/react-dialog';\nimport { forwardRef } from 'react';\nimport type { ComponentPropsWithoutRef, ElementRef, ReactNode } from 'react';\nimport { cx } from '../utils/cx.js';\n\ntype Placement = 'center' | 'bottom';\ntype Size = 'sm' | 'md' | 'lg';\n\n/**\n * 모달 루트. open/onOpenChange로 제어하거나, Modal.Trigger와 함께 비제어로도 사용.\n * Radix Dialog 기반 — 포커스 트랩·Esc·스크롤 락·포커스 복귀·aria 연결을 자동 제공.\n *\n * @example 제어형\n * <Modal open={open} onOpenChange={setOpen}>\n * <Modal.Content>...</Modal.Content>\n * </Modal>\n *\n * @example 비제어 (트리거)\n * <Modal>\n * <Modal.Trigger asChild><Button>열기</Button></Modal.Trigger>\n * <Modal.Content>...</Modal.Content>\n * </Modal>\n */\nconst Root = (props: ComponentPropsWithoutRef<typeof Dialog.Root>) => <Dialog.Root {...props} />;\nRoot.displayName = 'Modal';\n\n/** 모달을 여는 트리거. `asChild`로 임의 엘리먼트(Button 등)에 위임. */\nconst Trigger = Dialog.Trigger;\n\nexport interface ModalContentProps extends ComponentPropsWithoutRef<typeof Dialog.Content> {\n /**\n * 표시 위치.\n * - `center`: 화면 중앙 다이얼로그 (기본)\n * - `bottom`: 하단 바텀시트 (모바일에서 아래에서 올라옴)\n * @default 'center'\n */\n placement?: Placement;\n /**\n * 최대 너비. **`placement=\"center\"`에서만 적용** (bottom 바텀시트는 항상 풀 width).\n * @default 'md'\n */\n size?: Size;\n /** 백드롭(오버레이) 표시 여부. @default true */\n overlay?: boolean;\n /** 포털 렌더 대상. 기본은 document.body. 다른 요소에 렌더하려면 지정. */\n container?: HTMLElement | null;\n}\n\n/**\n * 오버레이 + 포털 + 다이얼로그 패널을 한 번에 렌더.\n * 내부에 Modal.Header / Modal.Body / Modal.Footer를 자유롭게 합성.\n */\nconst Content = forwardRef<ElementRef<typeof Dialog.Content>, ModalContentProps>(\n function ModalContent(\n { placement = 'center', size = 'md', overlay = true, container, className, children, ...rest },\n ref,\n ) {\n const classes = cx('nuri-modal', `nuri-modal--${placement}`, `nuri-modal--${size}`, className);\n\n return (\n <Dialog.Portal container={container}>\n {overlay && <Dialog.Overlay className=\"nuri-modal__overlay\" />}\n <Dialog.Content ref={ref} className={classes} {...rest}>\n {children}\n </Dialog.Content>\n </Dialog.Portal>\n );\n },\n);\n\n/** 헤더 영역 (Title/Description/CloseButton을 담는 레이아웃 슬롯). */\nconst Header = forwardRef<HTMLDivElement, ComponentPropsWithoutRef<'div'>>(function ModalHeader(\n { className, ...rest },\n ref,\n) {\n return <div ref={ref} className={cx('nuri-modal__header', className)} {...rest} />;\n});\n\nexport interface ModalTitleProps extends ComponentPropsWithoutRef<typeof Dialog.Title> {\n /**\n * 시각적으로는 숨기되 스크린리더에는 노출. 시각적 제목이 없는 모달도\n * 접근 가능한 이름을 가져야 하므로(Radix Dialog는 Title 필수), 이 옵션으로 처리.\n * @example <Modal.Title visuallyHidden>이미지 미리보기</Modal.Title>\n */\n visuallyHidden?: boolean;\n}\n\n/** 모달 제목. 렌더 시 자동으로 `aria-labelledby`로 연결됨 (Radix Dialog.Title). */\nconst Title = forwardRef<ElementRef<typeof Dialog.Title>, ModalTitleProps>(function ModalTitle(\n { visuallyHidden = false, className, ...rest },\n ref,\n) {\n return (\n <Dialog.Title\n ref={ref}\n className={cx('nuri-modal__title', visuallyHidden && 'nuri-sr-only', className)}\n {...rest}\n />\n );\n});\n\n/** 모달 설명. 렌더 시 자동으로 `aria-describedby`로 연결됨 (Radix Dialog.Description). */\nconst Description = forwardRef<\n ElementRef<typeof Dialog.Description>,\n ComponentPropsWithoutRef<typeof Dialog.Description>\n>(function ModalDescription({ className, ...rest }, ref) {\n return (\n <Dialog.Description ref={ref} className={cx('nuri-modal__description', className)} {...rest} />\n );\n});\n\n/** 본문 영역 (스크롤 가능). */\nconst Body = forwardRef<HTMLDivElement, ComponentPropsWithoutRef<'div'>>(function ModalBody(\n { className, ...rest },\n ref,\n) {\n return <div ref={ref} className={cx('nuri-modal__body', className)} {...rest} />;\n});\n\nexport interface ModalFooterProps extends ComponentPropsWithoutRef<'div'> {\n /**\n * 액션 버튼 정렬.\n * - `end`: 우측 (기본, 데스크탑 관례)\n * - `start`: 좌측\n * - `center`: 가운데\n * - `between`: 양끝 분산 (예: 좌측 위험 액션 + 우측 확인)\n * - `full`: 버튼들이 가로를 균등하게 채움 (모바일 전체너비)\n * @default 'end'\n */\n align?: 'start' | 'center' | 'end' | 'between' | 'full';\n}\n\n/** 푸터 영역 (액션 버튼 배치). 정렬은 `align` prop으로 제어. */\nconst Footer = forwardRef<HTMLDivElement, ModalFooterProps>(function ModalFooter(\n { align = 'end', className, ...rest },\n ref,\n) {\n return (\n <div\n ref={ref}\n className={cx('nuri-modal__footer', `nuri-modal__footer--${align}`, className)}\n {...rest}\n />\n );\n});\n\n/**\n * 모달을 닫는 액션. `asChild`로 임의 엘리먼트에 위임.\n * @example <Modal.Close asChild><Button variant=\"outline\">취소</Button></Modal.Close>\n */\nconst Close = Dialog.Close;\n\nconst CloseIcon = () => (\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 20 20\" fill=\"none\" aria-hidden=\"true\">\n <path d=\"M5 5l10 10M15 5L5 15\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" />\n </svg>\n);\n\n/** 우상단 X 닫기 버튼 (미리 스타일링됨). 기본 `aria-label=\"닫기\"`. */\nconst CloseButton = forwardRef<\n HTMLButtonElement,\n Omit<ComponentPropsWithoutRef<'button'>, 'children'> & { children?: ReactNode }\n>(function ModalCloseButton(\n { className, 'aria-label': ariaLabel = '닫기', children, ...rest },\n ref,\n) {\n return (\n <Dialog.Close asChild>\n <button\n ref={ref}\n type=\"button\"\n aria-label={ariaLabel}\n className={cx('nuri-modal__close', className)}\n {...rest}\n >\n {children ?? <CloseIcon />}\n </button>\n </Dialog.Close>\n );\n});\n\nexport const Modal = Object.assign(Root, {\n Trigger,\n Content,\n Header,\n Title,\n Description,\n Body,\n Footer,\n Close,\n CloseButton,\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":"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","names":["Root","Trigger","Content","Title","Description","Close"]}
|
package/dist/Modal/index.js
CHANGED
package/dist/Modal/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/Modal/Modal.tsx","../../src/utils/cx.ts"],"sourcesContent":["'use client';\n\nimport './styles.css';\nimport * as Dialog from '@radix-ui/react-dialog';\nimport { forwardRef } from 'react';\nimport type { ComponentPropsWithoutRef, ElementRef, ReactNode } from 'react';\nimport { cx } from '../utils/cx.js';\n\ntype Placement = 'center' | 'bottom';\ntype Size = 'sm' | 'md' | 'lg';\n\n/**\n * 모달 루트. open/onOpenChange로 제어하거나, Modal.Trigger와 함께 비제어로도 사용.\n * Radix Dialog 기반 — 포커스 트랩·Esc·스크롤 락·포커스 복귀·aria 연결을 자동 제공.\n *\n * @example 제어형\n * <Modal open={open} onOpenChange={setOpen}>\n * <Modal.Content>...</Modal.Content>\n * </Modal>\n *\n * @example 비제어 (트리거)\n * <Modal>\n * <Modal.Trigger asChild><Button>열기</Button></Modal.Trigger>\n * <Modal.Content>...</Modal.Content>\n * </Modal>\n */\nconst Root = (props: ComponentPropsWithoutRef<typeof Dialog.Root>) => <Dialog.Root {...props} />;\nRoot.displayName = 'Modal';\n\n/** 모달을 여는 트리거. `asChild`로 임의 엘리먼트(Button 등)에 위임. */\nconst Trigger = Dialog.Trigger;\n\nexport interface ModalContentProps extends ComponentPropsWithoutRef<typeof Dialog.Content> {\n /**\n * 표시 위치.\n * - `center`: 화면 중앙 다이얼로그 (기본)\n * - `bottom`: 하단 바텀시트 (모바일에서 아래에서 올라옴)\n * @default 'center'\n */\n placement?: Placement;\n /**\n * 최대 너비. **`placement=\"center\"`에서만 적용** (bottom 바텀시트는 항상 풀 width).\n * @default 'md'\n */\n size?: Size;\n /** 백드롭(오버레이) 표시 여부. @default true */\n overlay?: boolean;\n /** 포털 렌더 대상. 기본은 document.body. 다른 요소에 렌더하려면 지정. */\n container?: HTMLElement | null;\n}\n\n/**\n * 오버레이 + 포털 + 다이얼로그 패널을 한 번에 렌더.\n * 내부에 Modal.Header / Modal.Body / Modal.Footer를 자유롭게 합성.\n */\nconst Content = forwardRef<ElementRef<typeof Dialog.Content>, ModalContentProps>(\n function ModalContent(\n { placement = 'center', size = 'md', overlay = true, container, className, children, ...rest },\n ref,\n ) {\n const classes = cx('nuri-modal', `nuri-modal--${placement}`, `nuri-modal--${size}`, className);\n\n return (\n <Dialog.Portal container={container}>\n {overlay && <Dialog.Overlay className=\"nuri-modal__overlay\" />}\n <Dialog.Content ref={ref} className={classes} {...rest}>\n {children}\n </Dialog.Content>\n </Dialog.Portal>\n );\n },\n);\n\n/** 헤더 영역 (Title/Description/CloseButton을 담는 레이아웃 슬롯). */\nconst Header = forwardRef<HTMLDivElement, ComponentPropsWithoutRef<'div'>>(function ModalHeader(\n { className, ...rest },\n ref,\n) {\n return <div ref={ref} className={cx('nuri-modal__header', className)} {...rest} />;\n});\n\nexport interface ModalTitleProps extends ComponentPropsWithoutRef<typeof Dialog.Title> {\n /**\n * 시각적으로는 숨기되 스크린리더에는 노출. 시각적 제목이 없는 모달도\n * 접근 가능한 이름을 가져야 하므로(Radix Dialog는 Title 필수), 이 옵션으로 처리.\n * @example <Modal.Title visuallyHidden>이미지 미리보기</Modal.Title>\n */\n visuallyHidden?: boolean;\n}\n\n/** 모달 제목. 렌더 시 자동으로 `aria-labelledby`로 연결됨 (Radix Dialog.Title). */\nconst Title = forwardRef<ElementRef<typeof Dialog.Title>, ModalTitleProps>(function ModalTitle(\n { visuallyHidden = false, className, ...rest },\n ref,\n) {\n return (\n <Dialog.Title\n ref={ref}\n className={cx('nuri-modal__title', visuallyHidden && 'nuri-sr-only', className)}\n {...rest}\n />\n );\n});\n\n/** 모달 설명. 렌더 시 자동으로 `aria-describedby`로 연결됨 (Radix Dialog.Description). */\nconst Description = forwardRef<\n ElementRef<typeof Dialog.Description>,\n ComponentPropsWithoutRef<typeof Dialog.Description>\n>(function ModalDescription({ className, ...rest }, ref) {\n return (\n <Dialog.Description ref={ref} className={cx('nuri-modal__description', className)} {...rest} />\n );\n});\n\n/** 본문 영역 (스크롤 가능). */\nconst Body = forwardRef<HTMLDivElement, ComponentPropsWithoutRef<'div'>>(function ModalBody(\n { className, ...rest },\n ref,\n) {\n return <div ref={ref} className={cx('nuri-modal__body', className)} {...rest} />;\n});\n\nexport interface ModalFooterProps extends ComponentPropsWithoutRef<'div'> {\n /**\n * 액션 버튼 정렬.\n * - `end`: 우측 (기본, 데스크탑 관례)\n * - `start`: 좌측\n * - `center`: 가운데\n * - `between`: 양끝 분산 (예: 좌측 위험 액션 + 우측 확인)\n * - `full`: 버튼들이 가로를 균등하게 채움 (모바일 전체너비)\n * @default 'end'\n */\n align?: 'start' | 'center' | 'end' | 'between' | 'full';\n}\n\n/** 푸터 영역 (액션 버튼 배치). 정렬은 `align` prop으로 제어. */\nconst Footer = forwardRef<HTMLDivElement, ModalFooterProps>(function ModalFooter(\n { align = 'end', className, ...rest },\n ref,\n) {\n return (\n <div\n ref={ref}\n className={cx('nuri-modal__footer', `nuri-modal__footer--${align}`, className)}\n {...rest}\n />\n );\n});\n\n/**\n * 모달을 닫는 액션. `asChild`로 임의 엘리먼트에 위임.\n * @example <Modal.Close asChild><Button variant=\"outline\">취소</Button></Modal.Close>\n */\nconst Close = Dialog.Close;\n\nconst CloseIcon = () => (\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 20 20\" fill=\"none\" aria-hidden=\"true\">\n <path d=\"M5 5l10 10M15 5L5 15\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" />\n </svg>\n);\n\n/** 우상단 X 닫기 버튼 (미리 스타일링됨). 기본 `aria-label=\"닫기\"`. */\nconst CloseButton = forwardRef<\n HTMLButtonElement,\n Omit<ComponentPropsWithoutRef<'button'>, 'children'> & { children?: ReactNode }\n>(function ModalCloseButton(\n { className, 'aria-label': ariaLabel = '닫기', children, ...rest },\n ref,\n) {\n return (\n <Dialog.Close asChild>\n <button\n ref={ref}\n type=\"button\"\n aria-label={ariaLabel}\n className={cx('nuri-modal__close', className)}\n {...rest}\n >\n {children ?? <CloseIcon />}\n </button>\n </Dialog.Close>\n );\n});\n\nexport const Modal = Object.assign(Root, {\n Trigger,\n Content,\n Header,\n Title,\n Description,\n Body,\n Footer,\n Close,\n CloseButton,\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":"
|
|
1
|
+
{"version":3,"sources":["../../src/Modal/Modal.tsx","../../src/utils/cx.ts"],"sourcesContent":["'use client';\n\nimport './styles.css';\nimport * as Dialog from '@radix-ui/react-dialog';\nimport { forwardRef } from 'react';\nimport type { ComponentPropsWithoutRef, ElementRef, ReactNode } from 'react';\nimport { cx } from '../utils/cx.js';\n\ntype Placement = 'center' | 'bottom';\ntype Size = 'sm' | 'md' | 'lg';\n\n/**\n * 모달 루트. open/onOpenChange로 제어하거나, Modal.Trigger와 함께 비제어로도 사용.\n * Radix Dialog 기반 — 포커스 트랩·Esc·스크롤 락·포커스 복귀·aria 연결을 자동 제공.\n *\n * @example 제어형\n * <Modal open={open} onOpenChange={setOpen}>\n * <Modal.Content>...</Modal.Content>\n * </Modal>\n *\n * @example 비제어 (트리거)\n * <Modal>\n * <Modal.Trigger asChild><Button>열기</Button></Modal.Trigger>\n * <Modal.Content>...</Modal.Content>\n * </Modal>\n */\nconst Root = (props: ComponentPropsWithoutRef<typeof Dialog.Root>) => <Dialog.Root {...props} />;\nRoot.displayName = 'Modal';\n\n/** 모달을 여는 트리거. `asChild`로 임의 엘리먼트(Button 등)에 위임. */\nconst Trigger = Dialog.Trigger;\n\nexport interface ModalContentProps extends ComponentPropsWithoutRef<typeof Dialog.Content> {\n /**\n * 표시 위치.\n * - `center`: 화면 중앙 다이얼로그 (기본)\n * - `bottom`: 하단 바텀시트 (모바일에서 아래에서 올라옴)\n * @default 'center'\n */\n placement?: Placement;\n /**\n * 최대 너비. **`placement=\"center\"`에서만 적용** (bottom 바텀시트는 항상 풀 width).\n * @default 'md'\n */\n size?: Size;\n /** 백드롭(오버레이) 표시 여부. @default true */\n overlay?: boolean;\n /** 포털 렌더 대상. 기본은 document.body. 다른 요소에 렌더하려면 지정. */\n container?: HTMLElement | null;\n}\n\n/**\n * 오버레이 + 포털 + 다이얼로그 패널을 한 번에 렌더.\n * 내부에 Modal.Header / Modal.Body / Modal.Footer를 자유롭게 합성.\n */\nconst Content = forwardRef<ElementRef<typeof Dialog.Content>, ModalContentProps>(\n function ModalContent(\n { placement = 'center', size = 'md', overlay = true, container, className, children, ...rest },\n ref,\n ) {\n const classes = cx('nuri-modal', `nuri-modal--${placement}`, `nuri-modal--${size}`, className);\n\n return (\n <Dialog.Portal container={container}>\n {overlay && <Dialog.Overlay className=\"nuri-modal__overlay\" />}\n <Dialog.Content ref={ref} className={classes} {...rest}>\n {children}\n </Dialog.Content>\n </Dialog.Portal>\n );\n },\n);\n\n/** 헤더 영역 (Title/Description/CloseButton을 담는 레이아웃 슬롯). */\nconst Header = forwardRef<HTMLDivElement, ComponentPropsWithoutRef<'div'>>(function ModalHeader(\n { className, ...rest },\n ref,\n) {\n return <div ref={ref} className={cx('nuri-modal__header', className)} {...rest} />;\n});\n\nexport interface ModalTitleProps extends ComponentPropsWithoutRef<typeof Dialog.Title> {\n /**\n * 시각적으로는 숨기되 스크린리더에는 노출. 시각적 제목이 없는 모달도\n * 접근 가능한 이름을 가져야 하므로(Radix Dialog는 Title 필수), 이 옵션으로 처리.\n * @example <Modal.Title visuallyHidden>이미지 미리보기</Modal.Title>\n */\n visuallyHidden?: boolean;\n}\n\n/** 모달 제목. 렌더 시 자동으로 `aria-labelledby`로 연결됨 (Radix Dialog.Title). */\nconst Title = forwardRef<ElementRef<typeof Dialog.Title>, ModalTitleProps>(function ModalTitle(\n { visuallyHidden = false, className, ...rest },\n ref,\n) {\n return (\n <Dialog.Title\n ref={ref}\n className={cx('nuri-modal__title', visuallyHidden && 'nuri-sr-only', className)}\n {...rest}\n />\n );\n});\n\n/** 모달 설명. 렌더 시 자동으로 `aria-describedby`로 연결됨 (Radix Dialog.Description). */\nconst Description = forwardRef<\n ElementRef<typeof Dialog.Description>,\n ComponentPropsWithoutRef<typeof Dialog.Description>\n>(function ModalDescription({ className, ...rest }, ref) {\n return (\n <Dialog.Description ref={ref} className={cx('nuri-modal__description', className)} {...rest} />\n );\n});\n\n/** 본문 영역 (스크롤 가능). */\nconst Body = forwardRef<HTMLDivElement, ComponentPropsWithoutRef<'div'>>(function ModalBody(\n { className, ...rest },\n ref,\n) {\n return <div ref={ref} className={cx('nuri-modal__body', className)} {...rest} />;\n});\n\nexport interface ModalFooterProps extends ComponentPropsWithoutRef<'div'> {\n /**\n * 액션 버튼 정렬.\n * - `end`: 우측 (기본, 데스크탑 관례)\n * - `start`: 좌측\n * - `center`: 가운데\n * - `between`: 양끝 분산 (예: 좌측 위험 액션 + 우측 확인)\n * - `full`: 버튼들이 가로를 균등하게 채움 (모바일 전체너비)\n * @default 'end'\n */\n align?: 'start' | 'center' | 'end' | 'between' | 'full';\n}\n\n/** 푸터 영역 (액션 버튼 배치). 정렬은 `align` prop으로 제어. */\nconst Footer = forwardRef<HTMLDivElement, ModalFooterProps>(function ModalFooter(\n { align = 'end', className, ...rest },\n ref,\n) {\n return (\n <div\n ref={ref}\n className={cx('nuri-modal__footer', `nuri-modal__footer--${align}`, className)}\n {...rest}\n />\n );\n});\n\n/**\n * 모달을 닫는 액션. `asChild`로 임의 엘리먼트에 위임.\n * @example <Modal.Close asChild><Button variant=\"outline\">취소</Button></Modal.Close>\n */\nconst Close = Dialog.Close;\n\nconst CloseIcon = () => (\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 20 20\" fill=\"none\" aria-hidden=\"true\">\n <path d=\"M5 5l10 10M15 5L5 15\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" />\n </svg>\n);\n\n/** 우상단 X 닫기 버튼 (미리 스타일링됨). 기본 `aria-label=\"닫기\"`. */\nconst CloseButton = forwardRef<\n HTMLButtonElement,\n Omit<ComponentPropsWithoutRef<'button'>, 'children'> & { children?: ReactNode }\n>(function ModalCloseButton(\n { className, 'aria-label': ariaLabel = '닫기', children, ...rest },\n ref,\n) {\n return (\n <Dialog.Close asChild>\n <button\n ref={ref}\n type=\"button\"\n aria-label={ariaLabel}\n className={cx('nuri-modal__close', className)}\n {...rest}\n >\n {children ?? <CloseIcon />}\n </button>\n </Dialog.Close>\n );\n});\n\nexport const Modal = Object.assign(Root, {\n Trigger,\n Content,\n Header,\n Title,\n Description,\n Body,\n Footer,\n Close,\n CloseButton,\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,YAAY;AACxB,SAAS,kBAAkB;;;ACApB,SAAS,MAAM,SAA2D;AAC/E,SAAO,QAAQ,OAAO,OAAO,EAAE,KAAK,GAAG;AACzC;;;ADoBsE,cAqChE,YArCgE;AAAtE,IAAMA,QAAO,CAAC,UAAwD,oBAAQ,aAAP,EAAa,GAAG,OAAO;AAC9FA,MAAK,cAAc;AAGnB,IAAMC,WAAiB;AAyBvB,IAAMC,WAAU;AAAA,EACd,SAAS,aACP,EAAE,YAAY,UAAU,OAAO,MAAM,UAAU,MAAM,WAAW,WAAW,UAAU,GAAG,KAAK,GAC7F,KACA;AACA,UAAM,UAAU,GAAG,cAAc,eAAe,SAAS,IAAI,eAAe,IAAI,IAAI,SAAS;AAE7F,WACE,qBAAQ,eAAP,EAAc,WACZ;AAAA,iBAAW,oBAAQ,gBAAP,EAAe,WAAU,uBAAsB;AAAA,MAC5D,oBAAQ,gBAAP,EAAe,KAAU,WAAW,SAAU,GAAG,MAC/C,UACH;AAAA,OACF;AAAA,EAEJ;AACF;AAGA,IAAM,SAAS,WAA4D,SAAS,YAClF,EAAE,WAAW,GAAG,KAAK,GACrB,KACA;AACA,SAAO,oBAAC,SAAI,KAAU,WAAW,GAAG,sBAAsB,SAAS,GAAI,GAAG,MAAM;AAClF,CAAC;AAYD,IAAMC,SAAQ,WAA6D,SAAS,WAClF,EAAE,iBAAiB,OAAO,WAAW,GAAG,KAAK,GAC7C,KACA;AACA,SACE;AAAA,IAAQ;AAAA,IAAP;AAAA,MACC;AAAA,MACA,WAAW,GAAG,qBAAqB,kBAAkB,gBAAgB,SAAS;AAAA,MAC7E,GAAG;AAAA;AAAA,EACN;AAEJ,CAAC;AAGD,IAAMC,eAAc,WAGlB,SAAS,iBAAiB,EAAE,WAAW,GAAG,KAAK,GAAG,KAAK;AACvD,SACE,oBAAQ,oBAAP,EAAmB,KAAU,WAAW,GAAG,2BAA2B,SAAS,GAAI,GAAG,MAAM;AAEjG,CAAC;AAGD,IAAM,OAAO,WAA4D,SAAS,UAChF,EAAE,WAAW,GAAG,KAAK,GACrB,KACA;AACA,SAAO,oBAAC,SAAI,KAAU,WAAW,GAAG,oBAAoB,SAAS,GAAI,GAAG,MAAM;AAChF,CAAC;AAgBD,IAAM,SAAS,WAA6C,SAAS,YACnE,EAAE,QAAQ,OAAO,WAAW,GAAG,KAAK,GACpC,KACA;AACA,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,WAAW,GAAG,sBAAsB,uBAAuB,KAAK,IAAI,SAAS;AAAA,MAC5E,GAAG;AAAA;AAAA,EACN;AAEJ,CAAC;AAMD,IAAMC,SAAe;AAErB,IAAM,YAAY,MAChB,oBAAC,SAAI,OAAM,MAAK,QAAO,MAAK,SAAQ,aAAY,MAAK,QAAO,eAAY,QACtE,8BAAC,UAAK,GAAE,wBAAuB,QAAO,gBAAe,aAAY,OAAM,eAAc,SAAQ,GAC/F;AAIF,IAAM,cAAc,WAGlB,SAAS,iBACT,EAAE,WAAW,cAAc,YAAY,gBAAM,UAAU,GAAG,KAAK,GAC/D,KACA;AACA,SACE,oBAAQ,cAAP,EAAa,SAAO,MACnB;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,MAAK;AAAA,MACL,cAAY;AAAA,MACZ,WAAW,GAAG,qBAAqB,SAAS;AAAA,MAC3C,GAAG;AAAA,MAEH,sBAAY,oBAAC,aAAU;AAAA;AAAA,EAC1B,GACF;AAEJ,CAAC;AAEM,IAAM,QAAQ,OAAO,OAAOL,OAAM;AAAA,EACvC,SAAAC;AAAA,EACA,SAAAC;AAAA,EACA;AAAA,EACA,OAAAC;AAAA,EACA,aAAAC;AAAA,EACA;AAAA,EACA;AAAA,EACA,OAAAC;AAAA,EACA;AACF,CAAC;","names":["Root","Trigger","Content","Title","Description","Close"]}
|
package/dist/Radio/index.cjs
CHANGED
package/dist/Radio/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/Radio/index.ts","../../src/Radio/Radio.tsx","../../src/utils/cx.ts"],"sourcesContent":["export { RadioGroup, Radio } from './Radio.js';\nexport type { RadioGroupProps, RadioProps } from './Radio.js';\n","'use client';\n\nimport './styles.css';\nimport * as RadixRadioGroup from '@radix-ui/react-radio-group';\nimport { forwardRef, useId } from 'react';\nimport type { ReactNode } from 'react';\nimport { cx } from '../utils/cx.js';\n\nexport interface RadioGroupProps {\n children: ReactNode;\n /** 선택된 값 (제어형). */\n value?: string;\n /** 초기 값 (비제어형). */\n defaultValue?: string;\n onValueChange?: (value: string) => void;\n /** 배치 방향 — 키보드 화살표 탐색 방향에도 반영됨. @default 'vertical' */\n orientation?: 'vertical' | 'horizontal';\n disabled?: boolean;\n /** 폼 전송용 name. */\n name?: string;\n required?: boolean;\n className?: string;\n /** 그룹의 접근 가능한 이름 (시각 제목이 없을 때 필수). */\n 'aria-label'?: string;\n 'aria-labelledby'?: string;\n}\n\n/**\n * 라디오 그룹. Radix RadioGroup 기반 — 화살표 키 그룹 탐색·단일 선택·폼 참여 자동.\n * 그룹에 `aria-label`(또는 aria-labelledby)을 지정한다.\n *\n * @example\n * <RadioGroup aria-label=\"정렬\" value={sort} onValueChange={setSort}>\n * <Radio value=\"latest\" label=\"최신순\" />\n * <Radio value=\"edited\" label=\"수정순\" />\n * </RadioGroup>\n */\nexport const RadioGroup = forwardRef<HTMLDivElement, RadioGroupProps>(function RadioGroup(\n { children, orientation = 'vertical', className, ...rest },\n ref,\n) {\n return (\n <RadixRadioGroup.Root\n ref={ref}\n orientation={orientation}\n className={cx('nuri-radio-group', `nuri-radio-group--${orientation}`, className)}\n {...rest}\n >\n {children}\n </RadixRadioGroup.Root>\n );\n});\n\nexport interface RadioProps {\n /** 이 항목이 나타내는 값. */\n value: string;\n /** 라벨 (컨트롤 우측). 없으면 `aria-label` 필수. */\n label?: ReactNode;\n disabled?: boolean;\n id?: string;\n className?: string;\n 'aria-label'?: string;\n}\n\n/** 라디오 항목 — 반드시 RadioGroup 안에서 사용. */\nexport const Radio = forwardRef<HTMLButtonElement, RadioProps>(function Radio(\n { value, label, disabled = false, id: idProp, className, ...rest },\n ref,\n) {\n const reactId = useId();\n const id = idProp ?? `nuri-radio-${reactId}`;\n\n return (\n <div className={cx('nuri-radio', className)}>\n <RadixRadioGroup.Item\n ref={ref}\n id={id}\n value={value}\n disabled={disabled}\n className=\"nuri-radio__control\"\n {...rest}\n >\n <RadixRadioGroup.Indicator className=\"nuri-radio__indicator\" />\n </RadixRadioGroup.Item>\n {label != null && (\n <label htmlFor={id} className=\"nuri-radio__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":"
|
|
1
|
+
{"version":3,"sources":["../../src/Radio/index.ts","../../src/Radio/Radio.tsx","../../src/utils/cx.ts"],"sourcesContent":["export { RadioGroup, Radio } from './Radio.js';\nexport type { RadioGroupProps, RadioProps } from './Radio.js';\n","'use client';\n\nimport './styles.css';\nimport * as RadixRadioGroup from '@radix-ui/react-radio-group';\nimport { forwardRef, useId } from 'react';\nimport type { ReactNode } from 'react';\nimport { cx } from '../utils/cx.js';\n\nexport interface RadioGroupProps {\n children: ReactNode;\n /** 선택된 값 (제어형). */\n value?: string;\n /** 초기 값 (비제어형). */\n defaultValue?: string;\n onValueChange?: (value: string) => void;\n /** 배치 방향 — 키보드 화살표 탐색 방향에도 반영됨. @default 'vertical' */\n orientation?: 'vertical' | 'horizontal';\n disabled?: boolean;\n /** 폼 전송용 name. */\n name?: string;\n required?: boolean;\n className?: string;\n /** 그룹의 접근 가능한 이름 (시각 제목이 없을 때 필수). */\n 'aria-label'?: string;\n 'aria-labelledby'?: string;\n}\n\n/**\n * 라디오 그룹. Radix RadioGroup 기반 — 화살표 키 그룹 탐색·단일 선택·폼 참여 자동.\n * 그룹에 `aria-label`(또는 aria-labelledby)을 지정한다.\n *\n * @example\n * <RadioGroup aria-label=\"정렬\" value={sort} onValueChange={setSort}>\n * <Radio value=\"latest\" label=\"최신순\" />\n * <Radio value=\"edited\" label=\"수정순\" />\n * </RadioGroup>\n */\nexport const RadioGroup = forwardRef<HTMLDivElement, RadioGroupProps>(function RadioGroup(\n { children, orientation = 'vertical', className, ...rest },\n ref,\n) {\n return (\n <RadixRadioGroup.Root\n ref={ref}\n orientation={orientation}\n className={cx('nuri-radio-group', `nuri-radio-group--${orientation}`, className)}\n {...rest}\n >\n {children}\n </RadixRadioGroup.Root>\n );\n});\n\nexport interface RadioProps {\n /** 이 항목이 나타내는 값. */\n value: string;\n /** 라벨 (컨트롤 우측). 없으면 `aria-label` 필수. */\n label?: ReactNode;\n disabled?: boolean;\n id?: string;\n className?: string;\n 'aria-label'?: string;\n}\n\n/** 라디오 항목 — 반드시 RadioGroup 안에서 사용. */\nexport const Radio = forwardRef<HTMLButtonElement, RadioProps>(function Radio(\n { value, label, disabled = false, id: idProp, className, ...rest },\n ref,\n) {\n const reactId = useId();\n const id = idProp ?? `nuri-radio-${reactId}`;\n\n return (\n <div className={cx('nuri-radio', className)}>\n <RadixRadioGroup.Item\n ref={ref}\n id={id}\n value={value}\n disabled={disabled}\n className=\"nuri-radio__control\"\n {...rest}\n >\n <RadixRadioGroup.Indicator className=\"nuri-radio__indicator\" />\n </RadixRadioGroup.Item>\n {label != null && (\n <label htmlFor={id} className=\"nuri-radio__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;AAAA;;;ACEA,oBAAO;AACP,sBAAiC;AACjC,mBAAkC;;;ACA3B,SAAS,MAAM,SAA2D;AAC/E,SAAO,QAAQ,OAAO,OAAO,EAAE,KAAK,GAAG;AACzC;;;ADoCI;AALG,IAAM,iBAAa,yBAA4C,SAASA,YAC7E,EAAE,UAAU,cAAc,YAAY,WAAW,GAAG,KAAK,GACzD,KACA;AACA,SACE;AAAA,IAAiB;AAAA,IAAhB;AAAA,MACC;AAAA,MACA;AAAA,MACA,WAAW,GAAG,oBAAoB,qBAAqB,WAAW,IAAI,SAAS;AAAA,MAC9E,GAAG;AAAA,MAEH;AAAA;AAAA,EACH;AAEJ,CAAC;AAcM,IAAM,YAAQ,yBAA0C,SAASC,OACtE,EAAE,OAAO,OAAO,WAAW,OAAO,IAAI,QAAQ,WAAW,GAAG,KAAK,GACjE,KACA;AACA,QAAM,cAAU,oBAAM;AACtB,QAAM,KAAK,UAAU,cAAc,OAAO;AAE1C,SACE,6CAAC,SAAI,WAAW,GAAG,cAAc,SAAS,GACxC;AAAA;AAAA,MAAiB;AAAA,MAAhB;AAAA,QACC;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,WAAU;AAAA,QACT,GAAG;AAAA,QAEJ,sDAAiB,2BAAhB,EAA0B,WAAU,yBAAwB;AAAA;AAAA,IAC/D;AAAA,IACC,SAAS,QACR,4CAAC,WAAM,SAAS,IAAI,WAAU,qBAC3B,iBACH;AAAA,KAEJ;AAEJ,CAAC;","names":["RadioGroup","Radio"]}
|