@nexus-cross/design-system 2.0.0-beta.1 → 2.0.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/README.md +375 -0
- package/claude-rules/nexus/CLAUDE.md +68 -0
- package/cursor-rules/nexus-project-setup.mdc +50 -3
- package/cursor-rules/nexus-ui-api.mdc +374 -9
- package/dist/chunks/{chunk-2T7RUYEK.js → chunk-2BINGHGR.js} +11 -3
- package/dist/chunks/{chunk-QOREDNWO.mjs → chunk-53BHDUID.mjs} +2 -1
- package/dist/chunks/{chunk-QZ4QR3XV.mjs → chunk-ATZE57ZO.mjs} +11 -3
- package/dist/chunks/{chunk-OX5MEJ7B.js → chunk-HU6E2R2T.js} +2 -1
- package/dist/chunks/chunk-KT2WKVF7.mjs +5 -0
- package/dist/chunks/{chunk-5J63FUAS.mjs → chunk-LNC3TV6N.mjs} +53 -2
- package/dist/chunks/{chunk-NZHK76R3.js → chunk-LYPBQI3Y.js} +31 -6
- package/dist/chunks/chunk-MWWQMVXJ.js +7 -0
- package/dist/chunks/{chunk-BJM3NDT2.mjs → chunk-RL5UAEGQ.mjs} +11 -3
- package/dist/chunks/{chunk-LAGQ7J5A.js → chunk-VCN7DMCQ.js} +53 -2
- package/dist/chunks/{chunk-2ZXDXO4I.js → chunk-VDEB5BMT.js} +11 -3
- package/dist/chunks/{chunk-6BWOKTVQ.mjs → chunk-WZFKTTVX.mjs} +31 -6
- package/dist/components/ImageUpload.d.ts +14 -0
- package/dist/components/ImageUpload.d.ts.map +1 -1
- package/dist/components/NumberInput.d.ts +20 -1
- package/dist/components/NumberInput.d.ts.map +1 -1
- package/dist/components/NxImage.d.ts.map +1 -1
- package/dist/components/Select.d.ts +5 -1
- package/dist/components/Select.d.ts.map +1 -1
- package/dist/components/Tab.d.ts +12 -1
- package/dist/components/Tab.d.ts.map +1 -1
- package/dist/image-upload.js +3 -3
- package/dist/image-upload.mjs +1 -1
- package/dist/index.js +18 -18
- package/dist/index.mjs +5 -5
- package/dist/number-input.js +4 -4
- package/dist/number-input.mjs +1 -1
- package/dist/nx-image.js +2 -2
- package/dist/nx-image.mjs +1 -1
- package/dist/schemas/_all.json +48 -10
- package/dist/schemas/image-upload.d.ts +6 -0
- package/dist/schemas/image-upload.d.ts.map +1 -1
- package/dist/schemas/imageUpload.json +19 -1
- package/dist/schemas/number-input.d.ts +9 -3
- package/dist/schemas/number-input.d.ts.map +1 -1
- package/dist/schemas/numberInput.json +10 -3
- package/dist/schemas/spinner.json +2 -2
- package/dist/schemas/tab.d.ts +11 -0
- package/dist/schemas/tab.d.ts.map +1 -1
- package/dist/schemas/tab.json +17 -4
- package/dist/schemas.js +48 -10
- package/dist/schemas.mjs +48 -10
- package/dist/select.js +5 -5
- package/dist/select.mjs +1 -1
- package/dist/styles/.generated/built.d.ts +1 -1
- package/dist/styles/.generated/built.d.ts.map +1 -1
- package/dist/styles/layer.js +2 -2
- package/dist/styles/layer.mjs +1 -1
- package/dist/styles.css +185 -44
- package/dist/styles.js +2 -2
- package/dist/styles.layered.css +185 -44
- package/dist/styles.mjs +1 -1
- package/dist/tab.js +4 -4
- package/dist/tab.mjs +1 -1
- package/dist/tokens/TOKENS.md +13 -0
- package/dist/tokens/company.css +21 -1
- package/dist/tokens/css.css +21 -1
- package/dist/tokens/data/color.json +32 -0
- package/dist/tokens/data/space.json +1 -1
- package/dist/tokens/data/typography.json +55 -1
- package/dist/tokens-domains/data/gamehub/domain.json +258 -0
- package/dist/tokens-domains/data/index.ts +3 -1
- package/dist/tokens-domains/data/prediction/domain.json +0 -12
- package/dist/tokens-domains/gamehub.md +62 -0
- package/dist/tokens-domains/prediction-vars.css +1 -5
- package/dist/tokens-domains/prediction.css +1 -5
- package/dist/tokens-domains/prediction.md +0 -1
- package/package.json +3 -3
- package/dist/chunks/chunk-3SCSND6S.js +0 -7
- package/dist/chunks/chunk-QWK4CLS2.mjs +0 -5
|
@@ -660,10 +660,10 @@ ANTI-PATTERNS:
|
|
|
660
660
|
| Prop | Type | Default | Description |
|
|
661
661
|
|---|---|---|---|
|
|
662
662
|
| `size` | `number` | `20` | Size in px |
|
|
663
|
-
| `color` | `string` | - | Color (CSS color value
|
|
663
|
+
| `color` | `string` | - | Color (CSS color value). Default: brand primary (--color-accent-primary) |
|
|
664
664
|
| `aria-label` | `string` | `"Loading"` | Accessibility label |
|
|
665
665
|
| `style` | `ReactNode` | - | Inline style (CSSProperties) |
|
|
666
|
-
| `className` | `string` | - | Color override
|
|
666
|
+
| `className` | `string` | - | Color override (e.g. text-text-muted) — utility class wins over the default primary |
|
|
667
667
|
|
|
668
668
|
```tsx
|
|
669
669
|
<Spinner size={24} />
|
|
@@ -904,7 +904,7 @@ ANTI-PATTERNS:
|
|
|
904
904
|
| `shouldScaleBackground` | `boolean` | - | Background scale effect (default false) |
|
|
905
905
|
| `children` | `ReactNode` | - | Drawer sub-components (ReactNode, required) |
|
|
906
906
|
|
|
907
|
-
###
|
|
907
|
+
### Drawer.Content
|
|
908
908
|
|
|
909
909
|
Drawer.Content area.
|
|
910
910
|
|
|
@@ -917,6 +917,44 @@ Drawer.Content area.
|
|
|
917
917
|
| `overlayClassName` | `string` | - | Overlay style |
|
|
918
918
|
| `className` | `string` | - | Panel style |
|
|
919
919
|
|
|
920
|
+
### Drawer.Trigger
|
|
921
|
+
|
|
922
|
+
Drawer open trigger.
|
|
923
|
+
|
|
924
|
+
| Prop | Type | Default | Description |
|
|
925
|
+
|---|---|---|---|
|
|
926
|
+
| `asChild` | `boolean` | - | Render as child element |
|
|
927
|
+
| `children` | `ReactNode` | - | Trigger element (ReactNode, required) |
|
|
928
|
+
| `className` | `string` | - | Style override |
|
|
929
|
+
|
|
930
|
+
### Drawer.Close
|
|
931
|
+
|
|
932
|
+
Drawer close button.
|
|
933
|
+
|
|
934
|
+
| Prop | Type | Default | Description |
|
|
935
|
+
|---|---|---|---|
|
|
936
|
+
| `asChild` | `boolean` | - | Render as child element |
|
|
937
|
+
| `children` | `ReactNode` | - | Close element (ReactNode, required) |
|
|
938
|
+
| `className` | `string` | - | Style override |
|
|
939
|
+
|
|
940
|
+
### Drawer.Title
|
|
941
|
+
|
|
942
|
+
Drawer title (required for accessibility).
|
|
943
|
+
|
|
944
|
+
| Prop | Type | Default | Description |
|
|
945
|
+
|---|---|---|---|
|
|
946
|
+
| `children` | `ReactNode` | - | Title text (ReactNode, required) |
|
|
947
|
+
| `className` | `string` | - | Style override |
|
|
948
|
+
|
|
949
|
+
### Drawer.Description
|
|
950
|
+
|
|
951
|
+
Drawer description.
|
|
952
|
+
|
|
953
|
+
| Prop | Type | Default | Description |
|
|
954
|
+
|---|---|---|---|
|
|
955
|
+
| `children` | `ReactNode` | - | Description text (ReactNode, required) |
|
|
956
|
+
| `className` | `string` | - | Style override |
|
|
957
|
+
|
|
920
958
|
```tsx
|
|
921
959
|
<Drawer direction="bottom">
|
|
922
960
|
<Drawer.Trigger asChild>
|
|
@@ -1023,7 +1061,10 @@ function MyModal({ close, resolve }: { close: () => void; resolve: (value: any)
|
|
|
1023
1061
|
```tsx
|
|
1024
1062
|
import { modal, useModal, ModalContainer } from '@nexus-cross/design-system/modal';
|
|
1025
1063
|
|
|
1026
|
-
// ModalContainer MUST be placed at the app root
|
|
1064
|
+
// ModalContainer MUST be placed at the app root.
|
|
1065
|
+
// It uses useState internally → it's a client component.
|
|
1066
|
+
// In Next.js App Router, wrap it in a "use client" file (e.g. providers.tsx)
|
|
1067
|
+
// and render that wrapper from the (server) layout. NEVER add "use client" to layout.tsx itself.
|
|
1027
1068
|
<ModalContainer />
|
|
1028
1069
|
|
|
1029
1070
|
// Method 1: modal() function
|
|
@@ -1059,6 +1100,14 @@ WHEN TO USE:
|
|
|
1059
1100
|
• Immediate filter/option toggle → ToggleGroup
|
|
1060
1101
|
• Stacked collapsible sections → Accordion
|
|
1061
1102
|
|
|
1103
|
+
WIDTH BEHAVIOR (commonly misunderstood):
|
|
1104
|
+
• Default: each trigger sizes to its label content; the list is as wide as the sum of triggers
|
|
1105
|
+
• Want all triggers to evenly fill a parent? → set `fullWidth` (NOT just `tabListClassName="w-full"`)
|
|
1106
|
+
`tabListClassName="w-full"` only widens the list; triggers stay content-width because flex children
|
|
1107
|
+
default to `flex: 0 1 auto`. `fullWidth` adds `flex: 1` to every trigger.
|
|
1108
|
+
• Want one specific trigger wider? → use `items[i].className="flex-2"` (or any flex utility)
|
|
1109
|
+
• Want all triggers wider but not equal? → use `tabItemClassName="min-w-[120px]"` etc.
|
|
1110
|
+
|
|
1062
1111
|
destroyInactive=true unmounts hidden panels (saves memory but loses state).
|
|
1063
1112
|
|
|
1064
1113
|
ANTI-PATTERNS:
|
|
@@ -1066,6 +1115,7 @@ ANTI-PATTERNS:
|
|
|
1066
1115
|
✗ Tab with 8+ items — consider sub-routing or DropdownMenu
|
|
1067
1116
|
✗ Using Tab for form value selection → RadioGroup
|
|
1068
1117
|
✗ Custom <button> + onClick + state → Tab (a11y, keyboard, focus management)
|
|
1118
|
+
✗ `tabListClassName="w-full"` expecting triggers to stretch → use `fullWidth` prop
|
|
1069
1119
|
|
|
1070
1120
|
| Prop | Type | Default | Description |
|
|
1071
1121
|
|---|---|---|---|
|
|
@@ -1074,13 +1124,16 @@ ANTI-PATTERNS:
|
|
|
1074
1124
|
| `defaultActiveKey` | `string` | - | Uncontrolled initial key |
|
|
1075
1125
|
| `variant` | `'line'` \| `'pill'` | `"line"` | Style |
|
|
1076
1126
|
| `size` | `'sm'` \| `'md'` | `"md"` | Size |
|
|
1127
|
+
| `fullWidth` | `boolean` | `false` | Stretch all triggers to evenly fill the tab list width (`flex: 1`) and set the list to `width: 100%`. Use for top-level navigation tabs that should span the container. Defaults to false (each trigger sizes to its content). |
|
|
1077
1128
|
| `destroyInactive` | `boolean` | `false` | Unmount inactive panels |
|
|
1078
1129
|
| `onTabChange` | `ReactNode` | - | Tab change callback (key: string) => void |
|
|
1079
|
-
| `className` | `string` | - | Root
|
|
1080
|
-
| `tabListClassName` | `string` | - |
|
|
1081
|
-
| `
|
|
1130
|
+
| `className` | `string` | - | Root wrapper className |
|
|
1131
|
+
| `tabListClassName` | `string` | - | className for the tab list (the row of triggers). Width utilities (`w-full`, `max-w-md`, etc.) apply here. Note: `w-full` alone does NOT stretch individual triggers — combine with `fullWidth=true` for that. |
|
|
1132
|
+
| `tabItemClassName` | `string` | - | className applied to EVERY tab trigger <button>. Combine with `fullWidth` for equal-width tabs, or pass spacing/typography utilities to restyle all triggers at once. For per-item overrides use `items[].className`. |
|
|
1133
|
+
| `tabPanelClassName` | `string` | - | className for each panel <div> |
|
|
1082
1134
|
|
|
1083
1135
|
```tsx
|
|
1136
|
+
// Basic — triggers size to their content
|
|
1084
1137
|
<Tab
|
|
1085
1138
|
items={[
|
|
1086
1139
|
{ key: 'a', label: 'Tab A', children: <p>A</p> },
|
|
@@ -1089,6 +1142,28 @@ ANTI-PATTERNS:
|
|
|
1089
1142
|
defaultActiveKey="a"
|
|
1090
1143
|
variant="pill"
|
|
1091
1144
|
/>
|
|
1145
|
+
|
|
1146
|
+
// Equal-width tabs filling the parent (do NOT just use tabListClassName="w-full" —
|
|
1147
|
+
// that widens the list but leaves triggers content-sized).
|
|
1148
|
+
<Tab
|
|
1149
|
+
fullWidth
|
|
1150
|
+
variant="pill"
|
|
1151
|
+
items={[
|
|
1152
|
+
{ key: 'overview', label: 'Overview', children: <Overview /> },
|
|
1153
|
+
{ key: 'history', label: 'History', children: <History /> },
|
|
1154
|
+
{ key: 'settings', label: 'Settings', children: <Settings /> },
|
|
1155
|
+
]}
|
|
1156
|
+
/>
|
|
1157
|
+
|
|
1158
|
+
// Per-item sizing override — make one trigger twice as wide.
|
|
1159
|
+
<Tab
|
|
1160
|
+
fullWidth
|
|
1161
|
+
items={[
|
|
1162
|
+
{ key: 'a', label: 'A', children: <A /> },
|
|
1163
|
+
{ key: 'b', label: 'B (wider)', className: 'flex-[2]', children: <B /> },
|
|
1164
|
+
{ key: 'c', label: 'C', children: <C /> },
|
|
1165
|
+
]}
|
|
1166
|
+
/>
|
|
1092
1167
|
```
|
|
1093
1168
|
|
|
1094
1169
|
---
|
|
@@ -1118,6 +1193,32 @@ ANTI-PATTERNS:
|
|
|
1118
1193
|
| `children` | `ReactNode` | - | Carousel slides and sub-components (ReactNode) |
|
|
1119
1194
|
| `className` | `string` | - | Style override |
|
|
1120
1195
|
|
|
1196
|
+
### Carousel.Slide
|
|
1197
|
+
|
|
1198
|
+
Carousel slide. Used inside Carousel.
|
|
1199
|
+
|
|
1200
|
+
| Prop | Type | Default | Description |
|
|
1201
|
+
|---|---|---|---|
|
|
1202
|
+
| `className` | `string` | - | Slide style (use basis-1/3 etc. for multi-slide view) |
|
|
1203
|
+
| `children` | `ReactNode` | - | Slide content (ReactNode, required) |
|
|
1204
|
+
|
|
1205
|
+
### Carousel.Button
|
|
1206
|
+
|
|
1207
|
+
CarouselPrev / CarouselNext. Previous/next navigation buttons.
|
|
1208
|
+
|
|
1209
|
+
| Prop | Type | Default | Description |
|
|
1210
|
+
|---|---|---|---|
|
|
1211
|
+
| `className` | `string` | - | Button style override |
|
|
1212
|
+
| `children` | `ReactNode` | - | Custom icon (ReactNode, default: chevron) |
|
|
1213
|
+
|
|
1214
|
+
### Carousel.Dots
|
|
1215
|
+
|
|
1216
|
+
CarouselDots. Slide indicator dots.
|
|
1217
|
+
|
|
1218
|
+
| Prop | Type | Default | Description |
|
|
1219
|
+
|---|---|---|---|
|
|
1220
|
+
| `className` | `string` | - | Dot container style |
|
|
1221
|
+
|
|
1121
1222
|
Sub-components: `CarouselSlide`, `CarouselPrev`, `CarouselNext`, `CarouselDots`
|
|
1122
1223
|
|
|
1123
1224
|
```tsx
|
|
@@ -1618,13 +1719,16 @@ ANTI-PATTERNS:
|
|
|
1618
1719
|
✗ <TextInput type="number"> → <NumberInput> (loses keyboard ↑↓, step, accelerated long-press, max click)
|
|
1619
1720
|
✗ Custom +/- buttons + <input> → variant="bind" (or numberInputBind for external)
|
|
1620
1721
|
✗ Manual thousand separators for currency → PriceInput
|
|
1722
|
+
✗ Inline unit text inside label/description → use prefixIcon/suffixIcon (e.g. suffixIcon="%")
|
|
1621
1723
|
|
|
1622
1724
|
| Prop | Type | Default | Description |
|
|
1623
1725
|
|---|---|---|---|
|
|
1624
1726
|
| `variant` | `'basic'` \| `'bind'` | `"basic"` | Variant: basic (right chevron arrows) or bind (left/right +/- buttons) |
|
|
1625
1727
|
| `value` | `number` \| `string` | - | Current value |
|
|
1626
|
-
| `size` | `'lg'` \| `'xl'` | `"
|
|
1728
|
+
| `size` | `'md'` \| `'lg'` \| `'xl'` | `"md"` | Size (matches TextInput scale) |
|
|
1627
1729
|
| `error` | `boolean` | - | Error state |
|
|
1730
|
+
| `prefixIcon` | `ReactNode` | - | Prefix node — currency symbol, unit, or icon (ReactNode) |
|
|
1731
|
+
| `suffixIcon` | `ReactNode` | - | Suffix node — unit (%, kg, 원...) or icon. Pair with hideButtons or variant="bind" so it does not collide with arrow buttons |
|
|
1628
1732
|
| `min` | `number` | - | Minimum value |
|
|
1629
1733
|
| `max` | `number` | - | Maximum value. When set, "Max {value}" is displayed in the header. Clicking it fills the input with the max value |
|
|
1630
1734
|
| `step` | `number` | `1` | Step increment |
|
|
@@ -1646,9 +1750,12 @@ ANTI-PATTERNS:
|
|
|
1646
1750
|
|
|
1647
1751
|
Press-and-hold accelerates increment/decrement (100ms → 75ms → 50ms → 30ms). Default is right-side vertical spin buttons. Use `numberInputBind(ref, direction)` to bind the same acceleration events to external buttons for free placement.
|
|
1648
1752
|
|
|
1753
|
+
Sizes (md / lg / xl) match TextInput. Use `prefixIcon` / `suffixIcon` for unit indicators (currency, %, kg…). `suffixIcon` works best with `hideButtons` or `variant="bind"` so it does not collide with the right-side arrow buttons.
|
|
1754
|
+
|
|
1649
1755
|
```tsx
|
|
1650
1756
|
// Basic usage — right-side vertical spin buttons
|
|
1651
1757
|
<NumberInput
|
|
1758
|
+
label="Quantity"
|
|
1652
1759
|
value={count}
|
|
1653
1760
|
onValueChange={setCount}
|
|
1654
1761
|
min={0}
|
|
@@ -1656,6 +1763,14 @@ Press-and-hold accelerates increment/decrement (100ms → 75ms → 50ms → 30ms
|
|
|
1656
1763
|
step={5}
|
|
1657
1764
|
/>
|
|
1658
1765
|
|
|
1766
|
+
// Prefix / suffix — unit · currency
|
|
1767
|
+
<NumberInput label="Price" prefixIcon="$" digit={2} placeholder="0.00" />
|
|
1768
|
+
<NumberInput label="Discount" suffixIcon="%" hideButtons max={100} />
|
|
1769
|
+
<NumberInput label="Weight" suffixIcon="kg" variant="bind" />
|
|
1770
|
+
|
|
1771
|
+
// Bind variant — left/right ± buttons (good for mobile)
|
|
1772
|
+
<NumberInput variant="bind" label="Quantity" value={count} onValueChange={setCount} max={99} />
|
|
1773
|
+
|
|
1659
1774
|
// External buttons — bind events with numberInputBind
|
|
1660
1775
|
const ref = useRef<NumberInputRef>(null);
|
|
1661
1776
|
|
|
@@ -2098,15 +2213,22 @@ ImageUpload — drag-and-drop image upload with preview, file-type/size validati
|
|
|
2098
2213
|
|
|
2099
2214
|
WHEN TO USE:
|
|
2100
2215
|
• Single image upload: avatar, cover, KYC document, post thumbnail
|
|
2216
|
+
• Banner / hero / thumbnail slot where the picked image should fill the box → previewMode="cover"
|
|
2101
2217
|
• Multiple files / non-image → not yet supported; build custom or use a file dropzone
|
|
2102
2218
|
• Inline image picker without preview → custom <input type="file">
|
|
2103
2219
|
|
|
2104
2220
|
accept whitelist + maxSize together cover validation. onError fires with i18n-ready string.
|
|
2105
2221
|
|
|
2222
|
+
PREVIEW MODES:
|
|
2223
|
+
• card (default) — 작은 썸네일 + 우측에 "이미지 변경" 버튼 + 포맷 텍스트. form 필드용.
|
|
2224
|
+
• cover — 빈 상태 박스를 그대로 유지하고 이미지가 전체를 가득 채움. 클릭 시 변경, 우측 상단 X로 삭제.
|
|
2225
|
+
크기/비율은 className(h-64, aspect-video, w-full 등) 또는 size prop으로 조정.
|
|
2226
|
+
|
|
2106
2227
|
ANTI-PATTERNS:
|
|
2107
2228
|
✗ <input type="file"> + manual preview → ImageUpload (handles drag, validation, preview)
|
|
2108
2229
|
✗ ImageUpload without onError handler → silent failure on validation reject
|
|
2109
2230
|
✗ Storing image as data-URL value (use File via onChange and upload to server)
|
|
2231
|
+
✗ card 모드에서 작은 썸네일이 어색한 배너/카드 슬롯 → previewMode="cover"
|
|
2110
2232
|
|
|
2111
2233
|
| Prop | Type | Default | Description |
|
|
2112
2234
|
|---|---|---|---|
|
|
@@ -2122,7 +2244,247 @@ ANTI-PATTERNS:
|
|
|
2122
2244
|
| `description` | `ReactNode` | - | Field description below the upload area (ReactNode) |
|
|
2123
2245
|
| `disabled` | `boolean` | `false` | Disabled state |
|
|
2124
2246
|
| `size` | `'sm'` \| `'md'` \| `'lg'` | `"md"` | Upload area size |
|
|
2247
|
+
| `previewMode` | `'card'` \| `'cover'` | `"card"` | Preview layout after upload. "card" = small thumbnail + change button beside it (default). "cover" = image fills the entire box (banner / thumbnail slot UX). In cover mode, click the box to change, click the X in the top-right to remove. Box dimensions follow size or className (h-64, aspect-video, etc.) |
|
|
2248
|
+
| `previewFit` | `'cover'` \| `'contain'` | `"cover"` | Object-fit for previewMode="cover". "cover" fills the box (may crop), "contain" fits entirely inside (may letterbox). |
|
|
2249
|
+
| `className` | `string` | - | Style override |
|
|
2250
|
+
|
|
2251
|
+
Two preview layouts via `previewMode`:
|
|
2252
|
+
- `'card'` (default) — small thumbnail + side "이미지 변경" button + format text. Best for form fields.
|
|
2253
|
+
- `'cover'` — image fills the entire box (banner / thumbnail slot UX). Click box to change, X to remove. Box dimensions follow `size` or className (`h-64`, `aspect-video`, `w-full` …).
|
|
2254
|
+
|
|
2255
|
+
```tsx
|
|
2256
|
+
import { ImageUpload } from '@nexus-cross/design-system';
|
|
2257
|
+
import { Controller } from 'react-hook-form';
|
|
2258
|
+
|
|
2259
|
+
// Form field — card preview (default)
|
|
2260
|
+
<Controller
|
|
2261
|
+
control={control}
|
|
2262
|
+
name="thumbnail"
|
|
2263
|
+
render={({ field }) => (
|
|
2264
|
+
<ImageUpload
|
|
2265
|
+
label="썸네일"
|
|
2266
|
+
description="JPG · PNG · 최대 2MB"
|
|
2267
|
+
value={field.value}
|
|
2268
|
+
onChange={(file) => field.onChange(file ? URL.createObjectURL(file) : null)}
|
|
2269
|
+
onError={(msg) => toast.error(msg)}
|
|
2270
|
+
/>
|
|
2271
|
+
)}
|
|
2272
|
+
/>
|
|
2273
|
+
|
|
2274
|
+
// Banner / hero slot — cover preview, custom aspect ratio
|
|
2275
|
+
<ImageUpload
|
|
2276
|
+
previewMode="cover"
|
|
2277
|
+
className="aspect-[16/9] w-full"
|
|
2278
|
+
size="lg"
|
|
2279
|
+
value={banner}
|
|
2280
|
+
onChange={(file) => setBanner(file ? URL.createObjectURL(file) : null)}
|
|
2281
|
+
placeholder="배너 이미지 업로드"
|
|
2282
|
+
/>
|
|
2283
|
+
|
|
2284
|
+
// Card slot — fixed height, contain (no crop)
|
|
2285
|
+
<ImageUpload
|
|
2286
|
+
previewMode="cover"
|
|
2287
|
+
previewFit="contain"
|
|
2288
|
+
className="h-48"
|
|
2289
|
+
value={cover}
|
|
2290
|
+
onChange={handleCover}
|
|
2291
|
+
/>
|
|
2292
|
+
```
|
|
2293
|
+
|
|
2294
|
+
---
|
|
2295
|
+
|
|
2296
|
+
## Table
|
|
2297
|
+
|
|
2298
|
+
Table — tabular data with sortable columns, skeleton/loading/empty states. Compound: TableRow + TdColumn.
|
|
2299
|
+
|
|
2300
|
+
WHEN TO USE:
|
|
2301
|
+
• Comparable structured data (financial reports, transaction history, leaderboard)
|
|
2302
|
+
• Sortable per-column → enableSorting on TdColumn
|
|
2303
|
+
• Card-style entities → DataGrid
|
|
2304
|
+
• Single-column read-only list → DataList
|
|
2305
|
+
• Huge dataset (>500 rows) → wrap with VirtualList logic or paginate
|
|
2306
|
+
|
|
2307
|
+
list={null} → loading state. list=[] → noDataMsg. children: ({ item, index }) => <TableRow>...</TableRow>.
|
|
2308
|
+
|
|
2309
|
+
ANTI-PATTERNS:
|
|
2310
|
+
✗ Native <table> + manual loading/empty handling → Table component
|
|
2311
|
+
✗ Forcing card-style data into Table (use DataGrid)
|
|
2312
|
+
✗ Sorting in client when server pagination is in use → server-side sort
|
|
2313
|
+
|
|
2314
|
+
| Prop | Type | Default | Description |
|
|
2315
|
+
|---|---|---|---|
|
|
2316
|
+
| `list` | `ReactNode`[] | - | Data array. null/undefined = loading state (required) |
|
|
2317
|
+
| `children` | `ReactNode` | - | Row render function ({ item, index }) => ReactNode (required) |
|
|
2318
|
+
| `hideThead` | `boolean` | - | Hide table header |
|
|
2319
|
+
| `loading` | `boolean` | - | Force loading state |
|
|
2320
|
+
| `loadingType` | `'loading'` \| `'skeleton'` | `"skeleton"` | Loading display type |
|
|
2321
|
+
| `loadingElement` | `ReactNode` | - | Custom loading element (ReactElement) |
|
|
2322
|
+
| `skeletonCount` | `number` | `10` | Skeleton row count |
|
|
2323
|
+
| `noDataMsg` | `ReactNode` | - | No data message (ReactElement | string) |
|
|
2324
|
+
| `notification` | `ReactNode` | - | Table top notification area (ReactNode) |
|
|
2325
|
+
| `sortUpElement` | `ReactNode` | - | Ascending sort icon (ReactElement) |
|
|
2326
|
+
| `sortDownElement` | `ReactNode` | - | Descending sort icon (ReactElement) |
|
|
2327
|
+
| `className` | `string` | - | Table wrapper style |
|
|
2328
|
+
| `theadClassName` | `string` | - | Header row style |
|
|
2329
|
+
|
|
2330
|
+
### Table.Row
|
|
2331
|
+
|
|
2332
|
+
TableRow — a single row inside Table. Wraps TdColumn cells.
|
|
2333
|
+
|
|
2334
|
+
WHEN TO USE:
|
|
2335
|
+
• variant="accent" highlights selected/current row
|
|
2336
|
+
• onClick for row-level navigation (e.g. open detail page)
|
|
2337
|
+
• Per-row checkbox/action → place inside a TdColumn child
|
|
2338
|
+
|
|
2339
|
+
| Prop | Type | Default | Description |
|
|
2340
|
+
|---|---|---|---|
|
|
2341
|
+
| `variant` | `'default'` \| `'accent'` | `"default"` | Row style |
|
|
2125
2342
|
| `className` | `string` | - | Style override |
|
|
2343
|
+
| `children` | `ReactNode` | - | TdColumn list (ReactNode, required) |
|
|
2344
|
+
| `onClick` | `ReactNode` | - | Row click callback (e: MouseEvent) => void |
|
|
2345
|
+
|
|
2346
|
+
### Td
|
|
2347
|
+
|
|
2348
|
+
TdColumn — table cell. Drives both <th> and <td> for the column. Provides sorting, alignment, overflow handling.
|
|
2349
|
+
|
|
2350
|
+
WHEN TO USE:
|
|
2351
|
+
• textOverflow="truncate" (default) for fixed-width columns; "wrap" for narrative
|
|
2352
|
+
• align="right" for numeric/currency columns (a11y readability)
|
|
2353
|
+
• enableSorting=true when column is sortable; supply order + handleClickSort for controlled sort
|
|
2354
|
+
• highlightKey to group columns that highlight together on hover
|
|
2355
|
+
|
|
2356
|
+
ANTI-PATTERNS:
|
|
2357
|
+
✗ enableSorting without handleClickSort → sort indicator is dead
|
|
2358
|
+
✗ align="left" for currency/number columns
|
|
2359
|
+
✗ Mixing text and numeric in one column without consistent align
|
|
2360
|
+
|
|
2361
|
+
| Prop | Type | Default | Description |
|
|
2362
|
+
|---|---|---|---|
|
|
2363
|
+
| `label` | `ReactNode` | - | Header label (ReactElement | string) |
|
|
2364
|
+
| `fieldId` | `string` | - | Column identifier (sort key, required) |
|
|
2365
|
+
| `size` | `number` \| `string` | - | Column width (number → px, string → CSS value) |
|
|
2366
|
+
| `align` | `'left'` \| `'center'` \| `'right'` | `"left"` | Text alignment |
|
|
2367
|
+
| `textOverflow` | `'auto'` \| `'truncate'` \| `'wrap'` \| `'break-all'` | `"truncate"` | Text overflow handling |
|
|
2368
|
+
| `highlightKey` | `string` | - | Hover highlight group key |
|
|
2369
|
+
| `colSpan` | `number` | - | Column span |
|
|
2370
|
+
| `rowSpan` | `number` | - | Row span |
|
|
2371
|
+
| `thColSpan` | `number` | - | Header colSpan (<th>) |
|
|
2372
|
+
| `thRowSpan` | `number` | - | Header rowSpan (<th>) |
|
|
2373
|
+
| `enableSorting` | `boolean` | - | Enable sorting |
|
|
2374
|
+
| `order` | `'desc'` \| `'asc'` \| `''` | - | Current sort direction |
|
|
2375
|
+
| `sortValue` | `string` \| `number` | - | Sort criterion value |
|
|
2376
|
+
| `handleClickSort` | `ReactNode` | - | Sort click callback ({ index, fieldId, order }) => void |
|
|
2377
|
+
| `children` | `ReactNode` | - | Cell content (ReactNode, required) |
|
|
2378
|
+
| `className` | `string` | - | Style override |
|
|
2379
|
+
|
|
2380
|
+
Composable table with `<Table>`, `<Table.Row>`, `<Td>`. Headers go in the first `<Table.Row>` with `isHeader`. Width / alignment is set per cell via `<Td>` props (do NOT add custom `<th>/<td>` markup).
|
|
2381
|
+
|
|
2382
|
+
```tsx
|
|
2383
|
+
import { Table } from '@nexus-cross/design-system';
|
|
2384
|
+
|
|
2385
|
+
<Table>
|
|
2386
|
+
<Table.Row isHeader>
|
|
2387
|
+
<Td width="40%">Name</Td>
|
|
2388
|
+
<Td width="20%" align="center">Status</Td>
|
|
2389
|
+
<Td width="20%" align="right">Amount</Td>
|
|
2390
|
+
<Td width="20%" align="right">Date</Td>
|
|
2391
|
+
</Table.Row>
|
|
2392
|
+
|
|
2393
|
+
{items.map((item) => (
|
|
2394
|
+
<Table.Row key={item.id} onClick={() => onRowClick(item)}>
|
|
2395
|
+
<Td>{item.name}</Td>
|
|
2396
|
+
<Td align="center"><Badge>{item.status}</Badge></Td>
|
|
2397
|
+
<Td align="right">{formatPrice(item.amount)}</Td>
|
|
2398
|
+
<Td align="right">{item.date}</Td>
|
|
2399
|
+
</Table.Row>
|
|
2400
|
+
))}
|
|
2401
|
+
</Table>
|
|
2402
|
+
|
|
2403
|
+
// For large datasets / virtualization → use DataGrid instead.
|
|
2404
|
+
// For card-style key/value pairs → use DataList.
|
|
2405
|
+
```
|
|
2406
|
+
|
|
2407
|
+
---
|
|
2408
|
+
|
|
2409
|
+
## ErrorBoundary
|
|
2410
|
+
|
|
2411
|
+
ErrorBoundary — catches render-time errors in subtree and shows fallback UI.
|
|
2412
|
+
|
|
2413
|
+
WHEN TO USE:
|
|
2414
|
+
• Wrap risky areas: third-party widgets, dynamic imports, untrusted content rendering
|
|
2415
|
+
• Async errors / promise rejections → NOT caught (use try/catch in handlers)
|
|
2416
|
+
• Per-route error wrapping → use framework's error.tsx (Next.js) where possible
|
|
2417
|
+
|
|
2418
|
+
DataList / DataGrid have ErrorBoundary built-in — don't double-wrap.
|
|
2419
|
+
|
|
2420
|
+
ANTI-PATTERNS:
|
|
2421
|
+
✗ ErrorBoundary at app root only — granular boundaries give better UX (one widget fails, page survives)
|
|
2422
|
+
✗ Showing raw Error.message in production (leak risk) — use friendly fallback
|
|
2423
|
+
✗ Forgetting onError logging → silent failures in production
|
|
2424
|
+
|
|
2425
|
+
| Prop | Type | Default | Description |
|
|
2426
|
+
|---|---|---|---|
|
|
2427
|
+
| `children` | `ReactNode` | - | Child elements to wrap (ReactNode, required) |
|
|
2428
|
+
| `fallback` | `ReactNode` | - | Fallback UI on error (ReactNode) |
|
|
2429
|
+
| `onError` | `ReactNode` | - | Error callback (error: Error, errorInfo: ErrorInfo) => void |
|
|
2430
|
+
|
|
2431
|
+
React error boundary using class component pattern. Wrap any subtree that may throw — it shows `fallback` and (optionally) calls `onError` for logging. `resetKeys` reset the boundary when their values change (great for route changes).
|
|
2432
|
+
|
|
2433
|
+
```tsx
|
|
2434
|
+
import { ErrorBoundary } from '@nexus-cross/design-system';
|
|
2435
|
+
|
|
2436
|
+
<ErrorBoundary
|
|
2437
|
+
fallback={<EmptyState title="Something went wrong" />}
|
|
2438
|
+
onError={(error, info) => reportError(error, info)}
|
|
2439
|
+
resetKeys={[location.pathname]}
|
|
2440
|
+
>
|
|
2441
|
+
<MyRoute />
|
|
2442
|
+
</ErrorBoundary>
|
|
2443
|
+
|
|
2444
|
+
// Per-component fallback function (receives error + reset)
|
|
2445
|
+
<ErrorBoundary fallback={(err, reset) => (
|
|
2446
|
+
<Alert intent="error">
|
|
2447
|
+
{err.message}
|
|
2448
|
+
<Button onClick={reset}>Retry</Button>
|
|
2449
|
+
</Alert>
|
|
2450
|
+
)}>
|
|
2451
|
+
<RiskyWidget />
|
|
2452
|
+
</ErrorBoundary>
|
|
2453
|
+
```
|
|
2454
|
+
|
|
2455
|
+
---
|
|
2456
|
+
|
|
2457
|
+
## ClientOnly
|
|
2458
|
+
|
|
2459
|
+
ClientOnly — defers children to client-side render, preventing SSR hydration mismatch.
|
|
2460
|
+
|
|
2461
|
+
WHEN TO USE:
|
|
2462
|
+
• Wrap components that read window/document/localStorage at render time
|
|
2463
|
+
• Components depending on browser-only APIs (IntersectionObserver eager init, geolocation)
|
|
2464
|
+
• Server-renderable content → DON'T wrap (loses SEO/SSR perf benefits)
|
|
2465
|
+
• Conditional based on data → use proper SSR-safe state instead
|
|
2466
|
+
|
|
2467
|
+
Pass fallback to avoid layout shift during hydration.
|
|
2468
|
+
|
|
2469
|
+
ANTI-PATTERNS:
|
|
2470
|
+
✗ Wrapping the entire page in ClientOnly (defeats SSR)
|
|
2471
|
+
✗ ClientOnly without fallback for above-the-fold UI (CLS)
|
|
2472
|
+
✗ Using ClientOnly to "fix" any hydration warning — root-cause the mismatch first
|
|
2473
|
+
|
|
2474
|
+
| Prop | Type | Default | Description |
|
|
2475
|
+
|---|---|---|---|
|
|
2476
|
+
| `children` | `ReactNode` | - | Element to render only on client (ReactNode, required) |
|
|
2477
|
+
| `fallback` | `ReactNode` | - | Fallback UI during SSR (ReactNode, default: null) |
|
|
2478
|
+
|
|
2479
|
+
Renders children only on the client. Use to skip SSR for components that depend on `window`/`document` (Carousel, DatePicker, ImageUpload preview, etc.) when adopting this design system inside Next.js / Remix.
|
|
2480
|
+
|
|
2481
|
+
```tsx
|
|
2482
|
+
import { ClientOnly } from '@nexus-cross/design-system';
|
|
2483
|
+
|
|
2484
|
+
<ClientOnly fallback={<Skeleton className="h-64 w-full" />}>
|
|
2485
|
+
<Carousel slides={slides} />
|
|
2486
|
+
</ClientOnly>
|
|
2487
|
+
```
|
|
2126
2488
|
|
|
2127
2489
|
---
|
|
2128
2490
|
|
|
@@ -2164,7 +2526,10 @@ Toast notifications. Sonner-based.
|
|
|
2164
2526
|
```tsx
|
|
2165
2527
|
import { toast, Toaster } from '@nexus-cross/design-system';
|
|
2166
2528
|
|
|
2167
|
-
// Place Toaster at app root
|
|
2529
|
+
// Place Toaster at the app root.
|
|
2530
|
+
// It uses useState internally → it's a client component.
|
|
2531
|
+
// In Next.js App Router, wrap it in a "use client" file (e.g. providers.tsx)
|
|
2532
|
+
// and render that wrapper from the (server) layout. NEVER add "use client" to layout.tsx itself.
|
|
2168
2533
|
<Toaster position="top-right" />
|
|
2169
2534
|
|
|
2170
2535
|
// Usage
|
|
@@ -31,12 +31,14 @@ var numberInputVariants = classVarianceAuthority.cva("nexus-number-input", {
|
|
|
31
31
|
basic: "nexus-number-input--basic",
|
|
32
32
|
bind: "nexus-number-input--bind"
|
|
33
33
|
},
|
|
34
|
+
// TextInput과 동일한 사이즈 체계 (md / lg / xl, default md)
|
|
34
35
|
size: {
|
|
36
|
+
md: "nexus-number-input--md",
|
|
35
37
|
lg: "nexus-number-input--lg",
|
|
36
38
|
xl: "nexus-number-input--xl"
|
|
37
39
|
}
|
|
38
40
|
},
|
|
39
|
-
defaultVariants: { variant: "basic", size: "
|
|
41
|
+
defaultVariants: { variant: "basic", size: "md" }
|
|
40
42
|
});
|
|
41
43
|
var CHEVRON_PATH = "M3.606.179C3.82-.06 4.18-.06 4.394.179L7.846 4.01C8.18 4.382 7.934 5 7.452 5H.548C.066 5-.18 4.382.154 4.01L3.606.179Z";
|
|
42
44
|
var ChevronUpIcon = () => /* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -106,6 +108,8 @@ function numberInputBind(ref, direction) {
|
|
|
106
108
|
var NumberInput = React__namespace.forwardRef(
|
|
107
109
|
({
|
|
108
110
|
className,
|
|
111
|
+
containerClassName,
|
|
112
|
+
inputClassName,
|
|
109
113
|
variant = "basic",
|
|
110
114
|
size,
|
|
111
115
|
error,
|
|
@@ -118,6 +122,8 @@ var NumberInput = React__namespace.forwardRef(
|
|
|
118
122
|
description,
|
|
119
123
|
showMax,
|
|
120
124
|
hideButtons = false,
|
|
125
|
+
prefixIcon,
|
|
126
|
+
suffixIcon,
|
|
121
127
|
disabled,
|
|
122
128
|
readOnly,
|
|
123
129
|
onValueChange,
|
|
@@ -288,7 +294,7 @@ var NumberInput = React__namespace.forwardRef(
|
|
|
288
294
|
}
|
|
289
295
|
)
|
|
290
296
|
] }),
|
|
291
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "nexus-number-input__container", children: [
|
|
297
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: chunkCZC76ZD5_js.cn("nexus-number-input__container", containerClassName), children: [
|
|
292
298
|
!isBasic && showBtns && /* @__PURE__ */ jsxRuntime.jsx(
|
|
293
299
|
"button",
|
|
294
300
|
{
|
|
@@ -303,6 +309,7 @@ var NumberInput = React__namespace.forwardRef(
|
|
|
303
309
|
children: /* @__PURE__ */ jsxRuntime.jsx(MinusIcon, {})
|
|
304
310
|
}
|
|
305
311
|
),
|
|
312
|
+
prefixIcon && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "nexus-number-input__icon nexus-number-input__icon--prefix", children: prefixIcon }),
|
|
306
313
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
307
314
|
"input",
|
|
308
315
|
{
|
|
@@ -310,7 +317,7 @@ var NumberInput = React__namespace.forwardRef(
|
|
|
310
317
|
type: "text",
|
|
311
318
|
inputMode: "decimal",
|
|
312
319
|
role: "spinbutton",
|
|
313
|
-
className: "nexus-number-input__field",
|
|
320
|
+
className: chunkCZC76ZD5_js.cn("nexus-number-input__field", inputClassName),
|
|
314
321
|
value: internalValue,
|
|
315
322
|
disabled,
|
|
316
323
|
readOnly,
|
|
@@ -325,6 +332,7 @@ var NumberInput = React__namespace.forwardRef(
|
|
|
325
332
|
...props
|
|
326
333
|
}
|
|
327
334
|
),
|
|
335
|
+
suffixIcon && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "nexus-number-input__icon nexus-number-input__icon--suffix", children: suffixIcon }),
|
|
328
336
|
!isBasic && showBtns && /* @__PURE__ */ jsxRuntime.jsx(
|
|
329
337
|
"button",
|
|
330
338
|
{
|
|
@@ -41,6 +41,7 @@ var Select = ({
|
|
|
41
41
|
variant,
|
|
42
42
|
className = "",
|
|
43
43
|
triggerClassName = "",
|
|
44
|
+
contentClassName = "",
|
|
44
45
|
displayComponent
|
|
45
46
|
}) => {
|
|
46
47
|
const [isOpen, setIsOpen] = useState(false);
|
|
@@ -80,7 +81,7 @@ var Select = ({
|
|
|
80
81
|
/* @__PURE__ */ jsx(SelectPrimitive.Portal, { children: /* @__PURE__ */ jsx(
|
|
81
82
|
SelectPrimitive.Content,
|
|
82
83
|
{
|
|
83
|
-
className: cn(selectContentVariants({ size })),
|
|
84
|
+
className: cn(selectContentVariants({ size }), contentClassName),
|
|
84
85
|
position: "popper",
|
|
85
86
|
sideOffset: 4,
|
|
86
87
|
children: /* @__PURE__ */ jsx(SelectPrimitive.Viewport, { className: "nexus-select-viewport", children })
|
|
@@ -8,9 +8,13 @@ var tabListVariants = cva("nexus-tab-list", {
|
|
|
8
8
|
variant: {
|
|
9
9
|
line: "",
|
|
10
10
|
pill: "nexus-tab-list--pill"
|
|
11
|
+
},
|
|
12
|
+
fullWidth: {
|
|
13
|
+
true: "nexus-tab-list--full-width",
|
|
14
|
+
false: ""
|
|
11
15
|
}
|
|
12
16
|
},
|
|
13
|
-
defaultVariants: { variant: "line" }
|
|
17
|
+
defaultVariants: { variant: "line", fullWidth: false }
|
|
14
18
|
});
|
|
15
19
|
var tabTriggerVariants = cva("nexus-tab-trigger", {
|
|
16
20
|
variants: {
|
|
@@ -39,10 +43,12 @@ function Tab({
|
|
|
39
43
|
defaultActiveKey,
|
|
40
44
|
variant,
|
|
41
45
|
size,
|
|
46
|
+
fullWidth = false,
|
|
42
47
|
destroyInactive = false,
|
|
43
48
|
onTabChange,
|
|
44
49
|
className,
|
|
45
50
|
tabListClassName,
|
|
51
|
+
tabItemClassName,
|
|
46
52
|
tabPanelClassName
|
|
47
53
|
}) {
|
|
48
54
|
const [internalKey, setInternalKey] = React.useState(
|
|
@@ -83,7 +89,7 @@ function Tab({
|
|
|
83
89
|
"div",
|
|
84
90
|
{
|
|
85
91
|
role: "tablist",
|
|
86
|
-
className: cn(tabListVariants({ variant }), tabListClassName),
|
|
92
|
+
className: cn(tabListVariants({ variant, fullWidth }), tabListClassName),
|
|
87
93
|
children: items.map((item, i) => /* @__PURE__ */ jsx(
|
|
88
94
|
"button",
|
|
89
95
|
{
|
|
@@ -101,7 +107,9 @@ function Tab({
|
|
|
101
107
|
size,
|
|
102
108
|
active: item.key === currentKey
|
|
103
109
|
}),
|
|
104
|
-
item.disabled && "nexus-tab-trigger--disabled"
|
|
110
|
+
item.disabled && "nexus-tab-trigger--disabled",
|
|
111
|
+
tabItemClassName,
|
|
112
|
+
item.className
|
|
105
113
|
),
|
|
106
114
|
onClick: () => !item.disabled && handleSelect(item.key),
|
|
107
115
|
onKeyDown: (e) => handleKeyDown(e, i),
|
|
@@ -63,6 +63,7 @@ var Select = ({
|
|
|
63
63
|
variant,
|
|
64
64
|
className = "",
|
|
65
65
|
triggerClassName = "",
|
|
66
|
+
contentClassName = "",
|
|
66
67
|
displayComponent
|
|
67
68
|
}) => {
|
|
68
69
|
const [isOpen, setIsOpen] = react.useState(false);
|
|
@@ -102,7 +103,7 @@ var Select = ({
|
|
|
102
103
|
/* @__PURE__ */ jsxRuntime.jsx(SelectPrimitive__namespace.Portal, { children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
103
104
|
SelectPrimitive__namespace.Content,
|
|
104
105
|
{
|
|
105
|
-
className: chunkCZC76ZD5_js.cn(selectContentVariants({ size })),
|
|
106
|
+
className: chunkCZC76ZD5_js.cn(selectContentVariants({ size }), contentClassName),
|
|
106
107
|
position: "popper",
|
|
107
108
|
sideOffset: 4,
|
|
108
109
|
children: /* @__PURE__ */ jsxRuntime.jsx(SelectPrimitive__namespace.Viewport, { className: "nexus-select-viewport", children })
|