@nexus-cross/design-system 2.0.0 → 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.
Files changed (67) hide show
  1. package/README.md +375 -0
  2. package/cursor-rules/nexus-ui-api.mdc +366 -7
  3. package/dist/chunks/{chunk-2T7RUYEK.js → chunk-2BINGHGR.js} +11 -3
  4. package/dist/chunks/{chunk-QOREDNWO.mjs → chunk-53BHDUID.mjs} +2 -1
  5. package/dist/chunks/{chunk-QZ4QR3XV.mjs → chunk-ATZE57ZO.mjs} +11 -3
  6. package/dist/chunks/{chunk-OX5MEJ7B.js → chunk-HU6E2R2T.js} +2 -1
  7. package/dist/chunks/chunk-KT2WKVF7.mjs +5 -0
  8. package/dist/chunks/{chunk-5J63FUAS.mjs → chunk-LNC3TV6N.mjs} +53 -2
  9. package/dist/chunks/chunk-MWWQMVXJ.js +7 -0
  10. package/dist/chunks/{chunk-BJM3NDT2.mjs → chunk-RL5UAEGQ.mjs} +11 -3
  11. package/dist/chunks/{chunk-LAGQ7J5A.js → chunk-VCN7DMCQ.js} +53 -2
  12. package/dist/chunks/{chunk-2ZXDXO4I.js → chunk-VDEB5BMT.js} +11 -3
  13. package/dist/components/ImageUpload.d.ts +14 -0
  14. package/dist/components/ImageUpload.d.ts.map +1 -1
  15. package/dist/components/NumberInput.d.ts +20 -1
  16. package/dist/components/NumberInput.d.ts.map +1 -1
  17. package/dist/components/Select.d.ts +5 -1
  18. package/dist/components/Select.d.ts.map +1 -1
  19. package/dist/components/Tab.d.ts +12 -1
  20. package/dist/components/Tab.d.ts.map +1 -1
  21. package/dist/image-upload.js +3 -3
  22. package/dist/image-upload.mjs +1 -1
  23. package/dist/index.js +16 -16
  24. package/dist/index.mjs +4 -4
  25. package/dist/number-input.js +4 -4
  26. package/dist/number-input.mjs +1 -1
  27. package/dist/schemas/_all.json +48 -10
  28. package/dist/schemas/image-upload.d.ts +6 -0
  29. package/dist/schemas/image-upload.d.ts.map +1 -1
  30. package/dist/schemas/imageUpload.json +19 -1
  31. package/dist/schemas/number-input.d.ts +9 -3
  32. package/dist/schemas/number-input.d.ts.map +1 -1
  33. package/dist/schemas/numberInput.json +10 -3
  34. package/dist/schemas/spinner.json +2 -2
  35. package/dist/schemas/tab.d.ts +11 -0
  36. package/dist/schemas/tab.d.ts.map +1 -1
  37. package/dist/schemas/tab.json +17 -4
  38. package/dist/schemas.js +48 -10
  39. package/dist/schemas.mjs +48 -10
  40. package/dist/select.js +5 -5
  41. package/dist/select.mjs +1 -1
  42. package/dist/styles/.generated/built.d.ts +1 -1
  43. package/dist/styles/.generated/built.d.ts.map +1 -1
  44. package/dist/styles/layer.js +2 -2
  45. package/dist/styles/layer.mjs +1 -1
  46. package/dist/styles.css +185 -44
  47. package/dist/styles.js +2 -2
  48. package/dist/styles.layered.css +185 -44
  49. package/dist/styles.mjs +1 -1
  50. package/dist/tab.js +4 -4
  51. package/dist/tab.mjs +1 -1
  52. package/dist/tokens/TOKENS.md +13 -0
  53. package/dist/tokens/company.css +21 -1
  54. package/dist/tokens/css.css +21 -1
  55. package/dist/tokens/data/color.json +32 -0
  56. package/dist/tokens/data/space.json +1 -1
  57. package/dist/tokens/data/typography.json +55 -1
  58. package/dist/tokens-domains/data/gamehub/domain.json +258 -0
  59. package/dist/tokens-domains/data/index.ts +3 -1
  60. package/dist/tokens-domains/data/prediction/domain.json +0 -12
  61. package/dist/tokens-domains/gamehub.md +62 -0
  62. package/dist/tokens-domains/prediction-vars.css +1 -5
  63. package/dist/tokens-domains/prediction.css +1 -5
  64. package/dist/tokens-domains/prediction.md +0 -1
  65. package/package.json +3 -3
  66. package/dist/chunks/chunk-3SCSND6S.js +0 -7
  67. 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, default currentColor) |
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 etc. |
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
- ### DrawerContent
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>
@@ -1062,6 +1100,14 @@ WHEN TO USE:
1062
1100
  • Immediate filter/option toggle → ToggleGroup
1063
1101
  • Stacked collapsible sections → Accordion
1064
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
+
1065
1111
  destroyInactive=true unmounts hidden panels (saves memory but loses state).
1066
1112
 
1067
1113
  ANTI-PATTERNS:
@@ -1069,6 +1115,7 @@ ANTI-PATTERNS:
1069
1115
  ✗ Tab with 8+ items — consider sub-routing or DropdownMenu
1070
1116
  ✗ Using Tab for form value selection → RadioGroup
1071
1117
  ✗ Custom <button> + onClick + state → Tab (a11y, keyboard, focus management)
1118
+ ✗ `tabListClassName="w-full"` expecting triggers to stretch → use `fullWidth` prop
1072
1119
 
1073
1120
  | Prop | Type | Default | Description |
1074
1121
  |---|---|---|---|
@@ -1077,13 +1124,16 @@ ANTI-PATTERNS:
1077
1124
  | `defaultActiveKey` | `string` | - | Uncontrolled initial key |
1078
1125
  | `variant` | `'line'` \| `'pill'` | `"line"` | Style |
1079
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). |
1080
1128
  | `destroyInactive` | `boolean` | `false` | Unmount inactive panels |
1081
1129
  | `onTabChange` | `ReactNode` | - | Tab change callback (key: string) => void |
1082
- | `className` | `string` | - | Root style |
1083
- | `tabListClassName` | `string` | - | Tab list style |
1084
- | `tabPanelClassName` | `string` | - | Tab panel style |
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> |
1085
1134
 
1086
1135
  ```tsx
1136
+ // Basic — triggers size to their content
1087
1137
  <Tab
1088
1138
  items={[
1089
1139
  { key: 'a', label: 'Tab A', children: <p>A</p> },
@@ -1092,6 +1142,28 @@ ANTI-PATTERNS:
1092
1142
  defaultActiveKey="a"
1093
1143
  variant="pill"
1094
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
+ />
1095
1167
  ```
1096
1168
 
1097
1169
  ---
@@ -1121,6 +1193,32 @@ ANTI-PATTERNS:
1121
1193
  | `children` | `ReactNode` | - | Carousel slides and sub-components (ReactNode) |
1122
1194
  | `className` | `string` | - | Style override |
1123
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
+
1124
1222
  Sub-components: `CarouselSlide`, `CarouselPrev`, `CarouselNext`, `CarouselDots`
1125
1223
 
1126
1224
  ```tsx
@@ -1621,13 +1719,16 @@ ANTI-PATTERNS:
1621
1719
  ✗ <TextInput type="number"> → <NumberInput> (loses keyboard ↑↓, step, accelerated long-press, max click)
1622
1720
  ✗ Custom +/- buttons + <input> → variant="bind" (or numberInputBind for external)
1623
1721
  ✗ Manual thousand separators for currency → PriceInput
1722
+ ✗ Inline unit text inside label/description → use prefixIcon/suffixIcon (e.g. suffixIcon="%")
1624
1723
 
1625
1724
  | Prop | Type | Default | Description |
1626
1725
  |---|---|---|---|
1627
1726
  | `variant` | `'basic'` \| `'bind'` | `"basic"` | Variant: basic (right chevron arrows) or bind (left/right +/- buttons) |
1628
1727
  | `value` | `number` \| `string` | - | Current value |
1629
- | `size` | `'lg'` \| `'xl'` | `"lg"` | Size |
1728
+ | `size` | `'md'` \| `'lg'` \| `'xl'` | `"md"` | Size (matches TextInput scale) |
1630
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 |
1631
1732
  | `min` | `number` | - | Minimum value |
1632
1733
  | `max` | `number` | - | Maximum value. When set, "Max {value}" is displayed in the header. Clicking it fills the input with the max value |
1633
1734
  | `step` | `number` | `1` | Step increment |
@@ -1649,9 +1750,12 @@ ANTI-PATTERNS:
1649
1750
 
1650
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.
1651
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
+
1652
1755
  ```tsx
1653
1756
  // Basic usage — right-side vertical spin buttons
1654
1757
  <NumberInput
1758
+ label="Quantity"
1655
1759
  value={count}
1656
1760
  onValueChange={setCount}
1657
1761
  min={0}
@@ -1659,6 +1763,14 @@ Press-and-hold accelerates increment/decrement (100ms → 75ms → 50ms → 30ms
1659
1763
  step={5}
1660
1764
  />
1661
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
+
1662
1774
  // External buttons — bind events with numberInputBind
1663
1775
  const ref = useRef<NumberInputRef>(null);
1664
1776
 
@@ -2101,15 +2213,22 @@ ImageUpload — drag-and-drop image upload with preview, file-type/size validati
2101
2213
 
2102
2214
  WHEN TO USE:
2103
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"
2104
2217
  • Multiple files / non-image → not yet supported; build custom or use a file dropzone
2105
2218
  • Inline image picker without preview → custom <input type="file">
2106
2219
 
2107
2220
  accept whitelist + maxSize together cover validation. onError fires with i18n-ready string.
2108
2221
 
2222
+ PREVIEW MODES:
2223
+ • card (default) — 작은 썸네일 + 우측에 "이미지 변경" 버튼 + 포맷 텍스트. form 필드용.
2224
+ • cover — 빈 상태 박스를 그대로 유지하고 이미지가 전체를 가득 채움. 클릭 시 변경, 우측 상단 X로 삭제.
2225
+ 크기/비율은 className(h-64, aspect-video, w-full 등) 또는 size prop으로 조정.
2226
+
2109
2227
  ANTI-PATTERNS:
2110
2228
  ✗ <input type="file"> + manual preview → ImageUpload (handles drag, validation, preview)
2111
2229
  ✗ ImageUpload without onError handler → silent failure on validation reject
2112
2230
  ✗ Storing image as data-URL value (use File via onChange and upload to server)
2231
+ ✗ card 모드에서 작은 썸네일이 어색한 배너/카드 슬롯 → previewMode="cover"
2113
2232
 
2114
2233
  | Prop | Type | Default | Description |
2115
2234
  |---|---|---|---|
@@ -2125,7 +2244,247 @@ ANTI-PATTERNS:
2125
2244
  | `description` | `ReactNode` | - | Field description below the upload area (ReactNode) |
2126
2245
  | `disabled` | `boolean` | `false` | Disabled state |
2127
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 |
2128
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
+ ```
2129
2488
 
2130
2489
  ---
2131
2490
 
@@ -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: "lg" }
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 })