@simplysm/solid 13.0.57 → 13.0.59

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 (105) hide show
  1. package/README.md +1 -1
  2. package/dist/components/data/crud-detail/CrudDetail.d.ts.map +1 -1
  3. package/dist/components/data/crud-detail/CrudDetail.js +55 -42
  4. package/dist/components/data/crud-detail/CrudDetail.js.map +2 -2
  5. package/dist/components/data/crud-sheet/CrudSheet.d.ts.map +1 -1
  6. package/dist/components/data/crud-sheet/CrudSheet.js +120 -94
  7. package/dist/components/data/crud-sheet/CrudSheet.js.map +2 -2
  8. package/dist/components/data/crud-sheet/CrudSheetColumn.js +1 -1
  9. package/dist/components/data/crud-sheet/CrudSheetColumn.js.map +2 -2
  10. package/dist/components/data/crud-sheet/types.d.ts +4 -3
  11. package/dist/components/data/crud-sheet/types.d.ts.map +1 -1
  12. package/dist/components/data/kanban/Kanban.d.ts.map +1 -1
  13. package/dist/components/data/kanban/Kanban.js +3 -4
  14. package/dist/components/data/kanban/Kanban.js.map +2 -2
  15. package/dist/components/data/kanban/KanbanContext.d.ts +2 -3
  16. package/dist/components/data/kanban/KanbanContext.d.ts.map +1 -1
  17. package/dist/components/data/kanban/KanbanContext.js.map +1 -1
  18. package/dist/components/data/list/ListItem.d.ts.map +1 -1
  19. package/dist/components/data/list/ListItem.js +3 -3
  20. package/dist/components/data/list/ListItem.js.map +2 -2
  21. package/dist/components/data/sheet/DataSheet.styles.d.ts.map +1 -1
  22. package/dist/components/data/sheet/DataSheet.styles.js +3 -8
  23. package/dist/components/data/sheet/DataSheet.styles.js.map +1 -1
  24. package/dist/components/disclosure/Dialog.d.ts.map +1 -1
  25. package/dist/components/disclosure/Dialog.js +36 -27
  26. package/dist/components/disclosure/Dialog.js.map +2 -2
  27. package/dist/components/disclosure/Dropdown.d.ts.map +1 -1
  28. package/dist/components/disclosure/Dropdown.js +7 -15
  29. package/dist/components/disclosure/Dropdown.js.map +2 -2
  30. package/dist/components/display/Icon.js +1 -1
  31. package/dist/components/display/Icon.js.map +1 -1
  32. package/dist/components/feedback/notification/NotificationBanner.js +1 -1
  33. package/dist/components/feedback/notification/NotificationBanner.js.map +1 -1
  34. package/dist/components/feedback/notification/NotificationContext.d.ts +1 -1
  35. package/dist/components/feedback/notification/NotificationContext.d.ts.map +1 -1
  36. package/dist/components/feedback/notification/NotificationContext.js.map +1 -1
  37. package/dist/components/feedback/notification/NotificationProvider.d.ts.map +1 -1
  38. package/dist/components/feedback/notification/NotificationProvider.js +8 -12
  39. package/dist/components/feedback/notification/NotificationProvider.js.map +2 -2
  40. package/dist/components/form-control/color-picker/ColorPicker.d.ts +5 -3
  41. package/dist/components/form-control/color-picker/ColorPicker.d.ts.map +1 -1
  42. package/dist/components/form-control/color-picker/ColorPicker.js +11 -6
  43. package/dist/components/form-control/color-picker/ColorPicker.js.map +2 -2
  44. package/dist/components/form-control/field/NumberInput.d.ts.map +1 -1
  45. package/dist/components/form-control/field/NumberInput.js +2 -2
  46. package/dist/components/form-control/field/NumberInput.js.map +2 -2
  47. package/dist/components/form-control/field/TextInput.d.ts.map +1 -1
  48. package/dist/components/form-control/field/TextInput.js +3 -3
  49. package/dist/components/form-control/field/TextInput.js.map +2 -2
  50. package/dist/components/form-control/select/Select.d.ts.map +1 -1
  51. package/dist/components/form-control/select/Select.js +3 -4
  52. package/dist/components/form-control/select/Select.js.map +2 -2
  53. package/dist/components/form-control/select/SelectContext.d.ts +1 -2
  54. package/dist/components/form-control/select/SelectContext.d.ts.map +1 -1
  55. package/dist/components/form-control/select/SelectContext.js.map +1 -1
  56. package/dist/components/form-control/select/SelectItem.d.ts.map +1 -1
  57. package/dist/components/form-control/select/SelectItem.js +3 -3
  58. package/dist/components/form-control/select/SelectItem.js.map +2 -2
  59. package/dist/helpers/createAppStructure.d.ts +7 -4
  60. package/dist/helpers/createAppStructure.d.ts.map +1 -1
  61. package/dist/helpers/createAppStructure.js +20 -2
  62. package/dist/helpers/createAppStructure.js.map +1 -1
  63. package/dist/hooks/createPointerDrag.d.ts +1 -1
  64. package/dist/hooks/createPointerDrag.d.ts.map +1 -1
  65. package/dist/hooks/createPointerDrag.js +6 -4
  66. package/dist/hooks/createPointerDrag.js.map +1 -1
  67. package/dist/hooks/createSlotSignal.d.ts +9 -0
  68. package/dist/hooks/createSlotSignal.d.ts.map +1 -0
  69. package/dist/hooks/createSlotSignal.js +10 -0
  70. package/dist/hooks/createSlotSignal.js.map +6 -0
  71. package/dist/index.d.ts +15 -17
  72. package/dist/index.d.ts.map +1 -1
  73. package/dist/index.js +15 -39
  74. package/dist/index.js.map +1 -1
  75. package/docs/data-components.md +18 -8
  76. package/docs/feedback.md +17 -10
  77. package/docs/helpers.md +24 -0
  78. package/docs/hooks.md +166 -83
  79. package/docs/styling.md +1 -0
  80. package/package.json +3 -3
  81. package/src/components/data/crud-detail/CrudDetail.tsx +45 -40
  82. package/src/components/data/crud-sheet/CrudSheet.tsx +99 -103
  83. package/src/components/data/crud-sheet/CrudSheetColumn.tsx +1 -1
  84. package/src/components/data/crud-sheet/types.ts +4 -3
  85. package/src/components/data/kanban/Kanban.tsx +3 -5
  86. package/src/components/data/kanban/KanbanContext.ts +2 -3
  87. package/src/components/data/list/ListItem.tsx +2 -5
  88. package/src/components/data/sheet/DataSheet.styles.ts +3 -8
  89. package/src/components/disclosure/Dialog.tsx +26 -26
  90. package/src/components/disclosure/Dropdown.tsx +7 -20
  91. package/src/components/display/Icon.tsx +1 -1
  92. package/src/components/feedback/notification/NotificationBanner.tsx +1 -1
  93. package/src/components/feedback/notification/NotificationContext.ts +2 -7
  94. package/src/components/feedback/notification/NotificationProvider.tsx +8 -15
  95. package/src/components/form-control/color-picker/ColorPicker.tsx +19 -9
  96. package/src/components/form-control/field/NumberInput.tsx +2 -4
  97. package/src/components/form-control/field/TextInput.tsx +2 -5
  98. package/src/components/form-control/select/Select.tsx +3 -6
  99. package/src/components/form-control/select/SelectContext.ts +1 -2
  100. package/src/components/form-control/select/SelectItem.tsx +2 -5
  101. package/src/helpers/createAppStructure.ts +36 -6
  102. package/src/hooks/createPointerDrag.ts +8 -5
  103. package/src/hooks/createSlotSignal.ts +14 -0
  104. package/src/index.ts +15 -41
  105. package/tailwind.config.ts +1 -0
@@ -21,7 +21,7 @@ import { useNotification } from "../../feedback/notification/NotificationContext
21
21
  import { Button } from "../../form-control/Button";
22
22
  import { Icon } from "../../display/Icon";
23
23
  import { FormGroup } from "../../layout/FormGroup";
24
- import { TopbarContext, createTopbarActions } from "../../layout/topbar/TopbarContext";
24
+ import { createTopbarActions, TopbarContext } from "../../layout/topbar/TopbarContext";
25
25
  import { useDialogInstance } from "../../disclosure/DialogInstanceContext";
26
26
  import { Dialog } from "../../disclosure/Dialog";
27
27
  import { Link } from "../../display/Link";
@@ -29,6 +29,7 @@ import { createEventListener } from "@solid-primitives/event-listener";
29
29
  import clsx from "clsx";
30
30
  import {
31
31
  IconDeviceFloppy,
32
+ IconExternalLink,
32
33
  IconFileExcel,
33
34
  IconPlus,
34
35
  IconRefresh,
@@ -37,10 +38,10 @@ import {
37
38
  IconTrashOff,
38
39
  IconUpload,
39
40
  } from "@tabler/icons-solidjs";
40
- import { isCrudSheetColumnDef, CrudSheetColumn } from "./CrudSheetColumn";
41
- import { isCrudSheetFilterDef, CrudSheetFilter } from "./CrudSheetFilter";
42
- import { isCrudSheetToolsDef, CrudSheetTools } from "./CrudSheetTools";
43
- import { isCrudSheetHeaderDef, CrudSheetHeader } from "./CrudSheetHeader";
41
+ import { CrudSheetColumn, isCrudSheetColumnDef } from "./CrudSheetColumn";
42
+ import { CrudSheetFilter, isCrudSheetFilterDef } from "./CrudSheetFilter";
43
+ import { CrudSheetTools, isCrudSheetToolsDef } from "./CrudSheetTools";
44
+ import { CrudSheetHeader, isCrudSheetHeaderDef } from "./CrudSheetHeader";
44
45
  import type {
45
46
  CrudSheetColumnDef,
46
47
  CrudSheetContext,
@@ -70,6 +71,7 @@ const CrudSheetBase = <TItem, TFilter extends Record<string, any>>(
70
71
  "editable",
71
72
  "itemEditable",
72
73
  "itemDeletable",
74
+ "itemDeleted",
73
75
  "filterInitial",
74
76
  "items",
75
77
  "onItemsChange",
@@ -124,28 +126,27 @@ const CrudSheetBase = <TItem, TFilter extends Record<string, any>>(
124
126
 
125
127
  let formRef: HTMLFormElement | undefined;
126
128
 
127
- // -- Auto Refresh Effect --
128
129
  createEffect(() => {
129
- const currLastFilter = lastFilter();
130
- const currSorts = sorts();
131
- const currPage = page();
132
-
133
- queueMicrotask(async () => {
134
- setBusyCount((c) => c + 1);
135
- await noti.try(async () => {
136
- await refresh(currLastFilter, currSorts, currPage);
137
- }, "조회 실패");
138
- setBusyCount((c) => c - 1);
139
- setReady(true);
140
- });
130
+ void doRefresh();
141
131
  });
142
132
 
143
- async function refresh(currLastFilter: TFilter, currSorts: SortingDef[], currPage: number) {
133
+ async function doRefresh() {
134
+ setBusyCount((c) => c + 1);
135
+ try {
136
+ await refresh();
137
+ } catch (err) {
138
+ noti.error(err, "조회 실패");
139
+ }
140
+ setBusyCount((c) => c - 1);
141
+ setReady(true);
142
+ }
143
+
144
+ async function refresh() {
144
145
  const usePagination = local.itemsPerPage != null;
145
146
  const result: SearchResult<TItem> = await local.search(
146
- currLastFilter,
147
- usePagination ? currPage : 0,
148
- currSorts,
147
+ lastFilter(),
148
+ usePagination ? page() : 0,
149
+ sorts(),
149
150
  );
150
151
  setItems(reconcile(result.items));
151
152
  originalItems = objClone(result.items);
@@ -167,8 +168,8 @@ const CrudSheetBase = <TItem, TFilter extends Record<string, any>>(
167
168
  setLastFilter(() => objClone(filter));
168
169
  }
169
170
 
170
- function handleRefresh() {
171
- setLastFilter(() => ({ ...lastFilter() }));
171
+ async function handleRefresh() {
172
+ await doRefresh();
172
173
  }
173
174
 
174
175
  // -- Inline Edit --
@@ -210,17 +211,14 @@ const CrudSheetBase = <TItem, TFilter extends Record<string, any>>(
210
211
  return;
211
212
  }
212
213
 
213
- const currLastFilter = lastFilter();
214
- const currSorts = sorts();
215
- const currPage = page();
216
-
217
214
  setBusyCount((c) => c + 1);
218
- // eslint-disable-next-line solid/reactivity -- noti.try 내부에서 비동기 refresh 호출
219
- await noti.try(async () => {
220
- await local.inlineEdit!.submit(diffs);
215
+ try {
216
+ await local.inlineEdit.submit(diffs);
221
217
  noti.success("저장 완료", "저장되었습니다.");
222
- await refresh(currLastFilter, currSorts, currPage);
223
- }, "저장 실패");
218
+ await refresh();
219
+ } catch (err) {
220
+ noti.error(err, "저장 실패");
221
+ }
224
222
  setBusyCount((c) => c - 1);
225
223
  }
226
224
 
@@ -236,10 +234,11 @@ const CrudSheetBase = <TItem, TFilter extends Record<string, any>>(
236
234
  if (!result) return;
237
235
 
238
236
  setBusyCount((c) => c + 1);
239
- // eslint-disable-next-line solid/reactivity -- noti.try 내부에서 비동기 refresh 호출
240
- await noti.try(async () => {
241
- await refresh(lastFilter(), sorts(), page());
242
- }, "조회 실패");
237
+ try {
238
+ await refresh();
239
+ } catch (err) {
240
+ noti.error(err, "조회 실패");
241
+ }
243
242
  setBusyCount((c) => c - 1);
244
243
  }
245
244
 
@@ -249,11 +248,12 @@ const CrudSheetBase = <TItem, TFilter extends Record<string, any>>(
249
248
  if (!result) return;
250
249
 
251
250
  setBusyCount((c) => c + 1);
252
- // eslint-disable-next-line solid/reactivity -- noti.try 내부에서 비동기 refresh 호출
253
- await noti.try(async () => {
254
- await refresh(lastFilter(), sorts(), page());
251
+ try {
252
+ await refresh();
255
253
  noti.success("삭제 완료", "삭제되었습니다.");
256
- }, "삭제 실패");
254
+ } catch (err) {
255
+ noti.error(err, "삭제 실패");
256
+ }
257
257
  setBusyCount((c) => c - 1);
258
258
  }
259
259
 
@@ -262,11 +262,12 @@ const CrudSheetBase = <TItem, TFilter extends Record<string, any>>(
262
262
  if (!local.excel) return;
263
263
 
264
264
  setBusyCount((c) => c + 1);
265
- // eslint-disable-next-line solid/reactivity -- noti.try 내부에서 비동기 호출
266
- await noti.try(async () => {
265
+ try {
267
266
  const result = await local.search(lastFilter(), 0, sorts());
268
- await local.excel!.download(result.items);
269
- }, "엑셀 다운로드 실패");
267
+ await local.excel.download(result.items);
268
+ } catch (err) {
269
+ noti.error(err, "엑셀 다운로드 실패");
270
+ }
270
271
  setBusyCount((c) => c - 1);
271
272
  }
272
273
 
@@ -281,12 +282,13 @@ const CrudSheetBase = <TItem, TFilter extends Record<string, any>>(
281
282
  if (file == null) return;
282
283
 
283
284
  setBusyCount((c) => c + 1);
284
- // eslint-disable-next-line solid/reactivity -- noti.try 내부에서 비동기 호출
285
- await noti.try(async () => {
285
+ try {
286
286
  await local.excel!.upload!(file);
287
287
  noti.success("완료", "엑셀 업로드가 완료되었습니다.");
288
- await refresh(lastFilter(), sorts(), page());
289
- }, "엑셀 업로드 실패");
288
+ await refresh();
289
+ } catch (err) {
290
+ noti.error(err, "엑셀 업로드 실패");
291
+ }
290
292
  setBusyCount((c) => c - 1);
291
293
  };
292
294
  input.click();
@@ -307,14 +309,14 @@ const CrudSheetBase = <TItem, TFilter extends Record<string, any>>(
307
309
  }
308
310
 
309
311
  // -- Keyboard Shortcuts --
310
- createEventListener(document, "keydown", (e: KeyboardEvent) => {
312
+ createEventListener(document, "keydown", async (e: KeyboardEvent) => {
311
313
  if (e.ctrlKey && e.key === "s" && !isSelectMode()) {
312
314
  e.preventDefault();
313
315
  formRef?.requestSubmit();
314
316
  }
315
317
  if (e.ctrlKey && e.altKey && e.key === "l") {
316
318
  e.preventDefault();
317
- handleRefresh();
319
+ await doRefresh();
318
320
  }
319
321
  });
320
322
 
@@ -354,8 +356,7 @@ const CrudSheetBase = <TItem, TFilter extends Record<string, any>>(
354
356
  },
355
357
  save: handleSave,
356
358
  refresh: async () => {
357
- handleRefresh();
358
- await Promise.resolve();
359
+ await doRefresh();
359
360
  },
360
361
  addItem: handleAddRow,
361
362
  setPage,
@@ -364,6 +365,7 @@ const CrudSheetBase = <TItem, TFilter extends Record<string, any>>(
364
365
 
365
366
  // -- Render --
366
367
  const deleteProp = () => local.inlineEdit?.deleteProp;
368
+ const isItemDeleted = (item: TItem) => local.itemDeleted?.(item) ?? false;
367
369
 
368
370
  return (
369
371
  <>
@@ -385,17 +387,19 @@ const CrudSheetBase = <TItem, TFilter extends Record<string, any>>(
385
387
  class={clsx("flex h-full flex-col", local.class)}
386
388
  >
387
389
  {/* Control mode: inline save/refresh bar */}
388
- <Show when={!isModal && !topbarCtx && canEdit() && local.inlineEdit}>
390
+ <Show when={!isModal && !topbarCtx}>
389
391
  <div class="flex gap-2 p-2 pb-0">
390
- <Button
391
- size="sm"
392
- theme="primary"
393
- variant="ghost"
394
- onClick={() => formRef?.requestSubmit()}
395
- >
396
- <Icon icon={IconDeviceFloppy} class="mr-1" />
397
- 저장
398
- </Button>
392
+ <Show when={canEdit() && local.inlineEdit}>
393
+ <Button
394
+ size="sm"
395
+ theme="primary"
396
+ variant="ghost"
397
+ onClick={() => formRef?.requestSubmit()}
398
+ >
399
+ <Icon icon={IconDeviceFloppy} class="mr-1" />
400
+ 저장
401
+ </Button>
402
+ </Show>
399
403
  <Button size="sm" theme="info" variant="ghost" onClick={handleRefresh}>
400
404
  <Icon icon={IconRefresh} class="mr-1" />
401
405
  새로고침
@@ -496,9 +500,7 @@ const CrudSheetBase = <TItem, TFilter extends Record<string, any>>(
496
500
  onSortsChange={setSorts}
497
501
  selectMode={
498
502
  isSelectMode()
499
- ? local.selectMode === "multi"
500
- ? "multiple"
501
- : "single"
503
+ ? local.selectMode
502
504
  : local.modalEdit?.deleteItems != null
503
505
  ? "multiple"
504
506
  : undefined
@@ -506,43 +508,34 @@ const CrudSheetBase = <TItem, TFilter extends Record<string, any>>(
506
508
  selectedItems={selectedItems()}
507
509
  onSelectedItemsChange={setSelectedItems}
508
510
  autoSelect={isSelectMode() && local.selectMode === "single" ? "click" : undefined}
509
- cellClass={(item, _colKey) => {
510
- const dp = deleteProp();
511
- if (dp != null && Boolean((item as Record<string, unknown>)[dp])) {
511
+ cellClass={(item) => {
512
+ if (isItemDeleted(item)) {
512
513
  return clsx("line-through");
513
514
  }
514
515
  return undefined;
515
516
  }}
516
517
  >
517
- {/* Auto delete column */}
518
- <Show when={deleteProp() != null && canEdit() ? deleteProp() : undefined}>
519
- {(dp) => (
520
- <DataSheetColumn<TItem>
521
- key="__delete"
522
- header=""
523
- fixed
524
- sortable={false}
525
- resizable={false}
526
- >
527
- {(dsCtx) => (
528
- <div class="flex items-center justify-center px-1 py-0.5">
529
- <Link
530
- theme="danger"
531
- disabled={!(local.itemDeletable?.(dsCtx.item) ?? true)}
532
- onClick={() => handleToggleDelete(dsCtx.item, dsCtx.index)}
533
- >
534
- <Icon
535
- icon={
536
- Boolean((dsCtx.item as Record<string, unknown>)[dp()])
537
- ? IconTrashOff
538
- : IconTrash
539
- }
540
- />
541
- </Link>
542
- </div>
543
- )}
544
- </DataSheetColumn>
545
- )}
518
+ {/* Auto delete column (inline edit only) */}
519
+ <Show when={deleteProp() != null && canEdit()}>
520
+ <DataSheetColumn<TItem>
521
+ key="__delete"
522
+ header=""
523
+ fixed
524
+ sortable={false}
525
+ resizable={false}
526
+ >
527
+ {(dsCtx) => (
528
+ <div class="flex items-center justify-center px-1 py-0.5">
529
+ <Link
530
+ theme="danger"
531
+ disabled={!(local.itemDeletable?.(dsCtx.item) ?? true)}
532
+ onClick={() => handleToggleDelete(dsCtx.item, dsCtx.index)}
533
+ >
534
+ <Icon icon={isItemDeleted(dsCtx.item) ? IconTrashOff : IconTrash} />
535
+ </Link>
536
+ </div>
537
+ )}
538
+ </DataSheetColumn>
546
539
  </Show>
547
540
 
548
541
  {/* User-defined columns -- map CrudSheetColumn to DataSheetColumn */}
@@ -574,20 +567,23 @@ const CrudSheetBase = <TItem, TFilter extends Record<string, any>>(
574
567
  // modalEdit editable column -- wrap with edit link
575
568
  if (
576
569
  local.modalEdit &&
577
- col.editable &&
570
+ col.editTrigger &&
578
571
  canEdit() &&
579
572
  (local.itemEditable?.(dsCtx.item) ?? true)
580
573
  ) {
581
574
  return (
582
575
  <Link
583
- class="flex w-full"
576
+ class={clsx("flex", "gap-1")}
584
577
  onClick={(e) => {
585
578
  e.preventDefault();
586
579
  e.stopPropagation();
587
580
  void handleEditItem(dsCtx.item);
588
581
  }}
589
582
  >
590
- {col.cell(crudCtx)}
583
+ <div class={"p-1"}>
584
+ <Icon icon={IconExternalLink} />
585
+ </div>
586
+ <div class={"flex-1"}>{col.cell(crudCtx)}</div>
591
587
  </Link>
592
588
  );
593
589
  }
@@ -606,10 +602,10 @@ const CrudSheetBase = <TItem, TFilter extends Record<string, any>>(
606
602
  <div class="flex-1" />
607
603
  <Show when={selectedItems().length > 0}>
608
604
  <Button size="sm" theme="danger" onClick={handleSelectCancel}>
609
- {local.selectMode === "multi" ? "모두" : "선택"} 해제
605
+ {local.selectMode === "multiple" ? "모두" : "선택"} 해제
610
606
  </Button>
611
607
  </Show>
612
- <Show when={local.selectMode === "multi"}>
608
+ <Show when={local.selectMode === "multiple"}>
613
609
  <Button size="sm" theme="primary" onClick={handleSelectConfirm}>
614
610
  확인({selectedItems().length})
615
611
  </Button>
@@ -28,7 +28,7 @@ export function CrudSheetColumn<TItem>(props: CrudSheetColumnProps<TItem>): JSX.
28
28
  width: props.width,
29
29
  sortable: props.sortable ?? true,
30
30
  resizable: props.resizable ?? true,
31
- editable: props.editable ?? false,
31
+ editTrigger: props.editTrigger ?? false,
32
32
  } as unknown as JSX.Element;
33
33
  }
34
34
  /* eslint-enable solid/reactivity */
@@ -80,11 +80,12 @@ interface CrudSheetBaseProps<TItem, TFilter extends Record<string, any>> {
80
80
  editable?: boolean;
81
81
  itemEditable?: (item: TItem) => boolean;
82
82
  itemDeletable?: (item: TItem) => boolean;
83
+ itemDeleted?: (item: TItem) => boolean;
83
84
  filterInitial?: TFilter;
84
85
  items?: TItem[];
85
86
  onItemsChange?: (items: TItem[]) => void;
86
87
  excel?: ExcelConfig<TItem>;
87
- selectMode?: "single" | "multi";
88
+ selectMode?: "single" | "multiple";
88
89
  onSelect?: (result: SelectResult<TItem>) => void;
89
90
  hideAutoTools?: boolean;
90
91
  class?: string;
@@ -108,12 +109,12 @@ export interface CrudSheetColumnDef<TItem> {
108
109
  class?: string;
109
110
  sortable: boolean;
110
111
  resizable: boolean;
111
- editable: boolean;
112
+ editTrigger: boolean;
112
113
  cell: (ctx: CrudSheetCellContext<TItem>) => JSX.Element;
113
114
  }
114
115
 
115
116
  export interface CrudSheetColumnProps<TItem> extends Omit<DataSheetColumnProps<TItem>, "children"> {
116
- editable?: boolean;
117
+ editTrigger?: boolean;
117
118
  children: (ctx: CrudSheetCellContext<TItem>) => JSX.Element;
118
119
  }
119
120
 
@@ -18,6 +18,7 @@ import { Checkbox } from "../../form-control/checkbox/Checkbox";
18
18
  import { Icon } from "../../display/Icon";
19
19
  import { BusyContainer } from "../../feedback/busy/BusyContainer";
20
20
  import { createControllableSignal } from "../../../hooks/createControllableSignal";
21
+ import { createSlotSignal } from "../../../hooks/createSlotSignal";
21
22
  import "./Kanban.css";
22
23
  import { iconButtonBase } from "../../../styles/patterns.styles";
23
24
  import {
@@ -386,11 +387,8 @@ const KanbanLane: ParentComponent<KanbanLaneProps> = (props) => {
386
387
  };
387
388
 
388
389
  // Slot signals
389
- type SlotAccessor = (() => JSX.Element) | undefined;
390
- const [title, _setTitle] = createSignal<SlotAccessor>();
391
- const [tools, _setTools] = createSignal<SlotAccessor>();
392
- const setTitle = (content: SlotAccessor) => _setTitle(() => content);
393
- const setTools = (content: SlotAccessor) => _setTools(() => content);
390
+ const [title, setTitle] = createSlotSignal();
391
+ const [tools, setTools] = createSlotSignal();
394
392
 
395
393
  const laneContextValue: KanbanLaneContextValue = {
396
394
  value: () => local.value,
@@ -1,4 +1,5 @@
1
- import { createContext, useContext, type Accessor, type JSX, type Setter } from "solid-js";
1
+ import { createContext, useContext, type Accessor, type Setter } from "solid-js";
2
+ import type { SlotAccessor } from "../../../hooks/createSlotSignal";
2
3
 
3
4
  // ── 타입 ──────────────────────────────────────────────────────
4
5
 
@@ -50,8 +51,6 @@ export function useKanbanContext(): KanbanContextValue {
50
51
 
51
52
  // ── Lane Context ───────────────────────────────────────────────
52
53
 
53
- type SlotAccessor = (() => JSX.Element) | undefined;
54
-
55
54
  export interface KanbanLaneContextValue<L = unknown, T = unknown> {
56
55
  value: Accessor<L | undefined>;
57
56
  dropTarget: Accessor<KanbanDropTarget<T> | undefined>;
@@ -1,7 +1,6 @@
1
1
  import {
2
2
  type Component,
3
3
  createContext,
4
- createSignal,
5
4
  type JSX,
6
5
  onCleanup,
7
6
  type ParentComponent,
@@ -16,6 +15,7 @@ import { twMerge } from "tailwind-merge";
16
15
  import { ripple } from "../../../directives/ripple";
17
16
  import { Collapse } from "../../disclosure/Collapse";
18
17
  import { createControllableSignal } from "../../../hooks/createControllableSignal";
18
+ import { createSlotSignal, type SlotAccessor } from "../../../hooks/createSlotSignal";
19
19
  import { useListContext } from "./ListContext";
20
20
  import { List } from "./List";
21
21
  import {
@@ -32,8 +32,6 @@ import type { ComponentSize } from "../../../styles/tokens.styles";
32
32
 
33
33
  void ripple;
34
34
 
35
- type SlotAccessor = (() => JSX.Element) | undefined;
36
-
37
35
  interface ListItemSlotsContextValue {
38
36
  setChildren: (content: SlotAccessor) => void;
39
37
  }
@@ -166,8 +164,7 @@ export const ListItem: ListItemComponent = (props) => {
166
164
  onChange: () => local.onOpenChange,
167
165
  });
168
166
 
169
- const [childrenSlot, _setChildrenSlot] = createSignal<SlotAccessor>();
170
- const setChildrenSlot = (content: SlotAccessor) => _setChildrenSlot(() => content);
167
+ const [childrenSlot, setChildrenSlot] = createSlotSignal();
171
168
  const hasChildren = () => childrenSlot() !== undefined;
172
169
 
173
170
  const useRipple = () => !(local.readonly || local.disabled);
@@ -3,7 +3,8 @@ import { borderDefault, borderSubtle } from "../../../styles/tokens.styles";
3
3
 
4
4
  export const dataSheetContainerClass = clsx(
5
5
  "relative",
6
- "bg-white dark:bg-base-950",
6
+ // "bg-white dark:bg-base-950",
7
+ "bg-base-100 dark:bg-base-900",
7
8
  "overflow-auto",
8
9
  );
9
10
 
@@ -50,13 +51,7 @@ export const sortableThClass = clsx("cursor-pointer", "hover:underline");
50
51
  export const sortIconClass = clsx("px-1 py-0.5", "bg-base-100 dark:bg-base-900");
51
52
 
52
53
  // 상단 툴바 (설정 버튼 + 페이지네이션)
53
- export const toolbarClass = clsx(
54
- "flex items-center gap-2",
55
- "px-2 py-1",
56
- "bg-base-50 dark:bg-base-900",
57
- "border-b",
58
- borderDefault,
59
- );
54
+ export const toolbarClass = clsx("flex items-center gap-2", "px-2 py-1", "border-b", borderDefault);
60
55
 
61
56
  // 고정 컬럼 기본 (sticky)
62
57
  export const fixedClass = "sticky";
@@ -1,30 +1,30 @@
1
1
  import {
2
- type JSX,
3
- type ParentComponent,
4
2
  createContext,
5
3
  createEffect,
4
+ createSignal,
6
5
  createUniqueId,
6
+ For,
7
+ type JSX,
7
8
  onCleanup,
9
+ type ParentComponent,
8
10
  Show,
9
11
  splitProps,
10
- For,
11
12
  useContext,
12
- createSignal,
13
13
  } from "solid-js";
14
14
  import { Portal } from "solid-js/web";
15
15
  import clsx from "clsx";
16
16
  import { twMerge } from "tailwind-merge";
17
17
  import { IconX } from "@tabler/icons-solidjs";
18
18
  import { createControllableSignal } from "../../hooks/createControllableSignal";
19
+ import { createSlotSignal, type SlotAccessor } from "../../hooks/createSlotSignal";
19
20
  import { createMountTransition } from "../../hooks/createMountTransition";
20
21
  import { createPointerDrag } from "../../hooks/createPointerDrag";
21
22
  import { mergeStyles } from "../../helpers/mergeStyles";
22
23
  import { Icon } from "../display/Icon";
23
24
  import { borderSubtle } from "../../styles/tokens.styles";
24
25
  import { DialogDefaultsContext } from "./DialogContext";
25
- import { registerDialog, unregisterDialog, bringToFront } from "./dialogZIndex";
26
-
27
- type SlotAccessor = (() => JSX.Element) | undefined;
26
+ import { bringToFront, registerDialog, unregisterDialog } from "./dialogZIndex";
27
+ import { Button } from "../form-control/Button";
28
28
 
29
29
  interface DialogSlotsContextValue {
30
30
  setHeader: (content: SlotAccessor) => void;
@@ -188,10 +188,8 @@ export const Dialog: DialogComponent = (props) => {
188
188
 
189
189
  const headerId = "dialog-header-" + createUniqueId();
190
190
 
191
- const [header, _setHeader] = createSignal<SlotAccessor>();
192
- const setHeader = (content: SlotAccessor) => _setHeader(() => content);
193
- const [action, _setAction] = createSignal<SlotAccessor>();
194
- const setAction = (content: SlotAccessor) => _setAction(() => content);
191
+ const [header, setHeader] = createSlotSignal();
192
+ const [action, setAction] = createSlotSignal();
195
193
  const hasHeader = () => header() !== undefined;
196
194
 
197
195
  const [open, setOpen] = createControllableSignal({
@@ -212,11 +210,19 @@ export const Dialog: DialogComponent = (props) => {
212
210
  local.onCloseComplete?.();
213
211
  };
214
212
 
215
- // open 변경 시 closeCompleteEmitted 초기화
213
+ // open 변경 시 closeCompleteEmitted 초기화 + fallback unmount 감지
214
+ let wasMounted = false;
216
215
  createEffect(() => {
217
216
  if (open()) {
218
217
  closeCompleteEmitted = false;
219
218
  }
219
+ if (mounted()) {
220
+ wasMounted = true;
221
+ } else if (wasMounted) {
222
+ // fallback timer가 transitionend보다 먼저 실행되어 DOM이 제거된 경우,
223
+ // onCloseComplete가 호출되지 않는 문제 방지
224
+ emitCloseComplete();
225
+ }
220
226
  });
221
227
 
222
228
  // dialog ref
@@ -477,7 +483,8 @@ export const Dialog: DialogComponent = (props) => {
477
483
  );
478
484
 
479
485
  // 헤더 클래스
480
- const headerClass = () => clsx("flex items-center", "select-none", "border-b", borderSubtle);
486
+ const headerClass = () =>
487
+ clsx("flex items-center gap-2", "px-3 py-1", "select-none", "border-b", borderSubtle);
481
488
 
482
489
  return (
483
490
  <Show when={mounted()}>
@@ -516,26 +523,19 @@ export const Dialog: DialogComponent = (props) => {
516
523
  }
517
524
  onPointerDown={handleHeaderPointerDown}
518
525
  >
519
- <h5 id={headerId} class={clsx("flex-1", "px-4 py-2", "text-sm font-bold")}>
526
+ <h5 id={headerId} class={clsx("flex-1 font-bold")}>
520
527
  {header()!()}
521
528
  </h5>
522
529
  <Show when={action()}>{action()!()}</Show>
523
530
  <Show when={local.closable ?? true}>
524
- <button
531
+ <Button
525
532
  data-modal-close
526
- type="button"
527
- class={clsx(
528
- "inline-flex items-center justify-center",
529
- "px-3 py-2",
530
- "text-base-400 dark:text-base-500",
531
- "hover:text-base-600 dark:hover:text-base-300",
532
- "cursor-pointer",
533
- "transition-colors",
534
- )}
533
+ size={"sm"}
534
+ variant={"ghost"}
535
535
  onClick={handleCloseClick}
536
536
  >
537
- <Icon icon={IconX} size="1.25em" />
538
- </button>
537
+ <Icon icon={IconX} />
538
+ </Button>
539
539
  </Show>
540
540
  </div>
541
541
  </Show>