@pantoia/ui 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/accordion/AGENTS.md +60 -0
  2. package/accordion/index.module.css +10 -0
  3. package/accordion/index.tsx +91 -0
  4. package/alert-dialog/AGENTS.md +78 -0
  5. package/alert-dialog/index.tsx +119 -0
  6. package/badge/AGENTS.md +35 -0
  7. package/badge/index.tsx +34 -0
  8. package/button/AGENTS.md +39 -0
  9. package/button/index.tsx +98 -0
  10. package/callout/AGENTS.md +42 -0
  11. package/callout/index.tsx +70 -0
  12. package/checkbox/AGENTS.md +54 -0
  13. package/checkbox/index.tsx +151 -0
  14. package/code-block/AGENTS.md +60 -0
  15. package/code-block/index.module.css +46 -0
  16. package/code-block/index.tsx +127 -0
  17. package/combobox/AGENTS.md +150 -0
  18. package/combobox/index.tsx +420 -0
  19. package/command-palette/AGENTS.md +44 -0
  20. package/command-palette/index.tsx +108 -0
  21. package/copy-button/AGENTS.md +43 -0
  22. package/copy-button/index.tsx +119 -0
  23. package/dialog/AGENTS.md +81 -0
  24. package/dialog/index.tsx +108 -0
  25. package/dist/index.js +189 -0
  26. package/drawer/AGENTS.md +66 -0
  27. package/drawer/index.module.css +17 -0
  28. package/drawer/index.tsx +198 -0
  29. package/dropdown-menu/AGENTS.md +133 -0
  30. package/dropdown-menu/index.tsx +223 -0
  31. package/field/AGENTS.md +96 -0
  32. package/field/index.tsx +149 -0
  33. package/input/AGENTS.md +35 -0
  34. package/input/index.tsx +36 -0
  35. package/kbd/AGENTS.md +17 -0
  36. package/kbd/index.tsx +13 -0
  37. package/label/AGENTS.md +21 -0
  38. package/label/index.tsx +13 -0
  39. package/number-field/AGENTS.md +53 -0
  40. package/number-field/index.tsx +227 -0
  41. package/package.json +73 -0
  42. package/popover/AGENTS.md +64 -0
  43. package/popover/index.tsx +66 -0
  44. package/press.css +37 -0
  45. package/radio-group/AGENTS.md +81 -0
  46. package/radio-group/index.tsx +139 -0
  47. package/registry.json +726 -0
  48. package/select/AGENTS.md +90 -0
  49. package/select/index.tsx +169 -0
  50. package/styles.css +347 -0
  51. package/switch/AGENTS.md +37 -0
  52. package/switch/index.tsx +42 -0
  53. package/table/AGENTS.md +82 -0
  54. package/table/index.tsx +120 -0
  55. package/tabs/AGENTS.md +60 -0
  56. package/tabs/index.module.css +14 -0
  57. package/tabs/index.tsx +105 -0
  58. package/test-utils.ts +10 -0
  59. package/textarea/AGENTS.md +35 -0
  60. package/textarea/index.tsx +37 -0
  61. package/toast/AGENTS.md +42 -0
  62. package/toast/index.tsx +71 -0
  63. package/toggle/AGENTS.md +37 -0
  64. package/toggle/index.tsx +36 -0
  65. package/tooltip/AGENTS.md +50 -0
  66. package/tooltip/index.tsx +57 -0
  67. package/tv.ts +23 -0
@@ -0,0 +1,90 @@
1
+ # Select
2
+
3
+ ```
4
+ 받기 npx @pantoia/ui add select
5
+ 층 ui
6
+ 파일 select/index.tsx · select/AGENTS.md
7
+ ```
8
+
9
+ ## 부위
10
+
11
+ | 이름 | props |
12
+ |---|---|
13
+ | `Select` | `SelectProps` |
14
+ | `SelectGroup` | — |
15
+ | `SelectItem` | — |
16
+ | `SelectItemText` | — |
17
+ | `SelectItemDescription` | — |
18
+ | `SelectSeparator` | — |
19
+
20
+ ### `SelectGroup`
21
+
22
+ | 이름 | 타입 | 필수 |
23
+ |---|---|---|
24
+ | `label` | `string` | |
25
+ | `className` | `string` | |
26
+ | `children` | `ReactNode` | |
27
+
28
+ ### `SelectItem`
29
+
30
+ | 이름 | 타입 | 필수 |
31
+ |---|---|---|
32
+ | `value` | `string` | 예 |
33
+ | `disabled` | `boolean` | |
34
+ | `className` | `string` | |
35
+ | `children` | `ReactNode` | |
36
+
37
+ ### `SelectItemText`
38
+
39
+ | 이름 | 타입 | 필수 |
40
+ |---|---|---|
41
+ | `className` | `string` | |
42
+ | `children` | `ReactNode` | |
43
+
44
+ ### `SelectItemDescription`
45
+
46
+ | 이름 | 타입 | 필수 |
47
+ |---|---|---|
48
+ | `className` | `string` | |
49
+ | `children` | `ReactNode` | |
50
+
51
+ ### `SelectSeparator`
52
+
53
+ | 이름 | 타입 | 필수 |
54
+ |---|---|---|
55
+ | `className` | `string` | |
56
+
57
+ ## props
58
+
59
+ ### `SelectProps`
60
+
61
+ `Omit<ComponentProps<typeof SelectPrimitive.Trigger>, 'value' | 'defaultValue' | 'dir'>` 를 확장한다.
62
+
63
+ | 이름 | 타입 | 필수 |
64
+ |---|---|---|
65
+ | `value` | `string` | |
66
+ | `defaultValue` | `string` | |
67
+ | `onValueChange` | `(value: string) => void` | |
68
+ | `open` | `boolean` | |
69
+ | `defaultOpen` | `boolean` | |
70
+ | `onOpenChange` | `(open: boolean) => void` | |
71
+ | `placeholder` | `string` | |
72
+ | `surface` | `'bare' \| 'well'` | |
73
+ | `size` | `'sm' \| 'md' \| 'lg'` | |
74
+ | `arrow` | `boolean` | |
75
+ | `children` | `ReactNode` | |
76
+
77
+ ## 변형 — `selectTrigger`
78
+
79
+ | 축 | 값 | 기본 |
80
+ |---|---|---|
81
+ | `surface` | `bare` · `well` | `bare` |
82
+ | `size` | `sm` · `md` · `lg` | `md` |
83
+
84
+ ## 함께 받는 것
85
+
86
+ `tv`
87
+
88
+ ## npm 의존
89
+
90
+ `lucide-react` · `radix-ui`
@@ -0,0 +1,169 @@
1
+ 'use client'
2
+
3
+ import { Check, ChevronDown, ChevronUp } from 'lucide-react'
4
+ import { Select as SelectPrimitive } from 'radix-ui'
5
+ import type { ComponentProps, ReactNode } from 'react'
6
+
7
+ import { tv } from '../tv'
8
+
9
+ export const selectTrigger = tv({
10
+ base: 'flex w-full items-center justify-between gap-tight rounded-r8'
11
+ + ' text-value text-fg'
12
+ + ' transition-state'
13
+ + ' invalid:ring-2 invalid:ring-danger'
14
+ + ' focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring'
15
+ + ' disabled:bg-disabled disabled:text-fg-disabled disabled:cursor-not-allowed'
16
+ + ' unset:text-fg-placeholder',
17
+
18
+ variants: {
19
+ surface: {
20
+ bare: 'bg-transparent ring-1 ring-inset ring-line-strong',
21
+ well: 'bg-well',
22
+ },
23
+ size: {
24
+ sm: 'h-8 px-3',
25
+ md: 'h-9 px-3',
26
+ lg: 'h-10 px-3 text-t16',
27
+ },
28
+ },
29
+
30
+ defaultVariants: { surface: 'bare', size: 'md' },
31
+ })
32
+
33
+ export interface SelectProps extends Omit<ComponentProps<typeof SelectPrimitive.Trigger>, 'value' | 'defaultValue' | 'dir'> {
34
+ value?: string
35
+ defaultValue?: string
36
+ onValueChange?: (value: string) => void
37
+ open?: boolean
38
+ defaultOpen?: boolean
39
+ onOpenChange?: (open: boolean) => void
40
+ placeholder?: string
41
+ surface?: 'bare' | 'well'
42
+ size?: 'sm' | 'md' | 'lg'
43
+ arrow?: boolean
44
+ children?: ReactNode
45
+ }
46
+
47
+ export function Select({
48
+ value,
49
+ defaultValue,
50
+ onValueChange,
51
+ open,
52
+ defaultOpen,
53
+ onOpenChange,
54
+ placeholder,
55
+ surface,
56
+ size,
57
+ arrow = false,
58
+ className,
59
+ children,
60
+ ...rest
61
+ }: SelectProps) {
62
+ return (
63
+ <SelectPrimitive.Root
64
+ value={value}
65
+ defaultValue={defaultValue}
66
+ onValueChange={onValueChange}
67
+ open={open}
68
+ defaultOpen={defaultOpen}
69
+ onOpenChange={onOpenChange}
70
+ >
71
+ <SelectPrimitive.Trigger {...rest} className={selectTrigger({ surface, size, className })}>
72
+ <span className="min-w-0 truncate">
73
+ <SelectPrimitive.Value placeholder={placeholder} />
74
+ </span>
75
+ <SelectPrimitive.Icon className="text-fg-subtle">
76
+ <ChevronDown className="size-4" />
77
+ </SelectPrimitive.Icon>
78
+ </SelectPrimitive.Trigger>
79
+
80
+ <SelectPrimitive.Portal>
81
+ <SelectPrimitive.Content
82
+ className="bg-float text-fg rounded-float shadow-s3 z-overlay
83
+ min-w-(--radix-select-trigger-width) p-1
84
+ open:animate-enter closed:animate-exit"
85
+ position="popper"
86
+ sideOffset={8}
87
+ arrowPadding={16}
88
+ >
89
+ <SelectPrimitive.ScrollUpButton className="text-fg-subtle flex h-6 cursor-default items-center justify-center">
90
+ <ChevronUp className="size-4" />
91
+ </SelectPrimitive.ScrollUpButton>
92
+
93
+ <SelectPrimitive.Viewport className="flex flex-col gap-px">
94
+ {children}
95
+ </SelectPrimitive.Viewport>
96
+
97
+ <SelectPrimitive.ScrollDownButton className="text-fg-subtle flex h-6 cursor-default items-center justify-center">
98
+ <ChevronDown className="size-4" />
99
+ </SelectPrimitive.ScrollDownButton>
100
+
101
+ {arrow && <SelectPrimitive.Arrow className="fill-float" width={12} height={6} />}
102
+ </SelectPrimitive.Content>
103
+ </SelectPrimitive.Portal>
104
+ </SelectPrimitive.Root>
105
+ )
106
+ }
107
+
108
+ export function SelectGroup({ label, className, children }: {
109
+ label?: string
110
+ className?: string
111
+ children?: ReactNode
112
+ }) {
113
+ return (
114
+ <SelectPrimitive.Group className={tv({ base: 'flex flex-col gap-px' })({ className })}>
115
+ {label && (
116
+ <SelectPrimitive.Label className="text-caption text-fg-subtle px-2 pb-1 pt-2">
117
+ {label}
118
+ </SelectPrimitive.Label>
119
+ )}
120
+ {children}
121
+ </SelectPrimitive.Group>
122
+ )
123
+ }
124
+
125
+ export function SelectItem({ value, disabled, className, children }: {
126
+ value: string
127
+ disabled?: boolean
128
+ className?: string
129
+ children?: ReactNode
130
+ }) {
131
+ return (
132
+ <SelectPrimitive.Item
133
+ value={value}
134
+ disabled={disabled}
135
+ className={tv({ base: 'flex min-h-9 cursor-default items-center justify-between gap-stack'
136
+ + ' rounded-r8 px-2 py-1 transition-state'
137
+ + ' data-highlighted:bg-hover data-highlighted:outline-none'
138
+ + ' data-disabled:text-fg-disabled data-disabled:pointer-events-none' })({ className })}
139
+ >
140
+ <span className="flex min-w-0 flex-col">
141
+ {children}
142
+ </span>
143
+
144
+ <SelectPrimitive.ItemIndicator className="text-fg-muted flex shrink-0">
145
+ <Check className="size-4" />
146
+ </SelectPrimitive.ItemIndicator>
147
+ </SelectPrimitive.Item>
148
+ )
149
+ }
150
+
151
+ export function SelectItemText({ className, children }: { className?: string, children?: ReactNode }) {
152
+ return (
153
+ <SelectPrimitive.ItemText className={tv({ base: 'text-label text-fg' })({ className })}>
154
+ {children}
155
+ </SelectPrimitive.ItemText>
156
+ )
157
+ }
158
+
159
+ export function SelectItemDescription({ className, children }: { className?: string, children?: ReactNode }) {
160
+ return (
161
+ <span className={tv({ base: 'text-caption text-fg-subtle' })({ className })}>
162
+ {children}
163
+ </span>
164
+ )
165
+ }
166
+
167
+ export function SelectSeparator({ className }: { className?: string }) {
168
+ return <SelectPrimitive.Separator className={tv({ base: 'bg-line -mx-1 my-1 h-px' })({ className })} />
169
+ }
package/styles.css ADDED
@@ -0,0 +1,347 @@
1
+ @import "pretendard/dist/web/variable/pretendardvariable-dynamic-subset.css";
2
+ @import "./press.css";
3
+
4
+ @custom-variant dark (&:where(.dark, .dark *):not(:where(.light, .light *)));
5
+
6
+ @custom-variant current (&[aria-current="page"]);
7
+
8
+ @custom-variant invalid (&[aria-invalid="true"]);
9
+
10
+ @custom-variant invalid-within (&:has([aria-invalid="true"]));
11
+
12
+ @custom-variant in-invalid (&:where([aria-invalid="true"] *));
13
+
14
+ @custom-variant described (&:has([data-description]));
15
+
16
+ @custom-variant checked (&:where([data-state="checked"], [data-state="indeterminate"]));
17
+ @custom-variant open (&[data-state="open"]);
18
+ @custom-variant closed (&[data-state="closed"]);
19
+ @custom-variant on (&[data-state="on"]);
20
+ @custom-variant selected (&[data-selected]:not([data-selected="false"]));
21
+ @custom-variant unset (&[data-placeholder]);
22
+
23
+ @custom-variant menu-gutter (.menu:has(.menu-lead) &:not(:has(.menu-lead)));
24
+
25
+ :root, .light {
26
+ --gray-50:#f9fafb; --gray-100:#f2f4f6; --gray-200:#e5e8eb; --gray-300:#d1d6db;
27
+ --gray-400:#b0b8c1; --gray-500:#8b95a1; --gray-600:#6b7684; --gray-700:#4e5968;
28
+ --gray-800:#333d4b; --gray-900:#191f28;
29
+
30
+ --purple-50:#f5f1fd; --purple-100:#e4d6ff; --purple-200:#ceb0ff; --purple-300:#bb8ffa;
31
+ --purple-400:#aa74f2; --purple-500:#9c63e4; --purple-600:#8750c9; --purple-700:#7543b0;
32
+ --purple-800:#633997; --purple-900:#54317f;
33
+
34
+ --blue-50:#e8f3ff; --blue-100:#c9e2ff; --blue-200:#90c2ff; --blue-300:#64a8ff;
35
+ --blue-400:#4593fc; --blue-500:#3182f6; --blue-600:#2272eb; --blue-700:#1b64da;
36
+ --blue-800:#1957c2; --blue-900:#194aa6;
37
+
38
+ --green-50:#f0faf6; --green-100:#aeefd5; --green-200:#76e4b8; --green-300:#3fd599;
39
+ --green-400:#15c47e; --green-500:#03b26c; --green-600:#02a262; --green-700:#029359;
40
+ --green-800:#028450; --green-900:#027648;
41
+
42
+ --orange-50:#fff3e0; --orange-100:#ffe0b0; --orange-200:#ffcd80; --orange-300:#ffbd51;
43
+ --orange-400:#ffa927; --orange-500:#fe9800; --orange-600:#fb8800; --orange-700:#f57800;
44
+ --orange-800:#ed6700; --orange-900:#e45600;
45
+
46
+ --red-50:#ffeeee; --red-100:#ffd4d6; --red-200:#feafb4; --red-300:#fb8890;
47
+ --red-400:#f66570; --red-500:#f04452; --red-600:#e42939; --red-700:#d22030;
48
+ --red-800:#bc1b2a; --red-900:#a51926;
49
+ }
50
+
51
+ .dark {
52
+ --gray-50:#202027; --gray-100:#2c2c35; --gray-200:#3c3c47; --gray-300:#4d4d59;
53
+ --gray-400:#62626d; --gray-500:#7e7e87; --gray-600:#9e9ea4; --gray-700:#c3c3c6;
54
+ --gray-800:#e4e4e5; --gray-900:#ffffff;
55
+
56
+ --purple-50:#362949; --purple-100:#462e66; --purple-200:#583682; --purple-300:#6b3fa0;
57
+ --purple-400:#8451c4; --purple-500:#9c63e4; --purple-600:#ad75f7; --purple-700:#c094ff;
58
+ --purple-800:#d6baff; --purple-900:#eadeff;
59
+
60
+ --blue-50:#202c4d; --blue-100:#23386a; --blue-200:#25478c; --blue-300:#265ab3;
61
+ --blue-400:#2970d9; --blue-500:#3485fa; --blue-600:#449bff; --blue-700:#61b0ff;
62
+ --blue-800:#8fcdff; --blue-900:#c8e7ff;
63
+
64
+ --green-50:#153729; --green-100:#135338; --green-200:#136d47; --green-300:#138a59;
65
+ --green-400:#13a065; --green-500:#16bb76; --green-600:#26cf88; --green-700:#4ee4a6;
66
+ --green-800:#82f6c5; --green-900:#ccffea;
67
+
68
+ --orange-50:#3d2500; --orange-100:#563200; --orange-200:#804600; --orange-300:#a85f00;
69
+ --orange-400:#cf7200; --orange-500:#f18600; --orange-600:#fd9528; --orange-700:#ffa861;
70
+ --orange-800:#ffc39e; --orange-900:#ffe4d6;
71
+
72
+ --red-50:#3c2020; --red-100:#562025; --red-200:#7a242d; --red-300:#9e2733;
73
+ --red-400:#ca2f3d; --red-500:#f04251; --red-600:#fa616d; --red-700:#fe818b;
74
+ --red-800:#ffa8ad; --red-900:#ffd1d3;
75
+ }
76
+
77
+ @theme {
78
+ --color-*: initial;
79
+ --text-*: initial;
80
+ --leading-*: initial;
81
+ --font-weight-*: initial;
82
+ }
83
+
84
+ @theme {
85
+ --font-weight-normal: 400;
86
+ --font-weight-medium: 500;
87
+ --font-weight-bold: 700;
88
+
89
+ --font-sans: "Pretendard Variable", "Pretendard", -apple-system, system-ui, sans-serif;
90
+ --font-mono: "JetBrains Mono", "Pretendard Variable", "Pretendard", ui-monospace, monospace;
91
+
92
+ --text-display: 28px; --text-display--line-height: 38px; --text-display--font-weight: 700;
93
+ --text-title: 24px; --text-title--line-height: 32px; --text-title--font-weight: 700;
94
+ --text-section: 20px; --text-section--line-height: 27px; --text-section--font-weight: 700;
95
+ --text-subsection: 16px; --text-subsection--line-height: 22px; --text-subsection--font-weight: 700;
96
+ --text-label: 14px; --text-label--line-height: 19px; --text-label--font-weight: 500;
97
+ --text-caption: 13px; --text-caption--line-height: 18px; --text-caption--font-weight: 400;
98
+ --text-micro: 11px; --text-micro--line-height: 15px; --text-micro--font-weight: 500;
99
+
100
+ --text-t10: 10px; --text-t10--line-height: 14px; --text-t10--font-weight: 400;
101
+ --text-t11: 11px; --text-t11--line-height: 15px; --text-t11--font-weight: 400;
102
+ --text-t12: 12px; --text-t12--line-height: 16px; --text-t12--font-weight: 400;
103
+ --text-t13: 13px; --text-t13--line-height: 18px; --text-t13--font-weight: 400;
104
+ --text-t14: 14px; --text-t14--line-height: 19px; --text-t14--font-weight: 400;
105
+ --text-t16: 16px; --text-t16--line-height: 22px; --text-t16--font-weight: 400;
106
+ --text-t18: 18px; --text-t18--line-height: 24px; --text-t18--font-weight: 400;
107
+ --text-t20: 20px; --text-t20--line-height: 27px; --text-t20--font-weight: 400;
108
+ --text-t24: 24px; --text-t24--line-height: 32px; --text-t24--font-weight: 400;
109
+ --text-t28: 28px; --text-t28--line-height: 38px; --text-t28--font-weight: 400;
110
+ --text-t32: 32px; --text-t32--line-height: 43px; --text-t32--font-weight: 400;
111
+ --text-t40: 40px; --text-t40--line-height: 54px; --text-t40--font-weight: 400;
112
+ --text-t48: 48px; --text-t48--line-height: 65px; --text-t48--font-weight: 400;
113
+ --text-t56: 56px; --text-t56--line-height: 76px; --text-t56--font-weight: 400;
114
+ --text-t64: 64px; --text-t64--line-height: 86px; --text-t64--font-weight: 400;
115
+ }
116
+
117
+ @theme inline {
118
+ --text-prose: var(--ui-prose);
119
+ --text-prose--line-height: var(--ui-prose-lh);
120
+ --text-prose--font-weight: 400;
121
+
122
+ --text-value: var(--ui-value);
123
+ --text-value--line-height: var(--ui-value-lh);
124
+ --text-value--font-weight: 400;
125
+ }
126
+
127
+ :root {
128
+ --ui-prose: 16px; --ui-prose-lh: 24px;
129
+ --ui-value: 16px; --ui-value-lh: 22px;
130
+
131
+ @variant sm { --ui-prose: 14px; --ui-prose-lh: 22px; }
132
+ @variant sm { --ui-value: 14px; --ui-value-lh: 19px; }
133
+ }
134
+
135
+ @font-face {
136
+ font-family: "JetBrains Mono";
137
+ src: url("@fontsource/jetbrains-mono/files/jetbrains-mono-latin-400-normal.woff2") format("woff2");
138
+ font-weight: 400;
139
+ font-display: swap;
140
+ /* [TODO] size-adjust 를 지운다 — 시각 기준 이미지가 서고 나서 */
141
+ size-adjust: 96%;
142
+ unicode-range: U+0000-00FF, U+2000-206F;
143
+ }
144
+
145
+ @layer base {
146
+ :root { color-scheme: light; }
147
+ .dark { color-scheme: dark; }
148
+
149
+ body {
150
+ background-color: var(--color-canvas);
151
+ color: var(--color-fg);
152
+ font-family: var(--font-sans);
153
+ }
154
+
155
+ code, pre, kbd, samp { font-family: var(--font-mono); }
156
+ }
157
+
158
+ @theme inline {
159
+ --color-white:#ffffff;
160
+ --color-black:#000000;
161
+
162
+ --color-canvas: var(--ui-canvas);
163
+ --color-well: var(--ui-well);
164
+ --color-panel: var(--ui-panel);
165
+ --color-float: var(--ui-float);
166
+ --color-disabled: var(--gray-200);
167
+
168
+ --color-hover: var(--ui-hover);
169
+ --color-selected: var(--ui-selected);
170
+ --color-selected-hover: var(--ui-selected-hover);
171
+
172
+ --color-control-on: var(--gray-900);
173
+ --color-control-on-hover: var(--gray-800);
174
+ --color-control-on-disabled: var(--gray-600);
175
+ --color-fg-on-control: var(--gray-50);
176
+
177
+ --color-fg: var(--gray-900);
178
+ --color-fg-muted: var(--gray-700);
179
+ --color-fg-subtle: var(--gray-500);
180
+ --color-fg-placeholder: var(--gray-400);
181
+ --color-fg-disabled: var(--gray-600);
182
+ --color-fg-on-solid:#ffffff;
183
+
184
+ --color-line: var(--gray-200);
185
+ --color-line-strong: var(--gray-300);
186
+ --color-ring: var(--blue-500);
187
+
188
+ --color-neutral-solid: var(--gray-900);
189
+ --color-neutral-solid-hover: var(--gray-800);
190
+
191
+ --color-primary-solid: var(--purple-500);
192
+ --color-primary-solid-hover: var(--purple-600);
193
+ --color-primary-weak: var(--purple-50);
194
+ --color-primary-on-weak: var(--purple-700);
195
+ --color-primary-stroke: var(--purple-200);
196
+
197
+ --color-info: var(--blue-500);
198
+ --color-info-weak: var(--blue-50);
199
+ --color-info-on-weak: var(--blue-700);
200
+ --color-info-stroke: var(--blue-200);
201
+
202
+ --color-success: var(--green-500);
203
+ --color-success-weak: var(--green-50);
204
+ --color-success-on-weak: var(--green-700);
205
+ --color-success-stroke: var(--green-200);
206
+
207
+ --color-warning: var(--orange-500);
208
+ --color-warning-weak: var(--orange-50);
209
+ --color-warning-on-weak: var(--orange-700);
210
+ --color-warning-stroke: var(--orange-200);
211
+
212
+ --color-danger: var(--red-500);
213
+ --color-danger-weak: var(--red-50);
214
+ --color-danger-on-weak: var(--red-700);
215
+ --color-danger-stroke: var(--red-200);
216
+ --color-danger-solid: var(--red-500);
217
+ --color-danger-solid-hover: var(--red-600);
218
+ }
219
+
220
+ :root, .light {
221
+ --ui-canvas:#ffffff;
222
+ --ui-well:#f2f4f6;
223
+ --ui-panel:#ffffff;
224
+ --ui-float:#ffffff;
225
+ --ui-hover: rgba(0,23,51,0.02);
226
+ --ui-selected: rgba(2,32,71,0.05);
227
+ --ui-selected-hover: rgba(0,27,55,0.1);
228
+ }
229
+
230
+ .dark {
231
+ --ui-canvas:#17171c;
232
+ --ui-well:#101013;
233
+ --ui-panel:#202027;
234
+ --ui-float:#2c2c35;
235
+ --ui-hover: rgba(209,209,253,0.05);
236
+ --ui-selected: rgba(217,217,255,0.11);
237
+ --ui-selected-hover: rgba(222,222,255,0.19);
238
+ }
239
+
240
+ @theme {
241
+ --spacing-tight: 4px;
242
+ --spacing-stack: 16px;
243
+ --spacing-block: 32px;
244
+ --spacing-section: 48px;
245
+ --spacing-gutter: 16px;
246
+
247
+ --container-app: 1280px;
248
+ }
249
+
250
+ @theme {
251
+ --radius-*: initial;
252
+
253
+ --radius-mark: 4px;
254
+ --radius-well: 8px;
255
+ --radius-panel: 12px;
256
+ --radius-float: 16px;
257
+
258
+ --radius-r2: 2px; --radius-r4: 4px; --radius-r6: 6px;
259
+ --radius-r8: 8px; --radius-r10: 10px; --radius-r12: 12px;
260
+ --radius-r14: 14px; --radius-r16: 16px; --radius-r20: 20px;
261
+ --radius-r24: 24px;
262
+ }
263
+
264
+ @theme {
265
+ --ease-*: initial;
266
+
267
+ --ease-state: cubic-bezier(.35, 0, .65, 1);
268
+ --ease-enter: cubic-bezier(0, 0, .15, 1);
269
+ --ease-exit: cubic-bezier(.35, 0, 1, 1);
270
+ }
271
+
272
+ :root {
273
+ --dur-state: 150ms;
274
+ --dur-enter: 200ms;
275
+ --dur-exit: 150ms;
276
+ --enter-scale: 0.95;
277
+ --draw-len: 24;
278
+ }
279
+
280
+ @utility transition-state {
281
+ transition-property: color, background-color, box-shadow, scale;
282
+ transition-duration: var(--dur-state);
283
+ transition-timing-function: var(--ease-state);
284
+ }
285
+
286
+ @keyframes enter { from { opacity: 0; scale: var(--enter-scale); } }
287
+ @keyframes exit { to { opacity: 0; scale: var(--enter-scale); } }
288
+
289
+ @utility animate-enter { animation: enter var(--dur-enter) var(--ease-enter); }
290
+ @utility animate-exit { animation: exit var(--dur-exit) var(--ease-exit); }
291
+
292
+ @keyframes draw { from { stroke-dashoffset: var(--draw-len); } }
293
+
294
+ @utility animate-draw {
295
+ stroke-dasharray: var(--draw-len);
296
+ animation: draw var(--dur-enter) var(--ease-enter);
297
+ }
298
+
299
+ @keyframes fade-in { from { opacity: 0; } }
300
+ @keyframes fade-out { to { opacity: 0; } }
301
+
302
+ @utility animate-fade-in { animation: fade-in var(--dur-enter) var(--ease-enter); }
303
+ @utility animate-fade-out { animation: fade-out var(--dur-exit) var(--ease-exit); }
304
+
305
+ @media (prefers-reduced-motion: reduce) {
306
+ :root { --enter-scale: 1; }
307
+ }
308
+
309
+ @theme {
310
+ --shadow-*: initial;
311
+
312
+ --shadow-s1: 0 1px 4px 0 var(--shade-1);
313
+ --shadow-s2: 0 2px 10px 0 var(--shade-2);
314
+ --shadow-s3: 0 4px 16px 0 var(--shade-3);
315
+ }
316
+
317
+ :root, .light { --shade-1:#00000014; --shade-2:#0000001a; --shade-3:#0000001f; }
318
+ .dark { --shade-1:#00000080; --shade-2:#000000ad; --shade-3:#000000cc; }
319
+
320
+ @theme inline {
321
+ --color-overlay: var(--ui-overlay);
322
+ }
323
+ :root, .light { --ui-overlay: rgba(0,0,0,0.2); }
324
+ .dark { --ui-overlay: rgba(0,0,0,0.56); }
325
+
326
+ @utility z-sticky { z-index: 10; }
327
+ @utility z-overlay { z-index: 50; }
328
+ @utility z-toast { z-index: 60; }
329
+
330
+ :root { --header-h: 56px; }
331
+
332
+ @utility side-rail {
333
+ position: sticky;
334
+ top: var(--header-h);
335
+ max-height: calc(100dvh - var(--header-h));
336
+ overflow-y: auto;
337
+ }
338
+
339
+ @utility scroll-slim {
340
+ scrollbar-width: thin;
341
+ scrollbar-color: var(--color-line-strong) transparent;
342
+ }
343
+
344
+ @utility scroll-hidden {
345
+ scrollbar-width: none;
346
+ &::-webkit-scrollbar { display: none; }
347
+ }
@@ -0,0 +1,37 @@
1
+ # Switch
2
+
3
+ ```
4
+ 받기 npx @pantoia/ui add switch
5
+ 층 ui
6
+ 파일 switch/index.tsx · switch/AGENTS.md
7
+ ```
8
+
9
+ ## 부위
10
+
11
+ | 이름 | props |
12
+ |---|---|
13
+ | `Switch` | `SwitchProps` |
14
+
15
+ ## props
16
+
17
+ ### `SwitchProps`
18
+
19
+ `ComponentProps<typeof SwitchPrimitive.Root>` 를 확장한다.
20
+
21
+ | 이름 | 타입 | 필수 |
22
+ |---|---|---|
23
+ | `color` | `'control' \| 'info' \| 'success' \| 'warning' \| 'danger'` | |
24
+
25
+ ## 변형 — `uiSwitch`
26
+
27
+ | 축 | 값 | 기본 |
28
+ |---|---|---|
29
+ | `color` | `control` · `info` · `success` · `warning` · `danger` | `control` |
30
+
31
+ ## 함께 받는 것
32
+
33
+ `tv`
34
+
35
+ ## npm 의존
36
+
37
+ `radix-ui`
@@ -0,0 +1,42 @@
1
+ 'use client'
2
+
3
+ import { Switch as SwitchPrimitive } from 'radix-ui'
4
+ import type { ComponentProps } from 'react'
5
+
6
+ import { tv } from '../tv'
7
+
8
+ export const uiSwitch = tv({
9
+ base: 'h-6 w-10 shrink-0 rounded-full p-0.5 transition-state press press-shrink'
10
+ + ' bg-well ring-1 ring-inset ring-line-strong'
11
+ + ' invalid:ring-2 invalid:ring-danger'
12
+ + ' focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring'
13
+ + ' disabled:bg-disabled disabled:cursor-not-allowed'
14
+ + ' checked:disabled:bg-control-on-disabled checked:disabled:ring-control-on-disabled',
15
+
16
+ variants: {
17
+ color: {
18
+ control: 'checked:bg-control-on checked:ring-control-on',
19
+ info: 'checked:bg-info checked:ring-info',
20
+ success: 'checked:bg-success checked:ring-success',
21
+ warning: 'checked:bg-warning checked:ring-warning',
22
+ danger: 'checked:bg-danger checked:ring-danger',
23
+ },
24
+ },
25
+
26
+ defaultVariants: { color: 'control' },
27
+ })
28
+
29
+ export interface SwitchProps extends ComponentProps<typeof SwitchPrimitive.Root> {
30
+ color?: 'control' | 'info' | 'success' | 'warning' | 'danger'
31
+ }
32
+
33
+ export function Switch({ color, className, ...rest }: SwitchProps) {
34
+ return (
35
+ <SwitchPrimitive.Root {...rest} className={uiSwitch({ color, className })}>
36
+ <SwitchPrimitive.Thumb
37
+ className="bg-float block size-5 rounded-full checked:translate-x-4
38
+ transition-transform ease-state motion-reduce:transition-none"
39
+ />
40
+ </SwitchPrimitive.Root>
41
+ )
42
+ }