teachable-design-system 0.2.1 → 0.3.2

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 (50) hide show
  1. package/dist/index.cjs.js +875 -64
  2. package/dist/index.cjs.js.map +1 -1
  3. package/dist/index.d.ts +72 -2
  4. package/dist/index.esm.js +877 -67
  5. package/dist/index.esm.js.map +1 -1
  6. package/dist/types/components/Dropdown/Dropdown.d.ts +1 -1
  7. package/dist/types/components/Dropdown/Dropdown.d.ts.map +1 -1
  8. package/dist/types/components/Dropdown/style.d.ts +1 -0
  9. package/dist/types/components/Dropdown/style.d.ts.map +1 -1
  10. package/dist/types/components/Table/Table.d.ts +3 -0
  11. package/dist/types/components/Table/Table.d.ts.map +1 -0
  12. package/dist/types/components/Table/index.d.ts +3 -0
  13. package/dist/types/components/Table/index.d.ts.map +1 -0
  14. package/dist/types/components/Table/style.d.ts +98 -0
  15. package/dist/types/components/Table/style.d.ts.map +1 -0
  16. package/dist/types/components/Table/table-body.d.ts +3 -0
  17. package/dist/types/components/Table/table-body.d.ts.map +1 -0
  18. package/dist/types/components/Table/table-cell.d.ts +3 -0
  19. package/dist/types/components/Table/table-cell.d.ts.map +1 -0
  20. package/dist/types/components/Table/table-header.d.ts +4 -0
  21. package/dist/types/components/Table/table-header.d.ts.map +1 -0
  22. package/dist/types/components/index.d.ts +1 -0
  23. package/dist/types/components/index.d.ts.map +1 -1
  24. package/dist/types/types/Dropdown.types.d.ts +1 -0
  25. package/dist/types/types/Dropdown.types.d.ts.map +1 -1
  26. package/dist/types/types/index.d.ts +6 -0
  27. package/dist/types/types/index.d.ts.map +1 -1
  28. package/dist/types/types/table.d.ts +141 -0
  29. package/dist/types/types/table.d.ts.map +1 -0
  30. package/package.json +1 -1
  31. package/src/components/Dropdown/Dropdown.stories.tsx +4 -0
  32. package/src/components/Dropdown/Dropdown.tsx +2 -1
  33. package/src/components/Dropdown/style.ts +2 -1
  34. package/src/components/Sidebar/style.ts +4 -4
  35. package/src/components/Table/Table.stories.tsx +179 -0
  36. package/src/components/Table/Table.tsx +510 -0
  37. package/src/components/Table/index.ts +2 -0
  38. package/src/components/Table/style.ts +345 -0
  39. package/src/components/Table/table-body.tsx +112 -0
  40. package/src/components/Table/table-cell.tsx +153 -0
  41. package/src/components/Table/table-header.tsx +52 -0
  42. package/src/components/index.ts +5 -4
  43. package/src/types/Dropdown.types.ts +1 -0
  44. package/src/types/index.ts +6 -0
  45. package/src/types/table.ts +150 -0
  46. package/dist/assets/icons/arrow-down.png +0 -0
  47. package/dist/assets/icons/checked.png +0 -0
  48. package/dist/assets/icons/icon_size.png +0 -0
  49. package/dist/assets/images/.gitkeep +0 -0
  50. package/dist/assets/index.ts +0 -1
package/dist/index.cjs.js CHANGED
@@ -9,7 +9,7 @@ var arrowDownIcon = require('../../assets/icons/arrow-down.png');
9
9
  var lucideReact = require('lucide-react');
10
10
  var icon = require('../../assets/icons/icon_size.png');
11
11
 
12
- const colors = {
12
+ const colors$1 = {
13
13
  //mode
14
14
  background: {
15
15
  'white': '#FFFFFF',
@@ -744,26 +744,26 @@ const StyledButton = styled.button `
744
744
  `}
745
745
 
746
746
  ${props => props.buttonType === 'primary' && react.css `
747
- background-color: ${colors.button["primary-fill"]};
748
- color: ${colors.text["inverse-static"]};
747
+ background-color: ${colors$1.button["primary-fill"]};
748
+ color: ${colors$1.text["inverse-static"]};
749
749
  border: none;
750
750
 
751
751
  &:hover:not(:disabled) {
752
- background-color: ${colors.button["primary-fill-hover"]};
752
+ background-color: ${colors$1.button["primary-fill-hover"]};
753
753
  }
754
754
 
755
755
  &:active:not(:disabled) {
756
- background-color: ${colors.button["tertiary-fill"]};
756
+ background-color: ${colors$1.button["tertiary-fill"]};
757
757
  }
758
758
  `}
759
759
 
760
760
  ${props => props.buttonType === 'secondary' && react.css `
761
- background-color: ${colors.button['secondary-fill']};
762
- color: ${colors.text['primary']};
763
- border: 1px solid ${colors.button["secondary-border"]};
761
+ background-color: ${colors$1.button['secondary-fill']};
762
+ color: ${colors$1.text['primary']};
763
+ border: 1px solid ${colors$1.button["secondary-border"]};
764
764
 
765
765
  &:hover:not(:disabled) {
766
- background-color: ${colors.button['secondary-fill-hover']};
766
+ background-color: ${colors$1.button['secondary-fill-hover']};
767
767
  }
768
768
 
769
769
  &:active:not(:disabled) {
@@ -773,8 +773,8 @@ const StyledButton = styled.button `
773
773
 
774
774
  ${props => props.buttonType === 'tertiary' && react.css `
775
775
  background-color: transparent;
776
- color: ${colors.text["basic"]};
777
- border: 1px solid ${colors.button["tertiary-border"]};
776
+ color: ${colors$1.text["basic"]};
777
+ border: 1px solid ${colors$1.button["tertiary-border"]};
778
778
 
779
779
  &:hover:not(:disabled) {
780
780
  background-color: #f0f7ff;
@@ -812,30 +812,30 @@ const Wrapper$1 = styled.div `
812
812
 
813
813
  ${state === "disabled" &&
814
814
  react.css `
815
- background-color: ${colors.surface.disabled};
816
- border: 1px solid ${colors.border.disabled};
815
+ background-color: ${colors$1.surface.disabled};
816
+ border: 1px solid ${colors$1.border.disabled};
817
817
  `}
818
818
 
819
819
  ${state === "default" &&
820
820
  select === "off" &&
821
821
  react.css `
822
- background-color: ${colors.surface.white};
823
- border: 1px solid ${colors.border["gray-dark"]};
822
+ background-color: ${colors$1.surface.white};
823
+ border: 1px solid ${colors$1.border["gray-dark"]};
824
824
 
825
825
  &:hover {
826
- border-color: ${colors.border.primary};
826
+ border-color: ${colors$1.border.primary};
827
827
  }
828
828
  `}
829
829
 
830
830
  ${state === "default" &&
831
831
  (select === "on" || select === "indeterminate") &&
832
832
  react.css `
833
- background-color: ${colors.element.primary};
834
- border: 1px solid ${colors.element.primary};
833
+ background-color: ${colors$1.element.primary};
834
+ border: 1px solid ${colors$1.element.primary};
835
835
 
836
836
  &:hover {
837
- background-color: ${colors.light.primary["60"]};
838
- border-color: ${colors.light.primary["60"]};
837
+ background-color: ${colors$1.light.primary["60"]};
838
+ border-color: ${colors$1.light.primary["60"]};
839
839
  }
840
840
  `}
841
841
  `;
@@ -850,8 +850,8 @@ const DashIcon = styled.div `
850
850
  width: 60%;
851
851
  height: 2.5px;
852
852
  background-color: ${({ state }) => state === "disabled"
853
- ? colors.element["disabled-dark"]
854
- : colors.element.inverse};
853
+ ? colors$1.element["disabled-dark"]
854
+ : colors$1.element.inverse};
855
855
  border-radius: 2px;
856
856
  `;
857
857
 
@@ -958,15 +958,15 @@ const getOptionSize = (size) => {
958
958
  };
959
959
  const StyledDropDown = styled.button `
960
960
  font-family: ${typography.fontFamily.primary};
961
- background: ${colors.input.surface};
961
+ background: ${colors$1.input.surface};
962
962
  border-radius: 8px;
963
963
  display: flex;
964
964
  flex-direction: column;
965
965
  align-items: flex-start;
966
966
  gap: 8px;
967
- width: 320px;
967
+ width: ${({ width }) => width || '320px'};
968
968
 
969
- border: ${({ isOpen }) => isOpen ? `2px solid ${colors.input['border-active']}` : `1px solid ${colors.input.border}`};
969
+ border: ${({ isOpen }) => isOpen ? `2px solid ${colors$1.input['border-active']}` : `1px solid ${colors$1.input.border}`};
970
970
  ${props => getButtonSize(props.size)}
971
971
 
972
972
  `;
@@ -986,17 +986,17 @@ const StyledText = styled.p `
986
986
  flex: 1 0 0;
987
987
  text-align: left;
988
988
  ${props => getFontSize(props.size)}
989
- color: ${({ isOpen }) => isOpen ? colors.text.static : colors.text.disabled};
989
+ color: ${({ isOpen }) => isOpen ? colors$1.text.static : colors$1.text.disabled};
990
990
  `;
991
991
  const StyledLabel = styled.p `
992
992
  font-family: ${typography.fontFamily.primary};
993
- color: ${colors.text.static};
993
+ color: ${colors$1.text.static};
994
994
  `;
995
995
  const StyledIcon$1 = styled.div `
996
996
  ${props => getIconSize(props.size)}
997
997
  `;
998
998
  const StyledOptions = styled.div `
999
- border: 1px solid ${colors.border["gray-light"]};
999
+ border: 1px solid ${colors$1.border["gray-light"]};
1000
1000
  border-radius: 8px;
1001
1001
 
1002
1002
  display: flex;
@@ -1021,7 +1021,7 @@ const StyledOption = styled.div `
1021
1021
 
1022
1022
  border-radius: 8px;
1023
1023
 
1024
- color: ${colors.text.subtle};
1024
+ color: ${colors$1.text.subtle};
1025
1025
  position: relative;
1026
1026
 
1027
1027
  ${props => getFontSize(props.size)}
@@ -1029,8 +1029,8 @@ const StyledOption = styled.div `
1029
1029
 
1030
1030
 
1031
1031
  ${({ isSelected }) => isSelected && react.css `
1032
- background: ${colors.action["secondary-selected"]};
1033
- color: ${colors.text.secondary};
1032
+ background: ${colors$1.action["secondary-selected"]};
1033
+ color: ${colors$1.text.secondary};
1034
1034
 
1035
1035
  padding-left: 36px;
1036
1036
 
@@ -1049,14 +1049,14 @@ const StyledOption = styled.div `
1049
1049
  `}
1050
1050
 
1051
1051
  &:hover {
1052
- background: ${colors.action["secondary-hover"]};
1052
+ background: ${colors$1.action["secondary-hover"]};
1053
1053
  }
1054
1054
  &:active {
1055
- background: ${colors.action["secondary-pressed"]};
1055
+ background: ${colors$1.action["secondary-pressed"]};
1056
1056
  }
1057
1057
  `;
1058
1058
 
1059
- function Dropdown({ size, options, onSelect, label, placeholder }) {
1059
+ function Dropdown({ size, options, onSelect, label, placeholder, width }) {
1060
1060
  const [open, setOpen] = React.useState(false);
1061
1061
  const [selected, setSelected] = React.useState(null);
1062
1062
  const ref = React.useRef(null);
@@ -1074,7 +1074,7 @@ function Dropdown({ size, options, onSelect, label, placeholder }) {
1074
1074
  onSelect?.(option);
1075
1075
  setOpen(false);
1076
1076
  };
1077
- return (jsxRuntime.jsxs("div", { style: { position: 'relative' }, children: [jsxRuntime.jsx(StyledLabel, { children: label }), jsxRuntime.jsx(StyledDropDown, { onClick: () => setOpen((prev) => !prev), size: size, isOpen: open, children: jsxRuntime.jsxs(StyledBox, { children: [jsxRuntime.jsx(StyledText, { size: size, isOpen: open, children: selected ?? placeholder }), jsxRuntime.jsx(StyledIcon$1, { size: size, children: jsxRuntime.jsx("img", { src: arrowDownIcon, alt: "dropdown icon", style: { width: '100%', height: '100%' } }) })] }) }), open && (jsxRuntime.jsx(StyledOptions, { ref: ref, size: size, children: options?.map((option) => (jsxRuntime.jsx(StyledOption, { onClick: () => handleSelect(option), size: size, isSelected: option === selected, children: option }, option))) }))] }));
1077
+ return (jsxRuntime.jsxs("div", { style: { position: 'relative' }, children: [jsxRuntime.jsx(StyledLabel, { children: label }), jsxRuntime.jsx(StyledDropDown, { onClick: () => setOpen((prev) => !prev), size: size, width: width, isOpen: open, children: jsxRuntime.jsxs(StyledBox, { children: [jsxRuntime.jsx(StyledText, { size: size, isOpen: open, children: selected ?? placeholder }), jsxRuntime.jsx(StyledIcon$1, { size: size, children: jsxRuntime.jsx("img", { src: arrowDownIcon, alt: "dropdown icon", style: { width: '100%', height: '100%' } }) })] }) }), open && (jsxRuntime.jsx(StyledOptions, { ref: ref, size: size, children: options?.map((option) => (jsxRuntime.jsx(StyledOption, { onClick: () => handleSelect(option), size: size, isSelected: option === selected, children: option }, option))) }))] }));
1078
1078
  }
1079
1079
 
1080
1080
  const getInputSizeStyle = (size) => {
@@ -1122,7 +1122,7 @@ const Label = styled.label `
1122
1122
  line-height: ${typography.label.small.lineHeight};
1123
1123
  font-weight: ${typography.label.small.fontWeight};
1124
1124
  font-family: ${typography.fontFamily.primary};
1125
- color: ${colors.text.subtle};
1125
+ color: ${colors$1.text.subtle};
1126
1126
  `;
1127
1127
  const InputContainer = styled.div `
1128
1128
  position: relative;
@@ -1134,13 +1134,13 @@ const StyledInput = styled.input `
1134
1134
  width: ${(props) => props.width || "306px"};
1135
1135
  padding: ${(props) => (props.isPassword ? "0px 48px 0px 16px" : "0px 16px")};
1136
1136
  font-family: ${typography.fontFamily.primary};
1137
- border: 1px solid ${colors.input.border};
1137
+ border: 1px solid ${colors$1.input.border};
1138
1138
  border-radius: 4px;
1139
1139
  outline: none;
1140
1140
  transition: all 0.2s ease;
1141
- background-color: ${(props) => props.disabled ? colors.input["surface-disabled"] : colors.input.surface};
1141
+ background-color: ${(props) => props.disabled ? colors$1.input["surface-disabled"] : colors$1.input.surface};
1142
1142
  cursor: ${(props) => (props.disabled ? "not-allowed" : "text")};
1143
- color: ${colors.text.basic};
1143
+ color: ${colors$1.text.basic};
1144
1144
  box-sizing: border-box;
1145
1145
 
1146
1146
  ${(props) => getInputSizeStyle(props.inputSize)}
@@ -1151,17 +1151,17 @@ const StyledInput = styled.input `
1151
1151
  `}
1152
1152
 
1153
1153
  &:focus {
1154
- border: 1px solid ${colors.input["border-active"]};
1155
- box-shadow: 0 0 0 3px ${colors.light.primary["5"]};
1154
+ border: 1px solid ${colors$1.input["border-active"]};
1155
+ box-shadow: 0 0 0 3px ${colors$1.light.primary["5"]};
1156
1156
  }
1157
1157
 
1158
1158
  &:disabled {
1159
- border: 1px solid ${colors.input["border-disabled"]};
1160
- color: ${colors.text.disabled};
1159
+ border: 1px solid ${colors$1.input["border-disabled"]};
1160
+ color: ${colors$1.text.disabled};
1161
1161
  }
1162
1162
 
1163
1163
  &::placeholder {
1164
- color: ${colors.text.disabled};
1164
+ color: ${colors$1.text.disabled};
1165
1165
  }
1166
1166
  `;
1167
1167
  const IconButton = styled.button `
@@ -1180,7 +1180,7 @@ const IconButton = styled.button `
1180
1180
  transition: opacity 0.2s ease;
1181
1181
 
1182
1182
  svg {
1183
- color: ${(props) => props.disabled ? colors.icon.disabled : colors.icon.gray};
1183
+ color: ${(props) => props.disabled ? colors$1.icon.disabled : colors$1.icon.gray};
1184
1184
  }
1185
1185
  `;
1186
1186
 
@@ -1198,14 +1198,14 @@ const StyledIcon = styled.img `
1198
1198
  `;
1199
1199
  const StyledSidebar = styled.div `
1200
1200
  display: flex;
1201
- width: 240px;
1202
- height: 100vh;
1201
+ width: 208px;
1202
+ height: 621px;
1203
1203
  padding: 16px;
1204
1204
  flex-direction: column;
1205
1205
  align-items: flex-start;
1206
1206
  gap: 16px;
1207
1207
 
1208
- background: ${colors.background.white};
1208
+ background: ${colors$1.background.white};
1209
1209
  `;
1210
1210
  const StyledDescription = styled.div `
1211
1211
  width: 100%;
@@ -1214,7 +1214,7 @@ const StyledDescription = styled.div `
1214
1214
  align-items: flex-start;
1215
1215
  gap: 18px;
1216
1216
  flex: 1 0 0;
1217
- max-height: calc(100vh - 40px);
1217
+ max-height: calc(100% - 40px);
1218
1218
  overflow-y: auto;
1219
1219
  overflow-x: hidden;
1220
1220
 
@@ -1223,21 +1223,21 @@ const StyledDescription = styled.div `
1223
1223
  }
1224
1224
 
1225
1225
  ::-webkit-scrollbar-track {
1226
- background: ${colors.surface["gray-subtler"]};
1226
+ background: ${colors$1.surface["gray-subtler"]};
1227
1227
  border-radius: 4px;
1228
1228
  }
1229
1229
 
1230
1230
  ::-webkit-scrollbar-thumb {
1231
- background-color: ${colors.text.subtle};
1231
+ background-color: ${colors$1.text.subtle};
1232
1232
  border-radius: 4px;
1233
1233
  }
1234
1234
 
1235
1235
  ::-webkit-scrollbar-thumb:hover {
1236
- background-color: ${colors.action["secondary-active"]};
1236
+ background-color: ${colors$1.action["secondary-active"]};
1237
1237
  }
1238
1238
 
1239
1239
  ::-webkit-scrollbar-thumb:active {
1240
- background-color: ${colors.action["primary-active"]};
1240
+ background-color: ${colors$1.action["primary-active"]};
1241
1241
  }
1242
1242
  `;
1243
1243
  const StyledButtonArea = styled.div `
@@ -1268,15 +1268,15 @@ const StyledOpenContents = styled.div `
1268
1268
  align-items: flex-start;
1269
1269
  gap: 8px;
1270
1270
  align-self: stretch;
1271
- width: 186px;
1271
+ width: 154px;
1272
1272
 
1273
1273
  border-radius: 12px;
1274
- background: ${colors.surface["gray-subtler"]};
1274
+ background: ${colors$1.surface["gray-subtler"]};
1275
1275
  `;
1276
1276
  const StyledOpenContentsText = styled.p `
1277
1277
  align-self: stretch;
1278
1278
 
1279
- color: ${colors.text.subtle};
1279
+ color: ${colors$1.text.subtle};
1280
1280
 
1281
1281
  font-family: ${typography.fontFamily.primary};
1282
1282
  font-size: 13px;
@@ -1313,7 +1313,7 @@ const Wrapper = styled.div `
1313
1313
  width: 160px;
1314
1314
  height: 100vh;
1315
1315
  padding: 16px;
1316
- background-color: ${colors.surface.white};
1316
+ background-color: ${colors$1.surface.white};
1317
1317
  gap: 16px;
1318
1318
  `;
1319
1319
  const Title = styled.span `
@@ -1321,7 +1321,7 @@ const Title = styled.span `
1321
1321
  margin: 0;
1322
1322
  ${typography.heading.xxsmall}
1323
1323
  font-family:${typography.fontFamily.primary};
1324
- color: ${colors.text.bolder};
1324
+ color: ${colors$1.text.bolder};
1325
1325
  `;
1326
1326
  const Tablist = styled.div `
1327
1327
  display: flex;
@@ -1332,19 +1332,19 @@ const TabItem = styled.div `
1332
1332
  padding: 8px 8px;
1333
1333
  font-family: ${typography.fontFamily.primary};
1334
1334
  ${({ isSelected }) => isSelected ? typography.body.xsmallBold : typography.body.xsmall}
1335
- color: ${({ isSelected }) => isSelected ? colors.text.secondary : colors.text.subtle};
1336
- background-color: ${({ isSelected }) => isSelected ? colors.action["secondary-selected"] : "transparent"};
1335
+ color: ${({ isSelected }) => isSelected ? colors$1.text.secondary : colors$1.text.subtle};
1336
+ background-color: ${({ isSelected }) => isSelected ? colors$1.action["secondary-selected"] : "transparent"};
1337
1337
  border-radius: 4px;
1338
1338
  cursor: pointer;
1339
1339
 
1340
1340
  &:hover {
1341
1341
  background-color: ${({ isSelected }) => isSelected
1342
- ? colors.action["secondary-selected"]
1343
- : colors.action["secondary-hover"]};
1342
+ ? colors$1.action["secondary-selected"]
1343
+ : colors$1.action["secondary-hover"]};
1344
1344
  }
1345
1345
 
1346
1346
  &:active {
1347
- background-color: ${colors.action["secondary-pressed"]};
1347
+ background-color: ${colors$1.action["secondary-pressed"]};
1348
1348
  }
1349
1349
  `;
1350
1350
 
@@ -1359,12 +1359,823 @@ const TabBar = ({ title, items, defaultSelectedId, onChange }) => {
1359
1359
  return (jsxRuntime.jsxs(Wrapper, { children: [title && jsxRuntime.jsx(Title, { children: title }), jsxRuntime.jsx(Tablist, { children: items.map((item) => (jsxRuntime.jsx(TabItem, { isSelected: selectedId === item.id, onClick: () => handleTabClick(item.id), children: item.label }, item.id))) })] }));
1360
1360
  };
1361
1361
 
1362
+ // ============================================
1363
+ // 디자인 토큰
1364
+ // ============================================
1365
+ const colors = {
1366
+ header: colors$1.surface['secondary-subtler'],
1367
+ headerHover: colors$1.action['secondary-pressed'],
1368
+ body: colors$1.surface.white,
1369
+ bodyHover: colors$1.surface['gray-subtler'],
1370
+ border: colors$1.border['gray-light'],
1371
+ borderLight: colors$1.border['secondary-light'],
1372
+ text: colors$1.text.bolder,
1373
+ textSecondary: colors$1.text.subtle,
1374
+ scrollThumb: colors$1.surface['gray-subtle'],
1375
+ scrollThumbBorder: colors$1.border.gray,
1376
+ selected: colors$1.surface['information-subtler'],
1377
+ selectedBorder: colors$1.border.primary,
1378
+ disabledText: colors$1.text.disabled,
1379
+ };
1380
+ const spacing = {
1381
+ cellPadding: '4px 16px',
1382
+ headerPadding: '4px 16px',
1383
+ };
1384
+ // ============================================
1385
+ // 레이아웃 컴포넌트
1386
+ // ============================================
1387
+ const TableOuterWrapper = styled.div `
1388
+ position: relative;
1389
+ display: inline-block;
1390
+ outline: none;
1391
+
1392
+ &:focus {
1393
+ outline: none;
1394
+ }
1395
+ `;
1396
+ const TableWrapper = styled.div `
1397
+ display: flex;
1398
+ flex-direction: column;
1399
+ width: 100%;
1400
+ overflow: hidden;
1401
+ `;
1402
+ const TableContainer = styled.div `
1403
+ width: 100%;
1404
+ overflow: auto;
1405
+ position: relative;
1406
+ ${({ maxHeight }) => maxHeight && `max-height: ${maxHeight};`}
1407
+
1408
+ /* 스크롤바 스타일 */
1409
+ &::-webkit-scrollbar {
1410
+ width: 20px;
1411
+ height: 20px;
1412
+ }
1413
+
1414
+ &::-webkit-scrollbar-track {
1415
+ background: ${colors.body};
1416
+ border: 1px solid ${colors.border};
1417
+ margin-top: 20px; /* 상단 화살표 버튼 높이만큼 여백 */
1418
+ }
1419
+
1420
+ &::-webkit-scrollbar-thumb {
1421
+ background: ${colors.scrollThumb};
1422
+ border: 1px solid ${colors.scrollThumbBorder};
1423
+
1424
+ &:hover {
1425
+ background: ${colors.headerHover};
1426
+ }
1427
+ }
1428
+
1429
+ &::-webkit-scrollbar-button:vertical:start:decrement,
1430
+ &::-webkit-scrollbar-button:vertical:end:increment {
1431
+ display: block;
1432
+ height: 20px;
1433
+ background: ${colors.header};
1434
+ border: 1px solid ${colors.borderLight};
1435
+ }
1436
+
1437
+ &::-webkit-scrollbar-button:vertical:start:decrement:hover,
1438
+ &::-webkit-scrollbar-button:vertical:end:increment:hover {
1439
+ background: ${colors.headerHover};
1440
+ }
1441
+ `;
1442
+ // ============================================
1443
+ // 테이블 기본 컴포넌트
1444
+ // ============================================
1445
+ const StyledTable = styled.table `
1446
+ width: 100%;
1447
+ border-collapse: separate;
1448
+ border-spacing: 0;
1449
+ table-layout: auto;
1450
+ font-family: ${typography.fontFamily.primary};
1451
+ `;
1452
+ const TableHead = styled.thead `
1453
+ position: sticky;
1454
+ top: 0;
1455
+ z-index: 10;
1456
+ background: ${colors.header};
1457
+ `;
1458
+ const TableBody$1 = styled.tbody ``;
1459
+ const TableRow = styled.tr `
1460
+ &:nth-of-type(even) {
1461
+ ${({ striped }) => striped && `background-color: ${colors.bodyHover};`}
1462
+ }
1463
+
1464
+ &:hover {
1465
+ background-color: ${colors.bodyHover};
1466
+ }
1467
+ `;
1468
+ // ============================================
1469
+ // 셀 컴포넌트
1470
+ // ============================================
1471
+ const baseCellStyle = `
1472
+ box-sizing: border-box;
1473
+ vertical-align: middle;
1474
+ line-height: 1.5;
1475
+ `;
1476
+ const TableHeaderCell = styled.th `
1477
+ ${baseCellStyle}
1478
+ min-width: ${({ width }) => (width ? '0' : '80px')};
1479
+ background: ${colors.header};
1480
+ border: 1px solid ${colors.borderLight};
1481
+ border-left: none;
1482
+ padding: ${spacing.headerPadding};
1483
+ text-align: left;
1484
+ font-weight: 700;
1485
+ font-size: 15px;
1486
+ color: ${colors.text};
1487
+ height: 30px;
1488
+ white-space: nowrap;
1489
+ position: relative;
1490
+ ${({ width }) => width && `width: ${width};`}
1491
+
1492
+ &:first-of-type {
1493
+ border-left: 1px solid ${colors.borderLight};
1494
+ }
1495
+
1496
+ ${({ sortable }) => sortable &&
1497
+ `
1498
+ cursor: pointer;
1499
+ user-select: none;
1500
+
1501
+ &:hover {
1502
+ background: ${colors.headerHover};
1503
+ }
1504
+ `}
1505
+ `;
1506
+ const TableDataCell = styled.td `
1507
+ ${baseCellStyle}
1508
+ min-width: ${({ width }) => (width ? '0' : '80px')};
1509
+ background: ${({ isHeaderColumn, isSelected, $rowSelected }) => isSelected ? colors.selected : (isHeaderColumn ? colors.header : ($rowSelected ? 'inherit' : colors.body))};
1510
+ border-right: 1px solid ${({ isHeaderColumn }) => isHeaderColumn ? colors.borderLight : colors.border};
1511
+ border-bottom: 1px solid ${({ isHeaderColumn }) => isHeaderColumn ? colors.borderLight : colors.border};
1512
+ border-left: none;
1513
+ border-top: none;
1514
+ padding: ${({ isHeaderColumn }) => (isHeaderColumn ? spacing.headerPadding : spacing.cellPadding)};
1515
+ font-weight: ${({ isHeaderColumn }) => (isHeaderColumn ? 700 : 400)};
1516
+ font-size: ${({ isHeaderColumn }) => (isHeaderColumn ? '15px' : '13px')};
1517
+ color: ${({ isHeaderColumn }) => (isHeaderColumn ? colors.text : colors.textSecondary)};
1518
+ height: ${({ height }) => height ?? '30px'};
1519
+ position: relative;
1520
+ user-select: none;
1521
+
1522
+ &:first-of-type {
1523
+ border-left: 1px solid ${({ isHeaderColumn }) => isHeaderColumn ? colors.borderLight : colors.border};
1524
+ }
1525
+
1526
+ ${({ isSelected, $edgeTop, $edgeBottom, $edgeLeft, $edgeRight }) => isSelected &&
1527
+ `
1528
+ z-index: 1;
1529
+ box-shadow:
1530
+ ${$edgeTop ? `inset 0 2px 0 0 ${colors.selectedBorder}` : `inset 0 0.5px 0 0 ${colors.selectedBorder}50`}${$edgeBottom ? `, inset 0 -2px 0 0 ${colors.selectedBorder}` : `, inset 0 -0.5px 0 0 ${colors.selectedBorder}50`}${$edgeLeft ? `, inset 2px 0 0 0 ${colors.selectedBorder}` : `, inset 0.5px 0 0 0 ${colors.selectedBorder}50`}${$edgeRight ? `, inset -2px 0 0 0 ${colors.selectedBorder}` : `, inset -0.5px 0 0 0 ${colors.selectedBorder}50`};
1531
+ `}
1532
+
1533
+ ${({ editable, isSelected, $rowSelected }) => editable && !isSelected && !$rowSelected &&
1534
+ `
1535
+ cursor: cell;
1536
+ &:hover {
1537
+ background-color: ${colors.bodyHover};
1538
+ }
1539
+ `}
1540
+
1541
+ ${({ isSelected }) => isSelected &&
1542
+ `
1543
+ &:hover {
1544
+ background-color: ${colors.selected};
1545
+ }
1546
+ `}
1547
+ `;
1548
+ // ============================================
1549
+ // 정렬 및 입력 컴포넌트
1550
+ // ============================================
1551
+ const SortIcon = styled.span `
1552
+ display: inline-flex;
1553
+ flex-direction: column;
1554
+ margin-left: 4px;
1555
+ opacity: ${({ active }) => (active ? 1 : 0.3)};
1556
+
1557
+ svg {
1558
+ width: 12px;
1559
+ height: 12px;
1560
+ }
1561
+ `;
1562
+ const EditableInput = styled.input `
1563
+ width: 100%;
1564
+ border: none;
1565
+ outline: none;
1566
+ background: transparent;
1567
+ font: inherit;
1568
+ color: inherit;
1569
+ margin: -12px -16px;
1570
+ padding: ${spacing.cellPadding};
1571
+
1572
+ &:focus {
1573
+ outline: none;
1574
+ }
1575
+ `;
1576
+ // ============================================
1577
+ // 스크롤 컨트롤
1578
+ // ============================================
1579
+ const ScrollContainer = styled.div `
1580
+ display: flex;
1581
+ flex-direction: column;
1582
+ position: absolute;
1583
+ right: 0;
1584
+ top: 0;
1585
+ `;
1586
+ const ScrollButton = styled.button `
1587
+ width: 20px;
1588
+ height: 20px;
1589
+ padding: 0;
1590
+ background: ${colors.header};
1591
+ border: 1px solid ${colors.borderLight};
1592
+ cursor: pointer;
1593
+ display: flex;
1594
+ align-items: center;
1595
+ justify-content: center;
1596
+ transition: background-color 0.2s;
1597
+
1598
+ &:hover {
1599
+ background: ${colors.headerHover};
1600
+ }
1601
+
1602
+ &:disabled {
1603
+ opacity: 0.5;
1604
+ cursor: not-allowed;
1605
+ }
1606
+
1607
+ svg {
1608
+ width: 14px;
1609
+ height: 14px;
1610
+ color: ${colors.text};
1611
+ }
1612
+ `;
1613
+ // ============================================
1614
+ // 컨텍스트 메뉴
1615
+ // ============================================
1616
+ const ContextMenuOverlay = styled.div `
1617
+ position: fixed;
1618
+ top: 0;
1619
+ left: 0;
1620
+ right: 0;
1621
+ bottom: 0;
1622
+ z-index: 999;
1623
+ `;
1624
+ const ContextMenu = styled.div `
1625
+ position: fixed;
1626
+ top: ${({ y }) => y}px;
1627
+ left: ${({ x }) => x}px;
1628
+ z-index: 1000;
1629
+ min-width: 160px;
1630
+ background: ${colors.body};
1631
+ border: 1px solid ${colors.border};
1632
+ border-radius: 6px;
1633
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
1634
+ padding: 4px 0;
1635
+ font-family: ${typography.fontFamily.primary};
1636
+ `;
1637
+ const ContextMenuItem = styled.button `
1638
+ width: 100%;
1639
+ padding: 8px 12px;
1640
+ border: none;
1641
+ background: transparent;
1642
+ text-align: left;
1643
+ font-size: 13px;
1644
+ color: ${({ disabled }) => (disabled ? colors.disabledText : colors.text)};
1645
+ cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
1646
+ display: flex;
1647
+ align-items: center;
1648
+ gap: 8px;
1649
+
1650
+ &:hover {
1651
+ background: ${({ disabled }) => (disabled ? 'transparent' : colors.bodyHover)};
1652
+ }
1653
+
1654
+ span.shortcut {
1655
+ margin-left: auto;
1656
+ font-size: 11px;
1657
+ color: ${colors.disabledText};
1658
+ }
1659
+ `;
1660
+ const ContextMenuDivider = styled.div `
1661
+ height: 1px;
1662
+ background: ${colors.borderLight};
1663
+ margin: 4px 0;
1664
+ `;
1665
+
1666
+ /** 정렬 아이콘 컴포넌트 */
1667
+ function SortIndicator({ isActive, direction }) {
1668
+ if (!isActive || !direction) {
1669
+ return jsxRuntime.jsx(lucideReact.ChevronUp, { style: { opacity: 0.3 } });
1670
+ }
1671
+ return direction === 'asc' ? jsxRuntime.jsx(lucideReact.ChevronUp, {}) : jsxRuntime.jsx(lucideReact.ChevronDown, {});
1672
+ }
1673
+ /** 테이블 헤더 */
1674
+ function TableHeader({ columns, sortColumn, sortDirection, onSort, }) {
1675
+ const handleClick = (key, sortable) => {
1676
+ if (sortable)
1677
+ onSort?.(key);
1678
+ };
1679
+ return (jsxRuntime.jsx(TableHead, { children: jsxRuntime.jsx(TableRow, { children: columns.map(({ key, header, width, sortable, rowSpan, colSpan }) => {
1680
+ const isActive = sortColumn === key;
1681
+ return (jsxRuntime.jsxs(TableHeaderCell, { width: width, sortable: sortable, rowSpan: rowSpan, colSpan: colSpan, onClick: () => handleClick(key, sortable), children: [header, sortable && (jsxRuntime.jsx(SortIcon, { active: isActive, direction: isActive ? sortDirection : null, children: jsxRuntime.jsx(SortIndicator, { isActive: isActive, direction: sortDirection }) }))] }, key));
1682
+ }) }) }));
1683
+ }
1684
+
1685
+ const formatValue = (val, dataType) => {
1686
+ if (val == null)
1687
+ return '';
1688
+ switch (dataType) {
1689
+ case 'number':
1690
+ return typeof val === 'number' ? val.toLocaleString() : String(val);
1691
+ case 'date':
1692
+ return val instanceof Date ? val.toLocaleDateString('ko-KR') : String(val);
1693
+ case 'boolean':
1694
+ return val ? '예' : '아니오';
1695
+ default:
1696
+ return String(val);
1697
+ }
1698
+ };
1699
+ const parseValue = (val, dataType) => {
1700
+ switch (dataType) {
1701
+ case 'number': {
1702
+ const num = parseFloat(val.replace(/,/g, ''));
1703
+ return isNaN(num) ? 0 : num;
1704
+ }
1705
+ case 'date':
1706
+ return new Date(val);
1707
+ case 'boolean':
1708
+ return val === '예' || val === 'true' || val === '1';
1709
+ default:
1710
+ return val;
1711
+ }
1712
+ };
1713
+ const getInputType = (dataType) => {
1714
+ if (dataType === 'number')
1715
+ return 'number';
1716
+ if (dataType === 'date')
1717
+ return 'date';
1718
+ return 'text';
1719
+ };
1720
+ function TableCell({ value, editable = false, width, height, rowHeight, dataType = 'text', isHeaderColumn = false, isSelected = false, rowSelected = false, isEditingRequested, startEditingToken, startEditingValue, selectionEdge, rowSpan, colSpan, onEdit, render, onMouseDown, onMouseEnter, onMouseUp, }) {
1721
+ const [isEditing, setIsEditing] = React.useState(false);
1722
+ const [editValue, setEditValue] = React.useState('');
1723
+ const lastStartTokenRef = React.useRef(undefined);
1724
+ const startEditing = React.useCallback(() => {
1725
+ if (!editable)
1726
+ return;
1727
+ setEditValue(formatValue(value, dataType));
1728
+ setIsEditing(true);
1729
+ }, [editable, value, dataType]);
1730
+ // 부모에서 "편집 시작" 요청이 오면 (예: 선택 셀에서 타이핑) 편집 모드로 진입
1731
+ React.useEffect(() => {
1732
+ if (!isEditingRequested)
1733
+ return;
1734
+ if (startEditingToken == null)
1735
+ return;
1736
+ if (lastStartTokenRef.current === startEditingToken)
1737
+ return;
1738
+ lastStartTokenRef.current = startEditingToken;
1739
+ if (!editable)
1740
+ return;
1741
+ const nextValue = startEditingValue ?? formatValue(value, dataType);
1742
+ setEditValue(nextValue);
1743
+ setIsEditing(true);
1744
+ }, [isEditingRequested, startEditingToken, startEditingValue, editable, value, dataType]);
1745
+ const save = React.useCallback(() => {
1746
+ onEdit?.(parseValue(editValue, dataType));
1747
+ setIsEditing(false);
1748
+ }, [editValue, dataType, onEdit]);
1749
+ const cancel = React.useCallback(() => {
1750
+ setEditValue('');
1751
+ setIsEditing(false);
1752
+ }, []);
1753
+ const handleKeyDown = React.useCallback((e) => {
1754
+ if (e.key === 'Enter') {
1755
+ e.preventDefault();
1756
+ save();
1757
+ }
1758
+ else if (e.key === 'Escape') {
1759
+ e.preventDefault();
1760
+ cancel();
1761
+ }
1762
+ }, [save, cancel]);
1763
+ const handleMouseDown = React.useCallback((e) => {
1764
+ if (isEditing)
1765
+ return;
1766
+ e.preventDefault();
1767
+ onMouseDown?.();
1768
+ }, [isEditing, onMouseDown]);
1769
+ const handleMouseEnter = React.useCallback(() => {
1770
+ if (isEditing)
1771
+ return;
1772
+ onMouseEnter?.();
1773
+ }, [isEditing, onMouseEnter]);
1774
+ const displayValue = render ? render(value) : formatValue(value, dataType);
1775
+ return (jsxRuntime.jsx(TableDataCell, { editable: editable, width: width, height: height || rowHeight, isHeaderColumn: isHeaderColumn, isSelected: isSelected, "$rowSelected": rowSelected, "$edgeTop": selectionEdge?.top, "$edgeBottom": selectionEdge?.bottom, "$edgeLeft": selectionEdge?.left, "$edgeRight": selectionEdge?.right, rowSpan: rowSpan, colSpan: colSpan, onDoubleClick: startEditing, onMouseDown: handleMouseDown, onMouseEnter: handleMouseEnter, onMouseUp: onMouseUp, children: isEditing ? (jsxRuntime.jsx(EditableInput, { type: getInputType(dataType), value: editValue, onChange: (e) => setEditValue(e.target.value), onBlur: save, onKeyDown: handleKeyDown, autoFocus: true })) : (displayValue) }));
1776
+ }
1777
+
1778
+ const getSelectionInfo = (rowIndex, colIndex, start, end) => {
1779
+ if (!start || !end) {
1780
+ return { isSelected: false, edge: { top: false, bottom: false, left: false, right: false } };
1781
+ }
1782
+ const minRow = Math.min(start.row, end.row);
1783
+ const maxRow = Math.max(start.row, end.row);
1784
+ const minCol = Math.min(start.col, end.col);
1785
+ const maxCol = Math.max(start.col, end.col);
1786
+ const isSelected = rowIndex >= minRow && rowIndex <= maxRow && colIndex >= minCol && colIndex <= maxCol;
1787
+ if (!isSelected) {
1788
+ return { isSelected: false, edge: { top: false, bottom: false, left: false, right: false } };
1789
+ }
1790
+ return {
1791
+ isSelected: true,
1792
+ edge: {
1793
+ top: rowIndex === minRow,
1794
+ bottom: rowIndex === maxRow,
1795
+ left: colIndex === minCol,
1796
+ right: colIndex === maxCol,
1797
+ }
1798
+ };
1799
+ };
1800
+ function TableBody({ columns, data, rowHeight, onCellEdit, selectionStart, selectionEnd, editingCell, editStartValue, editToken, onCellMouseDown, onCellMouseEnter, onCellMouseUp, enableRowSelection, selectedRowIndex, hoveredRowIndex, onRowClick, onRowHover, }) {
1801
+ const getRowBackground = (rowIndex) => {
1802
+ if (!enableRowSelection)
1803
+ return undefined;
1804
+ if (selectedRowIndex === rowIndex)
1805
+ return '#e7f4fe';
1806
+ if (hoveredRowIndex === rowIndex)
1807
+ return '#f4f5f6';
1808
+ return undefined;
1809
+ };
1810
+ return (jsxRuntime.jsx(TableBody$1, { children: data.map((row, rowIndex) => (jsxRuntime.jsx(TableRow, { onClick: enableRowSelection ? () => onRowClick?.(rowIndex) : undefined, onMouseEnter: enableRowSelection ? () => onRowHover?.(rowIndex) : undefined, onMouseLeave: enableRowSelection ? () => onRowHover?.(null) : undefined, style: {
1811
+ cursor: enableRowSelection ? 'pointer' : undefined,
1812
+ backgroundColor: getRowBackground(rowIndex),
1813
+ transition: enableRowSelection ? 'background-color 0.15s ease' : undefined,
1814
+ }, children: columns.map((col, colIndex) => {
1815
+ const { isSelected, edge } = getSelectionInfo(rowIndex, colIndex, selectionStart, selectionEnd);
1816
+ const isEditingRequested = !!editingCell && editingCell.row === rowIndex && editingCell.col === colIndex;
1817
+ return (jsxRuntime.jsx(TableCell, { value: row[col.key], editable: col.editable !== false, width: col.width, height: col.height, rowHeight: rowHeight, dataType: col.dataType, isHeaderColumn: col.isHeaderColumn, isSelected: isSelected, rowSelected: enableRowSelection && (selectedRowIndex === rowIndex || hoveredRowIndex === rowIndex), isEditingRequested: isEditingRequested, startEditingToken: editToken, startEditingValue: isEditingRequested ? editStartValue : null, selectionEdge: isSelected ? edge : undefined, rowSpan: col.rowSpan, colSpan: col.colSpan, onEdit: (value) => onCellEdit?.(rowIndex, col.key, value), render: col.render ? (value) => col.render(value, row, rowIndex) : undefined, onMouseDown: enableRowSelection ? undefined : () => onCellMouseDown?.(rowIndex, colIndex), onMouseEnter: enableRowSelection ? undefined : () => onCellMouseEnter?.(rowIndex, colIndex), onMouseUp: enableRowSelection ? undefined : onCellMouseUp }, `${rowIndex}-${col.key}`));
1818
+ }) }, rowIndex))) }));
1819
+ }
1820
+
1821
+ const compareValues = (a, b, dataType) => {
1822
+ if (a == null)
1823
+ return 1;
1824
+ if (b == null)
1825
+ return -1;
1826
+ switch (dataType) {
1827
+ case 'number':
1828
+ return Number(a) - Number(b);
1829
+ case 'date':
1830
+ return new Date(a).getTime() - new Date(b).getTime();
1831
+ case 'boolean':
1832
+ return (a ? 1 : 0) - (b ? 1 : 0);
1833
+ default:
1834
+ return String(a).localeCompare(String(b), 'ko-KR');
1835
+ }
1836
+ };
1837
+ const getNextSortDirection = (current) => {
1838
+ if (current === 'asc')
1839
+ return 'desc';
1840
+ if (current === 'desc')
1841
+ return null;
1842
+ return 'asc';
1843
+ };
1844
+ const formatCellValue = (value) => {
1845
+ if (value == null)
1846
+ return '';
1847
+ if (value instanceof Date)
1848
+ return value.toLocaleDateString('ko-KR');
1849
+ if (typeof value === 'boolean')
1850
+ return value ? '예' : '아니오';
1851
+ if (typeof value === 'number')
1852
+ return value.toString();
1853
+ return String(value);
1854
+ };
1855
+ function Table({ columns, data, onCellEdit, onSort, onSelectionChange, onPaste, maxHeight, rowHeight, className, enableRowSelection, selectedRowIndex, onRowClick, enableKeyboardNavigation, }) {
1856
+ const outerRef = React.useRef(null);
1857
+ const containerRef = React.useRef(null);
1858
+ const [sortColumn, setSortColumn] = React.useState(null);
1859
+ const [sortDirection, setSortDirection] = React.useState(null);
1860
+ const [isSelecting, setIsSelecting] = React.useState(false);
1861
+ const [selectionStart, setSelectionStart] = React.useState(null);
1862
+ const [selectionEnd, setSelectionEnd] = React.useState(null);
1863
+ const [contextMenu, setContextMenu] = React.useState({ visible: false, x: 0, y: 0 });
1864
+ const [hoveredRowIndex, setHoveredRowIndex] = React.useState(null);
1865
+ const [editingCell, setEditingCell] = React.useState(null);
1866
+ const [editStartValue, setEditStartValue] = React.useState(null);
1867
+ const [editToken, setEditToken] = React.useState(0);
1868
+ const sortedData = React.useMemo(() => {
1869
+ if (!sortColumn || !sortDirection)
1870
+ return data;
1871
+ const column = columns.find((col) => col.key === sortColumn);
1872
+ if (!column)
1873
+ return data;
1874
+ return [...data].sort((a, b) => {
1875
+ const aVal = a[sortColumn];
1876
+ const bVal = b[sortColumn];
1877
+ const result = column.sortFn
1878
+ ? column.sortFn(aVal, bVal)
1879
+ : compareValues(aVal, bVal, column.dataType);
1880
+ return sortDirection === 'asc' ? result : -result;
1881
+ });
1882
+ }, [data, sortColumn, sortDirection, columns]);
1883
+ const handleSort = React.useCallback((columnKey) => {
1884
+ const isSameColumn = sortColumn === columnKey;
1885
+ const nextDirection = isSameColumn
1886
+ ? getNextSortDirection(sortDirection)
1887
+ : 'asc';
1888
+ setSortColumn(nextDirection ? columnKey : null);
1889
+ setSortDirection(nextDirection);
1890
+ onSort?.(columnKey, nextDirection);
1891
+ }, [sortColumn, sortDirection, onSort]);
1892
+ const scroll = React.useCallback((delta) => {
1893
+ containerRef.current?.scrollBy({ top: delta, behavior: 'smooth' });
1894
+ }, []);
1895
+ const handleCellMouseDown = React.useCallback((rowIndex, colIndex) => {
1896
+ setIsSelecting(true);
1897
+ setSelectionStart({ row: rowIndex, col: colIndex });
1898
+ setSelectionEnd({ row: rowIndex, col: colIndex });
1899
+ }, []);
1900
+ const handleCellMouseEnter = React.useCallback((rowIndex, colIndex) => {
1901
+ if (isSelecting) {
1902
+ setSelectionEnd({ row: rowIndex, col: colIndex });
1903
+ }
1904
+ }, [isSelecting]);
1905
+ const handleCellMouseUp = React.useCallback(() => {
1906
+ if (isSelecting && selectionStart && selectionEnd) {
1907
+ const cells = [];
1908
+ const minRow = Math.min(selectionStart.row, selectionEnd.row);
1909
+ const maxRow = Math.max(selectionStart.row, selectionEnd.row);
1910
+ const minCol = Math.min(selectionStart.col, selectionEnd.col);
1911
+ const maxCol = Math.max(selectionStart.col, selectionEnd.col);
1912
+ for (let r = minRow; r <= maxRow; r++) {
1913
+ for (let c = minCol; c <= maxCol; c++) {
1914
+ cells.push({ row: r, col: c });
1915
+ }
1916
+ }
1917
+ onSelectionChange?.(cells);
1918
+ }
1919
+ setIsSelecting(false);
1920
+ }, [isSelecting, selectionStart, selectionEnd, onSelectionChange]);
1921
+ const getSelectedData = React.useCallback(() => {
1922
+ if (!selectionStart || !selectionEnd)
1923
+ return '';
1924
+ const minRow = Math.min(selectionStart.row, selectionEnd.row);
1925
+ const maxRow = Math.max(selectionStart.row, selectionEnd.row);
1926
+ const minCol = Math.min(selectionStart.col, selectionEnd.col);
1927
+ const maxCol = Math.max(selectionStart.col, selectionEnd.col);
1928
+ const rows = [];
1929
+ for (let r = minRow; r <= maxRow; r++) {
1930
+ const cols = [];
1931
+ for (let c = minCol; c <= maxCol; c++) {
1932
+ const colKey = columns[c]?.key;
1933
+ const value = sortedData[r]?.[colKey];
1934
+ cols.push(formatCellValue(value));
1935
+ }
1936
+ rows.push(cols.join('\t'));
1937
+ }
1938
+ return rows.join('\n');
1939
+ }, [selectionStart, selectionEnd, columns, sortedData]);
1940
+ const handleKeyDown = React.useCallback((e) => {
1941
+ // 셀 편집(input/textarea) 중에는 전역 키 핸들러가 Backspace/Delete 등을 가로채지 않도록 무시
1942
+ const target = e.target;
1943
+ if (target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable)) {
1944
+ return;
1945
+ }
1946
+ // 행 선택 모드에서 키보드 네비게이션
1947
+ if (enableRowSelection && enableKeyboardNavigation && onRowClick) {
1948
+ if (e.key === 'ArrowUp') {
1949
+ e.preventDefault();
1950
+ const currentIndex = selectedRowIndex ?? 0;
1951
+ const newIndex = Math.max(0, currentIndex - 1);
1952
+ onRowClick(newIndex, sortedData[newIndex]);
1953
+ return;
1954
+ }
1955
+ if (e.key === 'ArrowDown') {
1956
+ e.preventDefault();
1957
+ const currentIndex = selectedRowIndex ?? -1;
1958
+ const newIndex = Math.min(sortedData.length - 1, currentIndex + 1);
1959
+ onRowClick(newIndex, sortedData[newIndex]);
1960
+ return;
1961
+ }
1962
+ }
1963
+ if (!selectionStart || !selectionEnd)
1964
+ return;
1965
+ // 셀 선택 상태에서 문자 입력 시 즉시 편집 모드로 전환 (스프레드시트 UX)
1966
+ // - 단일 셀 선택일 때만 동작
1967
+ // - onCellEdit가 있어야 의미 있는 편집이 가능
1968
+ if (onCellEdit &&
1969
+ !e.ctrlKey &&
1970
+ !e.metaKey &&
1971
+ !e.altKey &&
1972
+ e.key.length === 1) {
1973
+ const isSingleCell = selectionStart.row === selectionEnd.row && selectionStart.col === selectionEnd.col;
1974
+ if (!isSingleCell) {
1975
+ setSelectionEnd(selectionStart);
1976
+ }
1977
+ e.preventDefault();
1978
+ setEditingCell({ row: selectionStart.row, col: selectionStart.col });
1979
+ setEditStartValue(e.key);
1980
+ setEditToken((t) => t + 1);
1981
+ setContextMenu({ visible: false, x: 0, y: 0 });
1982
+ return;
1983
+ }
1984
+ if ((e.ctrlKey || e.metaKey) && e.key === 'c') {
1985
+ e.preventDefault();
1986
+ const text = getSelectedData();
1987
+ navigator.clipboard.writeText(text).catch(console.error);
1988
+ }
1989
+ if ((e.ctrlKey || e.metaKey) && e.key === 'v') {
1990
+ e.preventDefault();
1991
+ navigator.clipboard.readText().then((text) => {
1992
+ if (!text || !selectionStart)
1993
+ return;
1994
+ const rows = text
1995
+ .replace(/\r\n/g, '\n')
1996
+ .replace(/\r/g, '\n')
1997
+ .split('\n')
1998
+ .filter((row, idx, arr) => !(idx === arr.length - 1 && row === ''))
1999
+ .map(row => row.split('\t'));
2000
+ const startRow = Math.min(selectionStart.row, selectionEnd?.row ?? selectionStart.row);
2001
+ const startCol = Math.min(selectionStart.col, selectionEnd?.col ?? selectionStart.col);
2002
+ if (onPaste) {
2003
+ onPaste(startRow, startCol, rows);
2004
+ }
2005
+ else if (onCellEdit) {
2006
+ rows.forEach((row, rIdx) => {
2007
+ row.forEach((cellValue, cIdx) => {
2008
+ const targetRow = startRow + rIdx;
2009
+ const targetCol = startCol + cIdx;
2010
+ if (targetRow < data.length && targetCol < columns.length) {
2011
+ const col = columns[targetCol];
2012
+ if (col.editable !== false) {
2013
+ let parsedValue = cellValue;
2014
+ if (col.dataType === 'number') {
2015
+ const num = parseFloat(cellValue.replace(/,/g, ''));
2016
+ parsedValue = isNaN(num) ? 0 : num;
2017
+ }
2018
+ onCellEdit(targetRow, col.key, parsedValue);
2019
+ }
2020
+ }
2021
+ });
2022
+ });
2023
+ }
2024
+ }).catch(console.error);
2025
+ }
2026
+ if (e.key === 'Delete' || e.key === 'Backspace') {
2027
+ if (!onCellEdit)
2028
+ return;
2029
+ e.preventDefault();
2030
+ const minRow = Math.min(selectionStart.row, selectionEnd.row);
2031
+ const maxRow = Math.max(selectionStart.row, selectionEnd.row);
2032
+ const minCol = Math.min(selectionStart.col, selectionEnd.col);
2033
+ const maxCol = Math.max(selectionStart.col, selectionEnd.col);
2034
+ for (let r = minRow; r <= maxRow; r++) {
2035
+ for (let c = minCol; c <= maxCol; c++) {
2036
+ const col = columns[c];
2037
+ if (col.editable !== false) {
2038
+ onCellEdit(r, col.key, '');
2039
+ }
2040
+ }
2041
+ }
2042
+ }
2043
+ if (e.key === 'Escape') {
2044
+ setSelectionStart(null);
2045
+ setSelectionEnd(null);
2046
+ setContextMenu({ visible: false, x: 0, y: 0 });
2047
+ }
2048
+ }, [enableRowSelection, enableKeyboardNavigation, selectedRowIndex, sortedData, onRowClick, selectionStart, selectionEnd, getSelectedData, onPaste, onCellEdit, data, columns]);
2049
+ // 표 밖을 클릭하면 셀 선택(타겟) 해제
2050
+ React.useEffect(() => {
2051
+ const handleMouseDownOutside = (event) => {
2052
+ const root = outerRef.current;
2053
+ const target = event.target;
2054
+ if (!root || !target)
2055
+ return;
2056
+ if (root.contains(target))
2057
+ return;
2058
+ setIsSelecting(false);
2059
+ setSelectionStart(null);
2060
+ setSelectionEnd(null);
2061
+ setEditingCell(null);
2062
+ setEditStartValue(null);
2063
+ setContextMenu({ visible: false, x: 0, y: 0 });
2064
+ };
2065
+ document.addEventListener('mousedown', handleMouseDownOutside, true);
2066
+ return () => {
2067
+ document.removeEventListener('mousedown', handleMouseDownOutside, true);
2068
+ };
2069
+ }, []);
2070
+ const handleContextMenu = React.useCallback((e) => {
2071
+ e.preventDefault();
2072
+ if (selectionStart && selectionEnd) {
2073
+ setContextMenu({ visible: true, x: e.clientX, y: e.clientY });
2074
+ }
2075
+ }, [selectionStart, selectionEnd]);
2076
+ const closeContextMenu = React.useCallback(() => {
2077
+ setContextMenu({ visible: false, x: 0, y: 0 });
2078
+ }, []);
2079
+ const handleCopy = React.useCallback(() => {
2080
+ const text = getSelectedData();
2081
+ navigator.clipboard.writeText(text).catch(console.error);
2082
+ closeContextMenu();
2083
+ }, [getSelectedData, closeContextMenu]);
2084
+ const handlePaste = React.useCallback(() => {
2085
+ if (!selectionStart || !selectionEnd)
2086
+ return;
2087
+ navigator.clipboard.readText().then((text) => {
2088
+ if (!text)
2089
+ return;
2090
+ const rows = text
2091
+ .replace(/\r\n/g, '\n')
2092
+ .replace(/\r/g, '\n')
2093
+ .split('\n')
2094
+ .filter((row, idx, arr) => !(idx === arr.length - 1 && row === ''))
2095
+ .map(row => row.split('\t'));
2096
+ const startRow = Math.min(selectionStart.row, selectionEnd.row);
2097
+ const startCol = Math.min(selectionStart.col, selectionEnd.col);
2098
+ if (onPaste) {
2099
+ onPaste(startRow, startCol, rows);
2100
+ }
2101
+ else if (onCellEdit) {
2102
+ rows.forEach((row, rIdx) => {
2103
+ row.forEach((cellValue, cIdx) => {
2104
+ const targetRow = startRow + rIdx;
2105
+ const targetCol = startCol + cIdx;
2106
+ if (targetRow < data.length && targetCol < columns.length) {
2107
+ const col = columns[targetCol];
2108
+ if (col.editable !== false) {
2109
+ let parsedValue = cellValue;
2110
+ if (col.dataType === 'number') {
2111
+ const num = parseFloat(cellValue.replace(/,/g, ''));
2112
+ parsedValue = isNaN(num) ? 0 : num;
2113
+ }
2114
+ onCellEdit(targetRow, col.key, parsedValue);
2115
+ }
2116
+ }
2117
+ });
2118
+ });
2119
+ }
2120
+ }).catch(console.error);
2121
+ closeContextMenu();
2122
+ }, [selectionStart, selectionEnd, onPaste, onCellEdit, data, columns, closeContextMenu]);
2123
+ const handleDelete = React.useCallback(() => {
2124
+ if (!selectionStart || !selectionEnd || !onCellEdit)
2125
+ return;
2126
+ const minRow = Math.min(selectionStart.row, selectionEnd.row);
2127
+ const maxRow = Math.max(selectionStart.row, selectionEnd.row);
2128
+ const minCol = Math.min(selectionStart.col, selectionEnd.col);
2129
+ const maxCol = Math.max(selectionStart.col, selectionEnd.col);
2130
+ for (let r = minRow; r <= maxRow; r++) {
2131
+ for (let c = minCol; c <= maxCol; c++) {
2132
+ const col = columns[c];
2133
+ if (col.editable !== false) {
2134
+ onCellEdit(r, col.key, '');
2135
+ }
2136
+ }
2137
+ }
2138
+ closeContextMenu();
2139
+ }, [selectionStart, selectionEnd, onCellEdit, columns, closeContextMenu]);
2140
+ const handleClearSelection = React.useCallback(() => {
2141
+ setSelectionStart(null);
2142
+ setSelectionEnd(null);
2143
+ closeContextMenu();
2144
+ }, [closeContextMenu]);
2145
+ const hasSelection = selectionStart !== null && selectionEnd !== null;
2146
+ const selectionCellCount = React.useMemo(() => {
2147
+ if (!selectionStart || !selectionEnd)
2148
+ return 0;
2149
+ const rows = Math.abs(selectionEnd.row - selectionStart.row) + 1;
2150
+ const cols = Math.abs(selectionEnd.col - selectionStart.col) + 1;
2151
+ return rows * cols;
2152
+ }, [selectionStart, selectionEnd]);
2153
+ React.useEffect(() => {
2154
+ const container = containerRef.current;
2155
+ if (!container)
2156
+ return;
2157
+ const handleMouseUp = () => {
2158
+ if (isSelecting) {
2159
+ handleCellMouseUp();
2160
+ }
2161
+ };
2162
+ document.addEventListener('mouseup', handleMouseUp);
2163
+ document.addEventListener('keydown', handleKeyDown);
2164
+ return () => {
2165
+ document.removeEventListener('mouseup', handleMouseUp);
2166
+ document.removeEventListener('keydown', handleKeyDown);
2167
+ };
2168
+ }, [isSelecting, handleCellMouseUp, handleKeyDown]);
2169
+ return (jsxRuntime.jsxs(TableOuterWrapper, { ref: outerRef, className: className, tabIndex: 0, onContextMenu: handleContextMenu, children: [jsxRuntime.jsx(TableWrapper, { children: jsxRuntime.jsx(TableContainer, { ref: containerRef, maxHeight: maxHeight, children: jsxRuntime.jsxs(StyledTable, { children: [jsxRuntime.jsx("colgroup", { children: columns.map((col) => (jsxRuntime.jsx("col", { style: col.width ? { width: col.width } : undefined }, String(col.key)))) }), jsxRuntime.jsx(TableHeader, { columns: columns, sortColumn: sortColumn ?? undefined, sortDirection: sortDirection, onSort: handleSort }), jsxRuntime.jsx(TableBody, { columns: columns, data: sortedData, rowHeight: rowHeight, onCellEdit: onCellEdit, selectionStart: enableRowSelection ? null : selectionStart, selectionEnd: enableRowSelection ? null : selectionEnd, editingCell: enableRowSelection ? null : editingCell, editStartValue: editStartValue, editToken: editToken, onCellMouseDown: enableRowSelection ? undefined : handleCellMouseDown, onCellMouseEnter: enableRowSelection ? undefined : handleCellMouseEnter, onCellMouseUp: enableRowSelection ? undefined : handleCellMouseUp, enableRowSelection: enableRowSelection, selectedRowIndex: selectedRowIndex, hoveredRowIndex: hoveredRowIndex ?? undefined, onRowClick: (rowIndex) => onRowClick?.(rowIndex, sortedData[rowIndex]), onRowHover: setHoveredRowIndex })] }) }) }), maxHeight && (jsxRuntime.jsxs(ScrollContainer, { children: [jsxRuntime.jsx(ScrollButton, { position: "top", onClick: () => scroll(-100), children: jsxRuntime.jsx(lucideReact.ChevronUp, {}) }), jsxRuntime.jsx(ScrollButton, { position: "bottom", onClick: () => scroll(100), children: jsxRuntime.jsx(lucideReact.ChevronDown, {}) })] })), contextMenu.visible && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(ContextMenuOverlay, { onClick: closeContextMenu }), jsxRuntime.jsxs(ContextMenu, { x: contextMenu.x, y: contextMenu.y, children: [jsxRuntime.jsxs(ContextMenuItem, { onClick: handleCopy, disabled: !hasSelection, children: [jsxRuntime.jsx(lucideReact.Copy, { size: 14 }), "\uBCF5\uC0AC", jsxRuntime.jsx("span", { className: "shortcut", children: "\u2318C" })] }), jsxRuntime.jsxs(ContextMenuItem, { onClick: handlePaste, disabled: !hasSelection, children: [jsxRuntime.jsx(lucideReact.ClipboardPaste, { size: 14 }), "\uBD99\uC5EC\uB123\uAE30", jsxRuntime.jsx("span", { className: "shortcut", children: "\u2318V" })] }), jsxRuntime.jsx(ContextMenuDivider, {}), jsxRuntime.jsxs(ContextMenuItem, { onClick: handleDelete, disabled: !hasSelection || !onCellEdit, children: [jsxRuntime.jsx(lucideReact.Trash2, { size: 14 }), "\uC0AD\uC81C", jsxRuntime.jsx("span", { className: "shortcut", children: "Del" })] }), jsxRuntime.jsx(ContextMenuDivider, {}), jsxRuntime.jsxs(ContextMenuItem, { onClick: handleClearSelection, disabled: !hasSelection, children: [jsxRuntime.jsx(lucideReact.XCircle, { size: 14 }), "\uC120\uD0DD \uD574\uC81C", jsxRuntime.jsx("span", { className: "shortcut", children: "Esc" })] }), hasSelection && (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx(ContextMenuDivider, {}), jsxRuntime.jsxs(ContextMenuItem, { disabled: true, style: { fontSize: '11px', color: '#9ca3af' }, children: [selectionCellCount, "\uAC1C \uC140 \uC120\uD0DD\uB428"] })] }))] })] }))] }));
2170
+ }
2171
+
1362
2172
  exports.Button = Button;
1363
2173
  exports.CheckBox = CheckBox;
1364
2174
  exports.Dropdown = Dropdown;
1365
2175
  exports.Input = Input;
1366
2176
  exports.Sidebar = Sidebar;
1367
2177
  exports.TabBar = TabBar;
1368
- exports.colors = colors;
2178
+ exports.Table = Table;
2179
+ exports.colors = colors$1;
1369
2180
  exports.typography = typography;
1370
2181
  //# sourceMappingURL=index.cjs.js.map